parent
93bba8e338
commit
3c3f003a38
38 changed files with 4904 additions and 22 deletions
4
dependencies/server-all/pom.xml
vendored
4
dependencies/server-all/pom.xml
vendored
|
@ -48,6 +48,10 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-map-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-infinispan</artifactId>
|
||||
|
|
42
model/map-ldap/pom.xml
Normal file
42
model/map-ldap/pom.xml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
~ Copyright 2021 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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-model-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>18.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||
<name>Keycloak Model Map LDAP</name>
|
||||
<description/>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-map</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.QueryParameters;
|
||||
|
||||
public abstract class LdapMapKeycloakTransaction<RE, E extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<E, M> {
|
||||
|
||||
private boolean active;
|
||||
private boolean rollback;
|
||||
|
||||
public LdapMapKeycloakTransaction() {
|
||||
}
|
||||
|
||||
protected abstract static class MapTaskWithValue {
|
||||
public abstract void execute();
|
||||
}
|
||||
|
||||
protected abstract static class DeleteOperation extends MapTaskWithValue {
|
||||
}
|
||||
|
||||
protected final LinkedList<MapTaskWithValue> tasksOnRollback = new LinkedList<>();
|
||||
|
||||
protected final LinkedList<MapTaskWithValue> tasksOnCommit = new LinkedList<>();
|
||||
|
||||
protected final Map<String, RE> entities = new HashMap<>();
|
||||
|
||||
public long getCount(QueryParameters<M> queryParameters) {
|
||||
return read(queryParameters).count();
|
||||
}
|
||||
|
||||
public long delete(QueryParameters<M> queryParameters) {
|
||||
return read(queryParameters).map(m -> delete(m.getId()) ? 1 : 0).collect(Collectors.summarizingLong(val -> val)).getSum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
if (rollback) {
|
||||
throw new RuntimeException("Rollback only!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
rollback = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
return rollback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap;
|
||||
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||
import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag;
|
||||
|
||||
public class LdapMapStorageProvider implements MapStorageProvider {
|
||||
|
||||
private final LdapMapStorageProviderFactory factory;
|
||||
private final String sessionTxPrefix;
|
||||
|
||||
public LdapMapStorageProvider(LdapMapStorageProviderFactory factory, String sessionTxPrefix) {
|
||||
this.factory = factory;
|
||||
this.sessionTxPrefix = sessionTxPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, Flag... flags) {
|
||||
return session -> {
|
||||
MapKeycloakTransaction<V, M> sessionTx = session.getAttribute(sessionTxPrefix + modelType.hashCode(), MapKeycloakTransaction.class);
|
||||
if (sessionTx == null) {
|
||||
sessionTx = factory.createTransaction(session, modelType);
|
||||
session.setAttribute(sessionTxPrefix + modelType.hashCode(), sessionTx);
|
||||
}
|
||||
return sessionTx;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.AmphibianProviderFactory;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||
import org.keycloak.models.map.storage.MapStorageProviderFactory;
|
||||
import org.keycloak.models.map.storage.ldap.role.LdapRoleMapKeycloakTransaction;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public class LdapMapStorageProviderFactory implements
|
||||
AmphibianProviderFactory<MapStorageProvider>,
|
||||
MapStorageProviderFactory,
|
||||
EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "ldap-map-storage";
|
||||
private static final AtomicInteger SESSION_TX_PREFIX_ENUMERATOR = new AtomicInteger(0);
|
||||
private static final String SESSION_TX_PREFIX = "ldap-map-tx-";
|
||||
private final String sessionTxPrefixForFactoryInstance;
|
||||
|
||||
private Config.Scope config;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static final Map<Class<?>, LdapRoleMapKeycloakTransaction.LdapRoleMapKeycloakTransactionFunction<KeycloakSession, Config.Scope, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
|
||||
static {
|
||||
MODEL_TO_TX.put(RoleModel.class, LdapRoleMapKeycloakTransaction::new);
|
||||
}
|
||||
|
||||
public LdapMapStorageProviderFactory() {
|
||||
sessionTxPrefixForFactoryInstance = SESSION_TX_PREFIX + SESSION_TX_PREFIX_ENUMERATOR.getAndIncrement() + "-";
|
||||
}
|
||||
|
||||
public <M, V extends AbstractEntity> MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session, Class<M> modelType) {
|
||||
return MODEL_TO_TX.get(modelType).apply(session, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapStorageProvider create(KeycloakSession session) {
|
||||
return new LdapMapStorageProvider(this, sessionTxPrefixForFactoryInstance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "LDAP Map Storage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap;
|
||||
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
|
||||
import org.keycloak.models.map.storage.ldap.store.LdapMapUtil;
|
||||
import org.keycloak.models.map.storage.ldap.store.LdapMapEscapeStrategy;
|
||||
import org.keycloak.models.map.storage.ldap.store.LdapMapOctetStringEncoder;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Abstract class containing methods common to all Ldap*ModelCriteriaBuilder implementations
|
||||
*
|
||||
* @param <E> Entity
|
||||
* @param <M> Model
|
||||
* @param <Self> specific implementation of this class
|
||||
*/
|
||||
public abstract class LdapModelCriteriaBuilder<E, M, Self extends LdapModelCriteriaBuilder<E, M, Self>> implements ModelCriteriaBuilder<M, Self> {
|
||||
|
||||
private final Function<Supplier<StringBuilder>, Self> instantiator;
|
||||
private Supplier<StringBuilder> predicateFunc = null;
|
||||
|
||||
public LdapModelCriteriaBuilder(Function<Supplier<StringBuilder>, Self> instantiator) {
|
||||
this.instantiator = instantiator;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Self and(Self... builders) {
|
||||
return instantiator.apply(() -> {
|
||||
StringBuilder filter = new StringBuilder();
|
||||
for (Self builder : builders) {
|
||||
filter.append(builder.getPredicateFunc().get());
|
||||
}
|
||||
if (filter.length() > 0) {
|
||||
filter.insert(0, "(&");
|
||||
filter.append(")");
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Self or(Self... builders) {
|
||||
return instantiator.apply(() -> {
|
||||
StringBuilder filter = new StringBuilder();
|
||||
filter.append("(|");
|
||||
for (Self builder : builders) {
|
||||
filter.append(builder.getPredicateFunc().get());
|
||||
}
|
||||
filter.append(")");
|
||||
return filter;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Self not(Self builder) {
|
||||
return instantiator.apply(() -> {
|
||||
StringBuilder filter = new StringBuilder();
|
||||
filter.append("(!");
|
||||
filter.append(builder.getPredicateFunc().get());
|
||||
filter.append(")");
|
||||
return filter;
|
||||
});
|
||||
}
|
||||
|
||||
public Supplier<StringBuilder> getPredicateFunc() {
|
||||
return predicateFunc;
|
||||
}
|
||||
|
||||
public LdapModelCriteriaBuilder(Function<Supplier<StringBuilder>, Self> instantiator,
|
||||
Supplier<StringBuilder> predicateFunc) {
|
||||
this.instantiator = instantiator;
|
||||
this.predicateFunc = predicateFunc;
|
||||
}
|
||||
|
||||
protected StringBuilder equal(String field, Object value, LdapMapEscapeStrategy ldapMapEscapeStrategy, boolean isBinary) {
|
||||
Object parameterValue = value;
|
||||
if (value instanceof Date) {
|
||||
parameterValue = LdapMapUtil.formatDate((Date) parameterValue);
|
||||
}
|
||||
|
||||
String escaped = new LdapMapOctetStringEncoder(ldapMapEscapeStrategy).encode(parameterValue, isBinary);
|
||||
|
||||
return new StringBuilder().append("(").append(field).append(LDAPConstants.EQUAL).append(escaped).append(")");
|
||||
}
|
||||
|
||||
protected StringBuilder in(String name, Object[] valuesToCompare, boolean isBinary) {
|
||||
StringBuilder filter = new StringBuilder();
|
||||
filter.append("(|(");
|
||||
|
||||
for (Object o : valuesToCompare) {
|
||||
Object value = new LdapMapOctetStringEncoder().encode(o, false);
|
||||
|
||||
filter.append("(").append(name).append(LDAPConstants.EQUAL).append(value).append(")");
|
||||
}
|
||||
|
||||
filter.append("))");
|
||||
return filter;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap;
|
||||
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.StringKeyConvertor;
|
||||
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class MapModelCriteriaBuilderAssumingEqualForField<K, V extends AbstractEntity, M> extends MapModelCriteriaBuilder<K, V, M> {
|
||||
|
||||
private final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
|
||||
private final StringKeyConvertor<K> keyConvertor;
|
||||
private final SearchableModelField<? super M> modelFieldThatShouldCompareToTrueForEqual;
|
||||
|
||||
public MapModelCriteriaBuilderAssumingEqualForField(StringKeyConvertor<K> keyConvertor, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, SearchableModelField<? super M> modelFieldThatShouldCompareToTrueForEqual) {
|
||||
this(keyConvertor, fieldPredicates, ALWAYS_TRUE, ALWAYS_TRUE, modelFieldThatShouldCompareToTrueForEqual);
|
||||
}
|
||||
|
||||
protected MapModelCriteriaBuilderAssumingEqualForField(StringKeyConvertor<K> keyConvertor, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, Predicate<? super K> indexReadFilter, Predicate<? super V> sequentialReadFilter, SearchableModelField<? super M> modelFieldThatShouldCompareToTrueForEqual) {
|
||||
super(keyConvertor, fieldPredicates, indexReadFilter, sequentialReadFilter);
|
||||
this.keyConvertor = keyConvertor;
|
||||
this.modelFieldThatShouldCompareToTrueForEqual = modelFieldThatShouldCompareToTrueForEqual;
|
||||
this.fieldPredicates = fieldPredicates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapModelCriteriaBuilder<K, V, M> compare(SearchableModelField<? super M> modelField, Operator op, Object... values) {
|
||||
if (modelField == modelFieldThatShouldCompareToTrueForEqual && op == Operator.EQ) {
|
||||
return instantiateNewInstance(
|
||||
keyConvertor,
|
||||
fieldPredicates,
|
||||
ALWAYS_TRUE,
|
||||
ALWAYS_TRUE);
|
||||
}
|
||||
return super.compare(modelField, op, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapModelCriteriaBuilder<K, V, M> instantiateNewInstance(StringKeyConvertor<K> keyConvertor, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, Predicate<? super K> indexReadFilter, Predicate<? super V> sequentialReadFilter) {
|
||||
return new MapModelCriteriaBuilderAssumingEqualForField<>(keyConvertor, fieldPredicates, indexReadFilter, sequentialReadFilter, modelFieldThatShouldCompareToTrueForEqual);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.models.map.storage.ldap.config;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public abstract class LdapMapCommonGroupMapperConfig {
|
||||
|
||||
// Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
|
||||
public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
|
||||
|
||||
// See docs for MembershipType enum
|
||||
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
|
||||
|
||||
// Used just for membershipType=UID. Name of LDAP attribute on user, which is used for membership mappings. Usually it will be "uid"
|
||||
public static final String MEMBERSHIP_USER_LDAP_ATTRIBUTE = "membership.user.ldap.attribute";
|
||||
|
||||
// See docs for Mode enum
|
||||
public static final String MODE = "mode";
|
||||
|
||||
// See docs for UserRolesRetrieveStrategy enum
|
||||
public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy";
|
||||
|
||||
// Used just for UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute. It's the name of the attribute on LDAP user, which is used to track the groups which user is member.
|
||||
// Usually it will "memberof"
|
||||
public static final String MEMBEROF_LDAP_ATTRIBUTE = "memberof.ldap.attribute";
|
||||
|
||||
|
||||
protected final ComponentModel mapperModel;
|
||||
|
||||
public LdapMapCommonGroupMapperConfig(ComponentModel mapperModel) {
|
||||
this.mapperModel = mapperModel;
|
||||
}
|
||||
|
||||
public String getMembershipLdapAttribute() {
|
||||
String membershipAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_LDAP_ATTRIBUTE);
|
||||
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||
}
|
||||
|
||||
public String getMembershipUserLdapAttribute(LdapMapConfig ldapMapConfig) {
|
||||
String membershipUserAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_USER_LDAP_ATTRIBUTE);
|
||||
return membershipUserAttrName!=null ? membershipUserAttrName : ldapMapConfig.getUsernameLdapAttribute();
|
||||
}
|
||||
|
||||
public String getMemberOfLdapAttribute() {
|
||||
String memberOfLdapAttrName = mapperModel.getConfig().getFirst(MEMBEROF_LDAP_ATTRIBUTE);
|
||||
return memberOfLdapAttrName!=null ? memberOfLdapAttrName : LDAPConstants.MEMBER_OF;
|
||||
}
|
||||
|
||||
protected Set<String> getConfigValues(String str) {
|
||||
String[] objClasses = str.split(",");
|
||||
Set<String> trimmed = new HashSet<>();
|
||||
for (String objectClass : objClasses) {
|
||||
objectClass = objectClass.trim();
|
||||
if (objectClass.length() > 0) {
|
||||
trimmed.add(objectClass);
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
public abstract String getLDAPGroupNameLdapAttribute();
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.config;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.map.storage.ldap.role.config.LdapMapRoleMapperConfig;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
|
||||
import javax.naming.directory.SearchControls;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
public class LdapMapConfig {
|
||||
private final MultivaluedHashMap<String, String> config;
|
||||
|
||||
public LdapMapConfig(Config.Scope config) {
|
||||
this.config = hm(config);
|
||||
}
|
||||
|
||||
private static MultivaluedHashMap<String, String> hm(Config.Scope config) {
|
||||
return new MultivaluedHashMap<String, String>() {
|
||||
@Override
|
||||
public String getFirst(String key) {
|
||||
return config.get(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// from: RoleMapperConfig
|
||||
public Collection<String> getRoleObjectClasses() {
|
||||
String objectClasses = config.getFirst(LdapMapRoleMapperConfig.ROLE_OBJECT_CLASSES);
|
||||
if (objectClasses == null) {
|
||||
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
||||
objectClasses = isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
}
|
||||
|
||||
return getConfigValues(objectClasses);
|
||||
}
|
||||
|
||||
// from: RoleMapperConfig
|
||||
protected Set<String> getConfigValues(String str) {
|
||||
String[] objClasses = str.split(",");
|
||||
Set<String> trimmed = new HashSet<>();
|
||||
for (String objectClass : objClasses) {
|
||||
objectClass = objectClass.trim();
|
||||
if (objectClass.length() > 0) {
|
||||
trimmed.add(objectClass);
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private final Set<String> binaryAttributeNames = new HashSet<>();
|
||||
|
||||
public String getConnectionUrl() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_URL);
|
||||
}
|
||||
|
||||
public String getFactoryName() {
|
||||
// hardcoded for now
|
||||
return "com.sun.jndi.ldap.LdapCtxFactory";
|
||||
}
|
||||
|
||||
public String getAuthType() {
|
||||
String value = config.getFirst(LDAPConstants.AUTH_TYPE);
|
||||
if (value == null) {
|
||||
return LDAPConstants.AUTH_TYPE_SIMPLE;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean useExtendedPasswordModifyOp() {
|
||||
String value = config.getFirst(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP);
|
||||
return Boolean.parseBoolean(value);
|
||||
}
|
||||
|
||||
public String getUseTruststoreSpi() {
|
||||
return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
|
||||
}
|
||||
|
||||
public String getUsersDn() {
|
||||
String usersDn = config.getFirst(LDAPConstants.USERS_DN);
|
||||
|
||||
if (usersDn == null) {
|
||||
// Just for the backwards compatibility 1.2 -> 1.3 . Should be removed later.
|
||||
usersDn = config.getFirst("userDnSuffix");
|
||||
}
|
||||
|
||||
return usersDn;
|
||||
}
|
||||
|
||||
public Collection<String> getUserObjectClasses() {
|
||||
String objClassesCfg = config.getFirst(LDAPConstants.USER_OBJECT_CLASSES);
|
||||
String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson,organizationalPerson";
|
||||
|
||||
String[] objectClasses = objClassesStr.split(",");
|
||||
|
||||
// Trim them
|
||||
Set<String> userObjClasses = new HashSet<>();
|
||||
for (String objectClass : objectClasses) {
|
||||
userObjClasses.add(objectClass.trim());
|
||||
}
|
||||
return userObjClasses;
|
||||
}
|
||||
|
||||
public String getBindDN() {
|
||||
return config.getFirst(LDAPConstants.BIND_DN);
|
||||
}
|
||||
|
||||
public String getBindCredential() {
|
||||
return config.getFirst(LDAPConstants.BIND_CREDENTIAL);
|
||||
}
|
||||
|
||||
public String getVendor() {
|
||||
return config.getFirst(LDAPConstants.VENDOR);
|
||||
}
|
||||
|
||||
public boolean isActiveDirectory() {
|
||||
String vendor = getVendor();
|
||||
return vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
|
||||
}
|
||||
|
||||
public boolean isValidatePasswordPolicy() {
|
||||
String validatePPolicy = config.getFirst(LDAPConstants.VALIDATE_PASSWORD_POLICY);
|
||||
return Boolean.parseBoolean(validatePPolicy);
|
||||
}
|
||||
|
||||
public boolean isTrustEmail(){
|
||||
String trustEmail = config.getFirst(LDAPConstants.TRUST_EMAIL);
|
||||
return Boolean.parseBoolean(trustEmail);
|
||||
}
|
||||
|
||||
public String getConnectionPooling() {
|
||||
if(isStartTls()) {
|
||||
return null;
|
||||
} else {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING);
|
||||
}
|
||||
}
|
||||
|
||||
public String getConnectionPoolingAuthentication() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_AUTHENTICATION);
|
||||
}
|
||||
|
||||
public String getConnectionPoolingDebug() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_DEBUG);
|
||||
}
|
||||
|
||||
public String getConnectionPoolingInitSize() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_INITSIZE);
|
||||
}
|
||||
|
||||
public String getConnectionPoolingMaxSize() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_MAXSIZE);
|
||||
}
|
||||
|
||||
public String getConnectionPoolingPrefSize() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_PREFSIZE);
|
||||
}
|
||||
|
||||
public String getConnectionPoolingProtocol() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_PROTOCOL);
|
||||
}
|
||||
|
||||
public String getConnectionPoolingTimeout() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_POOLING_TIMEOUT);
|
||||
}
|
||||
|
||||
public String getConnectionTimeout() {
|
||||
return config.getFirst(LDAPConstants.CONNECTION_TIMEOUT);
|
||||
}
|
||||
|
||||
public String getReadTimeout() {
|
||||
return config.getFirst(LDAPConstants.READ_TIMEOUT);
|
||||
}
|
||||
|
||||
public Properties getAdditionalConnectionProperties() {
|
||||
// not supported for now
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getSearchScope() {
|
||||
String searchScope = config.getFirst(LDAPConstants.SEARCH_SCOPE);
|
||||
return searchScope == null ? SearchControls.SUBTREE_SCOPE : Integer.parseInt(searchScope);
|
||||
}
|
||||
|
||||
public String getUuidLDAPAttributeName() {
|
||||
String uuidAttrName = config.getFirst(LDAPConstants.UUID_LDAP_ATTRIBUTE);
|
||||
if (uuidAttrName == null) {
|
||||
// Differences of unique attribute among various vendors
|
||||
String vendor = getVendor();
|
||||
uuidAttrName = LDAPConstants.getUuidAttributeName(vendor);
|
||||
}
|
||||
|
||||
return uuidAttrName;
|
||||
}
|
||||
|
||||
public boolean isObjectGUID() {
|
||||
return getUuidLDAPAttributeName().equalsIgnoreCase(LDAPConstants.OBJECT_GUID);
|
||||
}
|
||||
|
||||
public boolean isEdirectoryGUID() {
|
||||
return isEdirectory() && getUuidLDAPAttributeName().equalsIgnoreCase(LDAPConstants.NOVELL_EDIRECTORY_GUID);
|
||||
}
|
||||
|
||||
public boolean isPagination() {
|
||||
String pagination = config.getFirst(LDAPConstants.PAGINATION);
|
||||
return Boolean.parseBoolean(pagination);
|
||||
}
|
||||
|
||||
public int getBatchSizeForSync() {
|
||||
String pageSizeConfig = config.getFirst(LDAPConstants.BATCH_SIZE_FOR_SYNC);
|
||||
return pageSizeConfig!=null ? Integer.parseInt(pageSizeConfig) : LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
|
||||
}
|
||||
|
||||
public String getUsernameLdapAttribute() {
|
||||
String username = config.getFirst(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
|
||||
if (username == null) {
|
||||
username = isActiveDirectory() ? LDAPConstants.CN : LDAPConstants.UID;
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getRdnLdapAttribute() {
|
||||
String rdn = config.getFirst(LDAPConstants.RDN_LDAP_ATTRIBUTE);
|
||||
if (rdn == null) {
|
||||
rdn = getUsernameLdapAttribute();
|
||||
|
||||
if (rdn.equalsIgnoreCase(LDAPConstants.SAM_ACCOUNT_NAME)) {
|
||||
// Just for the backwards compatibility 1.2 -> 1.3 . Should be removed later.
|
||||
rdn = LDAPConstants.CN;
|
||||
}
|
||||
|
||||
}
|
||||
return rdn;
|
||||
}
|
||||
|
||||
|
||||
public String getCustomUserSearchFilter() {
|
||||
String customFilter = config.getFirst(LDAPConstants.CUSTOM_USER_SEARCH_FILTER);
|
||||
if (customFilter != null) {
|
||||
customFilter = customFilter.trim();
|
||||
if (customFilter.length() > 0) {
|
||||
return customFilter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isStartTls() {
|
||||
return Boolean.parseBoolean(config.getFirst(LDAPConstants.START_TLS));
|
||||
}
|
||||
|
||||
public UserStorageProvider.EditMode getEditMode() {
|
||||
String editModeString = config.getFirst(LDAPConstants.EDIT_MODE);
|
||||
if (editModeString == null) {
|
||||
return UserStorageProvider.EditMode.READ_ONLY;
|
||||
} else {
|
||||
return UserStorageProvider.EditMode.valueOf(editModeString);
|
||||
}
|
||||
}
|
||||
|
||||
public void addBinaryAttribute(String attrName) {
|
||||
binaryAttributeNames.add(attrName);
|
||||
}
|
||||
|
||||
public Set<String> getBinaryAttributeNames() {
|
||||
return binaryAttributeNames;
|
||||
}
|
||||
|
||||
|
||||
public boolean isEdirectory() {
|
||||
return LDAPConstants.VENDOR_NOVELL_EDIRECTORY.equalsIgnoreCase(getVendor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<>(config);
|
||||
copy.remove(LDAPConstants.BIND_CREDENTIAL);
|
||||
return copy + ", binaryAttributes: " + binaryAttributeNames;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* 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.models.map.storage.ldap.model;
|
||||
|
||||
import javax.naming.ldap.Rdn;
|
||||
import java.util.Collection;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LdapMapDn {
|
||||
|
||||
private static final Pattern DN_PATTERN = Pattern.compile("(?<!\\\\),");
|
||||
private static final Pattern ENTRY_PATTERN = Pattern.compile("(?<!\\\\)\\+");
|
||||
private static final Pattern SUB_ENTRY_PATTERN = Pattern.compile("(?<!\\\\)=");
|
||||
|
||||
private final Deque<RDN> entries;
|
||||
|
||||
private LdapMapDn() {
|
||||
this.entries = new LinkedList<>();
|
||||
}
|
||||
|
||||
private LdapMapDn(Deque<RDN> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
public static LdapMapDn fromString(String dnString) {
|
||||
LdapMapDn dn = new LdapMapDn();
|
||||
|
||||
// In certain OpenLDAP implementations the uniqueMember attribute is mandatory
|
||||
// Thus, if a new group is created, it will contain an empty uniqueMember attribute
|
||||
// Later on, when adding members, this empty attribute will be kept
|
||||
// Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
|
||||
if(dnString.trim().isEmpty())
|
||||
return dn;
|
||||
|
||||
String[] rdns = DN_PATTERN.split(dnString);
|
||||
for (String entryStr : rdns) {
|
||||
if (entryStr.indexOf('+') == -1) {
|
||||
// This is 99.9% of cases where RDN consists of single key-value pair
|
||||
SubEntry subEntry = parseSingleSubEntry(dn, entryStr);
|
||||
dn.addLast(new RDN(subEntry));
|
||||
} else {
|
||||
// This is 0.1% of cases where RDN consists of more key-value pairs like "uid=foo+cn=bar"
|
||||
String[] subEntries = ENTRY_PATTERN.split(entryStr);
|
||||
RDN entry = new RDN();
|
||||
for (String subEntryStr : subEntries) {
|
||||
SubEntry subEntry = parseSingleSubEntry(dn, subEntryStr);
|
||||
entry.addSubEntry(subEntry);
|
||||
}
|
||||
dn.addLast(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return dn;
|
||||
}
|
||||
|
||||
// parse single sub-entry and add it to the "dn" . Assumption is that subentry is something like "uid=bar" and does not contain + character
|
||||
private static SubEntry parseSingleSubEntry(LdapMapDn dn, String subEntryStr) {
|
||||
String[] rdn = SUB_ENTRY_PATTERN.split(subEntryStr);
|
||||
if (rdn.length >1) {
|
||||
return new SubEntry(rdn[0].trim(), rdn[1].trim());
|
||||
} else {
|
||||
return new SubEntry(rdn[0].trim(), "");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof LdapMapDn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return toString().equals(obj.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(entries);
|
||||
}
|
||||
|
||||
private static String toString(Collection<RDN> entries) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
boolean first = true;
|
||||
for (RDN rdn : entries) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(",");
|
||||
}
|
||||
builder.append(rdn.toString());
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return first entry. Usually entry corresponding to something like "uid=joe" from the DN like "uid=joe,dc=something,dc=org"
|
||||
*/
|
||||
public RDN getFirstRdn() {
|
||||
return entries.getFirst();
|
||||
}
|
||||
|
||||
private static String unescapeValue(String escaped) {
|
||||
// Something needed to handle non-String types?
|
||||
return Rdn.unescapeValue(escaped).toString();
|
||||
}
|
||||
|
||||
private static String escapeValue(String unescaped) {
|
||||
// Something needed to handle non-String types?
|
||||
return Rdn.escapeValue(unescaped);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return DN like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org".
|
||||
* Returned DN will be new clone not related to the original DN instance.
|
||||
*
|
||||
*/
|
||||
public LdapMapDn getParentDn() {
|
||||
LinkedList<RDN> parentDnEntries = new LinkedList<>(entries);
|
||||
parentDnEntries.remove();
|
||||
return new LdapMapDn(parentDnEntries);
|
||||
}
|
||||
|
||||
public boolean isDescendantOf(LdapMapDn expectedParentDn) {
|
||||
int parentEntriesCount = expectedParentDn.entries.size();
|
||||
|
||||
Deque<RDN> myEntries = new LinkedList<>(this.entries);
|
||||
boolean someRemoved = false;
|
||||
while (myEntries.size() > parentEntriesCount) {
|
||||
myEntries.removeFirst();
|
||||
someRemoved = true;
|
||||
}
|
||||
|
||||
String myEntriesParentStr = toString(myEntries).toLowerCase();
|
||||
String expectedParentDnStr = expectedParentDn.toString().toLowerCase();
|
||||
return someRemoved && myEntriesParentStr.equals(expectedParentDnStr);
|
||||
}
|
||||
|
||||
public void addFirst(String rdnName, String rdnValue) {
|
||||
rdnValue = escapeValue(rdnValue);
|
||||
entries.addFirst(new RDN(new SubEntry(rdnName, rdnValue)));
|
||||
}
|
||||
|
||||
public void addFirst(RDN entry) {
|
||||
entries.addFirst(entry);
|
||||
}
|
||||
|
||||
private void addLast(RDN entry) {
|
||||
entries.addLast(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Single RDN inside the DN. RDN usually consists of single item like "uid=john" . In some rare cases, it can have multiple
|
||||
* sub-entries like "uid=john+sn=Doe"
|
||||
*/
|
||||
public static class RDN {
|
||||
|
||||
private final List<SubEntry> subs = new LinkedList<>();
|
||||
|
||||
private RDN() {
|
||||
}
|
||||
|
||||
private RDN(SubEntry subEntry) {
|
||||
subs.add(subEntry);
|
||||
}
|
||||
|
||||
private void addSubEntry(SubEntry subEntry) {
|
||||
subs.add(subEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Keys in the RDN. Returned list is the copy, which is not linked to the original RDN
|
||||
*/
|
||||
public List<String> getAllKeys() {
|
||||
return subs.stream().map(SubEntry::getAttrName).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assume that RDN is something like "uid=john", then this method will return "john" in case that attrName is "uid" .
|
||||
* This is useful in case that RDN is multi-key - something like "uid=john+cn=John Doe" and we want to return just "john" as the value of "uid"
|
||||
*
|
||||
* The returned value will be unescaped
|
||||
*
|
||||
*/
|
||||
public String getAttrValue(String attrName) {
|
||||
for (SubEntry sub : subs) {
|
||||
if (attrName.equalsIgnoreCase(sub.attrName)) {
|
||||
return LdapMapDn.unescapeValue(sub.attrValue);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setAttrValue(String attrName, String newAttrValue) {
|
||||
for (SubEntry sub : subs) {
|
||||
if (attrName.equalsIgnoreCase(sub.attrName)) {
|
||||
sub.attrValue = escapeValue(newAttrValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
addSubEntry(new SubEntry(attrName, escapeValue(newAttrValue)));
|
||||
}
|
||||
|
||||
public boolean removeAttrValue(String attrName) {
|
||||
SubEntry toRemove = null;
|
||||
for (SubEntry sub : subs) {
|
||||
if (attrName.equalsIgnoreCase(sub.attrName)) {
|
||||
toRemove = sub;
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove != null) {
|
||||
subs.remove(toRemove);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(true);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param escaped indicates whether return escaped or unescaped values. EG. "uid=john,comma" VS "uid=john\,comma"
|
||||
*/
|
||||
public String toString(boolean escaped) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
boolean first = true;
|
||||
for (SubEntry subEntry : subs) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append('+');
|
||||
}
|
||||
builder.append(subEntry.toString(escaped));
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SubEntry {
|
||||
private final String attrName;
|
||||
private String attrValue;
|
||||
|
||||
private SubEntry(String attrName, String attrValue) {
|
||||
this.attrName = attrName;
|
||||
this.attrValue = attrValue;
|
||||
}
|
||||
|
||||
private String getAttrName() {
|
||||
return attrName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(true);
|
||||
}
|
||||
|
||||
private String toString(boolean escaped) {
|
||||
String val = escaped ? attrValue : unescapeValue(attrValue);
|
||||
return attrName + '=' + val;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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.models.map.storage.ldap.model;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.EntityField;
|
||||
import org.keycloak.models.map.common.delegate.EntityFieldDelegate;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LdapMapObject implements AbstractEntity {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LdapMapObject.class);
|
||||
|
||||
private String id;
|
||||
private LdapMapDn dn;
|
||||
|
||||
// In most cases, there is single "rdnAttributeName" . Usually "uid" or "cn"
|
||||
private final List<String> rdnAttributeNames = new LinkedList<>();
|
||||
|
||||
private final List<String> objectClasses = new LinkedList<>();
|
||||
|
||||
// NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
|
||||
private final List<String> readOnlyAttributeNames = new LinkedList<>();
|
||||
|
||||
private final Map<String, Set<String>> attributes = new HashMap<>();
|
||||
|
||||
// Copy of "attributes" containing lower-cased keys
|
||||
private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
|
||||
|
||||
// range attributes are always read from 0 to max so just saving the top value
|
||||
private final Map<String, Integer> rangedAttributes = new HashMap<>();
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LdapMapDn getDn() {
|
||||
return dn;
|
||||
}
|
||||
|
||||
public void setDn(LdapMapDn dn) {
|
||||
this.dn = dn;
|
||||
}
|
||||
|
||||
public List<String> getObjectClasses() {
|
||||
return objectClasses;
|
||||
}
|
||||
|
||||
public void setObjectClasses(Collection<String> objectClasses) {
|
||||
this.objectClasses.clear();
|
||||
this.objectClasses.addAll(objectClasses);
|
||||
}
|
||||
|
||||
public List<String> getReadOnlyAttributeNames() {
|
||||
return readOnlyAttributeNames;
|
||||
}
|
||||
|
||||
public void addReadOnlyAttributeName(String readOnlyAttribute) {
|
||||
readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public void removeReadOnlyAttributeName(String readOnlyAttribute) {
|
||||
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public List<String> getRdnAttributeNames() {
|
||||
return rdnAttributeNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful when single value will be used as the "RDN" attribute. Which will be most of the cases
|
||||
*/
|
||||
public void setRdnAttributeName(String rdnAttributeName) {
|
||||
this.rdnAttributeNames.clear();
|
||||
this.rdnAttributeNames.add(rdnAttributeName);
|
||||
}
|
||||
|
||||
public void setRdnAttributeNames(List<String> rdnAttributeNames) {
|
||||
this.rdnAttributeNames.clear();
|
||||
this.rdnAttributeNames.addAll(rdnAttributeNames);
|
||||
}
|
||||
|
||||
public void addRdnAttributeName(String rdnAttributeName) {
|
||||
this.rdnAttributeNames.add(rdnAttributeName);
|
||||
}
|
||||
|
||||
public void setSingleAttribute(String attributeName, String attributeValue) {
|
||||
Set<String> asSet = new LinkedHashSet<>();
|
||||
asSet.add(attributeValue);
|
||||
setAttribute(attributeName, asSet);
|
||||
}
|
||||
|
||||
public void setAttribute(String attributeName, Set<String> attributeValue) {
|
||||
attributes.put(attributeName, attributeValue);
|
||||
lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
|
||||
}
|
||||
|
||||
// Case-insensitive
|
||||
public String getAttributeAsString(String name) {
|
||||
Set<String> attrValue = lowerCasedAttributes.get(name.toLowerCase());
|
||||
if (attrValue == null || attrValue.size() == 0) {
|
||||
return null;
|
||||
} else if (attrValue.size() > 1) {
|
||||
logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
|
||||
}
|
||||
|
||||
return attrValue.iterator().next();
|
||||
}
|
||||
|
||||
// Case-insensitive. Return null if there is not value of attribute with given name or set with all values otherwise
|
||||
public Set<String> getAttributeAsSet(String name) {
|
||||
Set<String> values = lowerCasedAttributes.get(name.toLowerCase());
|
||||
return (values == null) ? null : new LinkedHashSet<>(values);
|
||||
}
|
||||
|
||||
public boolean isRangeComplete(String name) {
|
||||
return !rangedAttributes.containsKey(name);
|
||||
}
|
||||
|
||||
public int getCurrentRange(String name) {
|
||||
return rangedAttributes.get(name);
|
||||
}
|
||||
|
||||
public boolean isRangeCompleteForAllAttributes() {
|
||||
return rangedAttributes.isEmpty();
|
||||
}
|
||||
|
||||
public void addRangedAttribute(String name, int max) {
|
||||
Integer current = rangedAttributes.get(name);
|
||||
if (current == null || max > current) {
|
||||
rangedAttributes.put(name, max);
|
||||
}
|
||||
}
|
||||
|
||||
public void populateRangedAttribute(LdapMapObject obj, String name) {
|
||||
Set<String> newValues = obj.getAttributes().get(name);
|
||||
if (newValues != null && attributes.containsKey(name)) {
|
||||
attributes.get(name).addAll(newValues);
|
||||
if (!obj.isRangeComplete(name)) {
|
||||
addRangedAttribute(name, obj.getCurrentRange(name));
|
||||
} else {
|
||||
rangedAttributes.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Set<String>> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getClass().isInstance(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LdapMapObject other = (LdapMapObject) obj;
|
||||
|
||||
return getId() != null && other.getId() != null && getId().equals(other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = getId() != null ? getId().hashCode() : 0;
|
||||
result = 31 * result + (getId() != null ? getId().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapMapObject [ dn: " + dn + " , id: " + id + ", attributes: " + attributes +
|
||||
", readOnly attribute names: " + readOnlyAttributeNames + ", ranges: " + rangedAttributes + " ]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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.models.map.storage.ldap.model;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.map.storage.ldap.LdapModelCriteriaBuilder;
|
||||
|
||||
import javax.naming.directory.SearchControls;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Collections.unmodifiableSet;
|
||||
|
||||
/**
|
||||
* Default IdentityQuery implementation.
|
||||
*
|
||||
* LDAPQuery should be closed after use in case that pagination was used (initPagination was called)
|
||||
* Closing LDAPQuery is very important in case ldapContextManager contains VaultSecret
|
||||
*
|
||||
* @author Shane Bryzak
|
||||
*/
|
||||
public class LdapMapQuery implements AutoCloseable {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LdapMapQuery.class);
|
||||
|
||||
private int offset;
|
||||
private int limit;
|
||||
private String searchDn;
|
||||
private LdapModelCriteriaBuilder<?, ?, ?> modelCriteriaBuilder;
|
||||
|
||||
private final Set<String> returningLdapAttributes = new LinkedHashSet<>();
|
||||
|
||||
// Contains just those returningLdapAttributes, which are read-only. They will be marked as read-only in returned LDAPObject instances as well
|
||||
// NOTE: names of attributes are lower-cased to avoid case sensitivity issues (LDAP searching is usually case-insensitive, so we want to be as well)
|
||||
private final Set<String> returningReadOnlyLdapAttributes = new LinkedHashSet<>();
|
||||
private final Set<String> objectClasses = new LinkedHashSet<>();
|
||||
|
||||
private final List<ComponentModel> mappers = new ArrayList<>();
|
||||
|
||||
private int searchScope = SearchControls.SUBTREE_SCOPE;
|
||||
|
||||
public void setSearchDn(String searchDn) {
|
||||
this.searchDn = searchDn;
|
||||
}
|
||||
|
||||
public void addObjectClasses(Collection<String> objectClasses) {
|
||||
this.objectClasses.addAll(objectClasses);
|
||||
}
|
||||
|
||||
public void addReturningLdapAttribute(String ldapAttributeName) {
|
||||
this.returningLdapAttributes.add(ldapAttributeName);
|
||||
}
|
||||
|
||||
public void addReturningReadOnlyLdapAttribute(String ldapAttributeName) {
|
||||
this.returningReadOnlyLdapAttributes.add(ldapAttributeName.toLowerCase());
|
||||
}
|
||||
|
||||
public LdapMapQuery addMappers(Collection<ComponentModel> mappers) {
|
||||
this.mappers.addAll(mappers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setSearchScope(int searchScope) {
|
||||
this.searchScope = searchScope;
|
||||
}
|
||||
|
||||
public String getSearchDn() {
|
||||
return this.searchDn;
|
||||
}
|
||||
|
||||
public Set<String> getObjectClasses() {
|
||||
return unmodifiableSet(this.objectClasses);
|
||||
}
|
||||
|
||||
public Set<String> getReturningLdapAttributes() {
|
||||
return unmodifiableSet(this.returningLdapAttributes);
|
||||
}
|
||||
|
||||
public Set<String> getReturningReadOnlyLdapAttributes() {
|
||||
return unmodifiableSet(this.returningReadOnlyLdapAttributes);
|
||||
}
|
||||
|
||||
public List<ComponentModel> getMappers() {
|
||||
return mappers;
|
||||
}
|
||||
|
||||
public int getSearchScope() {
|
||||
return searchScope;
|
||||
}
|
||||
|
||||
public int getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public int getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public LdapMapQuery setOffset(int offset) {
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LdapMapQuery setLimit(int limit) {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
public void setModelCriteriaBuilder(LdapModelCriteriaBuilder<?, ?, ?> ldapModelCriteriaBuilder) {
|
||||
this.modelCriteriaBuilder = ldapModelCriteriaBuilder;
|
||||
}
|
||||
|
||||
public LdapModelCriteriaBuilder<?, ?, ?> getModelCriteriaBuilder() {
|
||||
return modelCriteriaBuilder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,410 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.role;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.StreamUtils;
|
||||
import org.keycloak.models.map.common.StringKeyConvertor;
|
||||
import org.keycloak.models.map.role.MapRoleEntity;
|
||||
|
||||
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
|
||||
import org.keycloak.models.map.storage.QueryParameters;
|
||||
import org.keycloak.models.map.storage.chm.MapFieldPredicates;
|
||||
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
|
||||
import org.keycloak.models.map.storage.ldap.MapModelCriteriaBuilderAssumingEqualForField;
|
||||
import org.keycloak.models.map.storage.ldap.role.entity.LdapMapRoleEntityFieldDelegate;
|
||||
import org.keycloak.models.map.storage.ldap.store.LdapMapIdentityStore;
|
||||
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
|
||||
import org.keycloak.models.map.storage.ldap.LdapMapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapDn;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapObject;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapQuery;
|
||||
import org.keycloak.models.map.storage.ldap.role.config.LdapMapRoleMapperConfig;
|
||||
import org.keycloak.models.map.storage.ldap.role.entity.LdapRoleEntity;
|
||||
|
||||
import javax.naming.NamingException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class LdapRoleMapKeycloakTransaction extends LdapMapKeycloakTransaction<LdapMapRoleEntityFieldDelegate, MapRoleEntity, RoleModel> {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final StringKeyConvertor<String> keyConverter = new StringKeyConvertor.StringKey();
|
||||
private final Set<String> deletedKeys = new HashSet<>();
|
||||
private final LdapMapRoleMapperConfig roleMapperConfig;
|
||||
private final LdapMapConfig ldapMapConfig;
|
||||
private final LdapMapIdentityStore identityStore;
|
||||
|
||||
public LdapRoleMapKeycloakTransaction(KeycloakSession session, Config.Scope config) {
|
||||
this.session = session;
|
||||
this.roleMapperConfig = new LdapMapRoleMapperConfig(config);
|
||||
this.ldapMapConfig = new LdapMapConfig(config);
|
||||
this.identityStore = new LdapMapIdentityStore(session, ldapMapConfig);
|
||||
}
|
||||
|
||||
// interface matching the constructor of this class
|
||||
public interface LdapRoleMapKeycloakTransactionFunction<A, B, R> {
|
||||
R apply(A a, B b);
|
||||
}
|
||||
|
||||
// TODO: entries might get stale if a DN of an entry changes due to changes in the entity in the same transaction
|
||||
private final Map<String, String> dns = new HashMap<>();
|
||||
|
||||
public String readIdByDn(String dn) {
|
||||
// TODO: this might not be necessary if the LDAP server would support an extended OID
|
||||
// https://ldapwiki.com/wiki/LDAP_SERVER_EXTENDED_DN_OID
|
||||
|
||||
String id = dns.get(dn);
|
||||
if (id == null) {
|
||||
for (Map.Entry<String, LdapMapRoleEntityFieldDelegate> entry : entities.entrySet()) {
|
||||
LdapMapObject ldap = entry.getValue().getLdapMapObject();
|
||||
if (ldap.getDn().toString().equals(dn)) {
|
||||
id = ldap.getId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
return id;
|
||||
}
|
||||
|
||||
LdapMapQuery ldapQuery = new LdapMapQuery();
|
||||
|
||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||
ldapQuery.setSearchScope(ldapMapConfig.getSearchScope());
|
||||
ldapQuery.setSearchDn(roleMapperConfig.getCommonRolesDn());
|
||||
|
||||
// TODO: read them properly to be able to store them in the transaction so they are cached?!
|
||||
Collection<String> roleObjectClasses = ldapMapConfig.getRoleObjectClasses();
|
||||
ldapQuery.addObjectClasses(roleObjectClasses);
|
||||
|
||||
String rolesRdnAttr = roleMapperConfig.getRoleNameLdapAttribute();
|
||||
|
||||
ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
|
||||
|
||||
LdapMapDn.RDN rdn = LdapMapDn.fromString(dn).getFirstRdn();
|
||||
String key = rdn.getAllKeys().get(0);
|
||||
String value = rdn.getAttrValue(key);
|
||||
|
||||
LdapRoleModelCriteriaBuilder mcb =
|
||||
new LdapRoleModelCriteriaBuilder(roleMapperConfig).compare(RoleModel.SearchableFields.NAME, ModelCriteriaBuilder.Operator.EQ, value);
|
||||
mcb = mcb.withCustomFilter(roleMapperConfig.getCustomLdapFilter());
|
||||
ldapQuery.setModelCriteriaBuilder(mcb);
|
||||
|
||||
List<LdapMapObject> ldapObjects = identityStore.fetchQueryResults(ldapQuery);
|
||||
if (ldapObjects.size() == 1) {
|
||||
dns.put(dn, ldapObjects.get(0).getId());
|
||||
return ldapObjects.get(0).getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MapModelCriteriaBuilder<String, MapRoleEntity, RoleModel> createCriteriaBuilderMap() {
|
||||
// The realmId might not be set of instances retrieved by read(id) and we're still sure that they belong to the realm being searched.
|
||||
// Therefore, ignore the field realmId when searching the instances that are stored within the transaction.
|
||||
return new MapModelCriteriaBuilderAssumingEqualForField<>(keyConverter, MapFieldPredicates.getPredicates(RoleModel.class), RoleModel.SearchableFields.REALM_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LdapMapRoleEntityFieldDelegate create(MapRoleEntity value) {
|
||||
DeepCloner CLONER = new DeepCloner.Builder()
|
||||
.constructor(MapRoleEntity.class, cloner -> new LdapMapRoleEntityFieldDelegate(new LdapRoleEntity(cloner, roleMapperConfig, this, value.getClientId())))
|
||||
.build();
|
||||
|
||||
LdapMapRoleEntityFieldDelegate mapped = (LdapMapRoleEntityFieldDelegate) CLONER.from(value);
|
||||
|
||||
// LDAP should never use the UUID provided by the caller, as UUID is generated by the LDAP directory
|
||||
mapped.setId(null);
|
||||
// Roles as groups need to have at least one member on most directories. Add ourselves as a member as a dummy.
|
||||
if (mapped.getLdapMapObject().getId() == null && mapped.getLdapMapObject().getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute()) == null) {
|
||||
// insert our own name as dummy member of this role to avoid a schema conflict in LDAP
|
||||
mapped.getLdapMapObject().setAttribute(roleMapperConfig.getMembershipLdapAttribute(), Stream.of(mapped.getLdapMapObject().getDn().toString()).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
try {
|
||||
// in order to get the ID, we need to write it to LDAP
|
||||
identityStore.add(mapped.getLdapMapObject());
|
||||
// TODO: add a flag for temporary created roles until they are finally committed so that they don't show up in ready(query) in their temporary state
|
||||
} catch (ModelException ex) {
|
||||
if (value.isClientRole() && ex.getCause() instanceof NamingException) {
|
||||
// the client hasn't been created, therefore adding it here
|
||||
LdapMapObject client = new LdapMapObject();
|
||||
client.setObjectClasses(Arrays.asList("top", "organizationalUnit"));
|
||||
client.setRdnAttributeName("ou");
|
||||
client.setDn(LdapMapDn.fromString(roleMapperConfig.getRolesDn(mapped.isClientRole(), mapped.getClientId())));
|
||||
client.setSingleAttribute("ou", mapped.getClientId());
|
||||
identityStore.add(client);
|
||||
|
||||
tasksOnRollback.add(new DeleteOperation() {
|
||||
@Override
|
||||
public void execute() {
|
||||
identityStore.remove(client);
|
||||
}
|
||||
});
|
||||
|
||||
// retry creation of client role
|
||||
identityStore.add(mapped.getLdapMapObject());
|
||||
}
|
||||
}
|
||||
|
||||
entities.put(mapped.getId(), mapped);
|
||||
|
||||
tasksOnRollback.add(new DeleteOperation() {
|
||||
@Override
|
||||
public void execute() {
|
||||
identityStore.remove(mapped.getLdapMapObject());
|
||||
entities.remove(mapped.getId());
|
||||
}
|
||||
});
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
LdapMapRoleEntityFieldDelegate read = read(key);
|
||||
if (read == null) {
|
||||
throw new ModelException("unable to read entity with key " + key);
|
||||
}
|
||||
deletedKeys.add(key);
|
||||
tasksOnCommit.add(new DeleteOperation() {
|
||||
@Override
|
||||
public void execute() {
|
||||
identityStore.remove(read.getLdapMapObject());
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
public LdapRoleEntity readLdap(String key) {
|
||||
LdapMapRoleEntityFieldDelegate read = read(key);
|
||||
if (read == null) {
|
||||
return null;
|
||||
} else {
|
||||
return read.getEntityFieldDelegate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LdapMapRoleEntityFieldDelegate read(String key) {
|
||||
if (deletedKeys.contains(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// reuse an existing live entity
|
||||
LdapMapRoleEntityFieldDelegate val = entities.get(key);
|
||||
|
||||
if (val == null) {
|
||||
|
||||
// try to look it up as a realm role
|
||||
val = lookupEntityById(key, null);
|
||||
|
||||
if (val == null) {
|
||||
// try to find out the client ID
|
||||
LdapMapQuery ldapQuery = new LdapMapQuery();
|
||||
|
||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||
ldapQuery.setSearchScope(ldapMapConfig.getSearchScope());
|
||||
|
||||
// remove prefix with placeholder to allow for a broad search
|
||||
String sdn = roleMapperConfig.getClientRolesDn();
|
||||
ldapQuery.setSearchDn(sdn.replaceAll(".*\\{0},", ""));
|
||||
|
||||
LdapMapObject ldapObject = identityStore.fetchById(key, ldapQuery);
|
||||
if (ldapObject != null) {
|
||||
// as the client ID is now known, search again with the specific configuration
|
||||
LdapMapDn.RDN firstRdn = ldapObject.getDn().getParentDn().getFirstRdn();
|
||||
String clientId = firstRdn.getAttrValue(firstRdn.getAllKeys().get(0));
|
||||
// lookup with clientId, as the search above might have been broader than a restricted search
|
||||
val = lookupEntityById(key, clientId);
|
||||
}
|
||||
}
|
||||
|
||||
if (val != null) {
|
||||
entities.put(key, val);
|
||||
}
|
||||
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private LdapMapRoleEntityFieldDelegate lookupEntityById(String id, String clientId) {
|
||||
LdapMapQuery ldapQuery = getLdapQuery(clientId != null, clientId);
|
||||
|
||||
LdapMapObject ldapObject = identityStore.fetchById(id, ldapQuery);
|
||||
if (ldapObject != null) {
|
||||
return new LdapMapRoleEntityFieldDelegate(new LdapRoleEntity(ldapObject, roleMapperConfig, this, clientId));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<MapRoleEntity> read(QueryParameters<RoleModel> queryParameters) {
|
||||
LdapRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
|
||||
.flashToModelCriteriaBuilder(createLdapModelCriteriaBuilder());
|
||||
|
||||
Boolean isClientRole = mcb.isClientRole();
|
||||
String clientId = mcb.getClientId();
|
||||
|
||||
LdapMapQuery ldapQuery = getLdapQuery(isClientRole, clientId);
|
||||
|
||||
mcb = mcb.withCustomFilter(roleMapperConfig.getCustomLdapFilter());
|
||||
ldapQuery.setModelCriteriaBuilder(mcb);
|
||||
|
||||
Stream<MapRoleEntity> ldapStream;
|
||||
|
||||
MapModelCriteriaBuilder<String,MapRoleEntity,RoleModel> mapMcb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(createCriteriaBuilderMap());
|
||||
|
||||
Stream<LdapMapRoleEntityFieldDelegate> existingEntities = entities.entrySet().stream()
|
||||
.filter(me -> mapMcb.getKeyFilter().test(keyConverter.fromString(me.getKey())) && !deletedKeys.contains(me.getKey()))
|
||||
.map(Map.Entry::getValue)
|
||||
.filter(mapMcb.getEntityFilter())
|
||||
// snapshot list
|
||||
.collect(Collectors.toList()).stream();
|
||||
|
||||
// current approach: combine the results in a correct way from existing entities in the transaction and LDAP
|
||||
// problem here: pagination doesn't work any more as results are retrieved from both, and then need to be sorted
|
||||
// possible alternative: use search criteria only on LDAP, and replace found entities with those stored in transaction already
|
||||
// this will then not find additional entries modified or created in this transaction
|
||||
|
||||
try {
|
||||
List<LdapMapObject> ldapObjects = identityStore.fetchQueryResults(ldapQuery);
|
||||
|
||||
ldapStream = ldapObjects.stream().map(ldapMapObject -> {
|
||||
// we might have fetch client and realm roles at the same time, now try to decode what is what
|
||||
StreamUtils.Pair<Boolean, String> client = getClientId(ldapMapObject.getDn());
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
LdapMapRoleEntityFieldDelegate entity = new LdapMapRoleEntityFieldDelegate(new LdapRoleEntity(ldapMapObject, roleMapperConfig, this, client.getV()));
|
||||
LdapMapRoleEntityFieldDelegate existingEntry = entities.get(entity.getId());
|
||||
if (existingEntry != null) {
|
||||
// this entry will be part of the existing entities
|
||||
return null;
|
||||
}
|
||||
entities.put(entity.getId(), entity);
|
||||
return (MapRoleEntity) entity;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.filter(me -> !deletedKeys.contains(me.getId()))
|
||||
// re-apply filters about client roles that we might have skipped for LDAP
|
||||
.filter(me -> mapMcb.getKeyFilter().test(me.getId()))
|
||||
.filter(me -> mapMcb.getEntityFilter().test(me))
|
||||
// snapshot list, as the contents depends on entities and also updates the entities,
|
||||
// and two streams open at the same time could otherwise interfere
|
||||
.collect(Collectors.toList()).stream();
|
||||
} catch (ModelException ex) {
|
||||
if (clientId != null && ex.getCause() instanceof NamingException) {
|
||||
// the client wasn't found in LDAP, assume an empty result
|
||||
ldapStream = Stream.empty();
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
ldapStream = Stream.concat(ldapStream, existingEntities);
|
||||
|
||||
if (!queryParameters.getOrderBy().isEmpty()) {
|
||||
ldapStream = ldapStream.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream()));
|
||||
}
|
||||
if (queryParameters.getOffset() != null) {
|
||||
ldapStream = ldapStream.skip(queryParameters.getOffset());
|
||||
}
|
||||
if (queryParameters.getLimit() != null) {
|
||||
ldapStream = ldapStream.limit(queryParameters.getLimit());
|
||||
}
|
||||
|
||||
return ldapStream;
|
||||
}
|
||||
|
||||
private StreamUtils.Pair<Boolean, String> getClientId(LdapMapDn dn) {
|
||||
if (dn.getParentDn().equals(LdapMapDn.fromString(roleMapperConfig.getRealmRolesDn()))) {
|
||||
return new StreamUtils.Pair<>(false, null);
|
||||
}
|
||||
String clientsDnWildcard = roleMapperConfig.getClientRolesDn();
|
||||
if (clientsDnWildcard != null) {
|
||||
clientsDnWildcard = clientsDnWildcard.replaceAll(".*\\{0},", "");
|
||||
if (dn.getParentDn().getParentDn().equals(LdapMapDn.fromString(clientsDnWildcard))) {
|
||||
LdapMapDn.RDN firstRdn = dn.getParentDn().getFirstRdn();
|
||||
return new StreamUtils.Pair<>(true, firstRdn.getAttrValue(firstRdn.getAllKeys().get(0)));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LdapMapQuery getLdapQuery(Boolean isClientRole, String clientId) {
|
||||
LdapMapQuery ldapMapQuery = new LdapMapQuery();
|
||||
|
||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||
ldapMapQuery.setSearchScope(ldapMapConfig.getSearchScope());
|
||||
|
||||
String rolesDn = roleMapperConfig.getRolesDn(isClientRole, clientId);
|
||||
ldapMapQuery.setSearchDn(rolesDn);
|
||||
|
||||
Collection<String> roleObjectClasses = ldapMapConfig.getRoleObjectClasses();
|
||||
ldapMapQuery.addObjectClasses(roleObjectClasses);
|
||||
|
||||
String rolesRdnAttr = roleMapperConfig.getRoleNameLdapAttribute();
|
||||
|
||||
ldapMapQuery.addReturningLdapAttribute(rolesRdnAttr);
|
||||
ldapMapQuery.addReturningLdapAttribute("description");
|
||||
ldapMapQuery.addReturningLdapAttribute(roleMapperConfig.getMembershipLdapAttribute());
|
||||
roleMapperConfig.getRoleAttributes().forEach(ldapMapQuery::addReturningLdapAttribute);
|
||||
return ldapMapQuery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
super.commit();
|
||||
for (MapTaskWithValue mapTaskWithValue : tasksOnCommit) {
|
||||
mapTaskWithValue.execute();
|
||||
}
|
||||
|
||||
entities.forEach((entityKey, entity) -> {
|
||||
if (entity.isUpdated()) {
|
||||
identityStore.update(entity.getLdapMapObject());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
super.rollback();
|
||||
Iterator<MapTaskWithValue> iterator = tasksOnRollback.descendingIterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next().execute();
|
||||
}
|
||||
}
|
||||
|
||||
protected LdapRoleModelCriteriaBuilder createLdapModelCriteriaBuilder() {
|
||||
return new LdapRoleModelCriteriaBuilder(roleMapperConfig);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.role;
|
||||
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.map.storage.CriterionNotSupportedException;
|
||||
import org.keycloak.models.map.storage.ldap.LdapModelCriteriaBuilder;
|
||||
import org.keycloak.models.map.storage.ldap.store.LdapMapEscapeStrategy;
|
||||
import org.keycloak.models.map.storage.ldap.role.config.LdapMapRoleMapperConfig;
|
||||
import org.keycloak.models.map.storage.ldap.role.entity.LdapRoleEntity;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class LdapRoleModelCriteriaBuilder extends LdapModelCriteriaBuilder<LdapRoleEntity, RoleModel, LdapRoleModelCriteriaBuilder> {
|
||||
|
||||
private final LdapMapRoleMapperConfig roleMapperConfig;
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public Boolean isClientRole() {
|
||||
return isClientRole;
|
||||
}
|
||||
|
||||
public String getRealmId() {
|
||||
return realmId;
|
||||
}
|
||||
|
||||
private String clientId;
|
||||
|
||||
private Boolean isClientRole;
|
||||
|
||||
private String realmId;
|
||||
|
||||
@Override
|
||||
public LdapRoleModelCriteriaBuilder and(LdapRoleModelCriteriaBuilder... builders) {
|
||||
LdapRoleModelCriteriaBuilder and = super.and(builders);
|
||||
for (LdapRoleModelCriteriaBuilder builder : builders) {
|
||||
if (builder.isClientRole != null) {
|
||||
if (and.isClientRole != null && !Objects.equals(and.isClientRole, builder.isClientRole)) {
|
||||
throw new ModelException("isClientRole must be specified in query only once");
|
||||
}
|
||||
and.isClientRole = builder.isClientRole;
|
||||
}
|
||||
if (builder.clientId != null) {
|
||||
if (and.clientId != null && !Objects.equals(and.clientId, builder.clientId)) {
|
||||
throw new ModelException("clientId must be specified in query only once");
|
||||
}
|
||||
and.clientId = builder.clientId;
|
||||
}
|
||||
if (builder.realmId != null) {
|
||||
if (and.realmId != null && !Objects.equals(and.realmId, builder.realmId)) {
|
||||
throw new ModelException("realmId must be specified in query only once");
|
||||
}
|
||||
and.realmId = builder.realmId;
|
||||
}
|
||||
}
|
||||
return and;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LdapRoleModelCriteriaBuilder or(LdapRoleModelCriteriaBuilder... builders) {
|
||||
LdapRoleModelCriteriaBuilder or = super.or(builders);
|
||||
for (LdapRoleModelCriteriaBuilder builder : builders) {
|
||||
if (builder.isClientRole != null) {
|
||||
throw new ModelException("isClientRole not supported in OR condition");
|
||||
}
|
||||
if (builder.clientId != null) {
|
||||
throw new ModelException("clientId not supported in OR condition");
|
||||
}
|
||||
if (builder.realmId != null) {
|
||||
throw new ModelException("realmId not supported in OR condition");
|
||||
}
|
||||
}
|
||||
return or;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LdapRoleModelCriteriaBuilder not(LdapRoleModelCriteriaBuilder builder) {
|
||||
LdapRoleModelCriteriaBuilder not = super.not(builder);
|
||||
if (builder.isClientRole != null) {
|
||||
throw new ModelException("isClientRole not supported in NOT condition");
|
||||
}
|
||||
if (builder.clientId != null) {
|
||||
throw new ModelException("clientId not supported in NOT condition");
|
||||
}
|
||||
if (builder.realmId != null) {
|
||||
throw new ModelException("realmId not supported in NOT condition");
|
||||
}
|
||||
return not;
|
||||
}
|
||||
|
||||
public LdapRoleModelCriteriaBuilder(LdapMapRoleMapperConfig roleMapperConfig) {
|
||||
super(predicateFunc -> new LdapRoleModelCriteriaBuilder(roleMapperConfig, predicateFunc));
|
||||
this.roleMapperConfig = roleMapperConfig;
|
||||
}
|
||||
|
||||
private LdapRoleModelCriteriaBuilder(LdapMapRoleMapperConfig roleMapperConfig, Supplier<StringBuilder> predicateFunc) {
|
||||
super(pf -> new LdapRoleModelCriteriaBuilder(roleMapperConfig, pf), predicateFunc);
|
||||
this.roleMapperConfig = roleMapperConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LdapRoleModelCriteriaBuilder compare(SearchableModelField<? super RoleModel> modelField, Operator op, Object... value) {
|
||||
switch (op) {
|
||||
case EQ:
|
||||
if (modelField.equals(RoleModel.SearchableFields.IS_CLIENT_ROLE)) {
|
||||
LdapRoleModelCriteriaBuilder result = new LdapRoleModelCriteriaBuilder(roleMapperConfig, StringBuilder::new);
|
||||
result.isClientRole = (boolean) value[0];
|
||||
return result;
|
||||
} else if (modelField.equals(RoleModel.SearchableFields.CLIENT_ID)) {
|
||||
LdapRoleModelCriteriaBuilder result = new LdapRoleModelCriteriaBuilder(roleMapperConfig, StringBuilder::new);
|
||||
result.clientId = (String) value[0];
|
||||
return result;
|
||||
} else if (modelField.equals(RoleModel.SearchableFields.REALM_ID)) {
|
||||
LdapRoleModelCriteriaBuilder result = new LdapRoleModelCriteriaBuilder(roleMapperConfig, StringBuilder::new);
|
||||
result.realmId = (String) value[0];
|
||||
return result;
|
||||
} else if (modelField.equals(RoleModel.SearchableFields.NAME)) {
|
||||
// validateValue(value, modelField, op, String.class);
|
||||
String field = modelFieldNameToLdap(roleMapperConfig, modelField);
|
||||
return new LdapRoleModelCriteriaBuilder(roleMapperConfig,
|
||||
() -> equal(field, value[0], LdapMapEscapeStrategy.DEFAULT, false));
|
||||
} else {
|
||||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
|
||||
case NE:
|
||||
if (modelField.equals(RoleModel.SearchableFields.IS_CLIENT_ROLE)) {
|
||||
LdapRoleModelCriteriaBuilder result = new LdapRoleModelCriteriaBuilder(roleMapperConfig, StringBuilder::new);
|
||||
result.isClientRole = !((boolean) value[0]);
|
||||
return result;
|
||||
} else if (modelField.equals(RoleModel.SearchableFields.NAME)) {
|
||||
// validateValue(value, modelField, op, String.class);
|
||||
String field = modelFieldNameToLdap(roleMapperConfig, modelField);
|
||||
return not(new LdapRoleModelCriteriaBuilder(roleMapperConfig,
|
||||
() -> equal(field, value[0], LdapMapEscapeStrategy.DEFAULT, false)));
|
||||
} else {
|
||||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
|
||||
case ILIKE:
|
||||
case LIKE:
|
||||
if (modelField.equals(RoleModel.SearchableFields.NAME) ||
|
||||
modelField.equals(RoleModel.SearchableFields.DESCRIPTION)) {
|
||||
// validateValue(value, modelField, op, String.class);
|
||||
// first escape all elements of the string (which would not escape the percent sign)
|
||||
// then replace percent sign with the wildcard character asterisk
|
||||
// the result should then be used unescaped in the condition.
|
||||
String v = LdapMapEscapeStrategy.DEFAULT.escape(String.valueOf(value[0])).replaceAll("%", "*");
|
||||
// TODO: there is no placeholder for a single character wildcard ... use multicharacter wildcard instead?
|
||||
String field = modelFieldNameToLdap(roleMapperConfig, modelField);
|
||||
return new LdapRoleModelCriteriaBuilder(roleMapperConfig, () -> {
|
||||
if (v.equals("**")) {
|
||||
// wildcard everything is not well-understood by LDAP and will result in "ERR_01101_NULL_LENGTH The length should not be 0"
|
||||
return new StringBuilder();
|
||||
} else {
|
||||
return equal(field, v, LdapMapEscapeStrategy.NON_ASCII_CHARS_ONLY, false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
|
||||
case IN:
|
||||
if (modelField.equals(RoleModel.SearchableFields.NAME) ||
|
||||
modelField.equals(RoleModel.SearchableFields.DESCRIPTION) ||
|
||||
modelField.equals(RoleModel.SearchableFields.ID)) {
|
||||
String field = modelFieldNameToLdap(roleMapperConfig, modelField);
|
||||
return new LdapRoleModelCriteriaBuilder(roleMapperConfig, () -> {
|
||||
Object[] v;
|
||||
if (value[0] instanceof ArrayList) {
|
||||
v = ((ArrayList<?>) value[0]).toArray();
|
||||
} else {
|
||||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
return in(field, v, false);
|
||||
});
|
||||
} else {
|
||||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
}
|
||||
|
||||
private String modelFieldNameToLdap(LdapMapRoleMapperConfig roleMapperConfig, SearchableModelField<? super RoleModel> modelField) {
|
||||
if (modelField.equals(RoleModel.SearchableFields.NAME)) {
|
||||
return roleMapperConfig.getRoleNameLdapAttribute();
|
||||
} else if (modelField.equals(RoleModel.SearchableFields.ID)) {
|
||||
return roleMapperConfig.getLdapMapConfig().getUuidLDAPAttributeName();
|
||||
} else if (modelField.equals(RoleModel.SearchableFields.DESCRIPTION)) {
|
||||
return "description";
|
||||
} else {
|
||||
throw new CriterionNotSupportedException(modelField, null);
|
||||
}
|
||||
}
|
||||
|
||||
public LdapRoleModelCriteriaBuilder withCustomFilter(String customFilter) {
|
||||
if (customFilter != null && toString().length() > 0) {
|
||||
return and(this, new LdapRoleModelCriteriaBuilder(roleMapperConfig, () -> new StringBuilder(customFilter)));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.role.config;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.map.storage.ldap.config.LdapMapCommonGroupMapperConfig;
|
||||
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapDn;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
public class LdapMapRoleMapperConfig extends LdapMapCommonGroupMapperConfig {
|
||||
|
||||
private final Config.Scope config;
|
||||
private final LdapMapConfig ldapMapConfig;
|
||||
|
||||
public LdapMapRoleMapperConfig(Config.Scope config) {
|
||||
super(new ComponentModel() {
|
||||
@Override
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return new MultivaluedHashMap<String, String>() {
|
||||
@Override
|
||||
public String getFirst(String key) {
|
||||
return config.get(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
this.config = config;
|
||||
this.ldapMapConfig = new LdapMapConfig(config);
|
||||
}
|
||||
|
||||
public String getRealmRolesDn() {
|
||||
String rolesDn = config.get(REALM_ROLES_DN);
|
||||
if (rolesDn == null) {
|
||||
throw new ModelException("Roles DN is null! Check your configuration");
|
||||
}
|
||||
return rolesDn;
|
||||
}
|
||||
|
||||
public String getCommonRolesDn() {
|
||||
String rolesDn = config.get(COMMON_ROLES_DN);
|
||||
if (rolesDn == null) {
|
||||
throw new ModelException("Roles DN is null! Check your configuration");
|
||||
}
|
||||
return rolesDn;
|
||||
}
|
||||
|
||||
public String getClientRolesDn() {
|
||||
String rolesDn = config.get(CLIENT_ROLES_DN);
|
||||
if (rolesDn == null) {
|
||||
throw new ModelException("Roles DN is null! Check your configuration");
|
||||
}
|
||||
return rolesDn;
|
||||
}
|
||||
|
||||
public String getRolesDn(Boolean isClientRole, String clientId) {
|
||||
String rolesDn;
|
||||
if (isClientRole == null && clientId == null) {
|
||||
rolesDn = mapperModel.getConfig().getFirst(COMMON_ROLES_DN);
|
||||
} else {
|
||||
if (isClientRole != null && !isClientRole) {
|
||||
rolesDn = config.get(REALM_ROLES_DN);
|
||||
} else {
|
||||
rolesDn = config.get(CLIENT_ROLES_DN);
|
||||
if (rolesDn != null) {
|
||||
LdapMapDn dn = LdapMapDn.fromString(rolesDn);
|
||||
LdapMapDn.RDN firstRdn = dn.getFirstRdn();
|
||||
for (String key : firstRdn.getAllKeys()) {
|
||||
firstRdn.setAttrValue(key, firstRdn.getAttrValue(key).replaceAll("\\{0}", Matcher.quoteReplacement(clientId)));
|
||||
}
|
||||
rolesDn = dn.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rolesDn == null) {
|
||||
throw new ModelException("Roles DN is null! Check your configuration");
|
||||
}
|
||||
return rolesDn;
|
||||
}
|
||||
|
||||
public Set<String> getRoleAttributes() {
|
||||
String roleAttributes = mapperModel.getConfig().getFirst("role.attributes");
|
||||
if (roleAttributes == null) {
|
||||
roleAttributes = "";
|
||||
}
|
||||
return new HashSet<>(Arrays.asList(roleAttributes.trim().split("\\s+")));
|
||||
}
|
||||
|
||||
// LDAP DN where are realm roles of this tree saved.
|
||||
public static final String REALM_ROLES_DN = "roles.realm.dn";
|
||||
|
||||
// LDAP DN where are client roles of this tree saved.
|
||||
public static final String CLIENT_ROLES_DN = "roles.client.dn";
|
||||
|
||||
// LDAP DN to find both client and realm roles.
|
||||
public static final String COMMON_ROLES_DN = "roles.common.dn";
|
||||
|
||||
// Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
|
||||
public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
|
||||
|
||||
// Object classes of the role object.
|
||||
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
||||
|
||||
// Customized LDAP filter which is added to the whole LDAP query
|
||||
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
|
||||
|
||||
// See UserRolesRetrieveStrategy
|
||||
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE";
|
||||
public static final String GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE";
|
||||
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
|
||||
|
||||
public String getRoleNameLdapAttribute() {
|
||||
String rolesRdnAttr = mapperModel.getConfig().getFirst(ROLE_NAME_LDAP_ATTRIBUTE);
|
||||
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLDAPGroupNameLdapAttribute() {
|
||||
return getRoleNameLdapAttribute();
|
||||
}
|
||||
|
||||
public String getCustomLdapFilter() {
|
||||
return mapperModel.getConfig().getFirst(ROLES_LDAP_FILTER);
|
||||
}
|
||||
|
||||
public String getUserRolesRetrieveStrategy() {
|
||||
String strategyString = mapperModel.getConfig().getFirst(USER_ROLES_RETRIEVE_STRATEGY);
|
||||
return strategyString!=null ? strategyString : LOAD_ROLES_BY_MEMBER_ATTRIBUTE;
|
||||
}
|
||||
|
||||
public LdapMapConfig getLdapMapConfig() {
|
||||
return ldapMapConfig;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.role.entity;
|
||||
|
||||
import org.keycloak.models.map.common.delegate.EntityFieldDelegate;
|
||||
import org.keycloak.models.map.role.MapRoleEntity;
|
||||
import org.keycloak.models.map.role.MapRoleEntityFieldDelegate;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapObject;
|
||||
|
||||
public class LdapMapRoleEntityFieldDelegate extends MapRoleEntityFieldDelegate {
|
||||
|
||||
public LdapMapRoleEntityFieldDelegate(EntityFieldDelegate<MapRoleEntity> entityFieldDelegate) {
|
||||
super(entityFieldDelegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LdapRoleEntity getEntityFieldDelegate() {
|
||||
return (LdapRoleEntity) super.getEntityFieldDelegate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdated() {
|
||||
// TODO: EntityFieldDelegate.isUpdated is broken, as it is never updated
|
||||
return getEntityFieldDelegate().isUpdated();
|
||||
}
|
||||
|
||||
public LdapMapObject getLdapMapObject() {
|
||||
return getEntityFieldDelegate().getLdapMapObject();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.role.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.EntityField;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
import org.keycloak.models.map.common.delegate.EntityFieldDelegate;
|
||||
import org.keycloak.models.map.role.MapRoleEntity;
|
||||
import org.keycloak.models.map.role.MapRoleEntityFields;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapDn;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapObject;
|
||||
import org.keycloak.models.map.storage.ldap.role.config.LdapMapRoleMapperConfig;
|
||||
import org.keycloak.models.map.storage.ldap.role.LdapRoleMapKeycloakTransaction;
|
||||
|
||||
public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldDelegate<MapRoleEntity> {
|
||||
|
||||
private final LdapMapObject ldapMapObject;
|
||||
private final LdapMapRoleMapperConfig roleMapperConfig;
|
||||
private final LdapRoleMapKeycloakTransaction transaction;
|
||||
private final String clientId;
|
||||
|
||||
private static final EnumMap<MapRoleEntityFields, BiConsumer<LdapRoleEntity, Object>> SETTERS = new EnumMap<>(MapRoleEntityFields.class);
|
||||
static {
|
||||
SETTERS.put(MapRoleEntityFields.DESCRIPTION, (e, v) -> e.setDescription((String) v));
|
||||
SETTERS.put(MapRoleEntityFields.ID, (e, v) -> e.setId((String) v));
|
||||
SETTERS.put(MapRoleEntityFields.REALM_ID, (e, v) -> e.setRealmId((String) v));
|
||||
SETTERS.put(MapRoleEntityFields.CLIENT_ID, (e, v) -> e.setClientId((String) v));
|
||||
SETTERS.put(MapRoleEntityFields.CLIENT_ROLE, (e, v) -> e.setClientRole((Boolean) v));
|
||||
//noinspection unchecked
|
||||
SETTERS.put(MapRoleEntityFields.ATTRIBUTES, (e, v) -> e.setAttributes((Map<String, List<String>>) v));
|
||||
//noinspection unchecked
|
||||
SETTERS.put(MapRoleEntityFields.COMPOSITE_ROLES, (e, v) -> e.setCompositeRoles((Set<String>) v));
|
||||
SETTERS.put(MapRoleEntityFields.NAME, (e, v) -> e.setName((String) v));
|
||||
}
|
||||
|
||||
private static final EnumMap<MapRoleEntityFields, Function<LdapRoleEntity, Object>> GETTERS = new EnumMap<>(MapRoleEntityFields.class);
|
||||
static {
|
||||
GETTERS.put(MapRoleEntityFields.DESCRIPTION, LdapRoleEntity::getDescription);
|
||||
GETTERS.put(MapRoleEntityFields.ID, LdapRoleEntity::getId);
|
||||
GETTERS.put(MapRoleEntityFields.REALM_ID, LdapRoleEntity::getRealmId);
|
||||
GETTERS.put(MapRoleEntityFields.CLIENT_ID, LdapRoleEntity::getClientId);
|
||||
GETTERS.put(MapRoleEntityFields.CLIENT_ROLE, LdapRoleEntity::isClientRole);
|
||||
GETTERS.put(MapRoleEntityFields.ATTRIBUTES, LdapRoleEntity::getAttributes);
|
||||
GETTERS.put(MapRoleEntityFields.COMPOSITE_ROLES, LdapRoleEntity::getCompositeRoles);
|
||||
GETTERS.put(MapRoleEntityFields.NAME, LdapRoleEntity::getName);
|
||||
}
|
||||
|
||||
private static final EnumMap<MapRoleEntityFields, BiConsumer<LdapRoleEntity, Object>> ADDERS = new EnumMap<>(MapRoleEntityFields.class);
|
||||
static {
|
||||
ADDERS.put(MapRoleEntityFields.COMPOSITE_ROLES, (e, v) -> e.addCompositeRole((String) v));
|
||||
}
|
||||
|
||||
private static final EnumMap<MapRoleEntityFields, BiFunction<LdapRoleEntity, Object, Object>> REMOVERS = new EnumMap<>(MapRoleEntityFields.class);
|
||||
static {
|
||||
REMOVERS.put(MapRoleEntityFields.COMPOSITE_ROLES, (e, v) -> { e.removeCompositeRole((String) v); return null; });
|
||||
}
|
||||
|
||||
public LdapRoleEntity(DeepCloner cloner, LdapMapRoleMapperConfig roleMapperConfig, LdapRoleMapKeycloakTransaction transaction, String clientId) {
|
||||
ldapMapObject = new LdapMapObject();
|
||||
ldapMapObject.setObjectClasses(Arrays.asList("top", "groupOfNames"));
|
||||
ldapMapObject.setRdnAttributeName(roleMapperConfig.getRoleNameLdapAttribute());
|
||||
this.roleMapperConfig = roleMapperConfig;
|
||||
this.transaction = transaction;
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public LdapRoleEntity(LdapMapObject ldapMapObject, LdapMapRoleMapperConfig roleMapperConfig, LdapRoleMapKeycloakTransaction transaction, String clientId) {
|
||||
this.ldapMapObject = ldapMapObject;
|
||||
this.roleMapperConfig = roleMapperConfig;
|
||||
this.transaction = transaction;
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return ldapMapObject.getId();
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.updated |= !Objects.equals(getId(), id);
|
||||
ldapMapObject.setId(id);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
Map<String, List<String>> result = new HashMap<>();
|
||||
for (String roleAttribute : roleMapperConfig.getRoleAttributes()) {
|
||||
Set<String> attrs = ldapMapObject.getAttributeAsSet(roleAttribute);
|
||||
if (attrs != null) {
|
||||
result.put(roleAttribute, new ArrayList<>(attrs));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
// store all attributes
|
||||
if (attributes != null) {
|
||||
attributes.forEach(this::setAttribute);
|
||||
}
|
||||
// clear attributes not in the list
|
||||
for (String roleAttribute : roleMapperConfig.getRoleAttributes()) {
|
||||
if (attributes == null || !attributes.containsKey(roleAttribute)) {
|
||||
removeAttribute(roleAttribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getAttribute(String name) {
|
||||
if (!roleMapperConfig.getRoleAttributes().contains(name)) {
|
||||
throw new ModelException("can't read attribute '" + name + "' as it is not supported");
|
||||
}
|
||||
return new ArrayList<>(ldapMapObject.getAttributeAsSet(name));
|
||||
}
|
||||
|
||||
public void setAttribute(String name, List<String> value) {
|
||||
if (!roleMapperConfig.getRoleAttributes().contains(name)) {
|
||||
throw new ModelException("can't set attribute '" + name + "' as it is not supported");
|
||||
}
|
||||
if ((ldapMapObject.getAttributeAsSet(name) == null && (value == null || value.size() == 0)) ||
|
||||
Objects.equals(ldapMapObject.getAttributeAsSet(name), new HashSet<>(value))) {
|
||||
return;
|
||||
}
|
||||
if (ldapMapObject.getReadOnlyAttributeNames().contains(name)) {
|
||||
throw new ModelException("can't write attribute '" + name + "' as it is not writeable");
|
||||
}
|
||||
ldapMapObject.setAttribute(name, new HashSet<>(value));
|
||||
this.updated = true;
|
||||
}
|
||||
|
||||
public void removeAttribute(String name) {
|
||||
if (!roleMapperConfig.getRoleAttributes().contains(name)) {
|
||||
throw new ModelException("can't write attribute '" + name + "' as it is not supported");
|
||||
}
|
||||
if (ldapMapObject.getAttributeAsSet(name) == null || ldapMapObject.getAttributeAsSet(name).size() == 0) {
|
||||
return;
|
||||
}
|
||||
ldapMapObject.setAttribute(name, null);
|
||||
this.updated = true;
|
||||
}
|
||||
|
||||
public String getRealmId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return ldapMapObject.getAttributeAsString(roleMapperConfig.getRoleNameLdapAttribute());
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return ldapMapObject.getAttributeAsString("description");
|
||||
}
|
||||
|
||||
public void setClientRole(Boolean clientRole) {
|
||||
if (!Objects.equals(this.isClientRole(), clientRole)) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isClientRole() {
|
||||
return clientId != null;
|
||||
}
|
||||
|
||||
public void setRealmId(String realmId) {
|
||||
// we'll not store this information, as LDAP store might be used from different realms
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
if (!Objects.equals(this.getClientId(), clientId)) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.updated |= !Objects.equals(getName(), name);
|
||||
ldapMapObject.setSingleAttribute(roleMapperConfig.getRoleNameLdapAttribute(), name);
|
||||
LdapMapDn dn = LdapMapDn.fromString(roleMapperConfig.getRolesDn(clientId != null, clientId));
|
||||
dn.addFirst(roleMapperConfig.getRoleNameLdapAttribute(), name);
|
||||
ldapMapObject.setDn(dn);
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.updated |= !Objects.equals(getDescription(), description);
|
||||
if (description != null) {
|
||||
ldapMapObject.setSingleAttribute("description", description);
|
||||
} else if (getDescription() != null) {
|
||||
ldapMapObject.setAttribute("description", null);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getCompositeRoles() {
|
||||
Set<String> members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute());
|
||||
if (members == null) {
|
||||
members = new HashSet<>();
|
||||
}
|
||||
HashSet<String> compositeRoles = new HashSet<>();
|
||||
for (String member : members) {
|
||||
if (member.equals(ldapMapObject.getDn().toString())) {
|
||||
continue;
|
||||
}
|
||||
if (!member.startsWith(roleMapperConfig.getRoleNameLdapAttribute())) {
|
||||
// this is a real user, not a composite role, ignore
|
||||
// TODO: this will not work if users and role use the same!
|
||||
continue;
|
||||
}
|
||||
String roleId = transaction.readIdByDn(member);
|
||||
if (roleId == null) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
compositeRoles.add(roleId);
|
||||
}
|
||||
return compositeRoles;
|
||||
}
|
||||
|
||||
public void setCompositeRoles(Set<String> compositeRoles) {
|
||||
HashSet<String> translatedCompositeRoles = new HashSet<>();
|
||||
if (compositeRoles != null) {
|
||||
for (String compositeRole : compositeRoles) {
|
||||
LdapRoleEntity ldapRole = transaction.readLdap(compositeRole);
|
||||
translatedCompositeRoles.add(ldapRole.getLdapMapObject().getDn().toString());
|
||||
}
|
||||
}
|
||||
Set<String> members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute());
|
||||
if (members == null) {
|
||||
members = new HashSet<>();
|
||||
}
|
||||
for (String member : members) {
|
||||
if (!member.startsWith(roleMapperConfig.getRoleNameLdapAttribute())) {
|
||||
// this is a real user, not a composite role, ignore
|
||||
// TODO: this will not work if users and role use the same!
|
||||
translatedCompositeRoles.add(member);
|
||||
}
|
||||
}
|
||||
if (!translatedCompositeRoles.equals(members)) {
|
||||
ldapMapObject.setAttribute(roleMapperConfig.getMembershipLdapAttribute(), members);
|
||||
this.updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void addCompositeRole(String roleId) {
|
||||
LdapRoleEntity ldapRole = transaction.readLdap(roleId);
|
||||
Set<String> members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute());
|
||||
if (members == null) {
|
||||
members = new HashSet<>();
|
||||
}
|
||||
members.add(ldapRole.getLdapMapObject().getDn().toString());
|
||||
ldapMapObject.setAttribute(roleMapperConfig.getMembershipLdapAttribute(), members);
|
||||
this.updated = true;
|
||||
}
|
||||
|
||||
public void removeCompositeRole(String roleId) {
|
||||
LdapRoleEntity ldapRole = transaction.readLdap(roleId);
|
||||
Set<String> members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute());
|
||||
if (members == null) {
|
||||
members = new HashSet<>();
|
||||
}
|
||||
members.remove(ldapRole.getLdapMapObject().getDn().toString());
|
||||
ldapMapObject.setAttribute(roleMapperConfig.getMembershipLdapAttribute(), members);
|
||||
this.updated = true;
|
||||
}
|
||||
|
||||
public LdapMapObject getLdapMapObject() {
|
||||
return ldapMapObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> void set(EF field, T value) {
|
||||
BiConsumer<LdapRoleEntity, Object> consumer = SETTERS.get(field);
|
||||
if (consumer == null) {
|
||||
throw new ModelException("unsupported field for setters " + field);
|
||||
}
|
||||
consumer.accept(this, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> void collectionAdd(EF field, T value) {
|
||||
BiConsumer<LdapRoleEntity, Object> consumer = ADDERS.get(field);
|
||||
if (consumer == null) {
|
||||
throw new ModelException("unsupported field for setters " + field);
|
||||
}
|
||||
consumer.accept(this, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> Object collectionRemove(EF field, T value) {
|
||||
BiFunction<LdapRoleEntity, Object, Object> consumer = REMOVERS.get(field);
|
||||
if (consumer == null) {
|
||||
throw new ModelException("unsupported field for setters " + field);
|
||||
}
|
||||
return consumer.apply(this, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> Object get(EF field) {
|
||||
Function<LdapRoleEntity, Object> consumer = GETTERS.get(field);
|
||||
if (consumer == null) {
|
||||
throw new ModelException("unsupported field for getters " + field);
|
||||
}
|
||||
return consumer.apply(this);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
import org.keycloak.vault.VaultCharSecret;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.ldap.InitialLdapContext;
|
||||
import javax.naming.ldap.LdapContext;
|
||||
import javax.naming.ldap.StartTlsRequest;
|
||||
import javax.naming.ldap.StartTlsResponse;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import static javax.naming.Context.SECURITY_CREDENTIALS;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public final class LdapMapContextManager implements AutoCloseable {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LdapMapContextManager.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final LdapMapConfig ldapMapConfig;
|
||||
private StartTlsResponse tlsResponse;
|
||||
|
||||
private VaultCharSecret vaultCharSecret = new VaultCharSecret() {
|
||||
@Override
|
||||
public Optional<CharBuffer> get() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<char[]> getAsArray() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private LdapContext ldapContext;
|
||||
|
||||
public LdapMapContextManager(KeycloakSession session, LdapMapConfig connectionProperties) {
|
||||
this.session = session;
|
||||
this.ldapMapConfig = connectionProperties;
|
||||
}
|
||||
|
||||
public static LdapMapContextManager create(KeycloakSession session, LdapMapConfig connectionProperties) {
|
||||
return new LdapMapContextManager(session, connectionProperties);
|
||||
}
|
||||
|
||||
private void createLdapContext() throws NamingException {
|
||||
Hashtable<Object, Object> connProp = getConnectionProperties(ldapMapConfig);
|
||||
|
||||
if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapMapConfig.getAuthType())) {
|
||||
vaultCharSecret = getVaultSecret();
|
||||
|
||||
if (vaultCharSecret != null && !ldapMapConfig.isStartTls()) {
|
||||
connProp.put(SECURITY_CREDENTIALS, vaultCharSecret.getAsArray()
|
||||
.orElse(ldapMapConfig.getBindCredential().toCharArray()));
|
||||
}
|
||||
}
|
||||
|
||||
ldapContext = new InitialLdapContext(connProp, null);
|
||||
if (ldapMapConfig.isStartTls()) {
|
||||
SSLSocketFactory sslSocketFactory = null;
|
||||
String useTruststoreSpi = ldapMapConfig.getUseTruststoreSpi();
|
||||
if (useTruststoreSpi != null && useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_ALWAYS)) {
|
||||
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
|
||||
sslSocketFactory = provider.getSSLSocketFactory();
|
||||
}
|
||||
|
||||
tlsResponse = startTLS(ldapContext, ldapMapConfig.getAuthType(), ldapMapConfig.getBindDN(),
|
||||
vaultCharSecret.getAsArray().orElse(ldapMapConfig.getBindCredential().toCharArray()), sslSocketFactory);
|
||||
|
||||
// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
|
||||
if (tlsResponse == null) {
|
||||
throw new NamingException("Wasn't able to establish LDAP connection through StartTLS");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LdapContext getLdapContext() throws NamingException {
|
||||
if (ldapContext == null) createLdapContext();
|
||||
|
||||
return ldapContext;
|
||||
}
|
||||
|
||||
private VaultCharSecret getVaultSecret() {
|
||||
return LDAPConstants.AUTH_TYPE_NONE.equals(ldapMapConfig.getAuthType())
|
||||
? null
|
||||
: session.vault().getCharSecret(ldapMapConfig.getBindCredential());
|
||||
}
|
||||
|
||||
public static StartTlsResponse startTLS(LdapContext ldapContext, String authType, String bindDN, char[] bindCredential, SSLSocketFactory sslSocketFactory) throws NamingException {
|
||||
StartTlsResponse tls;
|
||||
|
||||
try {
|
||||
tls = (StartTlsResponse) ldapContext.extendedOperation(new StartTlsRequest());
|
||||
tls.negotiate(sslSocketFactory);
|
||||
|
||||
ldapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, authType);
|
||||
|
||||
if (!LDAPConstants.AUTH_TYPE_NONE.equals(authType)) {
|
||||
ldapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, bindDN);
|
||||
ldapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, bindCredential);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Could not negotiate TLS", e);
|
||||
throw new AuthenticationException("Could not negotiate TLS");
|
||||
}
|
||||
|
||||
// throws AuthenticationException when authentication fails
|
||||
ldapContext.lookup("");
|
||||
|
||||
return tls;
|
||||
}
|
||||
|
||||
// Get connection properties of admin connection
|
||||
private Hashtable<Object, Object> getConnectionProperties(LdapMapConfig ldapMapConfig) {
|
||||
Hashtable<Object, Object> env = getNonAuthConnectionProperties(ldapMapConfig);
|
||||
|
||||
if(!ldapMapConfig.isStartTls()) {
|
||||
String authType = ldapMapConfig.getAuthType();
|
||||
|
||||
env.put(Context.SECURITY_AUTHENTICATION, authType);
|
||||
|
||||
String bindDN = ldapMapConfig.getBindDN();
|
||||
|
||||
char[] bindCredential = null;
|
||||
|
||||
if (ldapMapConfig.getBindCredential() != null) {
|
||||
bindCredential = ldapMapConfig.getBindCredential().toCharArray();
|
||||
}
|
||||
|
||||
if (!LDAPConstants.AUTH_TYPE_NONE.equals(authType)) {
|
||||
env.put(Context.SECURITY_PRINCIPAL, bindDN);
|
||||
env.put(Context.SECURITY_CREDENTIALS, bindCredential);
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
Map<Object, Object> copyEnv = new Hashtable<>(env);
|
||||
if (copyEnv.containsKey(Context.SECURITY_CREDENTIALS)) {
|
||||
copyEnv.put(Context.SECURITY_CREDENTIALS, "**************************************");
|
||||
}
|
||||
logger.tracef("Creating LdapContext using properties: [%s]", copyEnv);
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method is used for admin connection and user authentication. Hence it returns just connection properties NOT related to
|
||||
* authentication (properties like bindType, bindDn, bindPassword). Caller of this method needs to fill auth-related connection properties
|
||||
* based on the fact whether he does admin connection or user authentication
|
||||
*
|
||||
*/
|
||||
public static Hashtable<Object, Object> getNonAuthConnectionProperties(LdapMapConfig ldapMapConfig) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
|
||||
env.put(Context.INITIAL_CONTEXT_FACTORY, ldapMapConfig.getFactoryName());
|
||||
|
||||
String url = ldapMapConfig.getConnectionUrl();
|
||||
|
||||
if (url != null) {
|
||||
env.put(Context.PROVIDER_URL, url);
|
||||
} else {
|
||||
logger.warn("LDAP URL is null. LDAPOperationManager won't work correctly");
|
||||
}
|
||||
|
||||
// when using Start TLS, use default socket factory for LDAP client but pass the TrustStore SSL socket factory later
|
||||
// when calling StartTlsResponse.negotiate(trustStoreSSLSocketFactory)
|
||||
if (!ldapMapConfig.isStartTls()) {
|
||||
String useTruststoreSpi = ldapMapConfig.getUseTruststoreSpi();
|
||||
LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, url, env);
|
||||
}
|
||||
|
||||
String connectionPooling = ldapMapConfig.getConnectionPooling();
|
||||
if (connectionPooling != null) {
|
||||
env.put("com.sun.jndi.ldap.connect.pool", connectionPooling);
|
||||
}
|
||||
|
||||
String connectionTimeout = ldapMapConfig.getConnectionTimeout();
|
||||
if (connectionTimeout != null && !connectionTimeout.isEmpty()) {
|
||||
env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout);
|
||||
}
|
||||
|
||||
String readTimeout = ldapMapConfig.getReadTimeout();
|
||||
if (readTimeout != null && !readTimeout.isEmpty()) {
|
||||
env.put("com.sun.jndi.ldap.read.timeout", readTimeout);
|
||||
}
|
||||
|
||||
// Just dump the additional properties
|
||||
Properties additionalProperties = ldapMapConfig.getAdditionalConnectionProperties();
|
||||
if (additionalProperties != null) {
|
||||
for (Object key : additionalProperties.keySet()) {
|
||||
env.put(key.toString(), additionalProperties.getProperty(key.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder binaryAttrsBuilder = new StringBuilder();
|
||||
if (ldapMapConfig.isObjectGUID()) {
|
||||
binaryAttrsBuilder.append(LDAPConstants.OBJECT_GUID).append(" ");
|
||||
}
|
||||
if (ldapMapConfig.isEdirectory()) {
|
||||
binaryAttrsBuilder.append(LDAPConstants.NOVELL_EDIRECTORY_GUID).append(" ");
|
||||
}
|
||||
for (String attrName : ldapMapConfig.getBinaryAttributeNames()) {
|
||||
binaryAttrsBuilder.append(attrName).append(" ");
|
||||
}
|
||||
|
||||
String binaryAttrs = binaryAttrsBuilder.toString().trim();
|
||||
if (!binaryAttrs.isEmpty()) {
|
||||
env.put("java.naming.ldap.attributes.binary", binaryAttrs);
|
||||
}
|
||||
|
||||
return new Hashtable<>(env);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (vaultCharSecret != null) vaultCharSecret.close();
|
||||
if (tlsResponse != null) {
|
||||
try {
|
||||
tlsResponse.close();
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not close Ldap tlsResponse.", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (ldapContext != null) {
|
||||
try {
|
||||
ldapContext.close();
|
||||
} catch (NamingException e) {
|
||||
logger.error("Could not close Ldap context.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public enum LdapMapEscapeStrategy {
|
||||
|
||||
|
||||
// LDAP special characters like * ( ) \ are not escaped. Only non-ASCII characters like é are escaped
|
||||
NON_ASCII_CHARS_ONLY {
|
||||
|
||||
@Override
|
||||
public String escape(String input) {
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
for (byte b : input.getBytes(StandardCharsets.UTF_8)) {
|
||||
appendByte(b, output);
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
// Escaping of LDAP special characters including non-ASCII characters like é
|
||||
DEFAULT {
|
||||
|
||||
|
||||
@Override
|
||||
public String escape(String input) {
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
for (byte b : input.getBytes(StandardCharsets.UTF_8)) {
|
||||
switch (b) {
|
||||
case 0x5c:
|
||||
output.append("\\5c"); // \
|
||||
break;
|
||||
case 0x2a:
|
||||
output.append("\\2a"); // *
|
||||
break;
|
||||
case 0x28:
|
||||
output.append("\\28"); // (
|
||||
break;
|
||||
case 0x29:
|
||||
output.append("\\29"); // )
|
||||
break;
|
||||
case 0x00:
|
||||
output.append("\\00"); // \u0000
|
||||
break;
|
||||
default: {
|
||||
appendByte(b, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// Escaping value as Octet-String
|
||||
OCTET_STRING {
|
||||
@Override
|
||||
public String escape(String input) {
|
||||
byte[] bytes;
|
||||
bytes = input.getBytes(StandardCharsets.UTF_8);
|
||||
return escapeHex(bytes);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static String escapeHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("\\%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public abstract String escape(String input);
|
||||
|
||||
protected void appendByte(byte b, StringBuilder output) {
|
||||
if (b >= 0) {
|
||||
output.append((char) b);
|
||||
} else {
|
||||
int i = -256 ^ b;
|
||||
output.append("\\").append(Integer.toHexString(i));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,533 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapDn;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapObject;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapQuery;
|
||||
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
|
||||
import org.keycloak.representations.idm.LDAPCapabilityRepresentation.CapabilityType;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.AttributeInUseException;
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.BasicAttribute;
|
||||
import javax.naming.directory.BasicAttributes;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.ModificationItem;
|
||||
import javax.naming.directory.NoSuchAttributeException;
|
||||
import javax.naming.directory.SchemaViolationException;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.SearchResult;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* An IdentityStore implementation backed by an LDAP directory
|
||||
*
|
||||
* @author Shane Bryzak
|
||||
* @author Anil Saldhana
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
|
||||
*/
|
||||
public class LdapMapIdentityStore {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LdapMapIdentityStore.class);
|
||||
private static final Pattern rangePattern = Pattern.compile("([^;]+);range=([0-9]+)-([0-9]+|\\*)");
|
||||
|
||||
private final LdapMapConfig config;
|
||||
private final LdapMapOperationManager operationManager;
|
||||
|
||||
public LdapMapIdentityStore(KeycloakSession session, LdapMapConfig config) {
|
||||
this.config = config;
|
||||
this.operationManager = new LdapMapOperationManager(session, config);
|
||||
}
|
||||
|
||||
public LdapMapConfig getConfig() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public void add(LdapMapObject ldapObject) {
|
||||
// id will be assigned by the ldap server
|
||||
if (ldapObject.getId() != null) {
|
||||
throw new ModelException("Can't add object with already assigned uuid");
|
||||
}
|
||||
|
||||
String entryDN = ldapObject.getDn().toString();
|
||||
BasicAttributes ldapAttributes = extractAttributesForSaving(ldapObject, true);
|
||||
this.operationManager.createSubContext(entryDN, ldapAttributes);
|
||||
ldapObject.setId(getEntryIdentifier(ldapObject));
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getId(), entryDN);
|
||||
}
|
||||
}
|
||||
|
||||
public void addMemberToGroup(String groupDn, String memberAttrName, String value) {
|
||||
// do not check EMPTY_MEMBER_ATTRIBUTE_VALUE, we save one useless query
|
||||
// the value will be there forever for objectclasses that enforces the attribute as MUST
|
||||
BasicAttribute attr = new BasicAttribute(memberAttrName, value);
|
||||
ModificationItem item = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr);
|
||||
try {
|
||||
this.operationManager.modifyAttributesNaming(groupDn, new ModificationItem[]{item}, null);
|
||||
} catch (AttributeInUseException e) {
|
||||
logger.debugf("Group %s already contains the member %s", groupDn, value);
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not modify attribute for DN [" + groupDn + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeMemberFromGroup(String groupDn, String memberAttrName, String value) {
|
||||
BasicAttribute attr = new BasicAttribute(memberAttrName, value);
|
||||
ModificationItem item = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr);
|
||||
try {
|
||||
this.operationManager.modifyAttributesNaming(groupDn, new ModificationItem[]{item}, null);
|
||||
} catch (NoSuchAttributeException e) {
|
||||
logger.debugf("Group %s does not contain the member %s", groupDn, value);
|
||||
} catch (SchemaViolationException e) {
|
||||
// schema violation removing one member => add the empty attribute, it cannot be other thing
|
||||
logger.infof("Schema violation in group %s removing member %s. Trying adding empty member attribute.", groupDn, value);
|
||||
try {
|
||||
this.operationManager.modifyAttributesNaming(groupDn,
|
||||
new ModificationItem[]{item, new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute(memberAttrName, LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE))},
|
||||
null);
|
||||
} catch (NamingException ex) {
|
||||
throw new ModelException("Could not modify attribute for DN [" + groupDn + "]", ex);
|
||||
}
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not modify attribute for DN [" + groupDn + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(LdapMapObject ldapObject) {
|
||||
checkRename(ldapObject);
|
||||
|
||||
BasicAttributes updatedAttributes = extractAttributesForSaving(ldapObject, false);
|
||||
NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();
|
||||
|
||||
String entryDn = ldapObject.getDn().toString();
|
||||
this.operationManager.modifyAttributes(entryDn, attributes);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getId(), entryDn);
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkRename(LdapMapObject ldapObject) {
|
||||
LdapMapDn.RDN firstRdn = ldapObject.getDn().getFirstRdn();
|
||||
String oldDn = ldapObject.getDn().toString();
|
||||
|
||||
// Detect which keys will need to be updated in RDN, which are new keys to be added, and which are to be removed
|
||||
List<String> toUpdateKeys = firstRdn.getAllKeys();
|
||||
toUpdateKeys.retainAll(ldapObject.getRdnAttributeNames());
|
||||
|
||||
List<String> toRemoveKeys = firstRdn.getAllKeys();
|
||||
toRemoveKeys.removeAll(ldapObject.getRdnAttributeNames());
|
||||
|
||||
List<String> toAddKeys = new ArrayList<>(ldapObject.getRdnAttributeNames());
|
||||
toAddKeys.removeAll(firstRdn.getAllKeys());
|
||||
|
||||
// Go through all the keys in the oldRDN and doublecheck if they are changed or not
|
||||
boolean changed = false;
|
||||
for (String attrKey : toUpdateKeys) {
|
||||
if (ldapObject.getReadOnlyAttributeNames().contains(attrKey.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String rdnAttrVal = ldapObject.getAttributeAsString(attrKey);
|
||||
|
||||
// Could be the case when RDN attribute of the target object is not included in Keycloak mappers
|
||||
if (rdnAttrVal == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String oldRdnAttrVal = firstRdn.getAttrValue(attrKey);
|
||||
|
||||
if (!oldRdnAttrVal.equalsIgnoreCase(rdnAttrVal)) {
|
||||
changed = true;
|
||||
firstRdn.setAttrValue(attrKey, rdnAttrVal);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new keys
|
||||
for (String attrKey : toAddKeys) {
|
||||
String rdnAttrVal = ldapObject.getAttributeAsString(attrKey);
|
||||
|
||||
// Could be the case when RDN attribute of the target object is not included in Keycloak mappers
|
||||
if (rdnAttrVal == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
firstRdn.setAttrValue(attrKey, rdnAttrVal);
|
||||
}
|
||||
|
||||
// Remove old keys
|
||||
for (String attrKey : toRemoveKeys) {
|
||||
changed |= firstRdn.removeAttrValue(attrKey);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
LdapMapDn newLdapMapDn = ldapObject.getDn().getParentDn();
|
||||
newLdapMapDn.addFirst(firstRdn);
|
||||
|
||||
String newDn = newLdapMapDn.toString();
|
||||
|
||||
logger.debugf("Renaming LDAP Object. Old DN: [%s], New DN: [%s]", oldDn, newDn);
|
||||
|
||||
// In case, that there is conflict (For example already existing "CN=John Anthony"), the different DN is returned
|
||||
newDn = this.operationManager.renameEntry(oldDn, newDn, true);
|
||||
|
||||
ldapObject.setDn(LdapMapDn.fromString(newDn));
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(LdapMapObject ldapObject) {
|
||||
this.operationManager.removeEntry(ldapObject.getDn().toString());
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf("Type with identifier [%s] and DN [%s] successfully removed from LDAP store.", ldapObject.getId(), ldapObject.getDn().toString());
|
||||
}
|
||||
}
|
||||
|
||||
public LdapMapObject fetchById(String id, LdapMapQuery identityQuery) {
|
||||
SearchResult search = this.operationManager
|
||||
.lookupById(identityQuery.getSearchDn(), id, identityQuery.getReturningLdapAttributes());
|
||||
|
||||
if (search != null) {
|
||||
return populateAttributedType(search, identityQuery);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<LdapMapObject> fetchQueryResults(LdapMapQuery identityQuery) {
|
||||
List<LdapMapObject> results = new ArrayList<>();
|
||||
|
||||
StringBuilder filter = null;
|
||||
|
||||
try {
|
||||
String baseDN = identityQuery.getSearchDn();
|
||||
|
||||
filter = createIdentityTypeSearchFilter(identityQuery);
|
||||
|
||||
List<SearchResult> search;
|
||||
search = this.operationManager.search(baseDN, filter.toString(), identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
|
||||
|
||||
for (SearchResult result : search) {
|
||||
// don't add the branch in subtree search
|
||||
if (identityQuery.getSearchScope() != SearchControls.SUBTREE_SCOPE || !result.getNameInNamespace().equalsIgnoreCase(baseDN)) {
|
||||
results.add(populateAttributedType(result, identityQuery));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ModelException("Querying of LDAP failed " + identityQuery + ", filter: " + filter, e);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public Set<LDAPCapabilityRepresentation> queryServerCapabilities() {
|
||||
Set<LDAPCapabilityRepresentation> result = new LinkedHashSet<>();
|
||||
try {
|
||||
List<String> attrs = new ArrayList<>();
|
||||
attrs.add("supportedControl");
|
||||
attrs.add("supportedExtension");
|
||||
attrs.add("supportedFeatures");
|
||||
List<SearchResult> searchResults = operationManager
|
||||
.search("", "(objectClass=*)", Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
|
||||
if (searchResults.size() != 1) {
|
||||
throw new ModelException("Could not query root DSE: unexpected result size");
|
||||
}
|
||||
SearchResult rootDse = searchResults.get(0);
|
||||
Attributes attributes = rootDse.getAttributes();
|
||||
for (String attr: attrs) {
|
||||
Attribute attribute = attributes.get(attr);
|
||||
if (null != attribute) {
|
||||
CapabilityType capabilityType = CapabilityType.fromRootDseAttributeName(attr);
|
||||
NamingEnumeration<?> values = attribute.getAll();
|
||||
while (values.hasMoreElements()) {
|
||||
Object o = values.nextElement();
|
||||
LDAPCapabilityRepresentation capability = new LDAPCapabilityRepresentation(o, capabilityType);
|
||||
logger.info("rootDSE query: " + capability);
|
||||
result.add(capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Failed to query root DSE: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// *************** CREDENTIALS AND USER SPECIFIC STUFF
|
||||
|
||||
public void validatePassword(LdapMapObject user, String password) throws AuthenticationException {
|
||||
String userDN = user.getDn().toString();
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Using DN [%s] for authentication of user", userDN);
|
||||
}
|
||||
|
||||
operationManager.authenticate(userDN, password);
|
||||
}
|
||||
|
||||
// ************ END CREDENTIALS AND USER SPECIFIC STUFF
|
||||
|
||||
protected StringBuilder createIdentityTypeSearchFilter(final LdapMapQuery identityQuery) {
|
||||
StringBuilder filter = identityQuery.getModelCriteriaBuilder().getPredicateFunc().get();
|
||||
|
||||
filter.insert(0, "(&");
|
||||
filter.append(getObjectClassesFilter(identityQuery.getObjectClasses()));
|
||||
filter.append(")");
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Using filter for LDAP search: %s . Searching in DN: %s", filter, identityQuery.getSearchDn());
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
|
||||
private StringBuilder getObjectClassesFilter(Collection<String> objectClasses) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (!objectClasses.isEmpty()) {
|
||||
for (String objectClass : objectClasses) {
|
||||
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
|
||||
}
|
||||
} else {
|
||||
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
private LdapMapObject populateAttributedType(SearchResult searchResult, LdapMapQuery ldapQuery) {
|
||||
Set<String> readOnlyAttrNames = ldapQuery.getReturningReadOnlyLdapAttributes();
|
||||
Set<String> lowerCasedAttrNames = new TreeSet<>();
|
||||
for (String attrName : ldapQuery.getReturningLdapAttributes()) {
|
||||
lowerCasedAttrNames.add(attrName.toLowerCase());
|
||||
}
|
||||
|
||||
try {
|
||||
String entryDN = searchResult.getNameInNamespace();
|
||||
Attributes attributes = searchResult.getAttributes();
|
||||
|
||||
LdapMapObject ldapObject = new LdapMapObject();
|
||||
LdapMapDn dn = LdapMapDn.fromString(entryDN);
|
||||
ldapObject.setDn(dn);
|
||||
ldapObject.setRdnAttributeNames(dn.getFirstRdn().getAllKeys());
|
||||
|
||||
NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
|
||||
|
||||
while (ldapAttributes.hasMore()) {
|
||||
Attribute ldapAttribute = ldapAttributes.next();
|
||||
|
||||
try {
|
||||
ldapAttribute.get();
|
||||
} catch (NoSuchElementException nsee) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String ldapAttributeName = ldapAttribute.getID();
|
||||
|
||||
// check for ranged attribute
|
||||
Matcher m = rangePattern.matcher(ldapAttributeName);
|
||||
if (m.matches()) {
|
||||
ldapAttributeName = m.group(1);
|
||||
// range=X-* means all the attributes returned
|
||||
if (!m.group(3).equals("*")) {
|
||||
try {
|
||||
int max = Integer.parseInt(m.group(3));
|
||||
ldapObject.addRangedAttribute(ldapAttributeName, max);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warnf("Invalid ranged expresion for attribute: %s", m.group(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName())) {
|
||||
Object uuidValue = ldapAttribute.get();
|
||||
ldapObject.setId(this.operationManager.decodeEntryUUID(uuidValue));
|
||||
}
|
||||
|
||||
// Note: UUID is normally not populated here. It's populated just in case that it's used for name of other attribute as well
|
||||
if (!ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName()) || (lowerCasedAttrNames.contains(ldapAttributeName.toLowerCase()))) {
|
||||
Set<String> attrValues = new LinkedHashSet<>();
|
||||
NamingEnumeration<?> enumm = ldapAttribute.getAll();
|
||||
while (enumm.hasMoreElements()) {
|
||||
Object val = enumm.next();
|
||||
|
||||
if (val instanceof byte[]) { // byte[]
|
||||
String attrVal = Base64.encodeBytes((byte[]) val);
|
||||
attrValues.add(attrVal);
|
||||
} else { // String
|
||||
String attrVal = val.toString().trim();
|
||||
attrValues.add(attrVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
|
||||
ldapObject.setObjectClasses(attrValues);
|
||||
} else {
|
||||
ldapObject.setAttribute(ldapAttributeName, attrValues);
|
||||
|
||||
// readOnlyAttrNames are lower-cased
|
||||
if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
|
||||
ldapObject.addReadOnlyAttributeName(ldapAttributeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Found ldap object and populated with the attributes. LDAP Object: %s", ldapObject.toString());
|
||||
}
|
||||
return ldapObject;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new ModelException("Could not populate attribute type " + searchResult.getNameInNamespace() + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected BasicAttributes extractAttributesForSaving(LdapMapObject ldapObject, boolean isCreate) {
|
||||
BasicAttributes entryAttributes = new BasicAttributes();
|
||||
|
||||
Set<String> rdnAttrNamesLowerCased = ldapObject.getRdnAttributeNames().stream()
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
|
||||
String attrName = attrEntry.getKey();
|
||||
Set<String> attrValue = attrEntry.getValue();
|
||||
|
||||
if (attrValue == null) {
|
||||
// Shouldn't happen
|
||||
logger.warnf("Attribute '%s' is null on LDAP object '%s' . Using empty value to be saved to LDAP", attrName, ldapObject.getDn().toString());
|
||||
attrValue = Collections.emptySet();
|
||||
}
|
||||
|
||||
String attrNameLowercased = attrName.toLowerCase();
|
||||
if (
|
||||
// Ignore empty attributes on create (changetype: add)
|
||||
!(isCreate && attrValue.isEmpty()) &&
|
||||
|
||||
// Since we're extracting for saving, skip read-only attributes. ldapObject.getReadOnlyAttributeNames() are lower-cased
|
||||
!ldapObject.getReadOnlyAttributeNames().contains(attrNameLowercased) &&
|
||||
|
||||
// Only extract RDN for create since it can't be changed on update
|
||||
(isCreate || !rdnAttrNamesLowerCased.contains(attrNameLowercased))
|
||||
) {
|
||||
if (getConfig().getBinaryAttributeNames().contains(attrName)) {
|
||||
// Binary attribute
|
||||
entryAttributes.put(createBinaryBasicAttribute(attrName, attrValue));
|
||||
} else {
|
||||
// Text attribute
|
||||
entryAttributes.put(createBasicAttribute(attrName, attrValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't extract object classes for update
|
||||
if (isCreate) {
|
||||
BasicAttribute objectClassAttribute = new BasicAttribute(LDAPConstants.OBJECT_CLASS);
|
||||
|
||||
for (String objectClassValue : ldapObject.getObjectClasses()) {
|
||||
objectClassAttribute.add(objectClassValue);
|
||||
}
|
||||
|
||||
entryAttributes.put(objectClassAttribute);
|
||||
}
|
||||
|
||||
return entryAttributes;
|
||||
}
|
||||
|
||||
private BasicAttribute createBasicAttribute(String attrName, Set<String> attrValue) {
|
||||
BasicAttribute attr = new BasicAttribute(attrName);
|
||||
|
||||
for (String value : attrValue) {
|
||||
if (value == null || value.trim().length() == 0) {
|
||||
value = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
|
||||
}
|
||||
|
||||
attr.add(value);
|
||||
}
|
||||
|
||||
return attr;
|
||||
}
|
||||
|
||||
private BasicAttribute createBinaryBasicAttribute(String attrName, Set<String> attrValue) {
|
||||
BasicAttribute attr = new BasicAttribute(attrName);
|
||||
|
||||
for (String value : attrValue) {
|
||||
if (value == null || value.trim().length() == 0) {
|
||||
value = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] bytes = Base64.decode(value);
|
||||
attr.add(bytes);
|
||||
} catch (IOException ioe) {
|
||||
logger.warnf("Wasn't able to Base64 decode the attribute value. Ignoring attribute update. Attribute: %s, Attribute value: %s", attrName, attrValue);
|
||||
}
|
||||
}
|
||||
|
||||
return attr;
|
||||
}
|
||||
|
||||
protected String getEntryIdentifier(final LdapMapObject ldapObject) {
|
||||
try {
|
||||
// we need this to retrieve the entry's identifier from the ldap server
|
||||
String uuidAttrName = getConfig().getUuidLDAPAttributeName();
|
||||
|
||||
String rdn = ldapObject.getDn().getFirstRdn().toString(false);
|
||||
String filter = "(" + LdapMapEscapeStrategy.DEFAULT.escape(rdn) + ")";
|
||||
List<SearchResult> search = this.operationManager.search(ldapObject.getDn().toString(), filter, Collections.singletonList(uuidAttrName), SearchControls.OBJECT_SCOPE);
|
||||
Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName());
|
||||
|
||||
if (id == null) {
|
||||
throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "].");
|
||||
}
|
||||
|
||||
return this.operationManager.decodeEntryUUID(id.get());
|
||||
} catch (NamingException ne) {
|
||||
throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "].");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
public class LdapMapOctetStringEncoder {
|
||||
|
||||
private final LdapMapEscapeStrategy fallback;
|
||||
|
||||
public LdapMapOctetStringEncoder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public LdapMapOctetStringEncoder(LdapMapEscapeStrategy fallback) {
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
|
||||
public String encode(Object parameterValue, boolean isBinary) {
|
||||
String escaped;
|
||||
if (parameterValue instanceof byte[]) {
|
||||
escaped = LdapMapEscapeStrategy.escapeHex((byte[]) parameterValue);
|
||||
} else {
|
||||
escaped = escapeAsString(parameterValue, isBinary);
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
private String escapeAsString(Object parameterValue, boolean isBinary) {
|
||||
String escaped;
|
||||
String stringValue = parameterValue.toString();
|
||||
if (isBinary) {
|
||||
escaped = LdapMapEscapeStrategy.OCTET_STRING.escape(stringValue);
|
||||
} else if (fallback == null){
|
||||
escaped = stringValue;
|
||||
} else {
|
||||
escaped = fallback.escape(stringValue);
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.ldap.LdapContext;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface LdapMapOperationDecorator {
|
||||
|
||||
<R> void beforeLDAPOperation(LdapContext ldapContext, LdapMapOperationManager.LdapOperation<R> ldapOperation) throws NamingException;
|
||||
|
||||
}
|
|
@ -0,0 +1,629 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
|
||||
import org.keycloak.models.map.storage.ldap.model.LdapMapDn;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.Binding;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NameAlreadyBoundException;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.ModificationItem;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.SearchResult;
|
||||
import javax.naming.ldap.InitialLdapContext;
|
||||
import javax.naming.ldap.LdapContext;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import javax.naming.ldap.StartTlsResponse;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* <p>This class provides a set of operations to manage LDAP trees.</p>
|
||||
*
|
||||
* @author Anil Saldhana
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
|
||||
*/
|
||||
public class LdapMapOperationManager {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LdapMapOperationManager.class);
|
||||
|
||||
private static final Logger perfLogger = Logger.getLogger(LdapMapOperationManager.class, "perf");
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final LdapMapConfig config;
|
||||
|
||||
public LdapMapOperationManager(KeycloakSession session, LdapMapConfig config) {
|
||||
this.session = session;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Modifies the given {@link Attribute} instance using the given DN. This method performs a REPLACE_ATTRIBUTE
|
||||
* operation.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
public void modifyAttribute(String dn, Attribute attribute) {
|
||||
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)};
|
||||
modifyAttributes(dn, mods, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Modifies the given {@link Attribute} instances using the given DN. This method performs a REPLACE_ATTRIBUTE
|
||||
* operation.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
public void modifyAttributes(String dn, NamingEnumeration<Attribute> attributes) {
|
||||
try {
|
||||
List<ModificationItem> modItems = new ArrayList<>();
|
||||
while (attributes.hasMore()) {
|
||||
ModificationItem modItem = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attributes.next());
|
||||
modItems.add(modItem);
|
||||
}
|
||||
|
||||
modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}), null);
|
||||
} catch (NamingException ne) {
|
||||
throw new ModelException("Could not modify attributes on entry from DN [" + dn + "]", ne);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Removes the given {@link Attribute} instance using the given DN. This method performs a REMOVE_ATTRIBUTE
|
||||
* operation.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
public void removeAttribute(String dn, Attribute attribute) {
|
||||
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)};
|
||||
modifyAttributes(dn, mods, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Adds the given {@link Attribute} instance using the given DN. This method performs a ADD_ATTRIBUTE operation.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
public void addAttribute(String dn, Attribute attribute) {
|
||||
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)};
|
||||
modifyAttributes(dn, mods, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Removes the object from the LDAP tree
|
||||
* </p>
|
||||
*/
|
||||
public void removeEntry(final String entryDn) {
|
||||
try {
|
||||
execute(new LdapOperation<SearchResult>() {
|
||||
|
||||
@Override
|
||||
public SearchResult execute(LdapContext context) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing entry with DN [%s]", entryDn);
|
||||
}
|
||||
destroySubcontext(context, entryDn);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapOperation: remove\n" +
|
||||
" dn: " + entryDn;
|
||||
}
|
||||
|
||||
});
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not remove entry from DN [" + entryDn + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rename LDAPObject name (DN)
|
||||
*
|
||||
* @param fallback With fallback=true, we will try to find the another DN in case of conflict. For example if there is an
|
||||
* attempt to rename to "CN=John Doe", but there is already existing "CN=John Doe", we will try "CN=John Doe0"
|
||||
* @return the non-conflicting DN, which was used in the end
|
||||
*/
|
||||
public String renameEntry(String oldDn, String newDn, boolean fallback) {
|
||||
try {
|
||||
return execute(new LdapOperation<String>() {
|
||||
|
||||
@Override
|
||||
public String execute(LdapContext context) throws NamingException {
|
||||
String dn = newDn;
|
||||
|
||||
// Max 5 attempts for now
|
||||
int max = 5;
|
||||
for (int i=0 ; i<max ; i++) {
|
||||
try {
|
||||
context.rename(new LdapName(oldDn), new LdapName(dn));
|
||||
return dn;
|
||||
} catch (NameAlreadyBoundException ex) {
|
||||
if (!fallback) {
|
||||
throw ex;
|
||||
} else {
|
||||
String failedDn = dn;
|
||||
dn = findNextDNForFallback(newDn, i);
|
||||
logger.warnf("Failed to rename DN [%s] to [%s]. Will try to fallback to DN [%s]", oldDn, failedDn, dn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ModelException("Could not rename entry from DN [" + oldDn + "] to new DN [" + newDn + "]. All fallbacks failed");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapOperation: renameEntry\n" +
|
||||
" oldDn: " + oldDn + "\n" +
|
||||
" newDn: " + newDn;
|
||||
}
|
||||
|
||||
});
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not rename entry from DN [" + oldDn + "] to new DN [" + newDn + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String findNextDNForFallback(String newDn, int counter) {
|
||||
LdapMapDn dn = LdapMapDn.fromString(newDn);
|
||||
LdapMapDn.RDN firstRdn = dn.getFirstRdn();
|
||||
String rdnAttrName = firstRdn.getAllKeys().get(0);
|
||||
String rdnAttrVal = firstRdn.getAttrValue(rdnAttrName);
|
||||
LdapMapDn parentDn = dn.getParentDn();
|
||||
parentDn.addFirst(rdnAttrName, rdnAttrVal + counter);
|
||||
return parentDn.toString();
|
||||
}
|
||||
|
||||
|
||||
public List<SearchResult> search(final String baseDN, final String filter, Collection<String> returningAttributes, int searchScope) throws NamingException {
|
||||
final List<SearchResult> result = new ArrayList<>();
|
||||
final SearchControls cons = getSearchControls(returningAttributes, searchScope);
|
||||
|
||||
return execute(new LdapOperation<List<SearchResult>>() {
|
||||
@Override
|
||||
public List<SearchResult> execute(LdapContext context) throws NamingException {
|
||||
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
|
||||
|
||||
while (search.hasMoreElements()) {
|
||||
result.add(search.nextElement());
|
||||
}
|
||||
|
||||
search.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapOperation: search\n" +
|
||||
" baseDn: " + baseDN + "\n" +
|
||||
" filter: " + filter + "\n" +
|
||||
" searchScope: " + searchScope + "\n" +
|
||||
" returningAttrs: " + returningAttributes + "\n" +
|
||||
" resultSize: " + result.size();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private SearchControls getSearchControls(Collection<String> returningAttributes, int searchScope) {
|
||||
final SearchControls cons = new SearchControls();
|
||||
|
||||
cons.setSearchScope(searchScope);
|
||||
cons.setReturningObjFlag(false);
|
||||
|
||||
returningAttributes = getReturningAttributes(returningAttributes);
|
||||
|
||||
cons.setReturningAttributes(returningAttributes.toArray(new String[0]));
|
||||
return cons;
|
||||
}
|
||||
|
||||
public String getFilterById(String id) {
|
||||
StringBuilder filter = new StringBuilder();
|
||||
filter.insert(0, "(&");
|
||||
|
||||
if (this.config.isObjectGUID()) {
|
||||
byte[] objectGUID = LdapMapUtil.encodeObjectGUID(id);
|
||||
filter.append("(objectClass=*)(").append(
|
||||
getUuidAttributeName()).append(LDAPConstants.EQUAL)
|
||||
.append(LdapMapUtil.convertObjectGUIDToByteString(
|
||||
objectGUID)).append(")");
|
||||
|
||||
} else if (this.config.isEdirectoryGUID()) {
|
||||
filter.append("(objectClass=*)(").append(getUuidAttributeName().toUpperCase())
|
||||
.append(LDAPConstants.EQUAL
|
||||
).append(LdapMapUtil.convertGUIDToEdirectoryHexString(id)).append(")");
|
||||
} else {
|
||||
filter.append("(objectClass=*)(").append(getUuidAttributeName()).append(LDAPConstants.EQUAL)
|
||||
.append(id).append(")");
|
||||
}
|
||||
|
||||
if (config.getCustomUserSearchFilter() != null) {
|
||||
filter.append(config.getCustomUserSearchFilter());
|
||||
}
|
||||
|
||||
filter.append(")");
|
||||
String ldapIdFilter = filter.toString();
|
||||
|
||||
logger.tracef("Using filter for lookup user by LDAP ID: %s", ldapIdFilter);
|
||||
|
||||
return ldapIdFilter;
|
||||
}
|
||||
|
||||
public SearchResult lookupById(final String baseDN, final String id, final Collection<String> returningAttributes) {
|
||||
final String filter = getFilterById(id);
|
||||
|
||||
try {
|
||||
final SearchControls cons = getSearchControls(returningAttributes, this.config.getSearchScope());
|
||||
|
||||
return execute(new LdapOperation<SearchResult>() {
|
||||
|
||||
@Override
|
||||
public SearchResult execute(LdapContext context) throws NamingException {
|
||||
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
|
||||
|
||||
try {
|
||||
if (search.hasMoreElements()) {
|
||||
return search.next();
|
||||
}
|
||||
} finally {
|
||||
if (search != null) {
|
||||
search.close();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapOperation: lookupById\n" +
|
||||
" baseDN: " + baseDN + "\n" +
|
||||
" filter: " + filter + "\n" +
|
||||
" searchScope: " + cons.getSearchScope() + "\n" +
|
||||
" returningAttrs: " + returningAttributes;
|
||||
}
|
||||
|
||||
});
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not query server using DN [" + baseDN + "] and filter [" + filter + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Destroys a subcontext with the given DN from the LDAP tree.
|
||||
* </p>
|
||||
*/
|
||||
private void destroySubcontext(LdapContext context, final String dn) {
|
||||
try {
|
||||
NamingEnumeration<Binding> enumeration = null;
|
||||
|
||||
try {
|
||||
enumeration = context.listBindings(new LdapName(dn));
|
||||
|
||||
while (enumeration.hasMore()) {
|
||||
Binding binding = enumeration.next();
|
||||
String name = binding.getNameInNamespace();
|
||||
|
||||
destroySubcontext(context, name);
|
||||
}
|
||||
|
||||
context.unbind(new LdapName(dn));
|
||||
} finally {
|
||||
if (enumeration != null) {
|
||||
try {
|
||||
enumeration.close();
|
||||
} catch (Exception e) {
|
||||
logger.warn("problem during close", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ModelException("Could not unbind DN [" + dn + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Performs a simple authentication using the given DN and password to bind to the authentication context.
|
||||
* </p>
|
||||
*
|
||||
* @throws AuthenticationException if authentication is not successful
|
||||
*
|
||||
*/
|
||||
public void authenticate(String dn, String password) throws AuthenticationException {
|
||||
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new AuthenticationException("Empty password used");
|
||||
}
|
||||
|
||||
LdapContext authCtx = null;
|
||||
StartTlsResponse tlsResponse = null;
|
||||
|
||||
try {
|
||||
|
||||
Hashtable<Object, Object> env = LdapMapContextManager.getNonAuthConnectionProperties(config);
|
||||
|
||||
// Never use connection pool to prevent password caching
|
||||
env.put("com.sun.jndi.ldap.connect.pool", "false");
|
||||
|
||||
if(!this.config.isStartTls()) {
|
||||
env.put(Context.SECURITY_AUTHENTICATION, "simple");
|
||||
env.put(Context.SECURITY_PRINCIPAL, dn);
|
||||
env.put(Context.SECURITY_CREDENTIALS, password);
|
||||
}
|
||||
|
||||
authCtx = new InitialLdapContext(env, null);
|
||||
if (config.isStartTls()) {
|
||||
SSLSocketFactory sslSocketFactory = null;
|
||||
String useTruststoreSpi = config.getUseTruststoreSpi();
|
||||
if (useTruststoreSpi != null && useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_ALWAYS)) {
|
||||
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
|
||||
sslSocketFactory = provider.getSSLSocketFactory();
|
||||
}
|
||||
|
||||
tlsResponse = LdapMapContextManager.startTLS(authCtx, "simple", dn, password.toCharArray(), sslSocketFactory);
|
||||
|
||||
// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
|
||||
if (tlsResponse == null) {
|
||||
throw new AuthenticationException("Null TLS Response returned from the authentication");
|
||||
}
|
||||
}
|
||||
} catch (AuthenticationException ae) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(ae, "Authentication failed for DN [%s]", dn);
|
||||
}
|
||||
|
||||
throw ae;
|
||||
} catch(RuntimeException re){
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(re, "LDAP Connection TimeOut for DN [%s]", dn);
|
||||
}
|
||||
|
||||
throw re;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.errorf(e, "Unexpected exception when validating password of DN [%s]", dn);
|
||||
throw new AuthenticationException("Unexpected exception when validating password of user");
|
||||
} finally {
|
||||
if (tlsResponse != null) {
|
||||
try {
|
||||
tlsResponse.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (authCtx != null) {
|
||||
try {
|
||||
authCtx.close();
|
||||
} catch (NamingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void modifyAttributesNaming(final String dn, final ModificationItem[] mods, LdapMapOperationDecorator decorator) throws NamingException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
||||
|
||||
for (ModificationItem item : mods) {
|
||||
Object values;
|
||||
|
||||
if (item.getAttribute().size() > 0) {
|
||||
values = item.getAttribute().get();
|
||||
} else {
|
||||
values = "No values";
|
||||
}
|
||||
|
||||
String attrName = item.getAttribute().getID().toUpperCase();
|
||||
if (attrName.contains("PASSWORD") || attrName.contains("UNICODEPWD")) {
|
||||
values = "********************";
|
||||
}
|
||||
|
||||
logger.tracef(" Op [%s]: %s = %s", item.getModificationOp(), item.getAttribute().getID(), values);
|
||||
}
|
||||
|
||||
logger.tracef("]");
|
||||
}
|
||||
|
||||
execute(new LdapOperation<Void>() {
|
||||
|
||||
@Override
|
||||
public Void execute(LdapContext context) throws NamingException {
|
||||
context.modifyAttributes(new LdapName(dn), mods);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapOperation: modify\n" +
|
||||
" dn: " + dn + "\n" +
|
||||
" modificationsSize: " + mods.length;
|
||||
}
|
||||
|
||||
}, decorator);
|
||||
}
|
||||
|
||||
public void modifyAttributes(final String dn, final ModificationItem[] mods, LdapMapOperationDecorator decorator) {
|
||||
try {
|
||||
modifyAttributesNaming(dn, mods, decorator);
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void createSubContext(final String name, final Attributes attributes) {
|
||||
try {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Creating entry [%s] with attributes: [", name);
|
||||
|
||||
NamingEnumeration<? extends Attribute> all = attributes.getAll();
|
||||
|
||||
while (all.hasMore()) {
|
||||
Attribute attribute = all.next();
|
||||
|
||||
String attrName = attribute.getID().toUpperCase();
|
||||
Object attrVal = attribute.get();
|
||||
if (attrName.contains("PASSWORD") || attrName.contains("UNICODEPWD")) {
|
||||
attrVal = "********************";
|
||||
}
|
||||
|
||||
logger.tracef(" %s = %s", attribute.getID(), attrVal);
|
||||
}
|
||||
|
||||
logger.tracef("]");
|
||||
}
|
||||
|
||||
execute(new LdapOperation<Void>() {
|
||||
@Override
|
||||
public Void execute(LdapContext context) throws NamingException {
|
||||
DirContext subcontext = context.createSubcontext(new LdapName(name), attributes);
|
||||
|
||||
subcontext.close();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LdapOperation: create\n" +
|
||||
" dn: " + name + "\n" +
|
||||
" attributesSize: " + attributes.size();
|
||||
}
|
||||
|
||||
});
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Error creating subcontext [" + name + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getUuidAttributeName() {
|
||||
return this.config.getUuidLDAPAttributeName();
|
||||
}
|
||||
|
||||
public Attributes getAttributes(final String entryUUID, final String baseDN, Set<String> returningAttributes) {
|
||||
SearchResult search = lookupById(baseDN, entryUUID, returningAttributes);
|
||||
|
||||
if (search == null) {
|
||||
throw new ModelException("Couldn't find item with ID [" + entryUUID + " under base DN [" + baseDN + "]");
|
||||
}
|
||||
|
||||
return search.getAttributes();
|
||||
}
|
||||
|
||||
public String decodeEntryUUID(final Object entryUUID) {
|
||||
if (entryUUID instanceof byte[]) {
|
||||
if (this.config.isObjectGUID()) {
|
||||
return LdapMapUtil.decodeObjectGUID((byte[]) entryUUID);
|
||||
}
|
||||
if (this.config.isEdirectory() && this.config.isEdirectoryGUID()) {
|
||||
return LdapMapUtil.decodeGuid((byte[]) entryUUID);
|
||||
}
|
||||
}
|
||||
return entryUUID.toString();
|
||||
}
|
||||
|
||||
private <R> R execute(LdapOperation<R> operation) throws NamingException {
|
||||
return execute(operation, null);
|
||||
}
|
||||
|
||||
private <R> R execute(LdapOperation<R> operation, LdapMapOperationDecorator decorator) throws NamingException {
|
||||
try (LdapMapContextManager ldapMapContextManager = LdapMapContextManager.create(session, config)) {
|
||||
return execute(operation, ldapMapContextManager.getLdapContext(), decorator);
|
||||
}
|
||||
}
|
||||
|
||||
private <R> R execute(LdapOperation<R> operation, LdapContext context, LdapMapOperationDecorator decorator) throws NamingException {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("Ldap context cannot be null");
|
||||
}
|
||||
|
||||
Long start = null;
|
||||
|
||||
if (perfLogger.isDebugEnabled()) {
|
||||
start = Time.currentTimeMillis();
|
||||
}
|
||||
|
||||
try {
|
||||
if (decorator != null) {
|
||||
decorator.beforeLDAPOperation(context, operation);
|
||||
}
|
||||
|
||||
return operation.execute(context);
|
||||
} finally {
|
||||
if (start != null) {
|
||||
long took = Time.currentTimeMillis() - start;
|
||||
|
||||
if (took > 100) {
|
||||
perfLogger.debugf("\n%s\ntook: %d ms\n", operation.toString(), took);
|
||||
} else if (perfLogger.isTraceEnabled()) {
|
||||
perfLogger.tracef("\n%s\ntook: %d ms\n", operation.toString(), took);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface LdapOperation<R> {
|
||||
R execute(LdapContext context) throws NamingException;
|
||||
}
|
||||
|
||||
private Set<String> getReturningAttributes(final Collection<String> returningAttributes) {
|
||||
Set<String> result = new HashSet<>(returningAttributes);
|
||||
result.add(getUuidAttributeName());
|
||||
result.add(LDAPConstants.OBJECT_CLASS);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import org.keycloak.models.ModelException;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* <p>Utility class for working with LDAP.</p>
|
||||
*
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class LdapMapUtil {
|
||||
|
||||
/**
|
||||
* <p>Formats the given date.</p>
|
||||
*
|
||||
* @param date The Date to format.
|
||||
*
|
||||
* @return A String representing the formatted date.
|
||||
*/
|
||||
public static String formatDate(Date date) {
|
||||
if (date == null) {
|
||||
throw new IllegalArgumentException("You must provide a date.");
|
||||
}
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'.0Z'");
|
||||
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
return dateFormat.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Parses dates/time stamps stored in LDAP. Some possible values:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>20020228150820</li>
|
||||
* <li>20030228150820Z</li>
|
||||
* <li>20050228150820.12</li>
|
||||
* <li>20060711011740.0Z</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param date The date string to parse from.
|
||||
*
|
||||
* @return the Date.
|
||||
*/
|
||||
public static Date parseDate(String date) {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
|
||||
try {
|
||||
if (date.endsWith("Z")) {
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
} else {
|
||||
dateFormat.setTimeZone(TimeZone.getDefault());
|
||||
}
|
||||
|
||||
return dateFormat.parse(date);
|
||||
} catch (Exception e) {
|
||||
throw new ModelException("Error converting ldap date.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* <p>Creates a byte-based {@link String} representation of a raw byte array representing the value of the
|
||||
* <code>objectGUID</code> attribute retrieved from Active Directory.</p>
|
||||
*
|
||||
* <p>The returned string is useful to perform queries on AD based on the <code>objectGUID</code> value. Eg.:</p>
|
||||
*
|
||||
* <p>
|
||||
* String filter = "(&(objectClass=*)(objectGUID" + EQUAL + convertObjectGUIDToByteString(objectGUID) + "))";
|
||||
* </p>
|
||||
*
|
||||
* @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
|
||||
* Active Directory.
|
||||
*
|
||||
* @return A byte-based String representation in the form of \[0]\[1]\[2]\[3]\[4]\[5]\[6]\[7]\[8]\[9]\[10]\[11]\[12]\[13]\[14]\[15]
|
||||
*/
|
||||
public static String convertObjectGUIDToByteString(byte[] objectGUID) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (byte b : objectGUID) {
|
||||
String transformed = prefixZeros((int) b & 0xFF);
|
||||
result.append("\\");
|
||||
result.append(transformed);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* see http://support.novell.com/docs/Tids/Solutions/10096551.html
|
||||
*
|
||||
* @param guid A GUID in the form of a dashed String as the result of (@see LDAPUtil#convertToDashedString)
|
||||
*
|
||||
* @return A String representation in the form of \[0][1]\[2][3]\[4][5]\[6][7]\[8][9]\[10][11]\[12][13]\[14][15]
|
||||
*/
|
||||
public static String convertGUIDToEdirectoryHexString(String guid) {
|
||||
String withoutDash = guid.replace("-", "");
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < withoutDash.length(); i++) {
|
||||
result.append("\\");
|
||||
result.append(withoutDash.charAt(i));
|
||||
result.append(withoutDash.charAt(++i));
|
||||
}
|
||||
|
||||
return result.toString().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Encode a string representing the display value of the <code>objectGUID</code> attribute retrieved from Active
|
||||
* Directory.</p>
|
||||
*
|
||||
* @param displayString A string representing the decoded value in the form of [3][2][1][0]-[5][4]-[7][6]-[8][9]-[10][11][12][13][14][15].
|
||||
*
|
||||
* @return A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
|
||||
* Active Directory.
|
||||
*/
|
||||
public static byte[] encodeObjectGUID(String displayString) {
|
||||
byte [] objectGUID = new byte[16];
|
||||
// [3][2][1][0]
|
||||
objectGUID[0] = (byte) ((Character.digit(displayString.charAt(6), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(7), 16));
|
||||
objectGUID[1] = (byte) ((Character.digit(displayString.charAt(4), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(5), 16));
|
||||
objectGUID[2] = (byte) ((Character.digit(displayString.charAt(2), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(3), 16));
|
||||
objectGUID[3] = (byte) ((Character.digit(displayString.charAt(0), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(1), 16));
|
||||
// [5][4]
|
||||
objectGUID[4] = (byte) ((Character.digit(displayString.charAt(11), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(12), 16));
|
||||
objectGUID[5] = (byte) ((Character.digit(displayString.charAt(9), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(10), 16));
|
||||
// [7][6]
|
||||
objectGUID[6] = (byte) ((Character.digit(displayString.charAt(16), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(17), 16));
|
||||
objectGUID[7] = (byte) ((Character.digit(displayString.charAt(14), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(15), 16));
|
||||
// [8][9]
|
||||
objectGUID[8] = (byte) ((Character.digit(displayString.charAt(19), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(20), 16));
|
||||
objectGUID[9] = (byte) ((Character.digit(displayString.charAt(21), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(22), 16));
|
||||
// [10][11][12][13][14][15]
|
||||
objectGUID[10] = (byte) ((Character.digit(displayString.charAt(24), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(25), 16));
|
||||
objectGUID[11] = (byte) ((Character.digit(displayString.charAt(26), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(27), 16));
|
||||
objectGUID[12] = (byte) ((Character.digit(displayString.charAt(28), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(29), 16));
|
||||
objectGUID[13] = (byte) ((Character.digit(displayString.charAt(30), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(31), 16));
|
||||
objectGUID[14] = (byte) ((Character.digit(displayString.charAt(32), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(33), 16));
|
||||
objectGUID[15] = (byte) ((Character.digit(displayString.charAt(34), 16) << 4)
|
||||
+ Character.digit(displayString.charAt(35), 16));
|
||||
return objectGUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Decode a raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from Active
|
||||
* Directory.</p>
|
||||
*
|
||||
* <p>The returned string is useful to directly bind an entry. Eg.:</p>
|
||||
*
|
||||
* <p>
|
||||
* String bindingString = decodeObjectGUID(objectGUID);
|
||||
* <br/>
|
||||
* Attributes attributes = ctx.getAttributes(bindingString);
|
||||
* </p>
|
||||
*
|
||||
* @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
|
||||
* Active Directory.
|
||||
*
|
||||
* @return A string representing the decoded value in the form of [3][2][1][0]-[5][4]-[7][6]-[8][9]-[10][11][12][13][14][15].
|
||||
*/
|
||||
public static String decodeObjectGUID(byte[] objectGUID) {
|
||||
return convertToDashedString(objectGUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Decode a raw byte array representing the value of the <code>guid</code> attribute retrieved from Novell
|
||||
* eDirectory.</p>
|
||||
*
|
||||
* @param guid A raw byte array representing the value of the <code>guid</code> attribute retrieved from
|
||||
* Novell eDirectory.
|
||||
*
|
||||
* @return A string representing the decoded value in the form of [0][1][2][3]-[4][5]-[6][7]-[8][9]-[10][11][12][13][14][15].
|
||||
*/
|
||||
public static String decodeGuid(byte[] guid) {
|
||||
byte[] withBigEndian = new byte[] { guid[3], guid[2], guid[1], guid[0],
|
||||
guid[5], guid[4],
|
||||
guid[7], guid[6],
|
||||
guid[8], guid[9], guid[10], guid[11], guid[12], guid[13], guid[14], guid[15]
|
||||
};
|
||||
return convertToDashedString(withBigEndian);
|
||||
}
|
||||
|
||||
private static String convertToDashedString(byte[] objectGUID) {
|
||||
return prefixZeros((int) objectGUID[3] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[2] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[1] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[0] & 0xFF) +
|
||||
"-" +
|
||||
prefixZeros((int) objectGUID[5] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[4] & 0xFF) +
|
||||
"-" +
|
||||
prefixZeros((int) objectGUID[7] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[6] & 0xFF) +
|
||||
"-" +
|
||||
prefixZeros((int) objectGUID[8] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[9] & 0xFF) +
|
||||
"-" +
|
||||
prefixZeros((int) objectGUID[10] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[11] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[12] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[13] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[14] & 0xFF) +
|
||||
prefixZeros((int) objectGUID[15] & 0xFF);
|
||||
}
|
||||
|
||||
private static String prefixZeros(int value) {
|
||||
if (value <= 0xF) {
|
||||
return "0" + Integer.toHexString(value);
|
||||
} else {
|
||||
return Integer.toHexString(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# Copyright 2021 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.map.storage.ldap.LdapMapStorageProviderFactory
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Simple configuration holder that allows for unit testing.
|
||||
*/
|
||||
public class Config extends org.keycloak.Config.SystemPropertiesScope {
|
||||
|
||||
private final Map<String, String> props;
|
||||
|
||||
private Config(String prefix, Map<String, String> props) {
|
||||
super(prefix);
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
public Config() {
|
||||
this("", new HashMap<>());
|
||||
}
|
||||
|
||||
public void put(String key, String value) {
|
||||
props.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(String key, String defaultValue) {
|
||||
String val = props.get(prefix + key);
|
||||
if (val != null) {
|
||||
return val;
|
||||
}
|
||||
return super.get(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.keycloak.Config.Scope scope(String... scope) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(prefix).append(".");
|
||||
for (String s : scope) {
|
||||
sb.append(s);
|
||||
sb.append(".");
|
||||
}
|
||||
return new Config(sb.toString(), props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.role.config;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.map.storage.ldap.Config;
|
||||
|
||||
public class LdapMapRoleMapperConfigTest {
|
||||
|
||||
@Test
|
||||
public void shouldEscapeClientNameForPlaceholder() {
|
||||
Config config = new Config();
|
||||
config.put(LdapMapRoleMapperConfig.CLIENT_ROLES_DN, "ou={0},dc=keycloak,dc=org");
|
||||
LdapMapRoleMapperConfig sut = new LdapMapRoleMapperConfig(config);
|
||||
|
||||
Assert.assertEquals("ou=myclient,dc=keycloak,dc=org",
|
||||
sut.getRolesDn(true, "myclient"));
|
||||
Assert.assertEquals("ou=\\ me\\=co\\\\ol\\, val\\=V\u00E9ronique,dc=keycloak,dc=org",
|
||||
sut.getRolesDn(true, " me=co\\ol, val=V\u00E9ronique"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2022. 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.models.map.storage.ldap.store;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test escaping of characters.
|
||||
*
|
||||
* Test cases extracted from https://docs.oracle.com/cd/E29127_01/doc.111170/e28969/ds-ldif-search-filters.htm#gdxoy
|
||||
*/
|
||||
public class LdapMapEscapeStrategyTest {
|
||||
|
||||
@Test
|
||||
public void shouldEscapeUtf8CharactersForDefaultStrategy() {
|
||||
Assert.assertEquals("V\\c3\\a9ronique", LdapMapEscapeStrategy.DEFAULT.escape("V\u00E9ronique"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEscapeLdapQueryCharactersCharactersForDefaultStrategy() {
|
||||
Assert.assertEquals("\\28\\29\\2a\\5c", LdapMapEscapeStrategy.DEFAULT.escape("()*\\"));
|
||||
}
|
||||
|
||||
}
|
|
@ -169,7 +169,7 @@ public class DeepCloner {
|
|||
*
|
||||
* @param <V> Class or interface that would be instantiated by the given methods
|
||||
* @param clazz Class or interface that would be instantiated by the given methods
|
||||
* @param constructor Function that creates a new instance of class {@code V}.
|
||||
* @param delegateCreator Function that creates a new instance of class {@code V}.
|
||||
* If {@code null}, such a single-parameter constructor is not available.
|
||||
* @return This builder.
|
||||
*/
|
||||
|
@ -185,7 +185,7 @@ public class DeepCloner {
|
|||
*
|
||||
* @param <V> Class or interface that would be instantiated by the given methods
|
||||
* @param clazz Class or interface that would be instantiated by the given methods
|
||||
* @param constructor Function that creates a new instance of class {@code V}.
|
||||
* @param delegateCreator Function that creates a new instance of class {@code V}.
|
||||
* If {@code null}, such a single-parameter constructor is not available.
|
||||
* @return This builder.
|
||||
*/
|
||||
|
@ -376,7 +376,7 @@ public class DeepCloner {
|
|||
/**
|
||||
* Returns a class type of an instance that would be instantiated by {@link #newInstance(java.lang.Class)} method.
|
||||
* @param <V> Type (class or a {@code @Root} interface) to create a new instance
|
||||
* @param clazz Type (class or a {@code @Root} interface) to create a new instance
|
||||
* @param valueType Type (class or a {@code @Root} interface) to create a new instance
|
||||
* @return See description
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
|
@ -71,7 +71,7 @@ public class MapRoleProvider implements RoleProvider {
|
|||
entity.setRealmId(realm.getId());
|
||||
entity.setName(name);
|
||||
entity.setClientRole(false);
|
||||
if (tx.read(entity.getId()) != null) {
|
||||
if (entity.getId() != null && tx.read(entity.getId()) != null) {
|
||||
throw new ModelDuplicateException("Role exists: " + id);
|
||||
}
|
||||
entity = tx.create(entity);
|
||||
|
@ -129,7 +129,7 @@ public class MapRoleProvider implements RoleProvider {
|
|||
entity.setName(name);
|
||||
entity.setClientRole(true);
|
||||
entity.setClientId(client.getId());
|
||||
if (tx.read(entity.getId()) != null) {
|
||||
if (entity.getId() != null && tx.read(entity.getId()) != null) {
|
||||
throw new ModelDuplicateException("Role exists: " + id);
|
||||
}
|
||||
entity = tx.create(entity);
|
||||
|
@ -244,7 +244,8 @@ public class MapRoleProvider implements RoleProvider {
|
|||
|
||||
MapRoleEntity entity = tx.read(id);
|
||||
String realmId = realm.getId();
|
||||
return (entity == null || ! Objects.equals(realmId, entity.getRealmId()))
|
||||
// when a store doesn't store information about all realms, it doesn't have the information about
|
||||
return (entity == null || (entity.getRealmId() != null && !Objects.equals(realmId, entity.getRealmId())))
|
||||
? null
|
||||
: entityToAdapterFunc(realm).apply(entity);
|
||||
}
|
||||
|
|
|
@ -29,8 +29,12 @@ public interface MapKeycloakTransaction<V extends AbstractEntity, M> extends Key
|
|||
* Updates to the returned instances of {@code V} would be visible in the current transaction
|
||||
* and will propagate into the underlying store upon commit.
|
||||
*
|
||||
* The ID of the entity passed in the parameter might change to a different value in the returned value
|
||||
* if the underlying storage decided this was necessary.
|
||||
* If the ID of the entity was null before, it will be set on the returned value.
|
||||
*
|
||||
* @param value the value
|
||||
* @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}
|
||||
* @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}.
|
||||
*/
|
||||
V create(V value);
|
||||
|
||||
|
@ -47,7 +51,7 @@ public interface MapKeycloakTransaction<V extends AbstractEntity, M> extends Key
|
|||
/**
|
||||
* Returns a stream of values from underlying storage that are updated based on the current transaction changes;
|
||||
* i.e. the result contains updates and excludes of records that have been created, updated or deleted in this
|
||||
* transaction by methods {@link MapKeycloakTransaction#create}, {@link MapKeycloakTransaction#update},
|
||||
* transaction by methods {@link MapKeycloakTransaction#create}, {@link MapKeycloakTransaction#create},
|
||||
* {@link MapKeycloakTransaction#delete}, etc.
|
||||
* <p>
|
||||
* Updates to the returned instances of {@code V} would be visible in the current transaction
|
||||
|
@ -79,8 +83,6 @@ public interface MapKeycloakTransaction<V extends AbstractEntity, M> extends Key
|
|||
/**
|
||||
* Instructs this transaction to remove values (identified by {@code mcb} filter) from the underlying store on commit.
|
||||
*
|
||||
* @param artificialKey key to record the transaction with, must be a key that does not exist in this transaction to
|
||||
* prevent collisions with other operations in this transaction
|
||||
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
|
||||
* @return number of removed objects (might return {@code -1} if not supported)
|
||||
*/
|
||||
|
|
|
@ -35,12 +35,12 @@ import java.util.stream.Collectors;
|
|||
public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements ModelCriteriaBuilder<M, MapModelCriteriaBuilder<K, V, M>> {
|
||||
|
||||
@FunctionalInterface
|
||||
public static interface UpdatePredicatesFunc<K, V extends AbstractEntity, M> {
|
||||
public interface UpdatePredicatesFunc<K, V extends AbstractEntity, M> {
|
||||
MapModelCriteriaBuilder<K, V, M> apply(MapModelCriteriaBuilder<K, V, M> builder, Operator op, Object[] params);
|
||||
}
|
||||
|
||||
private static final Predicate<Object> ALWAYS_TRUE = (e) -> true;
|
||||
private static final Predicate<Object> ALWAYS_FALSE = (e) -> false;
|
||||
protected static final Predicate<Object> ALWAYS_TRUE = (e) -> true;
|
||||
protected static final Predicate<Object> ALWAYS_FALSE = (e) -> false;
|
||||
private final Predicate<? super K> keyFilter;
|
||||
private final Predicate<? super V> entityFilter;
|
||||
private final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
|
||||
|
@ -50,7 +50,7 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
|||
this(keyConvertor, fieldPredicates, ALWAYS_TRUE, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
private MapModelCriteriaBuilder(StringKeyConvertor<K> keyConvertor, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, Predicate<? super K> indexReadFilter, Predicate<? super V> sequentialReadFilter) {
|
||||
protected MapModelCriteriaBuilder(StringKeyConvertor<K> keyConvertor, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, Predicate<? super K> indexReadFilter, Predicate<? super V> sequentialReadFilter) {
|
||||
this.keyConvertor = keyConvertor;
|
||||
this.fieldPredicates = fieldPredicates;
|
||||
this.keyFilter = indexReadFilter;
|
||||
|
@ -73,7 +73,7 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
|||
public final MapModelCriteriaBuilder<K, V, M> and(MapModelCriteriaBuilder<K, V, M>... builders) {
|
||||
Predicate<? super K> resIndexFilter = Stream.of(builders).map(MapModelCriteriaBuilder.class::cast).map(MapModelCriteriaBuilder::getKeyFilter).reduce(keyFilter, Predicate::and);
|
||||
Predicate<V> resEntityFilter = Stream.of(builders).map(MapModelCriteriaBuilder.class::cast).map(MapModelCriteriaBuilder::getEntityFilter).reduce(entityFilter, Predicate::and);
|
||||
return new MapModelCriteriaBuilder<>(keyConvertor, fieldPredicates, resIndexFilter, resEntityFilter);
|
||||
return instantiateNewInstance(keyConvertor, fieldPredicates, resIndexFilter, resEntityFilter);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
|
@ -82,7 +82,7 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
|||
public final MapModelCriteriaBuilder<K, V, M> or(MapModelCriteriaBuilder<K, V, M>... builders) {
|
||||
Predicate<? super K> resIndexFilter = Stream.of(builders).map(MapModelCriteriaBuilder.class::cast).map(MapModelCriteriaBuilder::getKeyFilter).reduce(ALWAYS_FALSE, Predicate::or);
|
||||
Predicate<V> resEntityFilter = Stream.of(builders).map(MapModelCriteriaBuilder.class::cast).map(MapModelCriteriaBuilder::getEntityFilter).reduce(ALWAYS_FALSE, Predicate::or);
|
||||
return new MapModelCriteriaBuilder<>(
|
||||
return instantiateNewInstance(
|
||||
keyConvertor,
|
||||
fieldPredicates,
|
||||
v -> keyFilter.test(v) && resIndexFilter.test(v),
|
||||
|
@ -90,14 +90,12 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
|||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public MapModelCriteriaBuilder<K, V, M> not(MapModelCriteriaBuilder<K, V, M> builder) {
|
||||
MapModelCriteriaBuilder<K, V, M> b = (MapModelCriteriaBuilder<K, V, M>) builder;
|
||||
Predicate<? super K> resIndexFilter = builder.getKeyFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : builder.getKeyFilter().negate();
|
||||
Predicate<? super V> resEntityFilter = builder.getEntityFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : builder.getEntityFilter().negate();
|
||||
|
||||
return new MapModelCriteriaBuilder<>(
|
||||
return instantiateNewInstance(
|
||||
keyConvertor,
|
||||
fieldPredicates,
|
||||
v -> keyFilter.test(v) && resIndexFilter.test(v),
|
||||
|
@ -125,7 +123,7 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
|||
case EXISTS:
|
||||
case NOT_EXISTS:
|
||||
case IN:
|
||||
return new MapModelCriteriaBuilder<>(keyConvertor, fieldPredicates, this.keyFilter.and(CriteriaOperator.predicateFor(op, convertedValues)), this.entityFilter);
|
||||
return instantiateNewInstance(keyConvertor, fieldPredicates, this.keyFilter.and(CriteriaOperator.predicateFor(op, convertedValues)), this.entityFilter);
|
||||
default:
|
||||
throw new AssertionError("Invalid operator: " + op);
|
||||
}
|
||||
|
@ -167,6 +165,16 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
|||
final Predicate<V> p = v -> valueComparator.test(getter.apply(v));
|
||||
resEntityFilter = p.and(entityFilter);
|
||||
}
|
||||
return new MapModelCriteriaBuilder<>(keyConvertor, fieldPredicates, this.keyFilter, resEntityFilter);
|
||||
return instantiateNewInstance(keyConvertor, fieldPredicates, this.keyFilter, resEntityFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance for nodes in this criteria tree.
|
||||
*
|
||||
* Subclasses can override this method to instantiate a new instance of their subclass. This allows this class to
|
||||
* be extendable.
|
||||
*/
|
||||
protected MapModelCriteriaBuilder<K, V, M> instantiateNewInstance(StringKeyConvertor<K> keyConvertor, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, Predicate<? super K> indexReadFilter, Predicate<? super V> sequentialReadFilter) {
|
||||
return new MapModelCriteriaBuilder<>(keyConvertor, fieldPredicates, indexReadFilter, sequentialReadFilter);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,5 +37,6 @@
|
|||
<module>map</module>
|
||||
<module>build-processor</module>
|
||||
<module>map-hot-rod</module>
|
||||
<module>map-ldap</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -1274,6 +1274,11 @@
|
|||
<artifactId>keycloak-model-map-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-infinispan</artifactId>
|
||||
|
|
|
@ -96,6 +96,10 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-map-hot-rod</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.infinispan</groupId>
|
||||
<artifactId>infinispan-server-core</artifactId>
|
||||
|
@ -303,6 +307,15 @@
|
|||
<keycloak.model.parameters>Jpa,Map,HotRodMapStorage</keycloak.model.parameters>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>map-ldap</id>
|
||||
<properties>
|
||||
<keycloak.profile.feature.map_storage>enabled</keycloak.profile.feature.map_storage>
|
||||
<keycloak.model.parameters>Jpa,Map,LdapMapStorage</keycloak.model.parameters>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2020 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.testsuite.model.parameters;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.models.DeploymentStateSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.map.storage.MapStorageSpi;
|
||||
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
||||
import org.keycloak.models.map.storage.ldap.LdapMapStorageProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
import org.keycloak.testsuite.model.Config;
|
||||
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.util.ldap.LDAPEmbeddedServer;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Alexander Schwartz
|
||||
*/
|
||||
public class LdapMapStorage extends KeycloakModelParameters {
|
||||
|
||||
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
||||
.build();
|
||||
|
||||
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
|
||||
.add(ConcurrentHashMapStorageProviderFactory.class)
|
||||
.add(LdapMapStorageProviderFactory.class)
|
||||
.build();
|
||||
|
||||
private final LDAPRule ldapRule = new LDAPRule();
|
||||
|
||||
public LdapMapStorage() {
|
||||
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateConfig(Config cf) {
|
||||
cf.spi(MapStorageSpi.NAME)
|
||||
.provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.config("dir", "${project.build.directory:target}");
|
||||
|
||||
cf.spi(MapStorageSpi.NAME)
|
||||
.provider(LdapMapStorageProviderFactory.PROVIDER_ID)
|
||||
.config("vendor", "other")
|
||||
.config("usernameLDAPAttribute", "uid")
|
||||
.config("rdnLDAPAttribute", "uid")
|
||||
.config("uuidLDAPAttribute", "entryUUID")
|
||||
.config("userObjectClasses", "inetOrgPerson, organizationalPerson")
|
||||
.config("connectionUrl", "ldap://localhost:10389")
|
||||
.config("usersDn", "ou=People,dc=keycloak,dc=org")
|
||||
.config("bindDn", "uid=admin,ou=system")
|
||||
.config("bindCredential", "secret")
|
||||
.config("roles.realm.dn", "ou=RealmRoles,dc=keycloak,dc=org")
|
||||
.config("roles.client.dn", "ou={0},dc=keycloak,dc=org")
|
||||
.config("roles.common.dn", "dc=keycloak,dc=org") // this is the top DN that finds both client and realm roles
|
||||
.config("membership.ldap.attribute", "member")
|
||||
.config("role.name.ldap.attribute", "cn")
|
||||
.config("role.object.classes", "groupOfNames")
|
||||
.config("role.attributes", "ou")
|
||||
.config("mode", "LDAP_ONLY")
|
||||
.config("use.realm.roles.mapping", "true");
|
||||
|
||||
cf.spi("client").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("clientScope").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("group").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("realm").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("role").config("map.storage.provider", LdapMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(DeploymentStateSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(StoreFactorySpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("user").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(UserSessionSpi.NAME).config("map.storage-user-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(UserSessionSpi.NAME).config("map.storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(UserLoginFailureSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("authorizationPersister").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("authenticationSessions").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
|
||||
|
||||
}
|
||||
|
||||
static {
|
||||
System.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL, "false");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement classRule(Statement base, Description description) {
|
||||
return ldapRule.apply(base, description);
|
||||
}
|
||||
|
||||
}
|
|
@ -286,7 +286,6 @@
|
|||
<configuration>
|
||||
<systemProperties>
|
||||
<systemProperty><key>keycloak.profile.feature.map_storage</key><value>enabled</value></systemProperty>
|
||||
<systemProperty><key>keycloak.mapStorage.provider</key><value>concurrenthashmap</value></systemProperty>
|
||||
<systemProperty><key>keycloak.realm.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.client.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.clientScope.provider</key><value>map</value></systemProperty>
|
||||
|
|
|
@ -117,6 +117,26 @@
|
|||
"driver": "org.postgresql.Driver",
|
||||
"driverDialect": "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect",
|
||||
"showSql": "${keycloak.map.storage.connectionsJpa,showSql:false}"
|
||||
},
|
||||
"ldap-map-storage": {
|
||||
"vendor": "other",
|
||||
"usernameLDAPAttribute": "uid",
|
||||
"rdnLDAPAttribute": "uid",
|
||||
"uuidLDAPAttribute": "entryUUID",
|
||||
"userObjectClasses": "inetOrgPerson, organizationalPerson",
|
||||
"connectionUrl": "${keycloak.map.storage.ldap.connectionUrl:}",
|
||||
"usersDn": "ou=People,dc=keycloak,dc=org",
|
||||
"bindDn": "${keycloak.map.storage.ldap.bindDn:}",
|
||||
"bindCredential": "${keycloak.map.storage.ldap.bindCredential:}",
|
||||
"roles.realm.dn": "ou=RealmRoles,dc=keycloak,dc=org",
|
||||
"roles.common.dn": "dc=keycloak,dc=org",
|
||||
"roles.client.dn": "ou={0},dc=keycloak,dc=org",
|
||||
"membership.ldap.attribute": "member",
|
||||
"role.name.ldap.attribute": "cn",
|
||||
"role.object.classes": "groupOfNames",
|
||||
"role.attributes": "ou",
|
||||
"mode": "LDAP_ONLY",
|
||||
"use.realm.roles.mapping": "true"
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -168,6 +188,13 @@
|
|||
}
|
||||
},
|
||||
|
||||
"realmCache": {
|
||||
"provider": "${keycloak.realm.cache.provider:default}",
|
||||
"default" : {
|
||||
"enabled": "${keycloak.realm.cache.provider.enabled:true}"
|
||||
}
|
||||
},
|
||||
|
||||
"connectionsInfinispan": {
|
||||
"default": {
|
||||
"jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
|
||||
|
|
Loading…
Reference in a new issue