feat: update to keycloak 23 + varrious fixes
This commit is contained in:
parent
7163d2a0f0
commit
182824a3b4
12 changed files with 153 additions and 151 deletions
28
build.gradle
28
build.gradle
|
@ -1,13 +1,13 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'sh.libre.scim'
|
group = 'sh.libre.scim'
|
||||||
version = '1.0-SNAPSHOT'
|
version = '1.0-SNAPSHOT'
|
||||||
description = 'keycloak-scim'
|
description = 'keycloak-scim'
|
||||||
|
|
||||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
@ -15,20 +15,12 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly 'org.keycloak:keycloak-core:18.0.0'
|
compileOnly 'org.keycloak:keycloak-core:23.0.4'
|
||||||
compileOnly 'org.keycloak:keycloak-server-spi:18.0.0'
|
compileOnly 'org.keycloak:keycloak-server-spi:23.0.4'
|
||||||
compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0'
|
compileOnly 'org.keycloak:keycloak-server-spi-private:23.0.4'
|
||||||
compileOnly 'org.keycloak:keycloak-services:18.0.0'
|
compileOnly 'org.keycloak:keycloak-services:23.0.4'
|
||||||
compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0'
|
compileOnly 'org.keycloak:keycloak-model-jpa:23.0.4'
|
||||||
implementation 'io.github.resilience4j:resilience4j-retry:1.7.1'
|
implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
|
||||||
implementation 'de.captaingoldfish:scim-sdk-common:1.15.3'
|
implementation 'de.captaingoldfish:scim-sdk-common:1.21.1'
|
||||||
|
implementation 'de.captaingoldfish:scim-sdk-client:1.21.1'
|
||||||
implementation files('/home/marcportabella/Documents/totmicro/Repos/keycloak-scim-aws/scim-sdk-client-1.15.4-SNAPSHOT.jar')
|
|
||||||
//implementation 'de.captaingoldfish:scim-sdk-client:1.15.3'
|
|
||||||
implementation('org.wildfly.client:wildfly-client-config:1.0.1.Final') {
|
|
||||||
transitive false
|
|
||||||
}
|
|
||||||
implementation('org.jboss.resteasy:resteasy-client:4.7.6.Final') {
|
|
||||||
transitive false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:18.0.0
|
image: quay.io/keycloak/keycloak:23.0.3
|
||||||
build: .
|
build: .
|
||||||
command: start-dev
|
command: start-dev
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -23,6 +23,7 @@ services:
|
||||||
KC_DB_PASSWORD: keycloak
|
KC_DB_PASSWORD: keycloak
|
||||||
KEYCLOAK_ADMIN: admin
|
KEYCLOAK_ADMIN: admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
KC_LOG_LEVEL: INFO,sh.libre.scim:debug,de.captaingoldfish.scim:debug
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8080:8080
|
- 127.0.0.1:8080:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'java'
|
|
||||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
group = 'sh.libre.scim'
|
|
||||||
version = '1.0-SNAPSHOT'
|
|
||||||
description = 'keycloak-scim'
|
|
||||||
|
|
||||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly 'org.keycloak:keycloak-core:18.0.0'
|
|
||||||
compileOnly 'org.keycloak:keycloak-server-spi:18.0.0'
|
|
||||||
compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0'
|
|
||||||
compileOnly 'org.keycloak:keycloak-services:18.0.0'
|
|
||||||
compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0'
|
|
||||||
implementation 'io.github.resilience4j:resilience4j-retry:1.7.1'
|
|
||||||
|
|
||||||
compileOnly 'org.wildfly.client:wildfly-client-config:1.0.1.Final'
|
|
||||||
compileOnly 'org.jboss.resteasy:resteasy-client:4.7.6.Final'
|
|
||||||
compileOnly 'org.jboss.resteasy:resteasy-client-api:4.7.6.Final'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
shadowJar {
|
|
||||||
archiveClassifier.set('all-legacy')
|
|
||||||
}
|
|
|
@ -2,10 +2,9 @@ package sh.libre.scim.core;
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import javax.persistence.NoResultException;
|
import jakarta.persistence.NoResultException;
|
||||||
import javax.persistence.TypedQuery;
|
import jakarta.persistence.TypedQuery;
|
||||||
import javax.ws.rs.NotFoundException;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||||
|
@ -61,7 +60,7 @@ public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode>
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSCIMEndpoint() {
|
public String getSCIMEndpoint() {
|
||||||
return type + "s";
|
return "/" + type + "s";
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScimResource toMapping() {
|
public ScimResource toMapping() {
|
||||||
|
@ -95,12 +94,8 @@ public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode>
|
||||||
if (this.externalId != null) {
|
if (this.externalId != null) {
|
||||||
return this.query("findByExternalId", externalId).getSingleResult();
|
return this.query("findByExternalId", externalId).getSingleResult();
|
||||||
}
|
}
|
||||||
} catch (NotFoundException e) {
|
|
||||||
} catch (NoResultException e) {
|
} catch (NoResultException e) {
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +119,7 @@ public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode>
|
||||||
|
|
||||||
public abstract Class<S> getResourceClass();
|
public abstract Class<S> getResourceClass();
|
||||||
|
|
||||||
public abstract S toSCIM(Boolean addMeta);
|
public abstract S toSCIM();
|
||||||
|
|
||||||
public abstract Boolean entityExists();
|
public abstract Boolean entityExists();
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,15 @@ package sh.libre.scim.core;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javax.persistence.NoResultException;
|
import jakarta.persistence.NoResultException;
|
||||||
import de.captaingoldfish.scim.sdk.common.resources.Group;
|
import de.captaingoldfish.scim.sdk.common.resources.Group;
|
||||||
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member;
|
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member;
|
||||||
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
|
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -59,44 +58,49 @@ public class GroupAdapter extends Adapter<GroupModel, Group> {
|
||||||
if (groupMembers != null && groupMembers.size() > 0) {
|
if (groupMembers != null && groupMembers.size() > 0) {
|
||||||
this.members = new HashSet<String>();
|
this.members = new HashSet<String>();
|
||||||
for (var groupMember : groupMembers) {
|
for (var groupMember : groupMembers) {
|
||||||
var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User")
|
try {
|
||||||
.getSingleResult();
|
var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User")
|
||||||
this.members.add(userMapping.getId());
|
.getSingleResult();
|
||||||
|
this.members.add(userMapping.getId());
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
LOGGER.warnf("member %s not found for scim group %s", groupMember.getValue().get(), group.getId().get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Group toSCIM(Boolean addMeta) {
|
public Group toSCIM() {
|
||||||
var group = new Group();
|
var group = new Group();
|
||||||
group.setId(externalId);
|
group.setId(externalId);
|
||||||
group.setExternalId(id);
|
group.setExternalId(id);
|
||||||
group.setDisplayName(displayName);
|
group.setDisplayName(displayName);
|
||||||
if (members.size() > 0) {
|
if (members.size() > 0) {
|
||||||
var groupMembers = new ArrayList<Member>();
|
|
||||||
for (var member : members) {
|
for (var member : members) {
|
||||||
var groupMember = new Member();
|
var groupMember = new Member();
|
||||||
try {
|
try {
|
||||||
var userMapping = this.query("findById", member, "User").getSingleResult();
|
var userMapping = this.query("findById", member, "User").getSingleResult();
|
||||||
|
LOGGER.debug(userMapping.getExternalId());
|
||||||
|
LOGGER.debug(userMapping.getId());
|
||||||
groupMember.setValue(userMapping.getExternalId());
|
groupMember.setValue(userMapping.getExternalId());
|
||||||
var ref = new URI(String.format("Users/%s", userMapping.getExternalId()));
|
var ref = new URI(String.format("Users/%s", userMapping.getExternalId()));
|
||||||
groupMember.setRef(ref.toString());
|
groupMember.setRef(ref.toString());
|
||||||
groupMembers.add(groupMember);
|
group.addMember(groupMember);
|
||||||
} catch (Exception e) {
|
} catch (NoResultException e) {
|
||||||
LOGGER.error(e);
|
LOGGER.warnf("member %s not found for group %s", member, id);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
LOGGER.warnf("bad ref uri");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
group.setMembers(groupMembers);
|
|
||||||
}
|
}
|
||||||
if (addMeta) {
|
var meta = new Meta();
|
||||||
var meta = new Meta();
|
try {
|
||||||
try {
|
var uri = new URI("Groups/" + externalId);
|
||||||
var uri = new URI("Groups/" + externalId);
|
meta.setLocation(uri.toString());
|
||||||
meta.setLocation(uri.toString());
|
} catch (URISyntaxException e) {
|
||||||
} catch (URISyntaxException e) {
|
LOGGER.warn(e);
|
||||||
}
|
|
||||||
group.setMeta(meta);
|
|
||||||
}
|
}
|
||||||
|
group.setMeta(meta);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +118,9 @@ public class GroupAdapter extends Adapter<GroupModel, Group> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean tryToMap() {
|
public Boolean tryToMap() {
|
||||||
var group = session.groups().getGroupsStream(realm).filter(x -> x.getName() == displayName).findFirst();
|
var group = session.groups().getGroupsStream(realm).filter(
|
||||||
|
x -> StringUtils.equals(x.getName(), externalId) || StringUtils.equals(x.getName(), displayName))
|
||||||
|
.findFirst();
|
||||||
if (group.isPresent()) {
|
if (group.isPresent()) {
|
||||||
setId(group.get().getId());
|
setId(group.get().getId());
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -3,9 +3,9 @@ package sh.libre.scim.core;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import javax.persistence.NoResultException;
|
import jakarta.persistence.NoResultException;
|
||||||
import javax.ws.rs.ProcessingException;
|
import jakarta.ws.rs.ProcessingException;
|
||||||
|
|
||||||
import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
|
import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
|
||||||
import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
|
import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
|
||||||
|
@ -51,12 +51,11 @@ public class ScimClient {
|
||||||
switch (model.get("auth-mode")) {
|
switch (model.get("auth-mode")) {
|
||||||
case "BEARER":
|
case "BEARER":
|
||||||
defaultHeaders.put(HttpHeaders.AUTHORIZATION,
|
defaultHeaders.put(HttpHeaders.AUTHORIZATION,
|
||||||
BearerAuthentication(model.get("auth-pass")));
|
BearerAuthentication());
|
||||||
break;
|
break;
|
||||||
case "BASIC_AUTH":
|
case "BASIC_AUTH":
|
||||||
defaultHeaders.put(HttpHeaders.AUTHORIZATION,
|
defaultHeaders.put(HttpHeaders.AUTHORIZATION,
|
||||||
BasicAuthentication(model.get("auth-user"),
|
BasicAuthentication());
|
||||||
model.get("auth-pass")));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,10 +72,10 @@ public class ScimClient {
|
||||||
registry = RetryRegistry.of(retryConfig);
|
registry = RetryRegistry.of(retryConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String BasicAuthentication(String username ,String password) {
|
protected String BasicAuthentication() {
|
||||||
return BasicAuth.builder()
|
return BasicAuth.builder()
|
||||||
.username(model.get(username))
|
.username(model.get("auth-user"))
|
||||||
.password(model.get(password))
|
.password(model.get("auth-pass"))
|
||||||
.build()
|
.build()
|
||||||
.getAuthorizationHeaderValue();
|
.getAuthorizationHeaderValue();
|
||||||
}
|
}
|
||||||
|
@ -92,17 +91,10 @@ public class ScimClient {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String BearerAuthentication(String token) {
|
protected String BearerAuthentication() {
|
||||||
return "Bearer " + token ;
|
return "Bearer " + model.get("auth-pass") ;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String genScimUrl(String scimEndpoint,String resourcePath) {
|
|
||||||
return String.format("%s%s/%s", scimApplicationBaseUrl ,
|
|
||||||
scimEndpoint,
|
|
||||||
resourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected EntityManager getEM() {
|
protected EntityManager getEM() {
|
||||||
return session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
return session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||||
}
|
}
|
||||||
|
@ -136,8 +128,8 @@ public class ScimClient {
|
||||||
ServerResponse<S> response = retry.executeSupplier(() -> {
|
ServerResponse<S> response = retry.executeSupplier(() -> {
|
||||||
try {
|
try {
|
||||||
return scimRequestBuilder
|
return scimRequestBuilder
|
||||||
.create(adapter.getResourceClass(), String.format("/" + adapter.getSCIMEndpoint()))
|
.create(adapter.getResourceClass(), adapter.getSCIMEndpoint())
|
||||||
.setResource(adapter.toSCIM(false))
|
.setResource(adapter.toSCIM())
|
||||||
.sendRequest();
|
.sendRequest();
|
||||||
} catch ( ResponseException e) {
|
} catch ( ResponseException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
@ -166,9 +158,8 @@ public class ScimClient {
|
||||||
ServerResponse<S> response = retry.executeSupplier(() -> {
|
ServerResponse<S> response = retry.executeSupplier(() -> {
|
||||||
try {
|
try {
|
||||||
return scimRequestBuilder
|
return scimRequestBuilder
|
||||||
.update(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()),
|
.update(adapter.getResourceClass(), adapter.getSCIMEndpoint(), adapter.getExternalId())
|
||||||
adapter.getResourceClass())
|
.setResource(adapter.toSCIM())
|
||||||
.setResource(adapter.toSCIM(false))
|
|
||||||
.sendRequest() ;
|
.sendRequest() ;
|
||||||
} catch ( ResponseException e) {
|
} catch ( ResponseException e) {
|
||||||
|
|
||||||
|
@ -199,8 +190,7 @@ public class ScimClient {
|
||||||
|
|
||||||
ServerResponse<S> response = retry.executeSupplier(() -> {
|
ServerResponse<S> response = retry.executeSupplier(() -> {
|
||||||
try {
|
try {
|
||||||
return scimRequestBuilder.delete(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()),
|
return scimRequestBuilder.delete(adapter.getResourceClass(), adapter.getSCIMEndpoint(), adapter.getExternalId())
|
||||||
adapter.getResourceClass())
|
|
||||||
.sendRequest();
|
.sendRequest();
|
||||||
} catch (ResponseException e) {
|
} catch (ResponseException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
@ -247,7 +237,7 @@ public class ScimClient {
|
||||||
LOGGER.info("Import");
|
LOGGER.info("Import");
|
||||||
try {
|
try {
|
||||||
var adapter = getAdapter(aClass);
|
var adapter = getAdapter(aClass);
|
||||||
ServerResponse<ListResponse<S>> response = scimRequestBuilder.list("url",adapter.getResourceClass()).get().sendRequest();
|
ServerResponse<ListResponse<S>> response = scimRequestBuilder.list(adapter.getResourceClass(), adapter.getSCIMEndpoint()).get().sendRequest();
|
||||||
ListResponse<S> resourceTypeListResponse = response.getResource();
|
ListResponse<S> resourceTypeListResponse = response.getResource();
|
||||||
|
|
||||||
for (var resource : resourceTypeListResponse.getListedResources()) {
|
for (var resource : resourceTypeListResponse.getListedResources()) {
|
||||||
|
@ -287,9 +277,7 @@ public class ScimClient {
|
||||||
case "DELETE_REMOTE":
|
case "DELETE_REMOTE":
|
||||||
LOGGER.info("Delete remote resource");
|
LOGGER.info("Delete remote resource");
|
||||||
scimRequestBuilder
|
scimRequestBuilder
|
||||||
.delete(genScimUrl(adapter.getSCIMEndpoint(),
|
.delete(adapter.getResourceClass(), adapter.getSCIMEndpoint(), resource.getId().get())
|
||||||
resource.getId().get()),
|
|
||||||
adapter.getResourceClass())
|
|
||||||
.sendRequest();
|
.sendRequest();
|
||||||
syncRes.increaseRemoved();
|
syncRes.increaseRemoved();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package sh.libre.scim.core;
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -21,7 +23,8 @@ public class ScimDispatcher {
|
||||||
public void run(String scope, Consumer<ScimClient> f) {
|
public void run(String scope, Consumer<ScimClient> f) {
|
||||||
session.getContext().getRealm().getComponentsStream()
|
session.getContext().getRealm().getComponentsStream()
|
||||||
.filter((m) -> {
|
.filter((m) -> {
|
||||||
return ScimStorageProviderFactory.ID.equals(m.getProviderId()) && m.get("enabled", true)
|
return StringUtils.equals(ScimStorageProviderFactory.ID, m.getProviderId())
|
||||||
|
&& m.get("enabled", true)
|
||||||
&& m.get("propagation-" + scope, false);
|
&& m.get("propagation-" + scope, false);
|
||||||
})
|
})
|
||||||
.forEach(m -> runOne(m, f));
|
.forEach(m -> runOne(m, f));
|
||||||
|
|
|
@ -3,8 +3,10 @@ package sh.libre.scim.core;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import de.captaingoldfish.scim.sdk.common.resources.User;
|
import de.captaingoldfish.scim.sdk.common.resources.User;
|
||||||
|
@ -13,8 +15,7 @@ import de.captaingoldfish.scim.sdk.common.resources.complex.Name;
|
||||||
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
|
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
|
||||||
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
|
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
@ -26,6 +27,8 @@ public class UserAdapter extends Adapter<UserModel, User> {
|
||||||
private String email;
|
private String email;
|
||||||
private Boolean active;
|
private Boolean active;
|
||||||
private String[] roles;
|
private String[] roles;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
|
||||||
public UserAdapter(KeycloakSession session, String componentId) {
|
public UserAdapter(KeycloakSession session, String componentId) {
|
||||||
super(session, componentId, "User", Logger.getLogger(UserAdapter.class));
|
super(session, componentId, "User", Logger.getLogger(UserAdapter.class));
|
||||||
|
@ -79,6 +82,22 @@ public class UserAdapter extends Adapter<UserModel, User> {
|
||||||
this.roles = roles;
|
this.roles = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstName(String firstName) {
|
||||||
|
this.firstName = firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastName(String lastName) {
|
||||||
|
this.lastName = lastName;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<User> getResourceClass() {
|
public Class<User> getResourceClass() {
|
||||||
return User.class;
|
return User.class;
|
||||||
|
@ -96,19 +115,17 @@ public class UserAdapter extends Adapter<UserModel, User> {
|
||||||
setDisplayName(displayName);
|
setDisplayName(displayName);
|
||||||
setEmail(user.getEmail());
|
setEmail(user.getEmail());
|
||||||
setActive(user.isEnabled());
|
setActive(user.isEnabled());
|
||||||
|
setFirstName(user.getFirstName());
|
||||||
|
setLastName(user.getLastName());
|
||||||
var rolesSet = new HashSet<String>();
|
var rolesSet = new HashSet<String>();
|
||||||
user.getGroupsStream().flatMap(g -> g.getRoleMappingsStream())
|
user.getGroupsStream().flatMap(g -> g.getRoleMappingsStream())
|
||||||
.filter((r) -> r.getFirstAttribute("scim").equals("true")).map((r) -> r.getName())
|
.filter((r) -> StringUtils.equals(r.getFirstAttribute("scim"), "true"))
|
||||||
|
.map((r) -> r.getName())
|
||||||
|
.forEach(r -> rolesSet.add(r));
|
||||||
|
user.getRoleMappingsStream()
|
||||||
|
.filter((r) -> StringUtils.equals(r.getFirstAttribute("scim"), "true"))
|
||||||
|
.map((r) -> r.getName())
|
||||||
.forEach(r -> rolesSet.add(r));
|
.forEach(r -> rolesSet.add(r));
|
||||||
|
|
||||||
user.getRoleMappingsStream().filter((r) -> {
|
|
||||||
var attr = r.getFirstAttribute("scim");
|
|
||||||
if (attr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return attr.equals("true");
|
|
||||||
}).map((r) -> r.getName()).forEach(r -> rolesSet.add(r));
|
|
||||||
|
|
||||||
var roles = new String[rolesSet.size()];
|
var roles = new String[rolesSet.size()];
|
||||||
rolesSet.toArray(roles);
|
rolesSet.toArray(roles);
|
||||||
setRoles(roles);
|
setRoles(roles);
|
||||||
|
@ -127,30 +144,31 @@ public class UserAdapter extends Adapter<UserModel, User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User toSCIM(Boolean addMeta) {
|
public User toSCIM() {
|
||||||
var user = new User();
|
var user = new User();
|
||||||
user.setExternalId(id);
|
user.setExternalId(id);
|
||||||
user.setUserName(username);
|
user.setUserName(username);
|
||||||
user.setId(externalId);
|
user.setId(externalId);
|
||||||
user.setDisplayName(displayName);
|
user.setDisplayName(displayName);
|
||||||
Name name = new Name();
|
Name name = new Name();
|
||||||
|
name.setFamilyName(lastName);
|
||||||
|
name.setGivenName(firstName);
|
||||||
user.setName(name);
|
user.setName(name);
|
||||||
var emails = new ArrayList<Email>();
|
var emails = new ArrayList<Email>();
|
||||||
if (email != null) {
|
if (email != null) {
|
||||||
emails.add(
|
emails.add(
|
||||||
Email.builder().value(getEmail()).build());
|
Email.builder().value(getEmail()).build());
|
||||||
}
|
}
|
||||||
user.setEmails(emails);
|
user.setEmails(emails);
|
||||||
user.setActive(active);
|
user.setActive(active);
|
||||||
if (addMeta) {
|
var meta = new Meta();
|
||||||
var meta = new Meta();
|
try {
|
||||||
try {
|
var uri = new URI("Users/" + externalId);
|
||||||
var uri = new URI("Users/" + externalId);
|
meta.setLocation(uri.toString());
|
||||||
meta.setLocation(uri.toString());
|
} catch (URISyntaxException e) {
|
||||||
} catch (URISyntaxException e) {
|
LOGGER.warn(e);
|
||||||
}
|
|
||||||
user.setMeta(meta);
|
|
||||||
}
|
}
|
||||||
|
user.setMeta(meta);
|
||||||
List<PersonRole> roles = new ArrayList<PersonRole>();
|
List<PersonRole> roles = new ArrayList<PersonRole>();
|
||||||
for (var r : this.roles) {
|
for (var r : this.roles) {
|
||||||
var role = new PersonRole();
|
var role = new PersonRole();
|
||||||
|
@ -212,11 +230,13 @@ public class UserAdapter extends Adapter<UserModel, User> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getResourceStream() {
|
public Stream<UserModel> getResourceStream() {
|
||||||
return this.session.users().getUsersStream(this.session.getContext().getRealm());
|
Map<String, String> params = new HashMap<String, String>() {
|
||||||
|
};
|
||||||
|
return this.session.users().searchForUserStream(realm, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean skipRefresh() {
|
public Boolean skipRefresh() {
|
||||||
return getUsername().equals("admin");
|
return StringUtils.equals(getUsername(), "admin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package sh.libre.scim.event;
|
package sh.libre.scim.event;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.regex.*;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.events.Event;
|
import org.keycloak.events.Event;
|
||||||
import org.keycloak.events.EventListenerProvider;
|
import org.keycloak.events.EventListenerProvider;
|
||||||
|
@ -99,7 +100,7 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
var groupId = matcher.group(2);
|
var groupId = matcher.group(2);
|
||||||
LOGGER.infof("%s %s from %s", event.getOperationType(), userId, groupId);
|
LOGGER.infof("%s %s from %s", event.getOperationType(), userId, groupId);
|
||||||
var group = getGroup(groupId);
|
var group = getGroup(groupId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_GROUP, (client) -> client.replace(GroupAdapter.class, group));
|
group.setSingleAttribute("scim-dirty", "true");
|
||||||
var user = getUser(userId);
|
var user = getUser(userId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
||||||
}
|
}
|
||||||
|
@ -107,10 +108,10 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
var type = matcher.group(1);
|
var type = matcher.group(1);
|
||||||
var id = matcher.group(2);
|
var id = matcher.group(2);
|
||||||
LOGGER.infof("%s %s %s roles", event.getOperationType(), type, id);
|
LOGGER.infof("%s %s %s roles", event.getOperationType(), type, id);
|
||||||
if (type.equals("users")) {
|
if (StringUtils.equals(type, "users")) {
|
||||||
var user = getUser(id);
|
var user = getUser(id);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
||||||
} else if (type.equals("groups")) {
|
} else if (StringUtils.equals(type, "groups")) {
|
||||||
var group = getGroup(id);
|
var group = getGroup(id);
|
||||||
session.users().getGroupMembersStream(session.getContext().getRealm(), group).forEach(user -> {
|
session.users().getGroupMembersStream(session.getContext().getRealm(), group).forEach(user -> {
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package sh.libre.scim.jpa;
|
package sh.libre.scim.jpa;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import javax.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import javax.persistence.IdClass;
|
import jakarta.persistence.IdClass;
|
||||||
import javax.persistence.NamedQuery;
|
import jakarta.persistence.NamedQuery;
|
||||||
import javax.persistence.NamedQueries;
|
import jakarta.persistence.NamedQueries;
|
||||||
import javax.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@IdClass(ScimResourceId.class)
|
@IdClass(ScimResourceId.class)
|
||||||
|
|
|
@ -68,6 +68,7 @@ public class ScimResourceId implements Serializable {
|
||||||
if (!(other instanceof ScimResourceId))
|
if (!(other instanceof ScimResourceId))
|
||||||
return false;
|
return false;
|
||||||
var o = (ScimResourceId) other;
|
var o = (ScimResourceId) other;
|
||||||
|
// TODO
|
||||||
return (o.id == id &&
|
return (o.id == id &&
|
||||||
o.realmId == realmId &&
|
o.realmId == realmId &&
|
||||||
o.componentId == componentId &&
|
o.componentId == componentId &&
|
||||||
|
|
|
@ -3,8 +3,9 @@ package sh.libre.scim.storage;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -17,6 +18,7 @@ import org.keycloak.storage.UserStorageProviderFactory;
|
||||||
import org.keycloak.storage.UserStorageProviderModel;
|
import org.keycloak.storage.UserStorageProviderModel;
|
||||||
import org.keycloak.storage.user.ImportSynchronization;
|
import org.keycloak.storage.user.ImportSynchronization;
|
||||||
import org.keycloak.storage.user.SynchronizationResult;
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
import org.keycloak.timer.TimerProvider;
|
||||||
|
|
||||||
import sh.libre.scim.core.GroupAdapter;
|
import sh.libre.scim.core.GroupAdapter;
|
||||||
import sh.libre.scim.core.ScimDispatcher;
|
import sh.libre.scim.core.ScimDispatcher;
|
||||||
|
@ -34,6 +36,7 @@ public class ScimStorageProviderFactory
|
||||||
.property()
|
.property()
|
||||||
.name("endpoint")
|
.name("endpoint")
|
||||||
.type(ProviderConfigProperty.STRING_TYPE)
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.required(true)
|
||||||
.label("SCIM 2.0 endpoint")
|
.label("SCIM 2.0 endpoint")
|
||||||
.helpText("External SCIM 2.0 base " +
|
.helpText("External SCIM 2.0 base " +
|
||||||
"URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)")
|
"URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)")
|
||||||
|
@ -70,12 +73,14 @@ public class ScimStorageProviderFactory
|
||||||
.name("propagation-user")
|
.name("propagation-user")
|
||||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
.label("Enable user propagation")
|
.label("Enable user propagation")
|
||||||
|
.helpText("Should operation on users be propagated to this provider ?")
|
||||||
.defaultValue("true")
|
.defaultValue("true")
|
||||||
.add()
|
.add()
|
||||||
.property()
|
.property()
|
||||||
.name("propagation-group")
|
.name("propagation-group")
|
||||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
.label("Enable group propagation")
|
.label("Enable group propagation")
|
||||||
|
.helpText("Should operation on groups be propagated to this provider ?")
|
||||||
.defaultValue("true")
|
.defaultValue("true")
|
||||||
.add()
|
.add()
|
||||||
.property()
|
.property()
|
||||||
|
@ -127,10 +132,10 @@ public class ScimStorageProviderFactory
|
||||||
var realm = session.realms().getRealm(realmId);
|
var realm = session.realms().getRealm(realmId);
|
||||||
session.getContext().setRealm(realm);
|
session.getContext().setRealm(realm);
|
||||||
var dispatcher = new ScimDispatcher(session);
|
var dispatcher = new ScimDispatcher(session);
|
||||||
if (model.get("propagation-user").equals("true")) {
|
if (StringUtils.equals(model.get("propagation-user"), "true")) {
|
||||||
dispatcher.runOne(model, (client) -> client.sync(UserAdapter.class, result));
|
dispatcher.runOne(model, (client) -> client.sync(UserAdapter.class, result));
|
||||||
}
|
}
|
||||||
if (model.get("propagation-group").equals("true")) {
|
if (StringUtils.equals(model.get("propagation-group"), "true")) {
|
||||||
dispatcher.runOne(model, (client) -> client.sync(GroupAdapter.class, result));
|
dispatcher.runOne(model, (client) -> client.sync(GroupAdapter.class, result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,4 +152,27 @@ public class ScimStorageProviderFactory
|
||||||
return this.sync(sessionFactory, realmId, model);
|
return this.sync(sessionFactory, realmId, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
var timer = factory.create().getProvider(TimerProvider.class);
|
||||||
|
timer.scheduleTask(taskSession -> {
|
||||||
|
for (var realm : taskSession.realms().getRealmsStream().toList()) {
|
||||||
|
KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
|
||||||
|
@Override
|
||||||
|
public void run(KeycloakSession session) {
|
||||||
|
session.getContext().setRealm(realm);
|
||||||
|
var dispatcher = new ScimDispatcher(session);
|
||||||
|
for (var group : session.groups().getGroupsStream(realm)
|
||||||
|
.filter(x -> StringUtils.equals(x.getFirstAttribute("scim-dirty"), "true")).toList()) {
|
||||||
|
LOGGER.debug(group.getName() + " is dirty");
|
||||||
|
dispatcher.run(ScimDispatcher.SCOPE_GROUP,
|
||||||
|
(client) -> client.replace(GroupAdapter.class, group));
|
||||||
|
group.removeAttribute("scim-dirty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 30000, "scim-background");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue