Skip to content

Commit

Permalink
Merge pull request #45 from kiwigrid/fix-client-clientscope-assignment
Browse files Browse the repository at this point in the history
Fix defaultClientScopes and optionalClientScopes assignment to Client
  • Loading branch information
manu11th authored Jun 22, 2021
2 parents ef52342 + 2dba677 commit 5e3f0ac
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/ci.provision.helm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ set -o errexit
HELM_VERSION="${1}"

echo -e "\n##### install helm #####\n"
curl --silent --show-error --fail --location --output get_helm.sh https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get
curl --silent --show-error --fail --location --output get_helm.sh https://raw.githubusercontent.com/kubernetes/helm/main/scripts/get
chmod 700 get_helm.sh
./get_helm.sh --version "${HELM_VERSION}"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- name: maven
run: .github/ci.maven.sh
- name: Create kind ${{ matrix.k8s-version }} cluster
uses: helm/kind-action@master
uses: helm/kind-action@main
with:
config: .github/kind-config.yaml
node_image: kindest/node:${{ matrix.k8s-version }}
Expand Down
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<version.lombok>1.18.16</version.lombok>
<version.com.spotify.ile>1.4.10</version.com.spotify.ile>
<version.janino>3.1.2</version.janino>
<version.mockito>3.7.7</version.mockito>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

