Skip to content

Commit

Permalink
improve keycloak migration
Browse files Browse the repository at this point in the history
  • Loading branch information
4rthem committed Nov 21, 2023
1 parent 05c2e11 commit bcbb7a9
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 82 deletions.
15 changes: 7 additions & 8 deletions UPGRADE-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@
## Upgrade docker compose stack

```bash
bin/setup.sh \
&& dc run --rm configurator bin/console migration:20230807
bin/migrate.sh \
&& bin/setup.sh \
&& dc run --rm configurator migration:v20230807 -vvv
```

## Upgrade HELM release

Upgrade helm release then run the following job:

```bash
export MIGRATION_NAME=20230807
helm -n <release-name> get values <release-name>-o yaml > /tmp/.current-values.yaml \
&& helm template <release-name> -f /tmp/.current-values.yaml \
export MIGRATION_NAME=v20230807
export RELEASE_NAME=<release-name>
helm -n ${RELEASE_NAME} get values -o yaml > /tmp/.current-values.yaml \
&& helm template ${RELEASE_NAME} -f /tmp/.current-values.yaml \
--set "configurator.executeMigration=${MIGRATION_NAME}" \
-s templates/job-tests.yaml | kubectl apply -f -
kubectl attach -it pod/${MIGRATION_NAME}
kubectl delete -it pod/${MIGRATION_NAME}
```


# TODO Link Accounts: Attach migrated users to newly configured IdP
41 changes: 14 additions & 27 deletions bin/dev/migrate-to-keycloak.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

set -e

NS=${NS:-"ps"}
RELEASE_NAME=${4:-"ps"}

kubectl config use-context minikube
NS=${NS:-"ps-test"}
RELEASE_NAME=${1:-"ps"}

source ./tmp/.helm.env

Expand All @@ -24,26 +22,12 @@ if [ -z "${NEW_CHART_VALUES}" ]; then
exit 1
fi

MIGRATION_NAME=20230807


# <TODO remove>
(cd ${NEW_CHART_DIR} \
&& helm -n ${NS} get values ${RELEASE_NAME} -o yaml > /tmp/.current-values.yaml \
&& helm -n ${NS} upgrade ${RELEASE_NAME} ./ \
-f ${NEW_CHART_VALUES} \
-f /tmp/.current-values.yaml \
--set "configurator.executeMigration=${MIGRATION_NAME}" \
--set "stack.runMigrations=false" \
)

exit
# </TODO remove>


MIGRATION_NAME="v20230807"

read -p "Reset? (y/N)" RESET_RELEASE

kubectl config use-context ${K8S_CONTEXT:-"minikube"}

if [[ $RESET_RELEASE == "y" ]]; then
echo "Resetting release..."
if [ -z "${OLD_CHART_VALUES}" ]; then
Expand Down Expand Up @@ -73,13 +57,16 @@ echo "Migrating..."
(cd ${NEW_CHART_DIR} \
&& helm -n ${NS} get values ${RELEASE_NAME} -o yaml > /tmp/.current-values.yaml \
&& helm -n ${NS} upgrade ${RELEASE_NAME} ./ \
-f /tmp/.current-values.yaml \
-f ${NEW_CHART_VALUES} \
&& echo "Executing migrations..." \
&& helm -n ${NS} upgrade ${RELEASE_NAME} ./ \
-f ${NEW_CHART_VALUES} \
-f /tmp/.current-values.yaml \
--set "configurator.executeMigration=${MIGRATION_NAME}" \
-f /tmp/.current-values.yaml \
-f ${NEW_CHART_VALUES} \
--set "configurator.executeMigration=${MIGRATION_NAME}" \
&& echo "Removing migrations..." \
&& helm -n ${NS} upgrade ${RELEASE_NAME} ./ \
-f ${NEW_CHART_VALUES} \
-f /tmp/.current-values.yaml \
--set "configurator.executeMigration="
-f /tmp/.current-values.yaml \
-f ${NEW_CHART_VALUES} \
--set "configurator.executeMigration="
)
1 change: 0 additions & 1 deletion bin/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ COMPOSE_PROFILES="${COMPOSE_PROFILES},setup" docker compose run --rm -T --entryp

docker compose restart keycloak
docker compose run --rm dockerize -wait http://keycloak:8080 -timeout 200s
docker compose run --rm --entrypoint="" configurator /bin/ash -c env
docker compose run --rm configurator configure -vvv

echo "Done."
166 changes: 154 additions & 12 deletions configurator/src/Command/Migration20230807Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public function __construct(
private readonly KeycloakManager $keycloakManager,
private readonly array $symfonyApplications,
private readonly DoctrineConnectionManager $connections,
)
{
private readonly string $keycloakRealm,
) {
parent::__construct();
}

Expand All @@ -29,6 +29,8 @@ public function execute(InputInterface $input, OutputInterface $output): int
$apps = $this->symfonyApplications;
$apps[] = 'auth';

$this->migrateIdP();

foreach ($apps as $app) {
$output->writeln(sprintf('Migrating <info>%s</info>', $app));
$connection = $this->connections->getConnection($app);
Expand Down Expand Up @@ -58,12 +60,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
continue;
}
if ('auth' === $app && in_array($row['id'], [
'databox-app',
'expose-app',
'uploader-app',
'databox-admin',
'expose-admin',
'uploader-admin',
'databox-app',
'expose-app',
'uploader-app',
'databox-admin',
'expose-admin',
'uploader-admin',
], true)) {
continue;
}
Expand Down Expand Up @@ -134,15 +136,15 @@ public function execute(InputInterface $input, OutputInterface $output): int
$user = $this->keycloakManager->createUser([
'createdTimestamp' => (new \DateTimeImmutable($row['created_at']))->getTimestamp(),
'username' => $row['username'],
'email' => $row['username'],
'email' => str_contains($row['username'], '@') ? $row['username'] : null,
'emailVerified' => $row['email_verified'],
'enabled' => $row['enabled'],
'attributes' => [
'ps-auth-legacy-id' => $row['id'],
],
'requiredActions' => [
'UPDATE_PASSWORD',
]
],
]);
$userMap[$row['id']] = $user['id'];

