Initial Version
This commit is contained in:
224
core/lib/Stores/ExternalIdentityStore.php
Normal file
224
core/lib/Stores/ExternalIdentityStore.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user