Expand Down Expand Up @@ -185,6 +186,13 @@
</exclusions>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${version.mockito}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.kiwigrid.keycloak.controller.client;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Singleton;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class AssignedClientScopesSyncer {
private final Logger log = LoggerFactory.getLogger(getClass());

public void manageClientScopes(RealmResource realmResource, String clientUuid, com.kiwigrid.keycloak.controller.client.ClientResource clientResource) {
var keycloak = clientResource.getSpec().getKeycloak();
var realm = clientResource.getSpec().getRealm();
var clientId = clientResource.getSpec().getClientId();

org.keycloak.admin.client.resource.ClientResource keycloakClientResource = realmResource.clients().get(clientUuid);
List<String> existingDefaultClientScopeNames = keycloakClientResource.toRepresentation().getDefaultClientScopes();
List<String> existingOptionalClientScopeNames = keycloakClientResource.toRepresentation()
.getOptionalClientScopes();

List<String> requestedDefaultClientScopes = clientResource.getSpec().getDefaultClientScopes().stream()
.map(String::toLowerCase)
.collect(Collectors.toList());
List<String> requestedOptionalClientScopes = clientResource.getSpec().getOptionalClientScopes().stream()
.map(String::toLowerCase)
.collect(Collectors.toList());

// add new
getClientScopesForName(realmResource, requestedDefaultClientScopes)
.filter(cs -> !existingDefaultClientScopeNames.contains(cs.getName()))
.forEach(cs -> {
keycloakClientResource.addDefaultClientScope(cs.getId());
log.info("{}/{}/{}: added default client scope {}", keycloak, realm, clientId, cs.getName());
});
getClientScopesForName(realmResource, requestedOptionalClientScopes)
.filter(cs -> !existingOptionalClientScopeNames.contains(cs.getName()))
.forEach(cs -> {
keycloakClientResource.addOptionalClientScope(cs.getId());
log.info("{}/{}/{}: added optional client scope {}", keycloak, realm, clientId, cs.getName());
});

// remove obsolete
keycloakClientResource.getDefaultClientScopes().stream()
.filter(cs -> !requestedDefaultClientScopes.contains(cs.getName().toLowerCase()))
.forEach(cs -> {
keycloakClientResource.removeDefaultClientScope(cs.getId());
log.info("{}/{}/{}: removed default client scope {}", keycloak, realm, clientId, cs.getName());
});
keycloakClientResource.getOptionalClientScopes().stream()
.filter(cs -> !requestedOptionalClientScopes.contains(cs.getName().toLowerCase()))
.forEach(cs -> {
keycloakClientResource.removeOptionalClientScope(cs.getId());
log.info("{}/{}/{}: removed optional client scope {}", keycloak, realm, clientId, cs.getName());
});
}

private Stream<ClientScopeRepresentation> getClientScopesForName(RealmResource realmResource, List<String> requestedClientScopes) {
return realmResource.clientScopes()
.findAll()
.stream()
.filter(cs -> requestedClientScopes.contains(cs.getName().toLowerCase()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@
public class ClientController extends KubernetesController<ClientResource> {

final KeycloakController keycloak;
final AssignedClientScopesSyncer assignedClientScopesSyncer;

public ClientController(KeycloakController keycloak,
KubernetesClient kubernetes,
AssignedClientScopesSyncer assignedClientScopesSyncer) {

public ClientController(KeycloakController keycloak, KubernetesClient kubernetes) {
super(kubernetes, ClientResource.DEFINITION, ClientResource.class, ClientResource.ClientResourceList.class,
ClientResource.ClientResourceDoneable.class);
this.keycloak = keycloak;
this.assignedClientScopesSyncer = assignedClientScopesSyncer;
}

@Override
Expand Down Expand Up @@ -85,6 +90,7 @@ public void apply(ClientResource clientResource) {
}
manageMapper(realmResource, clientUuid, clientResource);
manageRoles(realmResource, clientUuid, clientResource);
assignedClientScopesSyncer.manageClientScopes(realmResource, clientUuid, clientResource);

if (clientResource.getSpec().getServiceAccountsEnabled() == Boolean.TRUE) {
manageServiceAccountRealmRoles(realmResource, clientUuid, clientResource);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.kiwigrid.keycloak.controller.client;

import java.util.List;
import java.util.stream.Collectors;

import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.mockito.Mockito;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

public class AssignedClientScopesSyncerTest {

private final AssignedClientScopesSyncer assignedClientScopesSyncer = new AssignedClientScopesSyncer();

@Test
public void testNonRequestedClientScopesRemoved() {
String clientUuid = "clientUuid";
RealmResource keycloakRealmResource = Mockito.mock(RealmResource.class);
org.keycloak.admin.client.resource.ClientResource keycloakClientResource = prepareClientResource(
clientUuid,
keycloakRealmResource,
List.of("dcs2", "dcs3"),
List.of("ocs2", "ocs3"),
List.of("dcs1", "dcs2", "dcs3", "another-unrelated-dcs", "ocs1", "ocs2", "ocs3", "another-unrelated-ocs"));

ClientResource kubernetesClientResource = createKubernetesClientResource(
List.of("dcs1", "dcs2"), List.of("ocs1", "ocs2"));

assignedClientScopesSyncer.manageClientScopes(keycloakRealmResource, clientUuid, kubernetesClientResource);

verify(keycloakClientResource).removeDefaultClientScope("dcs3-id");
verify(keycloakClientResource).removeOptionalClientScope("ocs3-id");
}

@Test
public void testRequestedClientScopesAdded() {
String clientUuid = "clientUuid";
RealmResource keycloakRealmResource = Mockito.mock(RealmResource.class);
org.keycloak.admin.client.resource.ClientResource keycloakClientResource = prepareClientResource(
clientUuid,
keycloakRealmResource,
List.of("dcs2", "dcs3"),
List.of("ocs2", "ocs3"),
List.of("dcs1", "dcs2", "dcs3", "another-unrelated-dcs", "ocs1", "ocs2", "ocs3", "another-unrelated-ocs"));

ClientResource kubernetesClientResource = createKubernetesClientResource(
List.of("dcs1", "dcs2"), List.of("ocs1", "ocs2"));

assignedClientScopesSyncer.manageClientScopes(keycloakRealmResource, clientUuid, kubernetesClientResource);

verify(keycloakClientResource).addDefaultClientScope("dcs1-id");
verify(keycloakClientResource).addOptionalClientScope("ocs1-id");
}

@Test
public void testNonExistingClientScopeNotAdded() {
String clientUuid = "clientUuid";
RealmResource keycloakRealmResource = Mockito.mock(RealmResource.class);
org.keycloak.admin.client.resource.ClientResource keycloakClientResource = prepareClientResource(
clientUuid,
keycloakRealmResource,
List.of("dcs1", "dcs2", "non-existing-dcs"),
List.of("ocs1", "ocs2", "non-existing-ocs"),
List.of("dcs1", "dcs2", "another-unrelated-dcs", "ocs1", "ocs2", "another-unrelated-ocs"));

ClientResource kubernetesClientResource = createKubernetesClientResource(
List.of("dcs1", "dcs2"), List.of("ocs1", "ocs2"));

assignedClientScopesSyncer.manageClientScopes(keycloakRealmResource, clientUuid, kubernetesClientResource);

verify(keycloakClientResource, times(0)).addDefaultClientScope(anyString());
verify(keycloakClientResource, times(0)).addOptionalClientScope(anyString());
}

@Test
public void testUnchangedClientScopeNotTouched() {
String clientUuid = "clientUuid";
RealmResource keycloakRealmResource = Mockito.mock(RealmResource.class);
org.keycloak.admin.client.resource.ClientResource keycloakClientResource = prepareClientResource(
clientUuid,
keycloakRealmResource,
List.of("dcs1", "dcs2"),
List.of("ocs1", "ocs2"),
List.of("dcs1", "dcs2", "another-unrelated-dcs", "ocs1", "ocs2", "another-unrelated-ocs"));

ClientResource kubernetesClientResource = createKubernetesClientResource(
List.of("dcs1", "dcs2"), List.of("ocs1", "ocs2"));

assignedClientScopesSyncer.manageClientScopes(keycloakRealmResource, clientUuid, kubernetesClientResource);

verify(keycloakClientResource, times(0)).addDefaultClientScope(anyString());
verify(keycloakClientResource, times(0)).addOptionalClientScope(anyString());
}

@NotNull
private org.keycloak.admin.client.resource.ClientResource prepareClientResource(String clientUuid, RealmResource realmResource,
List<String> defaultClientScopes, List<String> optionalClientScopes, List<String> availableClientScopes) {
ClientRepresentation clientRepresentation = new ClientRepresentation();
org.keycloak.admin.client.resource.ClientsResource clientsResource = Mockito.mock(org.keycloak.admin.client.resource.ClientsResource.class);
org.keycloak.admin.client.resource.ClientResource clientResource = Mockito.mock(org.keycloak.admin.client.resource.ClientResource.class);
org.keycloak.admin.client.resource.ClientScopesResource clientScopesResource = Mockito.mock(org.keycloak.admin.client.resource.ClientScopesResource.class);

clientRepresentation.setDefaultClientScopes(defaultClientScopes);
clientRepresentation.setOptionalClientScopes(optionalClientScopes);

given(clientsResource.get(clientUuid)).willReturn(clientResource);
given(clientResource.toRepresentation()).willReturn(clientRepresentation);
given(clientResource.getDefaultClientScopes()).willReturn(defaultClientScopes.stream().map(this::mapToClientRepresentation).collect(Collectors.toList()));
given(clientResource.getOptionalClientScopes()).willReturn(optionalClientScopes.stream().map(this::mapToClientRepresentation).collect(Collectors.toList()));
given(realmResource.clients()).willReturn(clientsResource);
given(realmResource.clientScopes()).willReturn(clientScopesResource);
given(clientScopesResource.findAll()).willReturn(getClientScopeRepresentations(availableClientScopes));

return clientResource;
}

@NotNull
private ClientScopeRepresentation mapToClientRepresentation(String cs) {
ClientScopeRepresentation representation = new ClientScopeRepresentation();
representation.setName(cs);
representation.setId(cs + "-id");
return representation;
}

private com.kiwigrid.keycloak.controller.client.ClientResource createKubernetesClientResource(
List<String> defaultClientScopes, List<String> optionalClientScopes) {
com.kiwigrid.keycloak.controller.client.ClientResource clientResourceK8s = new com.kiwigrid.keycloak.controller.client.ClientResource();
clientResourceK8s.setSpec(new com.kiwigrid.keycloak.controller.client.ClientResource.ClientResourceSpec());
clientResourceK8s.getSpec().setDefaultClientScopes(defaultClientScopes);
clientResourceK8s.getSpec().setOptionalClientScopes(optionalClientScopes);
clientResourceK8s.getSpec().setRealm("realm");
clientResourceK8s.getSpec().setKeycloak("keycloak");
clientResourceK8s.getSpec().setClientId("clientId");
return clientResourceK8s;
}

private List<ClientScopeRepresentation> getClientScopeRepresentations(List<String> clientScopeNames) {
return clientScopeNames.stream()
.map(this::mapToClientRepresentation)
.collect(Collectors.toList());
}
}

0 comments on commit 5e3f0ac

Please sign in to comment.