From 1209963636742de4817a55fd3fe2eb55ed080a5b Mon Sep 17 00:00:00 2001 From: Arthur de Moulins Date: Tue, 26 Sep 2023 19:58:48 +0200 Subject: [PATCH] add phrasea-admin to phrasea realm --- .env | 3 ++ .../src/Command/Migration20230807Command.php | 7 ++- .../Vendor/Keycloak/KeycloakConfigurator.php | 26 ++++++++-- .../Vendor/Keycloak/KeycloakInterface.php | 9 ++-- .../Vendor/Keycloak/KeycloakManager.php | 47 +++++++++++++++++++ docker-compose.dev.yml | 2 + docker-compose.yml | 3 ++ .../DependencyInjection/Configuration.php | 2 +- .../Resources/config/services.yaml | 2 +- lib/php/auth-bundle/Security/RoleMapper.php | 5 +- 10 files changed, 87 insertions(+), 19 deletions(-) diff --git a/.env b/.env index fbd588cd8..53a82ee28 100644 --- a/.env +++ b/.env @@ -191,6 +191,9 @@ KEYCLOAK_DB_NAME=keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=__CHANGE_ME_rTLDzTAFiFIQiHDm +DEFAULT_ADMIN_USERNAME=phrasea-admin +DEFAULT_ADMIN_PASSWORD=__CHANGE_ME_CAZ7B1ZP4os2kZPL + # Keycloak2 KEYCLOAK2_DB_NAME=keycloak2 KEYCLOAK2_ADMIN=admin2 diff --git a/configurator/src/Command/Migration20230807Command.php b/configurator/src/Command/Migration20230807Command.php index 14b2aa643..dcd38b5ef 100644 --- a/configurator/src/Command/Migration20230807Command.php +++ b/configurator/src/Command/Migration20230807Command.php @@ -121,12 +121,11 @@ public function execute(InputInterface $input, OutputInterface $output): int foreach ($roles as $role) { $realmRoles = array_merge($realmRoles, match ($role) { 'ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_CHUCK-NORRIS' => [ - KeycloakInterface::GROUP_ADMIN, - KeycloakInterface::GROUP_SUPER_ADMIN, + KeycloakInterface::ROLE_ADMIN, ], - 'ROLE_TECH' => [KeycloakInterface::GROUP_TECH], + 'ROLE_TECH' => [KeycloakInterface::ROLE_TECH], 'ROLE_ADMIN_OAUTH_CLIENTS', - 'ROLE_ADMIN_USERS' => [KeycloakInterface::GROUP_USER_ADMIN, KeycloakInterface::GROUP_GROUP_ADMIN], + 'ROLE_ADMIN_USERS' => [KeycloakInterface::ROLE_USER_ADMIN, KeycloakInterface::ROLE_GROUP_ADMIN], default => [], }); } diff --git a/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php b/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php index ff1d6a969..c4c097a91 100644 --- a/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php +++ b/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php @@ -13,7 +13,6 @@ public function __construct( private KeycloakManager $keycloakManager, private array $symfonyApplications, private array $frontendApplications, - private string $keycloakRealm, ) { } @@ -22,10 +21,10 @@ public function configure(OutputInterface $output): void $this->configureRealm(); foreach ([ - KeycloakInterface::GROUP_SUPER_ADMIN => 'Can do anything', - KeycloakInterface::GROUP_TECH => 'Access to Dev/Ops Operations', - KeycloakInterface::GROUP_USER_ADMIN => 'Manage Users', - KeycloakInterface::GROUP_GROUP_ADMIN => 'Manage Groups', + KeycloakInterface::ROLE_ADMIN => 'Can do anything', + KeycloakInterface::ROLE_TECH => 'Access to Dev/Ops Operations', + KeycloakInterface::ROLE_USER_ADMIN => 'Manage Users', + KeycloakInterface::ROLE_GROUP_ADMIN => 'Manage Groups', ] as $role => $desc) { $this->keycloakManager->createRole($role, $desc); } @@ -72,6 +71,23 @@ public function configure(OutputInterface $output): void ] ); } + + $defaultAdmin = $this->keycloakManager->createUser([ + 'username' => getenv('DEFAULT_ADMIN_USERNAME'), + 'enabled' => true, + 'credentials' => [[ + 'type' => 'password', + 'value' => getenv('DEFAULT_ADMIN_PASSWORD'), + 'temporary' => true, + ]] + ]); + + $this->keycloakManager->addRolesToUser($defaultAdmin['id'], [ + KeycloakInterface::ROLE_ADMIN, + ]); + $this->keycloakManager->addClientRolesToUser($defaultAdmin['id'], [ + 'realm-admin', + ]); } private function getAppScopes(): array diff --git a/configurator/src/Configurator/Vendor/Keycloak/KeycloakInterface.php b/configurator/src/Configurator/Vendor/Keycloak/KeycloakInterface.php index c1795d225..980830d71 100644 --- a/configurator/src/Configurator/Vendor/Keycloak/KeycloakInterface.php +++ b/configurator/src/Configurator/Vendor/Keycloak/KeycloakInterface.php @@ -6,9 +6,8 @@ interface KeycloakInterface { - final public const GROUP_ADMIN = 'admin'; - final public const GROUP_SUPER_ADMIN = 'super-admin'; - final public const GROUP_TECH = 'tech'; - final public const GROUP_USER_ADMIN = 'user-admin'; - final public const GROUP_GROUP_ADMIN = 'group-admin'; + final public const ROLE_ADMIN = 'admin'; + final public const ROLE_TECH = 'tech'; + final public const ROLE_USER_ADMIN = 'user-admin'; + final public const ROLE_GROUP_ADMIN = 'group-admin'; } diff --git a/configurator/src/Configurator/Vendor/Keycloak/KeycloakManager.php b/configurator/src/Configurator/Vendor/Keycloak/KeycloakManager.php index 7e3df7926..effe62b64 100644 --- a/configurator/src/Configurator/Vendor/Keycloak/KeycloakManager.php +++ b/configurator/src/Configurator/Vendor/Keycloak/KeycloakManager.php @@ -18,6 +18,9 @@ public function __construct( private readonly string $keycloakRealm, ) { + if ('master' === $this->keycloakRealm) { + throw new \LogicException('Your Keycloak Realm cannot be named "master".'); + } } private function getAuthenticatedClient(): HttpClientInterface @@ -367,6 +370,19 @@ public function getRealmRoles(): array return $response->toArray(); } + public function getRealmClientRoles(): array + { + $realmClient = $this->getClientByClientId('realm-management'); + + $response = $this->getAuthenticatedClient() + ->request('GET', UriTemplate::resolve('{realm}/clients/{realmClientId}/roles', [ + 'realm' => $this->keycloakRealm, + 'realmClientId' => $realmClient['id'], + ])); + + return $response->toArray(); + } + public function getUserRoles(string $userId): array { return $this->getAuthenticatedClient() @@ -405,6 +421,37 @@ public function addRolesToUser(string $userId, array $roleNames): void ]), 409); } + public function addClientRolesToUser(string $userId, array $roleNames): void + { + $realmClient = $this->getClientByClientId('realm-management'); + $allRoles = $this->getRealmClientRoles(); + $userRoles = array_map(fn (array $r): string => $r['id'], $this->getUserRoles($userId)); + + $roles = array_map(function (string $roleName) use ($userRoles, $allRoles): ?array { + foreach ($allRoles as $r) { + if ($roleName === $r['name']) { + if (!in_array($r['id'], $userRoles, true)) { + return $r; + } else { + return null; + } + } + } + + throw new \InvalidArgumentException(sprintf('Role "%s" not found', $roleName)); + }, $roleNames); + $roles = array_filter($roles, fn (array|null $r): bool => null !== $r); + + HttpClientUtil::catchHttpCode(fn() => $this->getAuthenticatedClient() + ->request('POST', UriTemplate::resolve('{realm}/users/{userId}/role-mappings/clients/{realmClientId}', [ + 'realm' => $this->keycloakRealm, + 'userId' => $userId, + 'realmClientId' => $realmClient['id'], + ]), [ + 'json' => $roles, + ]), 409); + } + public function addUserToGroup(string $userId, string $groupId): void { HttpClientUtil::catchHttpCode(fn() => $this->getAuthenticatedClient() diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a175a2ec1..66dc34cfb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -179,6 +179,8 @@ services: - NOTIFY_DB_NAME - UPLOADER_DB_NAME - KEYCLOAK_REALM_NAME + - DEFAULT_ADMIN_USERNAME + - DEFAULT_ADMIN_PASSWORD working_dir: /var/workspace volumes: - ./:/var/workspace diff --git a/docker-compose.yml b/docker-compose.yml index 46aa9f635..1f6b1b536 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -975,6 +975,7 @@ services: profiles: - configurator environment: + - APP_ENV - KEYCLOAK_URL - KEYCLOAK_ADMIN - KEYCLOAK_ADMIN_PASSWORD @@ -1009,6 +1010,8 @@ services: - MAIL_FROM - MAILER_DSN - KEYCLOAK_REALM_NAME + - DEFAULT_ADMIN_USERNAME + - DEFAULT_ADMIN_PASSWORD extra_hosts: - keycloak.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP} diff --git a/lib/php/auth-bundle/DependencyInjection/Configuration.php b/lib/php/auth-bundle/DependencyInjection/Configuration.php index df55474cf..3110e1c9e 100644 --- a/lib/php/auth-bundle/DependencyInjection/Configuration.php +++ b/lib/php/auth-bundle/DependencyInjection/Configuration.php @@ -21,7 +21,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->addDefaultsIfNotSet() ->children() ->scalarNode('url')->defaultValue('%env(KEYCLOAK_URL)%')->end() - ->scalarNode('realm')->defaultValue('%env(KEYCLOAK_REALM)%')->end() + ->scalarNode('realm')->defaultValue('%env(KEYCLOAK_REALM_NAME)%')->end() ->end() ->end() ->scalarNode('client_id')->defaultValue('%env(ADMIN_CLIENT_ID)%')->end() diff --git a/lib/php/auth-bundle/Resources/config/services.yaml b/lib/php/auth-bundle/Resources/config/services.yaml index 83edb0148..6c84ba417 100644 --- a/lib/php/auth-bundle/Resources/config/services.yaml +++ b/lib/php/auth-bundle/Resources/config/services.yaml @@ -1,6 +1,6 @@ parameters: env(VERIFY_SSL): true - env(KEYCLOAK_REALM): master + env(KEYCLOAK_REALM_NAME): phrasea services: _defaults: diff --git a/lib/php/auth-bundle/Security/RoleMapper.php b/lib/php/auth-bundle/Security/RoleMapper.php index b236a7497..a2a6cb5b7 100644 --- a/lib/php/auth-bundle/Security/RoleMapper.php +++ b/lib/php/auth-bundle/Security/RoleMapper.php @@ -4,15 +4,14 @@ namespace Alchemy\AuthBundle\Security; -final class RoleMapper +final readonly class RoleMapper { public function __construct( - private readonly array $mapping = [ + private array $mapping = [ 'admin' => 'ROLE_ADMIN', ] ) { - } public function getRoles(array $idpRoles): array