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>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-model-map-jpa</artifactId>
|
<artifactId>keycloak-model-map-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-model-infinispan</artifactId>
|
<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 <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 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.
|
* If {@code null}, such a single-parameter constructor is not available.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
*/
|
*/
|
||||||
|
@ -185,7 +185,7 @@ public class DeepCloner {
|
||||||
*
|
*
|
||||||
* @param <V> Class or interface that would be instantiated by the given methods
|
* @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 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.
|
* If {@code null}, such a single-parameter constructor is not available.
|
||||||
* @return This builder.
|
* @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.
|
* 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 <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
|
* @return See description
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|
|
@ -71,7 +71,7 @@ public class MapRoleProvider implements RoleProvider {
|
||||||
entity.setRealmId(realm.getId());
|
entity.setRealmId(realm.getId());
|
||||||
entity.setName(name);
|
entity.setName(name);
|
||||||
entity.setClientRole(false);
|
entity.setClientRole(false);
|
||||||
if (tx.read(entity.getId()) != null) {
|
if (entity.getId() != null && tx.read(entity.getId()) != null) {
|
||||||
throw new ModelDuplicateException("Role exists: " + id);
|
throw new ModelDuplicateException("Role exists: " + id);
|
||||||
}
|
}
|
||||||
entity = tx.create(entity);
|
entity = tx.create(entity);
|
||||||
|
@ -129,7 +129,7 @@ public class MapRoleProvider implements RoleProvider {
|
||||||
entity.setName(name);
|
entity.setName(name);
|
||||||
entity.setClientRole(true);
|
entity.setClientRole(true);
|
||||||
entity.setClientId(client.getId());
|
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);
|
throw new ModelDuplicateException("Role exists: " + id);
|
||||||
}
|
}
|
||||||
entity = tx.create(entity);
|
entity = tx.create(entity);
|
||||||
|
@ -244,7 +244,8 @@ public class MapRoleProvider implements RoleProvider {
|
||||||
|
|
||||||
MapRoleEntity entity = tx.read(id);
|
MapRoleEntity entity = tx.read(id);
|
||||||
String realmId = realm.getId();
|
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
|
? null
|
||||||
: entityToAdapterFunc(realm).apply(entity);
|
: 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
|
* Updates to the returned instances of {@code V} would be visible in the current transaction
|
||||||
* and will propagate into the underlying store upon commit.
|
* 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
|
* @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);
|
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;
|
* 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
|
* 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.
|
* {@link MapKeycloakTransaction#delete}, etc.
|
||||||
* <p>
|
* <p>
|
||||||
* Updates to the returned instances of {@code V} would be visible in the current transaction
|
* 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.
|
* 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.
|
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
|
||||||
* @return number of removed objects (might return {@code -1} if not supported)
|
* @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>> {
|
public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements ModelCriteriaBuilder<M, MapModelCriteriaBuilder<K, V, M>> {
|
||||||
|
|
||||||
@FunctionalInterface
|
@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);
|
MapModelCriteriaBuilder<K, V, M> apply(MapModelCriteriaBuilder<K, V, M> builder, Operator op, Object[] params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Predicate<Object> ALWAYS_TRUE = (e) -> true;
|
protected static final Predicate<Object> ALWAYS_TRUE = (e) -> true;
|
||||||
private static final Predicate<Object> ALWAYS_FALSE = (e) -> false;
|
protected static final Predicate<Object> ALWAYS_FALSE = (e) -> false;
|
||||||
private final Predicate<? super K> keyFilter;
|
private final Predicate<? super K> keyFilter;
|
||||||
private final Predicate<? super V> entityFilter;
|
private final Predicate<? super V> entityFilter;
|
||||||
private final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
|
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);
|
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.keyConvertor = keyConvertor;
|
||||||
this.fieldPredicates = fieldPredicates;
|
this.fieldPredicates = fieldPredicates;
|
||||||
this.keyFilter = indexReadFilter;
|
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) {
|
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<? 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);
|
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
|
@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) {
|
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<? 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);
|
Predicate<V> resEntityFilter = Stream.of(builders).map(MapModelCriteriaBuilder.class::cast).map(MapModelCriteriaBuilder::getEntityFilter).reduce(ALWAYS_FALSE, Predicate::or);
|
||||||
return new MapModelCriteriaBuilder<>(
|
return instantiateNewInstance(
|
||||||
keyConvertor,
|
keyConvertor,
|
||||||
fieldPredicates,
|
fieldPredicates,
|
||||||
v -> keyFilter.test(v) && resIndexFilter.test(v),
|
v -> keyFilter.test(v) && resIndexFilter.test(v),
|
||||||
|
@ -90,14 +90,12 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
public MapModelCriteriaBuilder<K, V, M> not(MapModelCriteriaBuilder<K, V, M> builder) {
|
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 K> resIndexFilter = builder.getKeyFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : builder.getKeyFilter().negate();
|
||||||
Predicate<? super V> resEntityFilter = builder.getEntityFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : builder.getEntityFilter().negate();
|
Predicate<? super V> resEntityFilter = builder.getEntityFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : builder.getEntityFilter().negate();
|
||||||
|
|
||||||
return new MapModelCriteriaBuilder<>(
|
return instantiateNewInstance(
|
||||||
keyConvertor,
|
keyConvertor,
|
||||||
fieldPredicates,
|
fieldPredicates,
|
||||||
v -> keyFilter.test(v) && resIndexFilter.test(v),
|
v -> keyFilter.test(v) && resIndexFilter.test(v),
|
||||||
|
@ -125,7 +123,7 @@ public class MapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements
|
||||||
case EXISTS:
|
case EXISTS:
|
||||||
case NOT_EXISTS:
|
case NOT_EXISTS:
|
||||||
case IN:
|
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:
|
default:
|
||||||
throw new AssertionError("Invalid operator: " + op);
|
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));
|
final Predicate<V> p = v -> valueComparator.test(getter.apply(v));
|
||||||
resEntityFilter = p.and(entityFilter);
|
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>map</module>
|
||||||
<module>build-processor</module>
|
<module>build-processor</module>
|
||||||
<module>map-hot-rod</module>
|
<module>map-hot-rod</module>
|
||||||
|
<module>map-ldap</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
|
|
5
pom.xml
5
pom.xml
|
@ -1274,6 +1274,11 @@
|
||||||
<artifactId>keycloak-model-map-jpa</artifactId>
|
<artifactId>keycloak-model-map-jpa</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-model-infinispan</artifactId>
|
<artifactId>keycloak-model-infinispan</artifactId>
|
||||||
|
|
|
@ -96,6 +96,10 @@
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-model-map-hot-rod</artifactId>
|
<artifactId>keycloak-model-map-hot-rod</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-model-map-ldap</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.infinispan</groupId>
|
<groupId>org.infinispan</groupId>
|
||||||
<artifactId>infinispan-server-core</artifactId>
|
<artifactId>infinispan-server-core</artifactId>
|
||||||
|
@ -303,6 +307,15 @@
|
||||||
<keycloak.model.parameters>Jpa,Map,HotRodMapStorage</keycloak.model.parameters>
|
<keycloak.model.parameters>Jpa,Map,HotRodMapStorage</keycloak.model.parameters>
|
||||||
</properties>
|
</properties>
|
||||||
</profile>
|
</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>
|
</profiles>
|
||||||
|
|
||||||
</project>
|
</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>
|
<configuration>
|
||||||
<systemProperties>
|
<systemProperties>
|
||||||
<systemProperty><key>keycloak.profile.feature.map_storage</key><value>enabled</value></systemProperty>
|
<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.realm.provider</key><value>map</value></systemProperty>
|
||||||
<systemProperty><key>keycloak.client.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>
|
<systemProperty><key>keycloak.clientScope.provider</key><value>map</value></systemProperty>
|
||||||
|
|
|
@ -117,6 +117,26 @@
|
||||||
"driver": "org.postgresql.Driver",
|
"driver": "org.postgresql.Driver",
|
||||||
"driverDialect": "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect",
|
"driverDialect": "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect",
|
||||||
"showSql": "${keycloak.map.storage.connectionsJpa,showSql:false}"
|
"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": {
|
"connectionsInfinispan": {
|
||||||
"default": {
|
"default": {
|
||||||
"jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
|
"jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}",
|
||||||
|
|
Loading…
Reference in a new issue