From 4f1fb0673f1ce2f1e5644d9cf9a8cebd88b9b858 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Mon, 29 Apr 2024 16:34:25 +0200 Subject: [PATCH 1/3] Dbug --- Dockerfile | 2 +- pom.xml | 4 +- .../mappers/membership/MembershipType.java | 143 ++++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java diff --git a/Dockerfile b/Dockerfile index 21531c0..b95eed8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG TAG=23.0.1 +ARG TAG=24.0.3 FROM quay.io/keycloak/keycloak:${TAG} diff --git a/pom.xml b/pom.xml index e707fc0..97361de 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.scality.keycloak keycloak-extensions - 23.0.1-SNAPSHOT + 24.0.3-SNAPSHOT Keycloak: Scality Extensions Scality owned extensions to support multiple hostname @@ -45,7 +45,7 @@ 17 - 23.0.3 + 24.0.3 ${version.keycloak} diff --git a/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java b/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java new file mode 100644 index 0000000..017f297 --- /dev/null +++ b/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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. + */ + +package org.keycloak.storage.ldap.mappers.membership; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.storage.ldap.LDAPConfig; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPUtils; +import org.keycloak.storage.ldap.idm.model.LDAPDn; +import org.keycloak.storage.ldap.idm.model.LDAPObject; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Marek Posolda + */ +public enum MembershipType { + + /** + * Used if LDAP role has it's members declared in form of their full DN. For + * example ( "member: uid=john,ou=users,dc=example,dc=com" ) + */ + DN { + + private Logger logger = LoggerFactory.getLogger(MembershipType.class); + + @Override + public Set getLDAPSubgroups(CommonLDAPGroupMapper groupMapper, LDAPObject ldapGroup) { + CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); + return getLDAPMembersWithParent(groupMapper.getLdapProvider(), ldapGroup, + config.getMembershipLdapAttribute(), + LDAPDn.fromString(config.getLDAPGroupsDn()), config.getLDAPGroupNameLdapAttribute()); + } + + // Get just those members of specified group, which are descendants of + // "requiredParentDn" + protected Set getLDAPMembersWithParent(LDAPStorageProvider ldapProvider, LDAPObject ldapGroup, + String membershipLdapAttribute, LDAPDn requiredParentDn, String rdnAttr) { + Set allMemberships = LDAPUtils.getExistingMemberships(ldapProvider, membershipLdapAttribute, + ldapGroup); + + logger.warn("MembershipType allMemberships: " + allMemberships); + + // Filter and keep just descendants of requiredParentDn and with the correct RDN + Set result = new LinkedHashSet<>(); + for (String membership : allMemberships) { + LDAPDn childDn = LDAPDn.fromString(membership); + logger.warn("MembershipType getFirstRDN attr value: " + childDn.getFirstRdn().getAttrValue(rdnAttr)); + logger.warn("MembershipType is descendant: " + childDn.isDescendantOf(requiredParentDn)); + if (childDn.getFirstRdn().getAttrValue(rdnAttr) != null && childDn.isDescendantOf(requiredParentDn)) { + result.add(childDn); + } + } + return result; + } + + @Override + public List getGroupMembers(RealmModel realm, CommonLDAPGroupMapper groupMapper, + LDAPObject ldapGroup, int firstResult, int maxResults) { + LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider(); + CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); + + LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig(); + LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); + logger.warn("MembershipType usersDn: " + usersDn); + logger.warn("MembershipType getMembershipLdapAttribute: " + config.getMembershipLdapAttribute()); + logger.warn("MembershipType getRdnLdapAttribute: " + ldapConfig.getRdnLdapAttribute()); + logger.warn("MembershipType LDAPGroup: " + ldapGroup); + Set userDns = getLDAPMembersWithParent(ldapProvider, ldapGroup, config.getMembershipLdapAttribute(), + usersDn, ldapConfig.getRdnLdapAttribute()); + + logger.warn("MembershipType userDns: " + userDns); + + if (userDns == null || userDns.size() <= firstResult) { + return Collections.emptyList(); + } + + return ldapProvider.loadUsersByDNs(realm, userDns, firstResult, maxResults) + .collect(Collectors.toList()); + } + }, + + /** + * Used if LDAP role has it's members declared in form of pure user uids. For + * example ( "memberUid: john" ) + */ + UID { + + // Group inheritance not supported for this config + @Override + public Set getLDAPSubgroups(CommonLDAPGroupMapper groupMapper, LDAPObject ldapGroup) { + return Collections.emptySet(); + } + + @Override + public List getGroupMembers(RealmModel realm, CommonLDAPGroupMapper groupMapper, + LDAPObject ldapGroup, int firstResult, int maxResults) { + LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider(); + LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig(); + + String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute(); + Set memberUids = LDAPUtils.getExistingMemberships(ldapProvider, memberAttrName, ldapGroup); + + if (memberUids == null || memberUids.size() <= firstResult) { + return Collections.emptyList(); + } + + String membershipUserAttrName = groupMapper.getConfig().getMembershipUserLdapAttribute(ldapConfig); + + return ldapProvider + .loadUsersByUniqueAttribute(realm, membershipUserAttrName, memberUids, firstResult, maxResults) + .collect(Collectors.toList()); + } + }; + + public abstract Set getLDAPSubgroups(CommonLDAPGroupMapper groupMapper, LDAPObject ldapGroup); + + public abstract List getGroupMembers(RealmModel realm, CommonLDAPGroupMapper groupMapper, + LDAPObject ldapGroup, int firstResult, int maxResults); +} From 452959bca48e12ec85da6f36144735fe1c59ebc1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Mon, 29 Apr 2024 16:37:50 +0200 Subject: [PATCH 2/3] debug --- .../com/scality/keycloak/truststore/DBTruststoreProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/scality/keycloak/truststore/DBTruststoreProvider.java b/src/main/java/com/scality/keycloak/truststore/DBTruststoreProvider.java index 94512e9..d48bf71 100644 --- a/src/main/java/com/scality/keycloak/truststore/DBTruststoreProvider.java +++ b/src/main/java/com/scality/keycloak/truststore/DBTruststoreProvider.java @@ -20,7 +20,7 @@ import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; -import org.keycloak.truststore.HostnameVerificationPolicy; +import org.keycloak.common.enums.HostnameVerificationPolicy; import org.keycloak.truststore.TruststoreProvider; public class DBTruststoreProvider implements TruststoreProvider { From f396277d0e5679a01b502f9ea3eb7092e5b2b59f Mon Sep 17 00:00:00 2001 From: YanJin Date: Fri, 3 May 2024 18:40:45 +0200 Subject: [PATCH 3/3] Add ldap-groups to sync ldap group --- .../GroupWithLinkLDAPStorageMapper.java | 4 +- .../groupSync/GroupSyncAdminResource.java | 96 ++++++++++++ .../GroupSyncAdminResourceProvider.java | 42 +++++ .../mappers/membership/MembershipType.java | 143 ------------------ ...dmin.ext.AdminRealmResourceProviderFactory | 1 + 5 files changed, 142 insertions(+), 144 deletions(-) create mode 100644 src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResource.java create mode 100644 src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResourceProvider.java delete mode 100644 src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java diff --git a/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java b/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java index f3f17ca..da6e72e 100644 --- a/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java +++ b/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java @@ -29,10 +29,12 @@ private EntityManager getEntityManager() { protected GroupModel createKcGroup(RealmModel realm, String ldapGroupName, GroupModel parentGroup) { GroupModel groupModel = super.createKcGroup(realm, ldapGroupName, parentGroup); + EntityManager em = getEntityManager(); GroupFederationLinkEntity groupFederationLinkEntity = new GroupFederationLinkEntity(); groupFederationLinkEntity.setGroupId(groupModel.getId()); groupFederationLinkEntity.setFederationLink(ldapProvider.getModel().getId()); - getEntityManager().persist(groupFederationLinkEntity); + em.persist(groupFederationLinkEntity); + em.flush(); return groupModel; } diff --git a/src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResource.java b/src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResource.java new file mode 100644 index 0000000..4dfe2e7 --- /dev/null +++ b/src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResource.java @@ -0,0 +1,96 @@ +package com.scality.keycloak.groupSync; + +import java.util.HashMap; + +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.NoCache; +import org.keycloak.component.ComponentModel; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.ServicesLogger; +import org.keycloak.services.resources.KeycloakOpenAPI; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.services.resources.admin.UserStorageProviderResource; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; +import org.keycloak.storage.user.SynchronizationResult; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") +public class GroupSyncAdminResource { + protected static final Logger logger = Logger.getLogger(GroupSyncAdminResource.class); + protected final AdminPermissionEvaluator auth; + protected final KeycloakSession session; + protected final RealmModel realm; + + public GroupSyncAdminResource(KeycloakSession session, AdminPermissionEvaluator auth, + AdminEventBuilder adminEvent) { + this.session = session; + this.auth = auth; + this.realm = session.getContext().getRealm(); + } + + /** + * Trigger sync of mapper data related to ldap mapper (roles, groups, ...) + * + * direction is "fedToKeycloak" or "keycloakToFed" + * + * @return + */ + @POST + @Path("{parentId}/mappers/{id}/sync") + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public SynchronizationResult syncMapperData(@PathParam("parentId") String parentId, + @PathParam("id") String mapperId, @QueryParam("direction") String direction) { + auth.users().requireManage(); + + ComponentModel parentModel = realm.getComponent(parentId); + if (parentModel == null) + throw new NotFoundException("Parent model not found"); + ComponentModel mapperModel = realm.getComponent(mapperId); + if (mapperModel == null) + throw new NotFoundException("Mapper model not found"); + + LDAPStorageProvider ldapProvider = (LDAPStorageProvider) session.getProvider(UserStorageProvider.class, + parentModel); + LDAPStorageMapper mapper = session.getProvider(LDAPStorageMapper.class, mapperModel); + + ServicesLogger.LOGGER.syncingDataForMapper(mapperModel.getName(), mapperModel.getProviderId(), direction); + + SynchronizationResult syncResult; + if ("fedToKeycloak".equals(direction)) { + try { + syncResult = mapper.syncDataFromFederationProviderToKeycloak(realm); + } catch (Exception e) { + String errorMsg = UserStorageProviderResource.getErrorCode(e); + throw ErrorResponse.error(errorMsg, Response.Status.BAD_REQUEST); + } + } else if ("keycloakToFed".equals(direction)) { + try { + syncResult = mapper.syncDataFromKeycloakToFederationProvider(realm); + } catch (Exception e) { + String errorMsg = UserStorageProviderResource.getErrorCode(e); + throw ErrorResponse.error(errorMsg, Response.Status.BAD_REQUEST); + } + } else { + throw new BadRequestException("Unknown direction: " + direction); + } + + return syncResult; + } +} diff --git a/src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResourceProvider.java b/src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResourceProvider.java new file mode 100644 index 0000000..41ec2a1 --- /dev/null +++ b/src/main/java/com/scality/keycloak/groupSync/GroupSyncAdminResourceProvider.java @@ -0,0 +1,42 @@ +package com.scality.keycloak.groupSync; + +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; + +public class GroupSyncAdminResourceProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider { + + @Override + public AdminRealmResourceProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "ldap-groups"; + } + + @Override + public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, + AdminEventBuilder adminEvent) { + return new GroupSyncAdminResource(session, auth, adminEvent); + } + +} diff --git a/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java b/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java deleted file mode 100644 index 017f297..0000000 --- a/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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. - */ - -package org.keycloak.storage.ldap.mappers.membership; - -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.storage.ldap.LDAPConfig; -import org.keycloak.storage.ldap.LDAPStorageProvider; -import org.keycloak.storage.ldap.LDAPUtils; -import org.keycloak.storage.ldap.idm.model.LDAPDn; -import org.keycloak.storage.ldap.idm.model.LDAPObject; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Marek Posolda - */ -public enum MembershipType { - - /** - * Used if LDAP role has it's members declared in form of their full DN. For - * example ( "member: uid=john,ou=users,dc=example,dc=com" ) - */ - DN { - - private Logger logger = LoggerFactory.getLogger(MembershipType.class); - - @Override - public Set getLDAPSubgroups(CommonLDAPGroupMapper groupMapper, LDAPObject ldapGroup) { - CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); - return getLDAPMembersWithParent(groupMapper.getLdapProvider(), ldapGroup, - config.getMembershipLdapAttribute(), - LDAPDn.fromString(config.getLDAPGroupsDn()), config.getLDAPGroupNameLdapAttribute()); - } - - // Get just those members of specified group, which are descendants of - // "requiredParentDn" - protected Set getLDAPMembersWithParent(LDAPStorageProvider ldapProvider, LDAPObject ldapGroup, - String membershipLdapAttribute, LDAPDn requiredParentDn, String rdnAttr) { - Set allMemberships = LDAPUtils.getExistingMemberships(ldapProvider, membershipLdapAttribute, - ldapGroup); - - logger.warn("MembershipType allMemberships: " + allMemberships); - - // Filter and keep just descendants of requiredParentDn and with the correct RDN - Set result = new LinkedHashSet<>(); - for (String membership : allMemberships) { - LDAPDn childDn = LDAPDn.fromString(membership); - logger.warn("MembershipType getFirstRDN attr value: " + childDn.getFirstRdn().getAttrValue(rdnAttr)); - logger.warn("MembershipType is descendant: " + childDn.isDescendantOf(requiredParentDn)); - if (childDn.getFirstRdn().getAttrValue(rdnAttr) != null && childDn.isDescendantOf(requiredParentDn)) { - result.add(childDn); - } - } - return result; - } - - @Override - public List getGroupMembers(RealmModel realm, CommonLDAPGroupMapper groupMapper, - LDAPObject ldapGroup, int firstResult, int maxResults) { - LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider(); - CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); - - LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig(); - LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); - logger.warn("MembershipType usersDn: " + usersDn); - logger.warn("MembershipType getMembershipLdapAttribute: " + config.getMembershipLdapAttribute()); - logger.warn("MembershipType getRdnLdapAttribute: " + ldapConfig.getRdnLdapAttribute()); - logger.warn("MembershipType LDAPGroup: " + ldapGroup); - Set userDns = getLDAPMembersWithParent(ldapProvider, ldapGroup, config.getMembershipLdapAttribute(), - usersDn, ldapConfig.getRdnLdapAttribute()); - - logger.warn("MembershipType userDns: " + userDns); - - if (userDns == null || userDns.size() <= firstResult) { - return Collections.emptyList(); - } - - return ldapProvider.loadUsersByDNs(realm, userDns, firstResult, maxResults) - .collect(Collectors.toList()); - } - }, - - /** - * Used if LDAP role has it's members declared in form of pure user uids. For - * example ( "memberUid: john" ) - */ - UID { - - // Group inheritance not supported for this config - @Override - public Set getLDAPSubgroups(CommonLDAPGroupMapper groupMapper, LDAPObject ldapGroup) { - return Collections.emptySet(); - } - - @Override - public List getGroupMembers(RealmModel realm, CommonLDAPGroupMapper groupMapper, - LDAPObject ldapGroup, int firstResult, int maxResults) { - LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider(); - LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig(); - - String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute(); - Set memberUids = LDAPUtils.getExistingMemberships(ldapProvider, memberAttrName, ldapGroup); - - if (memberUids == null || memberUids.size() <= firstResult) { - return Collections.emptyList(); - } - - String membershipUserAttrName = groupMapper.getConfig().getMembershipUserLdapAttribute(ldapConfig); - - return ldapProvider - .loadUsersByUniqueAttribute(realm, membershipUserAttrName, memberUids, firstResult, maxResults) - .collect(Collectors.toList()); - } - }; - - public abstract Set getLDAPSubgroups(CommonLDAPGroupMapper groupMapper, LDAPObject ldapGroup); - - public abstract List getGroupMembers(RealmModel realm, CommonLDAPGroupMapper groupMapper, - LDAPObject ldapGroup, int firstResult, int maxResults); -} diff --git a/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory b/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory index 9d35fd3..b380cb1 100644 --- a/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory +++ b/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory @@ -1,2 +1,3 @@ com.scality.keycloak.truststore.TruststoreAdminResourceProvider com.scality.keycloak.groupFederationLink.GroupWithLinkAdminResourceProvider +com.scality.keycloak.groupSync.GroupSyncAdminResourceProvider \ No newline at end of file