Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 4ae6befc7b
422 changed files with 47225 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace KTXC\Stores;
use KTXC\Db\DataStore;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Models\Firewall\FirewallLogObject;
/**
* Store for firewall rules and access logs
*/
class FirewallStore
{
protected const RULES_COLLECTION = 'firewall_rules';
protected const LOGS_COLLECTION = 'firewall_logs';
public function __construct(
protected readonly DataStore $dataStore
) {}
// ========================================
// Rule Operations
// ========================================
/**
* List all rules for a tenant
*/
public function listRules(string $tenantId, bool $activeOnly = true): array
{
$filter = ['tenantId' => $tenantId];
if ($activeOnly) {
$filter['enabled'] = true;
$filter['$or'] = [
['expiresAt' => null],
['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]]
];
}
$cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter);
$list = [];
foreach ($cursor as $entry) {
$rule = (new FirewallRuleObject())->jsonDeserialize((array)$entry);
$list[] = $rule;
}
return $list;
}
/**
* Find rules by IP address
*/
public function findRulesByIp(string $tenantId, string $ipAddress): array
{
$filter = [
'tenantId' => $tenantId,
'type' => ['$in' => [FirewallRuleObject::TYPE_IP, FirewallRuleObject::TYPE_IP_RANGE]],
'enabled' => true,
'$or' => [
['expiresAt' => null],
['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]]
]
];
$cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter);
$list = [];
foreach ($cursor as $entry) {
$rule = (new FirewallRuleObject())->jsonDeserialize((array)$entry);
$list[] = $rule;
}
return $list;
}
/**
* Find rules by device fingerprint
*/
public function findRulesByDevice(string $tenantId, string $deviceFingerprint): array
{
$filter = [
'tenantId' => $tenantId,
'type' => FirewallRuleObject::TYPE_DEVICE,
'value' => $deviceFingerprint,
'enabled' => true,
'$or' => [
['expiresAt' => null],
['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]]
]
];
$cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter);
$list = [];
foreach ($cursor as $entry) {
$rule = (new FirewallRuleObject())->jsonDeserialize((array)$entry);
$list[] = $rule;
}
return $list;
}
/**
* Fetch a specific rule by ID
*/
public function fetchRule(string $id): ?FirewallRuleObject
{
$entry = $this->dataStore->selectCollection(self::RULES_COLLECTION)->findOne(['_id' => $id]);
if (!$entry) {
return null;
}
return (new FirewallRuleObject())->jsonDeserialize((array)$entry);
}
/**
* Check if exact IP rule exists
*/
public function findExactIpRule(string $tenantId, string $ipAddress, string $action): ?FirewallRuleObject
{
$entry = $this->dataStore->selectCollection(self::RULES_COLLECTION)->findOne([
'tenantId' => $tenantId,
'type' => FirewallRuleObject::TYPE_IP,
'value' => $ipAddress,
'action' => $action,
'enabled' => true,
]);
if (!$entry) {
return null;
}
return (new FirewallRuleObject())->jsonDeserialize((array)$entry);
}
/**
* Create or update a rule
*/
public function depositRule(FirewallRuleObject $rule): ?FirewallRuleObject
{
if ($rule->getId()) {
return $this->updateRule($rule);
} else {
return $this->createRule($rule);
}
}
private function createRule(FirewallRuleObject $rule): ?FirewallRuleObject
{
$data = $rule->jsonSerialize();
unset($data['id']); // Remove id for insert
$result = $this->dataStore->selectCollection(self::RULES_COLLECTION)->insertOne($data);
$rule->setId((string)$result->getInsertedId());
return $rule;
}
private function updateRule(FirewallRuleObject $rule): ?FirewallRuleObject
{
$id = $rule->getId();
if (!$id) {
return null;
}
$data = $rule->jsonSerialize();
unset($data['id']);
$this->dataStore->selectCollection(self::RULES_COLLECTION)->updateOne(
['_id' => $id],
['$set' => $data]
);
return $rule;
}
/**
* Delete a rule
*/
public function destroyRule(FirewallRuleObject $rule): void
{
$id = $rule->getId();
if (!$id) {
return;
}
$this->dataStore->selectCollection(self::RULES_COLLECTION)->deleteOne(['_id' => $id]);
}
/**
* Delete expired rules
*/
public function cleanupExpiredRules(): int
{
$result = $this->dataStore->selectCollection(self::RULES_COLLECTION)->deleteMany([
'expiresAt' => ['$lt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)],
'expiresAt' => ['$ne' => null]
]);
return $result->getDeletedCount();
}
// ========================================
// Log Operations
// ========================================
/**
* Log a firewall event
*/
public function createLog(FirewallLogObject $log): FirewallLogObject
{
$data = $log->jsonSerialize();
unset($data['id']);
$result = $this->dataStore->selectCollection(self::LOGS_COLLECTION)->insertOne($data);
$log->setId((string)$result->getInsertedId());
return $log;
}
/**
* Get logs for a tenant with optional filters
*/
public function listLogs(
string $tenantId,
?string $ipAddress = null,
?string $eventType = null,
?string $result = null,
int $limit = 100,
int $offset = 0
): array {
$filter = ['tenantId' => $tenantId];
if ($ipAddress !== null) {
$filter['ipAddress'] = $ipAddress;
}
if ($eventType !== null) {
$filter['eventType'] = $eventType;
}
if ($result !== null) {
$filter['result'] = $result;
}
$cursor = $this->dataStore->selectCollection(self::LOGS_COLLECTION)->find(
$filter,
[
'sort' => ['timestamp' => -1],
'limit' => $limit,
'skip' => $offset
]
);
$list = [];
foreach ($cursor as $entry) {
$log = (new FirewallLogObject())->jsonDeserialize((array)$entry);
$list[] = $log;
}
return $list;
}
/**
* Count recent failures from an IP within a time window
*/
public function countRecentFailures(
string $tenantId,
string $ipAddress,
int $windowSeconds = 300
): int {
$since = (new \DateTimeImmutable())->modify("-{$windowSeconds} seconds");
return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments([
'tenantId' => $tenantId,
'ipAddress' => $ipAddress,
'eventType' => FirewallLogObject::EVENT_AUTH_FAILURE,
'timestamp' => ['$gte' => $since->format(\DateTimeInterface::ATOM)]
]);
}
/**
* Get blocked requests count for dashboard
*/
public function countBlockedRequests(
string $tenantId,
?\DateTimeImmutable $since = null
): int {
$filter = [
'tenantId' => $tenantId,
'result' => FirewallLogObject::RESULT_BLOCKED
];
if ($since !== null) {
$filter['timestamp'] = ['$gte' => $since->format(\DateTimeInterface::ATOM)];
}
return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments($filter);
}
/**
* Clean up old logs
*/
public function cleanupOldLogs(int $daysToKeep = 30): int
{
$cutoff = (new \DateTimeImmutable())->modify("-{$daysToKeep} days");
$result = $this->dataStore->selectCollection(self::LOGS_COLLECTION)->deleteMany([
'timestamp' => ['$lt' => $cutoff->format(\DateTimeInterface::ATOM)]
]);
return $result->getDeletedCount();
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace KTXC\Stores;
use KTXC\Db\DataStore;
use KTXC\Models\Tenant\TenantObject;
class TenantStore
{
protected const COLLECTION_NAME = 'tenants';
public function __construct(
protected readonly DataStore $dataStore
) { }
public function list(): array
{
$cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find();
$list = [];
foreach ($cursor as $entry) {
$entry = (new TenantObject())->jsonDeserialize((array)$entry);
$list[$entry->getId()] = $entry;
}
return $list;
}
public function fetch(string $identifier): ?TenantObject
{
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(['identifier' => $identifier]);
if (!$entry) { return null; }
return (new TenantObject())->jsonDeserialize((array)$entry);
}
public function fetchByDomain(string $domain): ?TenantObject
{
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(['domains' => $domain]);
if (!$entry) { return null; }
$entity = new TenantObject();
$entity->jsonDeserialize((array)$entry);
return $entity;
}
public function deposit(TenantObject $entry): ?TenantObject
{
if ($entry->getId()) {
return $this->update($entry);
} else {
return $this->create($entry);
}
}
private function create(TenantObject $entry): ?TenantObject
{
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->insertOne($entry->jsonSerialize());
$entry->setId((string)$result->getInsertedId());
return $entry;
}
private function update(TenantObject $entry): ?TenantObject
{
$id = $entry->getId();
if (!$id) { return null; }
$this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(['_id' => $id], ['$set' => $entry->jsonSerialize()]);
return $entry;
}
public function destroy(TenantObject $entry): void
{
$id = $entry->getId();
if (!$id) { return; }
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne([ '_id' => $id]);
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace KTXC\Stores;
use KTXC\Db\DataStore;
use KTXF\Utile\UUID;
class UserAccountsStore
{
public function __construct(protected DataStore $store)
{ }
// =========================================================================
// User Operations (Full User Object)
// =========================================================================
/**
* List all users for a tenant with optional filters
*/
public function listUsers(string $tenant, array $filters = []): array
{
// Build filter
$filter = ['tid' => $tenant];
if (isset($filters['enabled'])) {
$filter['enabled'] = (bool)$filters['enabled'];
}
if (isset($filters['role'])) {
$filter['roles'] = $filters['role'];
}
// Fetch users with aggregated role data
$pipeline = [
['$match' => $filter],
[
'$lookup' => [
'from' => 'user_roles',
'localField' => 'roles',
'foreignField' => 'rid',
'as' => 'role_details'
]
],
[
'$addFields' => [
'permissions' => [
'$reduce' => [
'input' => [
'$map' => [
'input' => '$role_details',
'as' => 'r',
'in' => ['$ifNull' => ['$$r.permissions', []]]
]
],
'initialValue' => [],
'in' => ['$setUnion' => ['$$value', '$$this']]
]
]
]
],
['$unset' => 'role_details'],
['$sort' => ['label' => 1]]
];
$cursor = $this->store->selectCollection('user_accounts')->aggregate($pipeline);
$users = [];
foreach ($cursor as $entry) {
$users[] = (array)$entry;
}
return $users;
}
public function fetchByIdentity(string $tenant, string $identity): array | null
{
$pipeline = [
[
'$match' => [
'tid' => $tenant,
'identity' => $identity
]
],
[
'$lookup' => [
'from' => 'user_roles',
'localField' => 'roles', // Array field in `users`
'foreignField' => 'rid', // Scalar field in `user_roles`
'as' => 'role_details'
]
],
// Add flattened, deduplicated permissions while preserving all original user fields
[
'$addFields' => [
'permissions' => [
'$reduce' => [
'input' => [
'$map' => [
'input' => '$role_details',
'as' => 'r',
'in' => [ '$ifNull' => ['$$r.permissions', []] ]
]
],
'initialValue' => [],
'in' => [ '$setUnion' => ['$$value', '$$this'] ]
]
]
]
],
// Optionally remove expanded role documents from output
[ '$unset' => 'role_details' ]
];
$entry = $this->store->selectCollection('user_accounts')->aggregate($pipeline)->toArray()[0] ?? null;
if (!$entry) { return null; }
return (array)$entry;
}
public function fetchByIdentifier(string $tenant, string $identifier): array | null
{
$pipeline = [
[
'$match' => [
'tid' => $tenant,
'uid' => $identifier
]
],
[
'$lookup' => [
'from' => 'user_roles',
'localField' => 'roles',
'foreignField' => 'rid',
'as' => 'role_details'
]
],
[
'$addFields' => [
'permissions' => [
'$reduce' => [
'input' => [
'$map' => [
'input' => '$role_details',
'as' => 'r',
'in' => [ '$ifNull' => ['$$r.permissions', []] ]
]
],
'initialValue' => [],
'in' => [ '$setUnion' => ['$$value', '$$this'] ]
]
]
]
],
[ '$unset' => 'role_details' ]
];
$entry = $this->store->selectCollection('user_accounts')->aggregate($pipeline)->toArray()[0] ?? null;
if (!$entry) { return null; }
return (array)$entry;
}
public function fetchByProviderSubject(string $tenant, string $provider, string $subject): array | null
{
$entry = $this->store->selectCollection('user_accounts')->findOne([
'tid' => $tenant,
'provider' => $provider,
'provider_subject' => $subject
]);
if (!$entry) { return null; }
return (array)$entry;
}
public function createUser(string $tenant, array $userData): array
{
$userData['tid'] = $tenant;
$userData['uid'] = $userData['uid'] ?? UUID::v4();
$userData['enabled'] = $userData['enabled'] ?? true;
$userData['roles'] = $userData['roles'] ?? [];
$userData['profile'] = $userData['profile'] ?? [];
$userData['settings'] = $userData['settings'] ?? [];
$this->store->selectCollection('user_accounts')->insertOne($userData);
return $this->fetchByIdentifier($tenant, $userData['uid']);
}
public function updateUser(string $tenant, string $uid, array $updates): bool
{
$result = $this->store->selectCollection('user_accounts')->updateOne(
['tid' => $tenant, 'uid' => $uid],
['$set' => $updates]
);
return $result->getModifiedCount() > 0;
}
public function deleteUser(string $tenant, string $uid): bool
{
$result = $this->store->selectCollection('user_accounts')->deleteOne([
'tid' => $tenant,
'uid' => $uid
]);
return $result->getDeletedCount() > 0;
}
// =========================================================================
// Profile Operations
// =========================================================================
public function fetchProfile(string $tenant, string $uid): ?array
{
$user = $this->store->selectCollection('user_accounts')->findOne(
['tid' => $tenant, 'uid' => $uid],
['projection' => ['profile' => 1, 'provider_managed_fields' => 1]]
);
if (!$user) {
return null;
}
return [
'profile' => $user['profile'] ?? [],
'provider_managed_fields' => $user['provider_managed_fields'] ?? [],
];
}
public function storeProfile(string $tenant, string $uid, array $profileFields): bool
{
if (empty($profileFields)) {
return false;
}
$updates = [];
foreach ($profileFields as $key => $value) {
$updates["profile.{$key}"] = $value;
}
$result = $this->store->selectCollection('user_accounts')->updateOne(
['tid' => $tenant, 'uid' => $uid],
['$set' => $updates]
);
return $result->getModifiedCount() > 0;
}
// =========================================================================
// Settings Operations
// =========================================================================
public function fetchSettings(string $tenant, string $uid, array $settings = []): ?array
{
// Only fetch the settings field from the database
$user = $this->store->selectCollection('user_accounts')->findOne(
['tid' => $tenant, 'uid' => $uid],
['projection' => ['settings' => 1]]
);
if (!$user) {
return null;
}
$userSettings = $user['settings'] ?? [];
if (empty($settings)) {
return $userSettings;
}
$result = [];
foreach ($settings as $key) {
$result[$key] = $userSettings[$key] ?? null;
}
return $result;
}
public function storeSettings(string $tenant, string $uid, array $settings): bool
{
if (empty($settings)) {
return false;
}
$updates = [];
foreach ($settings as $key => $value) {
$updates["settings.{$key}"] = $value;
}
$result = $this->store->selectCollection('user_accounts')->updateOne(
['tid' => $tenant, 'uid' => $uid],
['$set' => $updates]
);
// Return true if document was matched (exists), even if not modified
return $result->getMatchedCount() > 0;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace KTXC\Stores;
use KTXC\Db\DataStore;
use KTXC\Module\ModuleManager;
use KTXF\Utile\UUID;
/**
* Role Store - Database operations for user roles
*/
class UserRolesStore
{
protected const COLLECTION_NAME = 'user_roles';
public function __construct(
protected readonly DataStore $store,
protected readonly ModuleManager $moduleManager
) {}
// =========================================================================
// Role Operations
// =========================================================================
/**
* List all roles for a tenant
*/
public function listRoles(string $tenant): array
{
$cursor = $this->store->selectCollection(self::COLLECTION_NAME)->find(
['tid' => $tenant],
['sort' => ['label' => 1]]
);
$roles = [];
foreach ($cursor as $entry) {
$role = (array)$entry;
// Ensure permissions is an array
if (isset($role['permissions'])) {
$role['permissions'] = (array)$role['permissions'];
}
$roles[] = $role;
}
return $roles;
}
/**
* Fetch role by tenant and role ID
*/
public function fetchByRid(string $tenant, string $rid): ?array
{
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
'tid' => $tenant,
'rid' => $rid
]);
if (!$entry) {
return null;
}
return (array)$entry;
}
/**
* Create a new role
*/
public function createRole(string $tenant, array $roleData): array
{
$roleData['tid'] = $tenant;
$roleData['rid'] = $roleData['rid'] ?? UUID::v4();
$roleData['label'] = $roleData['label'] ?? 'Unnamed Role';
$roleData['description'] = $roleData['description'] ?? '';
$roleData['permissions'] = $roleData['permissions'] ?? [];
$roleData['system'] = $roleData['system'] ?? false;
$this->store->selectCollection(self::COLLECTION_NAME)->insertOne($roleData);
return $this->fetchByRid($tenant, $roleData['rid']);
}
/**
* Update an existing role
*/
public function updateRole(string $tenant, string $rid, array $updates): bool
{
// Prevent updating system flag
unset($updates['tid'], $updates['rid'], $updates['system']);
if (empty($updates)) {
return false;
}
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
['tid' => $tenant, 'rid' => $rid],
['$set' => $updates]
);
return $result->getModifiedCount() > 0;
}
/**
* Delete a role
*/
public function deleteRole(string $tenant, string $rid): bool
{
// Check if role is system role
$role = $this->fetchByRid($tenant, $rid);
if (!$role || ($role['system'] ?? false)) {
return false; // Cannot delete system roles
}
$result = $this->store->selectCollection(self::COLLECTION_NAME)->deleteOne([
'tid' => $tenant,
'rid' => $rid
]);
return $result->getDeletedCount() > 0;
}
/**
* Count users assigned to a role
*/
public function countUsersInRole(string $tenant, string $rid): int
{
$count = $this->store->selectCollection('user_accounts')->countDocuments([
'tid' => $tenant,
'roles' => $rid
]);
return (int)$count;
}
/**
* Get all available permissions from modules
* Grouped by category with metadata
*/
public function availablePermissions(): array
{
return $this->moduleManager->availablePermissions();
}
}