Expand All @@ -159,6 +161,35 @@ public function execute(InputInterface $input, OutputInterface $output): int
$this->keycloakManager->addRolesToUser($user['id'], $realmRoles);
}

$samlIdentities = $connection->fetchAllAssociative('SELECT
user_id,
provider,
attributes
FROM "saml_identity"');
foreach ($samlIdentities as $row) {
$attributes = json_decode($row['attributes'], true, 512, JSON_THROW_ON_ERROR);
$username = $this->extractUsernameFromAttributes($attributes);

$this->keycloakManager->linkAccountToIdentityProvider($userMap[$row['user_id']], $row['provider'], [
'userId' => $username,
'userName' => $username,
]);
}

$oauthUsers = $connection->fetchAllAssociative('SELECT
user_id,
identifier,
provider
FROM "external_access_token"');
foreach ($oauthUsers as $row) {
$username = $row['identifier'];

$this->keycloakManager->linkAccountToIdentityProvider($userMap[$row['user_id']], $row['provider'], [
'userId' => $username,
'userName' => $username,
]);
}

$this->replaceInDb([
'databox' => [
'asset_data_template' => [
Expand Down Expand Up @@ -213,8 +244,8 @@ public function execute(InputInterface $input, OutputInterface $output): int
'databox' => [
'access_control_entry' => [
'user_id',
]
]
],
],
], $groupMap);


Expand All @@ -237,4 +268,115 @@ private function replaceInDb(array $tableMap, array $valueMap): void
}
}
}

private function migrateIdP(): void
{
$configSrc = '/configs/config.json';
if (!file_exists($configSrc)) {
return;
}

$config = json_decode(file_get_contents($configSrc), true, 512, JSON_THROW_ON_ERROR);

foreach ($config['auth']['identity_providers'] ?? [] as $idp) {
$alias = $idp['name'];
$options = $idp['options'];

$normalizeOAuthUrl = function (string $key, string $default) use ($options, $alias): string {
if (isset($options[$key])) {
return str_replace('{base_url}', $options['base_url'] ?? '', $options[$key]);
}

if (!isset($options['base_url'])) {
throw new \InvalidArgumentException(sprintf('Missing "base_url" for IdP "%s"', $alias));
}

return $options['base_url'].$default;
};

$idpType = $idp['type'];
if ('oauth' === $idpType) {
$idpType = 'oidc';
}

$config = match ($idpType) {
'saml' => [
'allowCreate' => 'true',
'guiOrder' => '',
'entityId' => getenv('KEYCLOAK_URL').'/realms/'.$this->keycloakRealm,
'idpEntityId' => $options['entity_id'],
'singleSignOnServiceUrl' => $options['sso_url'],
'singleLogoutServiceUrl' => '',
'attributeConsumingServiceName' => '',
'backchannelSupported' => 'false',
'nameIDPolicyFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
'principalType' => 'Subject NameID',
'postBindingResponse' => 'false',
'postBindingAuthnRequest' => 'false',
'postBindingLogout' => 'false',
'wantAuthnRequestsSigned' => 'false',
'wantAssertionsSigned' => 'false',
'wantAssertionsEncrypted' => 'false',
'forceAuthn' => 'false',
'validateSignature' => 'false',
'signSpMetadata' => 'false',
'loginHint' => 'false',
'allowedClockSkew' => 0,
'attributeConsumingServiceIndex' => 0,
],
'oidc' => [
'allowCreate' => true,
'authorizationUrl' => $normalizeOAuthUrl('authorization_url', '/auth'),
'tokenUrl' => $normalizeOAuthUrl('token_url', '/token'),
'userInfoUrl' => $normalizeOAuthUrl('userinfo', '/userinfo'),
'clientAssertionSigningAlg' => '',
'clientAuthMethod' => 'client_secret_post',
'validateSignature' => 'false',
'clientId' => $options['client_id'],
'clientSecret' => $options['client_secret'],
],
};

$data = [
'alias' => $alias,
'config' => $config,
'displayName' => $idp['title'],
'providerId' => $idpType,
// 'enabled' => true,
// 'trustEmail' => true,
];

$this->keycloakManager->createIdentityProvider($data);

if (isset($idp['group_jq_normalizer'])) {
$this->keycloakManager->createIdpMapper($alias, [
'name' => 'groups',
'identityProviderAlias' => $alias,
'identityProviderMapper' => 'jq-groups-idp-mapper',
'config' => [
'syncMode' => 'FORCE',
'jq_filter' => $idp['group_jq_normalizer'],
],
]);
}
}
}

private function extractUsernameFromAttributes(array $attributes): string
{
foreach ([
'username',
'email',
] as $key) {
if (!empty($attributes[$key])) {
if (is_string($attributes[$key])) {
return $attributes[$key];
} elseif (is_array($attributes[$key])) {
return $attributes[$key][0];
}
}
}

throw new \InvalidArgumentException(sprintf('Cannot extract username in attributes: %s', print_r($attributes, true)));
}
}
Loading

0 comments on commit bcbb7a9

Please sign in to comment.