commit 362a4f0c8e73dd6e833e3b3b2b90adfdc4c1e2c3 Author: Anton Afanasyeu Date: Tue Jun 23 12:29:31 2026 +0200 initial diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cdc06b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ac-ms-rbac + +Microservice API. Remote: `git://f0xx.org/ac/ac-ms-rbac` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ad5a0d7 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "androidcast/ms-rbac", + "description": "Companies and privileges", + "type": "project", + "require": { + "php": ">=8.1", + "androidcast/platform-php": "dev-next", + "androidcast/platform-db": "dev-next" + }, + "repositories": [ + { + "type": "vcs", + "url": "git://f0xx.org/ac/ac-platform-php" + }, + { + "type": "vcs", + "url": "git://f0xx.org/ac/ac-platform-db" + } + ], + "autoload": { + "classmap": [ + "src/" + ] + } +} diff --git a/config/config.example.php b/config/config.example.php new file mode 100644 index 0000000..9a59c16 --- /dev/null +++ b/config/config.example.php @@ -0,0 +1,2 @@ + ['driver' => 'sqlite']]; diff --git a/public/api/rbac.php b/public/api/rbac.php new file mode 100644 index 0000000..b2ce844 --- /dev/null +++ b/public/api/rbac.php @@ -0,0 +1,70 @@ + false, 'error' => 'forbidden'], 403); +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +$action = trim((string) ($_GET['action'] ?? '')); + +if ($method === 'GET' && $action === 'panel') { + $panel = RbacAdminRepository::buildPanel(); + json_out($panel, empty($panel['ok']) ? 403 : 200); +} + +if ($method !== 'POST') { + json_out(['ok' => false, 'error' => 'method_not_allowed'], 405); +} + +$raw = file_get_contents('php://input') ?: ''; +$body = json_decode($raw, true); +if (!is_array($body)) { + json_out(['ok' => false, 'error' => 'invalid_json'], 400); +} + +if ($action === 'set_global_role') { + $result = RbacAdminRepository::setGlobalRole( + (int) ($body['user_id'] ?? 0), + (string) ($body['role'] ?? '') + ); + json_out($result, empty($result['ok']) ? 400 : 200); +} + +if ($action === 'set_company_role') { + $result = RbacAdminRepository::setCompanyRole( + (int) ($body['user_id'] ?? 0), + (int) ($body['company_id'] ?? 0), + (string) ($body['role'] ?? '') + ); + json_out($result, empty($result['ok']) ? 400 : 200); +} + +if ($action === 'apply_privilege_set') { + $result = RbacAdminRepository::applyPrivilegeSet( + (int) ($body['user_id'] ?? 0), + (int) ($body['company_id'] ?? 0), + (string) ($body['privilege_set'] ?? '') + ); + json_out($result, empty($result['ok']) ? 400 : 200); +} + +if ($action === 'clear_auth_lockouts') { + if (!Rbac::isRoot() && !Rbac::isGlobalAdmin()) { + json_out(['ok' => false, 'error' => 'forbidden'], 403); + } + $userId = (int) ($body['user_id'] ?? 0); + if ($userId <= 0) { + json_out(['ok' => false, 'error' => 'invalid_user'], 400); + } + $deleted = AuthAttempts::clearForUser($userId); + json_out(['ok' => true, 'cleared' => $deleted]); +} + +json_out(['ok' => false, 'error' => 'unknown_action'], 400); diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..922b7c2 --- /dev/null +++ b/public/index.php @@ -0,0 +1,2 @@ + */ + private const GLOBAL_ADMIN_ROLES = [self::GLOBAL_ROOT, self::GLOBAL_PLATFORM_ADMIN, self::GLOBAL_ADMIN_LEGACY]; + + /** Company role => granted actions (unless global admin). */ + /** + * Named privilege sets stored in company_memberships.permissions_json as {"grants":[...]}. + * Company role defaults still apply; grants extend (not replace) role actions. + */ + public const PRIVILEGE_SET_NONE = ''; + public const PRIVILEGE_SET_REMOTE_ACCESS_USER = 'remote_access_user'; + public const PRIVILEGE_SET_REMOTE_ACCESS_ADMIN = 'remote_access_admin'; + public const PRIVILEGE_SET_SHORT_LINKS_VIEWER = 'short_links_viewer'; + public const PRIVILEGE_SET_SHORT_LINKS_USER = 'short_links_user'; + + /** @var array> */ + public const PRIVILEGE_SETS = [ + self::PRIVILEGE_SET_REMOTE_ACCESS_USER => [ + 'remote_access_view', + 'remote_access_operate', + ], + self::PRIVILEGE_SET_REMOTE_ACCESS_ADMIN => [ + 'remote_access_view', + 'remote_access_operate', + 'remote_access_admin', + ], + self::PRIVILEGE_SET_SHORT_LINKS_VIEWER => [ + 'short_links_view', + ], + self::PRIVILEGE_SET_SHORT_LINKS_USER => [ + 'short_links_view', + 'short_links_operate', + ], + ]; + + private const COMPANY_ROLE_ACTIONS = [ + self::COMPANY_OWNER => [ + 'view_reports', + 'edit_tags', + 'upload_reports', + 'manage_devices', + 'manage_company_users', + 'manage_company_settings', + 'manage_self', + 'remote_access_view', + 'remote_access_operate', + 'remote_access_admin', + 'short_links_view', + 'short_links_operate', + ], + self::COMPANY_ADMIN => [ + 'view_reports', + 'edit_tags', + 'upload_reports', + 'manage_devices', + 'manage_company_users', + 'manage_self', + 'remote_access_view', + 'remote_access_operate', + 'remote_access_admin', + 'short_links_view', + 'short_links_operate', + ], + self::COMPANY_MEMBER => [ + 'view_reports', + 'manage_self', + 'remote_access_view', + ], + ]; + + public static function normalizeGlobalRole(string $role): string { + $role = strtolower(trim($role)); + if ($role === self::GLOBAL_ADMIN_LEGACY) { + return self::GLOBAL_PLATFORM_ADMIN; + } + return $role; + } + + public static function isGlobalAdmin(?array $user = null): bool { + $user ??= Auth::user(); + if (!$user) { + return false; + } + $role = self::normalizeGlobalRole((string) ($user['role'] ?? '')); + return in_array($role, self::GLOBAL_ADMIN_ROLES, true); + } + + /** @return list|null null = all companies (global admin) */ + public static function allowedCompanyIds(?array $user = null): ?array { + if (self::isGlobalAdmin($user)) { + return null; + } + $user ??= Auth::user(); + if (!$user) { + return []; + } + $ids = []; + foreach ($user['companies'] ?? [] as $c) { + if (isset($c['id'])) { + $ids[] = (int) $c['id']; + } + } + return array_values(array_unique($ids)); + } + + public static function activeCompanyId(?array $user = null): ?int { + $user ??= Auth::user(); + if (!$user) { + return null; + } + $id = (int) ($user['active_company_id'] ?? 0); + return $id > 0 ? $id : null; + } + + public static function companyRole(int $userId, int $companyId): ?string { + $stmt = Database::pdo()->prepare( + 'SELECT role FROM company_memberships WHERE user_id = ? AND company_id = ? LIMIT 1' + ); + $stmt->execute([$userId, $companyId]); + $role = $stmt->fetchColumn(); + return is_string($role) && $role !== '' ? $role : null; + } + + public static function can(string $action, ?int $companyId = null, ?array $user = null): bool { + $user ??= Auth::user(); + if (!$user) { + return false; + } + if (self::isGlobalAdmin($user)) { + return true; + } + if ($action === 'manage_platform') { + return false; + } + if ($action === 'manage_self') { + return true; + } + $companyId ??= self::activeCompanyId($user); + if ($companyId === null || $companyId <= 0) { + return false; + } + $role = self::membershipRoleForUser($user, $companyId); + if ($role === null) { + return false; + } + $grants = self::COMPANY_ROLE_ACTIONS[$role] ?? []; + if (in_array($action, $grants, true)) { + return true; + } + return self::hasPermissionOverride($user, $companyId, $action); + } + + public static function canAccessCompany(int $companyId, ?array $user = null): bool { + $allowed = self::allowedCompanyIds($user); + if ($allowed === null) { + return true; + } + return in_array($companyId, $allowed, true); + } + + public static function canAccessReport(array $report, ?array $user = null): bool { + $companyId = (int) ($report['company_id'] ?? 0); + if ($companyId <= 0) { + return self::isGlobalAdmin($user); + } + return self::canAccessCompany($companyId, $user); + } + + public static function defaultCompanyId(): int { + static $cached = 0; + if ($cached > 0) { + return $cached; + } + $fromCfg = (int) cfg('rbac.default_company_id', 0); + if ($fromCfg > 0) { + $cached = $fromCfg; + return $cached; + } + $slug = (string) cfg('rbac.default_company_slug', 'default'); + $pdo = Database::pdo(); + $stmt = $pdo->prepare('SELECT id FROM companies WHERE slug = ? LIMIT 1'); + $stmt->execute([$slug]); + $id = (int) $stmt->fetchColumn(); + if ($id > 0) { + $cached = $id; + return $cached; + } + $cached = 1; + return $cached; + } + + /** + * Append company scope to WHERE fragments. + * + * @param list $where + * @param list $params + */ + public static function appendCompanyScope(string $column, array &$where, array &$params, ?array $user = null): void { + $allowed = self::allowedCompanyIds($user); + if ($allowed === null) { + return; + } + if ($allowed === []) { + $where[] = '0=1'; + return; + } + $placeholders = implode(',', array_fill(0, count($allowed), '?')); + $where[] = "$column IN ($placeholders)"; + foreach ($allowed as $id) { + $params[] = $id; + } + } + + /** Match fingerprint counts within the same company. */ + public static function fingerprintCountSql(string $rAlias = 'r', string $r2Alias = 'r2'): string { + return " AND $r2Alias.company_id = $rAlias.company_id"; + } + + /** + * @return list + */ + public static function fetchMemberships(int $userId): array { + $stmt = Database::pdo()->prepare( + 'SELECT c.id, c.slug, c.name, m.role + FROM company_memberships m + INNER JOIN companies c ON c.id = m.company_id + WHERE m.user_id = ? + ORDER BY c.name ASC' + ); + $stmt->execute([$userId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + $out = []; + foreach ($rows as $row) { + $out[] = [ + 'id' => (int) $row['id'], + 'slug' => (string) $row['slug'], + 'name' => (string) $row['name'], + 'role' => (string) $row['role'], + ]; + } + return $out; + } + + /** + * @param list $memberships + */ + public static function pickActiveCompanyId(array $memberships): int { + if ($memberships === []) { + return self::defaultCompanyId(); + } + return (int) $memberships[0]['id']; + } + + /** @return array session user payload */ + public static function buildSessionUser(array $dbUser): array { + $uid = (int) $dbUser['id']; + $memberships = self::fetchMemberships($uid); + return [ + 'id' => $uid, + 'username' => (string) $dbUser['username'], + 'role' => self::normalizeGlobalRole((string) ($dbUser['role'] ?? self::GLOBAL_VIEWER)), + 'companies' => $memberships, + 'active_company_id' => self::pickActiveCompanyId($memberships), + ]; + } + + public static function seedMembershipsForUser(int $userId, string $globalRole): void { + $pdo = Database::pdo(); + $companyId = self::defaultCompanyId(); + $globalRole = self::normalizeGlobalRole($globalRole); + $companyRole = self::COMPANY_MEMBER; + if (in_array($globalRole, self::GLOBAL_ADMIN_ROLES, true)) { + $companyRole = self::COMPANY_OWNER; + } + if (Database::isMysql()) { + $sql = 'INSERT IGNORE INTO company_memberships (user_id, company_id, role) VALUES (?, ?, ?)'; + } else { + $sql = 'INSERT OR IGNORE INTO company_memberships (user_id, company_id, role) VALUES (?, ?, ?)'; + } + $stmt = $pdo->prepare($sql); + $stmt->execute([$userId, $companyId, $companyRole]); + } + + private static function membershipRoleForUser(array $user, int $companyId): ?string { + foreach ($user['companies'] ?? [] as $c) { + if ((int) ($c['id'] ?? 0) === $companyId) { + return (string) ($c['role'] ?? ''); + } + } + return null; + } + + /** @return list */ + public static function privilegeSetKeys(): array { + return array_keys(self::PRIVILEGE_SETS); + } + + /** @return list */ + public static function grantsForPrivilegeSet(string $setKey): array { + $setKey = trim($setKey); + if ($setKey === '' || $setKey === self::PRIVILEGE_SET_NONE) { + return []; + } + return self::PRIVILEGE_SETS[$setKey] ?? []; + } + + public static function encodePermissionsJson(?string $privilegeSetKey): ?string { + $grants = self::grantsForPrivilegeSet((string) $privilegeSetKey); + if ($grants === []) { + return null; + } + return json_encode(['grants' => $grants], JSON_UNESCAPED_UNICODE); + } + + public static function privilegeSetFromPermissionsJson(?string $raw): string { + if (!is_string($raw) || trim($raw) === '') { + return self::PRIVILEGE_SET_NONE; + } + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + return self::PRIVILEGE_SET_NONE; + } + $grants = $decoded['grants'] ?? $decoded; + if (!is_array($grants)) { + return self::PRIVILEGE_SET_NONE; + } + sort($grants); + foreach (self::PRIVILEGE_SETS as $key => $want) { + $sorted = $want; + sort($sorted); + if ($grants === $sorted) { + return $key; + } + } + return 'custom'; + } + + public static function isRoot(?array $user = null): bool { + $user ??= Auth::user(); + if (!$user) { + return false; + } + return self::normalizeGlobalRole((string) ($user['role'] ?? '')) === self::GLOBAL_ROOT; + } + + public static function canManageRbac(?array $user = null): bool { + $user ??= Auth::user(); + if (!$user) { + return false; + } + if (self::isGlobalAdmin($user)) { + return true; + } + foreach ($user['companies'] ?? [] as $c) { + $role = (string) ($c['role'] ?? ''); + if ($role === self::COMPANY_OWNER || $role === self::COMPANY_ADMIN) { + return true; + } + } + return false; + } + + private static function hasPermissionOverride(array $user, int $companyId, string $action): bool { + $pdo = Database::pdo(); + $stmt = $pdo->prepare( + 'SELECT permissions_json FROM company_memberships WHERE user_id = ? AND company_id = ? LIMIT 1' + ); + $stmt->execute([(int) $user['id'], $companyId]); + $raw = $stmt->fetchColumn(); + if (!is_string($raw) || $raw === '') { + return false; + } + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + return false; + } + $grants = $decoded['grants'] ?? $decoded; + if (!is_array($grants)) { + return false; + } + return in_array($action, $grants, true); + } +} diff --git a/src/RbacAdminRepository.php b/src/RbacAdminRepository.php new file mode 100644 index 0000000..d6d8dc8 --- /dev/null +++ b/src/RbacAdminRepository.php @@ -0,0 +1,252 @@ + false, 'error' => 'forbidden']; + } + $actorId = (int) ($actor['id'] ?? 0); + $globalAdmin = Rbac::isGlobalAdmin($actor); + $companies = self::listCompaniesForActor($actor); + $companyIds = array_map(static fn($c) => (int) $c['id'], $companies); + + return [ + 'ok' => true, + 'actor' => [ + 'id' => $actorId, + 'username' => (string) ($actor['username'] ?? ''), + 'role' => (string) ($actor['role'] ?? ''), + 'is_root' => Rbac::isRoot($actor), + 'is_global_admin' => $globalAdmin, + ], + 'companies' => $companies, + 'users' => self::listUsersWithMemberships($companyIds, $globalAdmin), + 'global_roles' => [ + Rbac::GLOBAL_ROOT, + Rbac::GLOBAL_PLATFORM_ADMIN, + Rbac::GLOBAL_VIEWER, + ], + 'company_roles' => [ + Rbac::COMPANY_OWNER, + Rbac::COMPANY_ADMIN, + Rbac::COMPANY_MEMBER, + ], + 'privilege_sets' => array_map( + static fn($key) => [ + 'key' => $key, + 'label' => str_replace('_', ' ', $key), + 'grants' => Rbac::grantsForPrivilegeSet($key), + ], + Rbac::privilegeSetKeys() + ), + 'permissions' => [ + 'can_edit_global_roles' => Rbac::isRoot($actor), + 'can_manage_platform' => Rbac::can('manage_platform', null, $actor), + 'can_manage_company_users' => $globalAdmin, + ], + ]; + } + + public static function setGlobalRole(int $targetUserId, string $role, ?array $actor = null): array { + $actor ??= Auth::user(); + if (!$actor || !Rbac::isRoot($actor)) { + return ['ok' => false, 'error' => 'forbidden']; + } + $role = Rbac::normalizeGlobalRole($role); + $allowed = [Rbac::GLOBAL_ROOT, Rbac::GLOBAL_PLATFORM_ADMIN, Rbac::GLOBAL_VIEWER]; + if (!in_array($role, $allowed, true)) { + return ['ok' => false, 'error' => 'invalid_role']; + } + if ($targetUserId <= 0) { + return ['ok' => false, 'error' => 'invalid_user']; + } + $pdo = Database::pdo(); + $stmt = $pdo->prepare('UPDATE users SET role = ? WHERE id = ?'); + $stmt->execute([$role, $targetUserId]); + if ($stmt->rowCount() < 1) { + return ['ok' => false, 'error' => 'user_not_found']; + } + if (in_array($role, [Rbac::GLOBAL_ROOT, Rbac::GLOBAL_PLATFORM_ADMIN], true)) { + Rbac::seedMembershipsForUser($targetUserId, $role); + } + return ['ok' => true]; + } + + public static function setCompanyRole(int $targetUserId, int $companyId, string $role, ?array $actor = null): array { + $actor ??= Auth::user(); + if (!$actor || !self::canEditMembership($actor, $companyId)) { + return ['ok' => false, 'error' => 'forbidden']; + } + $allowed = [Rbac::COMPANY_OWNER, Rbac::COMPANY_ADMIN, Rbac::COMPANY_MEMBER]; + if (!in_array($role, $allowed, true)) { + return ['ok' => false, 'error' => 'invalid_role']; + } + if ($targetUserId <= 0 || $companyId <= 0) { + return ['ok' => false, 'error' => 'invalid_input']; + } + $pdo = Database::pdo(); + if (Database::isMysql()) { + $sql = 'INSERT INTO company_memberships (user_id, company_id, role) VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE role = VALUES(role)'; + } else { + $sql = 'INSERT INTO company_memberships (user_id, company_id, role) VALUES (?, ?, ?) + ON CONFLICT(user_id, company_id) DO UPDATE SET role = excluded.role'; + } + $stmt = $pdo->prepare($sql); + $stmt->execute([$targetUserId, $companyId, $role]); + return ['ok' => true]; + } + + public static function applyPrivilegeSet(int $targetUserId, int $companyId, string $setKey, ?array $actor = null): array { + $actor ??= Auth::user(); + if (!$actor || !self::canEditMembership($actor, $companyId)) { + return ['ok' => false, 'error' => 'forbidden']; + } + if ($targetUserId <= 0 || $companyId <= 0) { + return ['ok' => false, 'error' => 'invalid_input']; + } + $setKey = trim($setKey); + if ($setKey !== '' && $setKey !== Rbac::PRIVILEGE_SET_NONE && !isset(Rbac::PRIVILEGE_SETS[$setKey])) { + return ['ok' => false, 'error' => 'invalid_privilege_set']; + } + $json = Rbac::encodePermissionsJson($setKey === Rbac::PRIVILEGE_SET_NONE ? '' : $setKey); + $pdo = Database::pdo(); + $stmt = $pdo->prepare( + 'UPDATE company_memberships SET permissions_json = ? WHERE user_id = ? AND company_id = ?' + ); + $stmt->execute([$json, $targetUserId, $companyId]); + if ($stmt->rowCount() < 1) { + return ['ok' => false, 'error' => 'membership_not_found']; + } + return ['ok' => true]; + } + + /** @return list */ + private static function listCompaniesForActor(array $actor): array { + $pdo = Database::pdo(); + $allowed = Rbac::allowedCompanyIds($actor); + if ($allowed === null) { + $stmt = $pdo->query('SELECT id, slug, name FROM companies ORDER BY name ASC'); + return self::mapCompanies($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + if ($allowed === []) { + return []; + } + $ph = implode(',', array_fill(0, count($allowed), '?')); + $stmt = $pdo->prepare("SELECT id, slug, name FROM companies WHERE id IN ($ph) ORDER BY name ASC"); + $stmt->execute($allowed); + return self::mapCompanies($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + + /** @param list> $rows @return list */ + private static function mapCompanies(array $rows): array { + $out = []; + foreach ($rows as $row) { + $out[] = [ + 'id' => (int) $row['id'], + 'slug' => (string) $row['slug'], + 'name' => (string) $row['name'], + ]; + } + return $out; + } + + /** + * @param list $companyIds + * @return list> + */ + private static function listUsersWithMemberships(array $companyIds, bool $globalAdmin): array { + $pdo = Database::pdo(); + if ($globalAdmin) { + $users = $pdo->query('SELECT id, username, role FROM users ORDER BY username ASC')->fetchAll(PDO::FETCH_ASSOC); + } else { + if ($companyIds === []) { + return []; + } + $ph = implode(',', array_fill(0, count($companyIds), '?')); + $stmt = $pdo->prepare( + "SELECT DISTINCT u.id, u.username, u.role + FROM users u + INNER JOIN company_memberships m ON m.user_id = u.id + WHERE m.company_id IN ($ph) + ORDER BY u.username ASC" + ); + $stmt->execute($companyIds); + $users = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + $out = []; + foreach ($users as $u) { + $uid = (int) $u['id']; + $memberships = self::membershipsForUser($uid, $companyIds, $globalAdmin); + $out[] = [ + 'id' => $uid, + 'username' => (string) $u['username'], + 'global_role' => Rbac::normalizeGlobalRole((string) ($u['role'] ?? '')), + 'memberships' => $memberships, + 'auth_failures' => AuthAttempts::recentFailureCount((string) $u['username']), + ]; + } + return $out; + } + + /** + * @param list $companyIds + * @return list> + */ + private static function membershipsForUser(int $userId, array $companyIds, bool $globalAdmin): array { + $pdo = Database::pdo(); + if ($globalAdmin) { + $stmt = $pdo->prepare( + 'SELECT m.company_id, c.slug, c.name, m.role, m.permissions_json + FROM company_memberships m + INNER JOIN companies c ON c.id = m.company_id + WHERE m.user_id = ? + ORDER BY c.name ASC' + ); + $stmt->execute([$userId]); + } else { + if ($companyIds === []) { + return []; + } + $ph = implode(',', array_fill(0, count($companyIds), '?')); + $params = array_merge([$userId], $companyIds); + $stmt = $pdo->prepare( + "SELECT m.company_id, c.slug, c.name, m.role, m.permissions_json + FROM company_memberships m + INNER JOIN companies c ON c.id = m.company_id + WHERE m.user_id = ? AND m.company_id IN ($ph) + ORDER BY c.name ASC" + ); + $stmt->execute($params); + } + $out = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $raw = isset($row['permissions_json']) ? (string) $row['permissions_json'] : ''; + $out[] = [ + 'company_id' => (int) $row['company_id'], + 'slug' => (string) $row['slug'], + 'name' => (string) $row['name'], + 'role' => (string) $row['role'], + 'privilege_set' => Rbac::privilegeSetFromPermissionsJson($raw), + ]; + } + return $out; + } + + private static function canEditMembership(array $actor, int $companyId): bool { + if (Rbac::isGlobalAdmin($actor)) { + return true; + } + if (!Rbac::canAccessCompany($companyId, $actor)) { + return false; + } + $uid = (int) ($actor['id'] ?? 0); + $role = $uid > 0 ? Rbac::companyRole($uid, $companyId) : null; + return $role === Rbac::COMPANY_OWNER || $role === Rbac::COMPANY_ADMIN; + } +}