Files
server/core/lib/Stores/ExternalIdentityStore.php
2025-12-21 10:09:54 -05:00

225 lines
7.0 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXC\Stores;
use KTXC\Db\DataStore;
/**
* External Identity Store
* Maps external identity provider subjects to local users
*
* Collection: external_identities
* Schema: {
* tid: string, // Tenant identifier
* uid: string, // Local user identifier
* provider: string, // Provider identifier (e.g., 'oidc', 'saml')
* external_subject: string, // External subject identifier (e.g., OIDC sub, SAML NameID)
* attributes: object, // Cached attributes from provider
* linked_at: int, // Timestamp when identity was linked
* last_login: int // Last login via this external identity
* }
*/
class ExternalIdentityStore
{
protected const COLLECTION_NAME = 'external_identities';
public function __construct(protected DataStore $store)
{ }
/**
* Find external identity by provider and external subject
*
* @param string $tenant Tenant identifier
* @param string $provider Provider identifier
* @param string $externalSubject External subject identifier
* @return array|null External identity record or null
*/
public function findByExternalSubject(string $tenant, string $provider, string $externalSubject): ?array
{
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
'tid' => $tenant,
'provider' => $provider,
'external_subject' => $externalSubject
]);
if (!$entry) {
return null;
}
return (array)$entry;
}
/**
* Find all external identities for a user
*
* @param string $tenant Tenant identifier
* @param string $userId Local user identifier
* @return array<array> List of external identity records
*/
public function findByUser(string $tenant, string $userId): array
{
$cursor = $this->store->selectCollection(self::COLLECTION_NAME)->find([
'tid' => $tenant,
'uid' => $userId
]);
$result = [];
foreach ($cursor as $entry) {
$result[] = (array)$entry;
}
return $result;
}
/**
* Find external identity for a user from a specific provider
*
* @param string $tenant Tenant identifier
* @param string $userId Local user identifier
* @param string $provider Provider identifier
* @return array|null External identity record or null
*/
public function findByUserAndProvider(string $tenant, string $userId, string $provider): ?array
{
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
'tid' => $tenant,
'uid' => $userId,
'provider' => $provider
]);
if (!$entry) {
return null;
}
return (array)$entry;
}
/**
* Link an external identity to a local user
*
* @param string $tenant Tenant identifier
* @param string $userId Local user identifier
* @param string $provider Provider identifier
* @param string $externalSubject External subject identifier
* @param array $attributes Optional attributes from provider
* @return bool Whether the operation was successful
*/
public function linkIdentity(
string $tenant,
string $userId,
string $provider,
string $externalSubject,
array $attributes = []
): bool {
$now = time();
// Use upsert to handle both create and update
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
[
'tid' => $tenant,
'provider' => $provider,
'external_subject' => $externalSubject
],
[
'$set' => [
'uid' => $userId,
'attributes' => $attributes,
'last_login' => $now
],
'$setOnInsert' => [
'tid' => $tenant,
'provider' => $provider,
'external_subject' => $externalSubject,
'linked_at' => $now
]
],
['upsert' => true]
);
return $result->isAcknowledged();
}
/**
* Unlink an external identity from a user
*
* @param string $tenant Tenant identifier
* @param string $userId Local user identifier
* @param string $provider Provider identifier
* @return bool Whether the operation was successful
*/
public function unlinkIdentity(string $tenant, string $userId, string $provider): bool
{
$result = $this->store->selectCollection(self::COLLECTION_NAME)->deleteOne([
'tid' => $tenant,
'uid' => $userId,
'provider' => $provider
]);
return $result->isAcknowledged();
}
/**
* Update last login timestamp for an external identity
*
* @param string $tenant Tenant identifier
* @param string $provider Provider identifier
* @param string $externalSubject External subject identifier
* @return bool Whether the operation was successful
*/
public function updateLastLogin(string $tenant, string $provider, string $externalSubject): bool
{
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
[
'tid' => $tenant,
'provider' => $provider,
'external_subject' => $externalSubject
],
['$set' => ['last_login' => time()]]
);
return $result->isAcknowledged();
}
/**
* Update cached attributes for an external identity
*
* @param string $tenant Tenant identifier
* @param string $provider Provider identifier
* @param string $externalSubject External subject identifier
* @param array $attributes New attributes to store
* @return bool Whether the operation was successful
*/
public function updateAttributes(string $tenant, string $provider, string $externalSubject, array $attributes): bool
{
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
[
'tid' => $tenant,
'provider' => $provider,
'external_subject' => $externalSubject
],
['$set' => ['attributes' => $attributes]]
);
return $result->isAcknowledged();
}
/**
* Delete all external identities for a user (used when deleting user)
*
* @param string $tenant Tenant identifier
* @param string $userId Local user identifier
* @return int Number of deleted records
*/
public function deleteAllForUser(string $tenant, string $userId): int
{
$result = $this->store->selectCollection(self::COLLECTION_NAME)->deleteMany([
'tid' => $tenant,
'uid' => $userId
]);
return $result->getDeletedCount();
}
}