Skip to content

Commit

Permalink
feat(keycloak): add seeder for clientScopeMappers (#352)
Browse files Browse the repository at this point in the history
* extend the keycloak seeding to seed the clientScopeMappers

--------------------

Refs: CPLP-3530
Reviewed-By: Evelyn Gurschler <evelyn.gurschler@bmw.de>
  • Loading branch information
Phil91 authored Nov 23, 2023
1 parent b9653b8 commit 6ee761e
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 11 deletions.
12 changes: 6 additions & 6 deletions src/keycloak/Keycloak.Library/ScopeMappings/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,37 +159,37 @@ public async Task<Mapping> GetScopeMappingsForClientAsync(string realm, string c
.GetJsonAsync<Mapping>()
.ConfigureAwait(false);

public async Task AddClientRolesScopeMappingToClientAsync(string realm, string clientId, string scopeClientId, IEnumerable<Role> roles) =>
public async Task AddClientRolesScopeMappingToClientAsync(string realm, string clientId, string scopeClientId, IEnumerable<Role> roles, CancellationToken cancellationToken = default) =>
await (await GetBaseUrlAsync(realm).ConfigureAwait(false))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/clients/")
.AppendPathSegment(clientId, true)
.AppendPathSegment("/scope-mappings/clients/")
.AppendPathSegment(scopeClientId, true)
.PostJsonAsync(roles)
.PostJsonAsync(roles, cancellationToken)
.ConfigureAwait(false);

public async Task<IEnumerable<Role>> GetClientRolesScopeMappingsForClientAsync(string realm, string clientId, string scopeClientId) =>
public async Task<IEnumerable<Role>> GetClientRolesScopeMappingsForClientAsync(string realm, string clientId, string scopeClientId, CancellationToken cancellationToken = default) =>
await (await GetBaseUrlAsync(realm).ConfigureAwait(false))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/clients/")
.AppendPathSegment(clientId, true)
.AppendPathSegment("/scope-mappings/clients/")
.AppendPathSegment(scopeClientId, true)
.GetJsonAsync<IEnumerable<Role>>()
.GetJsonAsync<IEnumerable<Role>>(cancellationToken)
.ConfigureAwait(false);

public async Task RemoveClientRolesFromClientScopeForClientAsync(string realm, string clientId, string scopeClientId, IEnumerable<Role> roles) =>
public async Task RemoveClientRolesFromClientScopeForClientAsync(string realm, string clientId, string scopeClientId, IEnumerable<Role> roles, CancellationToken cancellationToken = default) =>
await (await GetBaseUrlAsync(realm).ConfigureAwait(false))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/clients/")
.AppendPathSegment(clientId, true)
.AppendPathSegment("/scope-mappings/clients/")
.AppendPathSegment(scopeClientId, true)
.SendJsonAsync(HttpMethod.Delete, roles)
.SendJsonAsync(HttpMethod.Delete, roles, cancellationToken)
.ConfigureAwait(false);

public async Task<IEnumerable<Role>> GetAvailableClientRolesForClientScopeForClientAsync(string realm, string clientId, string scopeClientId) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/********************************************************************************
* Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling;
using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Factory;
using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library;
using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Roles;

namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic;

public class ClientScopeMapperUpdater : IClientScopeMapperUpdater
{
private readonly IKeycloakFactory _keycloakFactory;
private readonly ISeedDataHandler _seedData;

public ClientScopeMapperUpdater(IKeycloakFactory keycloakFactory, ISeedDataHandler seedDataHandler)
{
_keycloakFactory = keycloakFactory;
_seedData = seedDataHandler;
}

public async Task UpdateClientScopeMapper(string instanceName, CancellationToken cancellationToken)
{
var keycloak = _keycloakFactory.CreateKeycloakClient(instanceName);
var realm = _seedData.Realm;

var clients = await keycloak.GetClientsAsync(realm, null, true, cancellationToken).ConfigureAwait(false);
foreach (var (clientName, mappingModels) in _seedData.ClientScopeMappings)
{
var client = clients.SingleOrDefault(x => x.ClientId == clientName);
if (client?.Id is null)
{
throw new ConflictException($"No client id found with name {clientName}");
}

var roles = await keycloak.GetRolesAsync(realm, client.Id, cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var mappingModel in mappingModels)
{
var clientScope = clients.SingleOrDefault(x => x.ClientId == mappingModel.Client);
if (clientScope?.Id is null)
{
throw new ConflictException($"No client id found with name {clientName}");
}
var clientRoles = await keycloak.GetClientRolesScopeMappingsForClientAsync(realm, clientScope.Id, client.Id, cancellationToken).ConfigureAwait(false);
var mappingModelRoles = mappingModel.Roles.Select(roleName => roles.SingleOrDefault(r => r.Name == roleName) ?? throw new ConflictException($"No role with name {roleName} found"));
await AddAndDeleteRoles(keycloak, realm, clientScope.Id, client.Id, clientRoles, mappingModelRoles, cancellationToken).ConfigureAwait(false);
}
}
}

private static async Task AddAndDeleteRoles(KeycloakClient keycloak, string realm, string clientScopeId, string clientId, IEnumerable<Role> roles, IEnumerable<Role> updateRoles, CancellationToken cancellationToken)
{
var rolesToAdd = updateRoles.ExceptBy(roles.Select(role => role.Name), roleModel => roleModel.Name).ToList();
var rolesToDelete = roles.ExceptBy(updateRoles.Select(roleModel => roleModel.Name), role => role.Name).ToList();
if (rolesToDelete.Any())
{
await keycloak.RemoveClientRolesFromClientScopeForClientAsync(realm, clientScopeId, clientId, rolesToDelete, cancellationToken).ConfigureAwait(false);
}

if (rolesToAdd.Any())
{
await keycloak.AddClientRolesScopeMappingToClientAsync(realm, clientScopeId, clientId, rolesToAdd, cancellationToken).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/********************************************************************************
* Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic;

public interface IClientScopeMapperUpdater
{
Task UpdateClientScopeMapper(string instanceName, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public interface ISeedDataHandler

IReadOnlyDictionary<string, string> ClientsDictionary { get; }

IReadOnlyDictionary<string, IEnumerable<ClientScopeMappingModel>> ClientScopeMappings { get; }

Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds);

string GetIdOfClient(string clientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public class KeycloakSeeder : IKeycloakSeeder
private readonly IUsersUpdater _usersUpdater;
private readonly IClientScopesUpdater _clientScopesUpdater;
private readonly IAuthenticationFlowsUpdater _authenticationFlowsUpdater;
public KeycloakSeeder(ISeedDataHandler seedDataHandler, IRealmUpdater realmUpdater, IRolesUpdater rolesUpdater, IClientsUpdater clientsUpdater, IIdentityProvidersUpdater identityProvidersUpdater, IUsersUpdater usersUpdater, IClientScopesUpdater clientScopesUpdater, IAuthenticationFlowsUpdater authenticationFlowsUpdater, IOptions<KeycloakSeederSettings> options)
private readonly IClientScopeMapperUpdater _clientScopeMapperUpdater;

public KeycloakSeeder(ISeedDataHandler seedDataHandler, IRealmUpdater realmUpdater, IRolesUpdater rolesUpdater, IClientsUpdater clientsUpdater, IIdentityProvidersUpdater identityProvidersUpdater, IUsersUpdater usersUpdater, IClientScopesUpdater clientScopesUpdater, IAuthenticationFlowsUpdater authenticationFlowsUpdater, IClientScopeMapperUpdater clientScopeMapperUpdater, IOptions<KeycloakSeederSettings> options)
{
_seedData = seedDataHandler;
_realmUpdater = realmUpdater;
Expand All @@ -43,6 +45,7 @@ public KeycloakSeeder(ISeedDataHandler seedDataHandler, IRealmUpdater realmUpdat
_usersUpdater = usersUpdater;
_clientScopesUpdater = clientScopesUpdater;
_authenticationFlowsUpdater = authenticationFlowsUpdater;
_clientScopeMapperUpdater = clientScopeMapperUpdater;
_settings = options.Value;
}

Expand All @@ -59,6 +62,7 @@ public async Task Seed(CancellationToken cancellationToken)
await _identityProvidersUpdater.UpdateIdentityProviders(_settings.InstanceName, cancellationToken).ConfigureAwait(false);
await _usersUpdater.UpdateUsers(_settings.InstanceName, _settings.ExcludedUserAttributes, cancellationToken).ConfigureAwait(false);
await _clientScopesUpdater.UpdateClientScopes(_settings.InstanceName, cancellationToken).ConfigureAwait(false);
await _clientScopeMapperUpdater.UpdateClientScopeMapper(_settings.InstanceName, cancellationToken).ConfigureAwait(false);
await _authenticationFlowsUpdater.UpdateAuthenticationFlows(_settings.InstanceName, cancellationToken).ConfigureAwait(false);
}
}
Expand Down
25 changes: 21 additions & 4 deletions src/keycloak/Keycloak.Seeding/BusinessLogic/SeedDataHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,25 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic;

public class SeedDataHandler : ISeedDataHandler
{
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, IncludeFields = true, PropertyNameCaseInsensitive = false };
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
IncludeFields = true,
PropertyNameCaseInsensitive = false
};

private KeycloakRealm? jsonRealm;
private IReadOnlyDictionary<string, string>? _idOfClients;

public async Task Import(string path, CancellationToken cancellationToken)
{
using (var stream = File.OpenRead(path))
{
jsonRealm = await JsonSerializer.DeserializeAsync<KeycloakRealm>(stream, Options, cancellationToken).ConfigureAwait(false) ?? throw new ConfigurationException($"cannot deserialize realm from {path}");
jsonRealm =
await JsonSerializer.DeserializeAsync<KeycloakRealm>(stream, Options, cancellationToken)
.ConfigureAwait(false) ?? throw new ConfigurationException($"cannot deserialize realm from {path}");
}

_idOfClients = null;
}

Expand All @@ -57,7 +66,8 @@ public IEnumerable<ClientModel> Clients

public IReadOnlyDictionary<string, IEnumerable<RoleModel>> ClientRoles
{
get => jsonRealm?.Roles?.Client ?? Enumerable.Empty<(string, IEnumerable<RoleModel>)>().ToImmutableDictionary(x => x.Item1, x => x.Item2);
get => jsonRealm?.Roles?.Client ?? Enumerable.Empty<(string, IEnumerable<RoleModel>)>()
.ToImmutableDictionary(x => x.Item1, x => x.Item2);
}

public IEnumerable<RoleModel> RealmRoles
Expand All @@ -82,7 +92,8 @@ public IEnumerable<UserModel> Users

public IEnumerable<AuthenticationFlowModel> TopLevelCustomAuthenticationFlows
{
get => jsonRealm?.AuthenticationFlows?.Where(x => (x.TopLevel ?? false) && !(x.BuiltIn ?? false)) ?? Enumerable.Empty<AuthenticationFlowModel>();
get => jsonRealm?.AuthenticationFlows?.Where(x => (x.TopLevel ?? false) && !(x.BuiltIn ?? false)) ??
Enumerable.Empty<AuthenticationFlowModel>();
}

public IEnumerable<ClientScopeModel> ClientScopes
Expand All @@ -95,6 +106,12 @@ public IReadOnlyDictionary<string, string> ClientsDictionary
get => _idOfClients ?? throw new InvalidOperationException("ClientInternalIds have not been set");
}

public IReadOnlyDictionary<string, IEnumerable<ClientScopeMappingModel>> ClientScopeMappings
{
get => jsonRealm?.ClientScopeMappings ?? Enumerable.Empty<(string, IEnumerable<ClientScopeMappingModel>)>()
.ToImmutableDictionary(x => x.Item1, x => x.Item2);
}

public async Task SetClientInternalIds(IAsyncEnumerable<(string ClientId, string Id)> clientInternalIds)
{
var clientIds = new Dictionary<string, string>();
Expand Down
1 change: 1 addition & 0 deletions src/keycloak/Keycloak.Seeding/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
.AddTransient<IUsersUpdater, UsersUpdater>()
.AddTransient<IClientScopesUpdater, ClientScopesUpdater>()
.AddTransient<IAuthenticationFlowsUpdater, AuthenticationFlowsUpdater>()
.AddTransient<IClientScopeMapperUpdater, ClientScopeMapperUpdater>()
.AddTransient<IKeycloakFactory, KeycloakFactory>()
.ConfigureKeycloakSettingsMap(hostContext.Configuration.GetSection("Keycloak"))
.AddTransient<IKeycloakSeeder, KeycloakSeeder>()
Expand Down

0 comments on commit 6ee761e

Please sign in to comment.