Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput
Map<String, String> state = new HashMap<String, String>();
if (spnegoAuthenticator.isAuthenticated()) {
String username = spnegoAuthenticator.getAuthenticatedUsername();
if (!spnegoAuthenticator.getKerberosRealm().equals(kerberosConfig.getKerberosRealm())) {
// TODO: we would probably allow for users of other realms to authenticate as well if there is a trust relationship
// TODO: on the other hand how would we handle duplicate user names in the two realms? Would we combine the two to a long, distinct user name?
return CredentialValidationOutput.failed();
}
UserModel user = findOrCreateAuthenticatedUser(realm, username);
if (user == null) {
return CredentialValidationOutput.failed();
Expand Down Expand Up @@ -242,7 +247,8 @@ protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String usern
user = session.users().getUserById(realm, user.getId()); // make sure we get a cached instance
logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage");

if (!model.getId().equals(user.getFederationLink())) {
// TODO: revisit how federationLink would be set on map storage and if `user.getFederationLink() != null` is the right way to go
if (user.getFederationLink() != null && !model.getId().equals(user.getFederationLink())) {
logger.warn("User with username " + username + " already exists, but is not linked to provider [" + model.getName() + "]");
return null;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ public String getAuthenticatedUsername() {
return username;
}

/**
* @return username to be used in Keycloak. Username is authenticated kerberos principal without realm name
*/
public String getKerberosRealm() {
String[] tokens = authenticatedKerberosPrincipal.split("@");
return tokens[1];
}

private class AcceptSecContext implements PrivilegedExceptionAction<Boolean> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,26 @@
package org.keycloak.models.map.storage.ldap;

import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
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;
import org.keycloak.models.map.storage.ldap.user.LdapUserMapKeycloakTransaction;
import org.keycloak.models.map.user.MapUserEntity;

public class LdapMapStorageProvider implements MapStorageProvider {

private final LdapMapStorageProviderFactory factory;
private final String sessionTxPrefix;
@Deprecated
private final MapStorageProvider delegate;

public LdapMapStorageProvider(LdapMapStorageProviderFactory factory, String sessionTxPrefix) {
public LdapMapStorageProvider(LdapMapStorageProviderFactory factory, String sessionTxPrefix, MapStorageProvider delegate) {
this.factory = factory;
this.sessionTxPrefix = sessionTxPrefix;
this.delegate = delegate;
}

@Override
Expand All @@ -49,6 +55,13 @@ public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
if (sessionTx == null) {
sessionTx = factory.createTransaction(session, modelType);
session.setAttribute(sessionTxPrefix + modelType.hashCode(), sessionTx);

if (modelType == UserModel.class) {
MapStorage<V, M> delegateStorage = delegate.getStorage(modelType, flags);
MapKeycloakTransaction<V, M> delegateTransaction = delegateStorage.createTransaction(session);
((LdapUserMapKeycloakTransaction) sessionTx).setDelegate((MapKeycloakTransaction<MapUserEntity, UserModel>) delegateTransaction);
}

}
return sessionTx;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
Expand All @@ -32,6 +34,7 @@
import org.keycloak.models.map.storage.MapStorageProviderFactory;
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
import org.keycloak.models.map.storage.ldap.role.LdapRoleMapKeycloakTransaction;
import org.keycloak.models.map.storage.ldap.user.LdapUserMapKeycloakTransaction;
import org.keycloak.provider.EnvironmentDependentProviderFactory;

public class LdapMapStorageProviderFactory implements
Expand All @@ -46,23 +49,34 @@ public class LdapMapStorageProviderFactory implements

private Config.Scope config;

/*
* TODO: This delegate will disappear in the final implementation. It's a helper for development when an entity is not fully
* supported and the tree storage can't be configured for it yet.
*/
@Deprecated
private volatile MapStorageProvider delegate;

@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);
MODEL_TO_TX.put(UserModel.class, LdapUserMapKeycloakTransaction::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);
LdapRoleMapKeycloakTransaction.LdapRoleMapKeycloakTransactionFunction<KeycloakSession, Config.Scope, MapKeycloakTransaction> tx = MODEL_TO_TX.get(modelType);
Objects.requireNonNull(tx, "model " + modelType + " is not supported for " + this.getClass());
return tx.apply(session, config);
}

@Override
public MapStorageProvider create(KeycloakSession session) {
return new LdapMapStorageProvider(this, sessionTxPrefixForFactoryInstance);
lazyInit(session);
return new LdapMapStorageProvider(this, sessionTxPrefixForFactoryInstance, delegate);
}

@Override
Expand All @@ -89,6 +103,16 @@ private static void checkSystemProperty(String name, String cfgValue, String def
System.setProperty(name, value);
}

private void lazyInit(KeycloakSession session) {
if (delegate == null) {
synchronized (this) {
if (delegate == null) {
delegate = session.getProvider(MapStorageProvider.class, "concurrenthashmap");
}
}
}
}

@Override
public void postInit(KeycloakSessionFactory factory) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.config;

import org.keycloak.models.map.storage.ldap.user.kerberos.CommonKerberosConfig;

/**
* @author Alexander Schwartz
*/
public class LdapKerberosConfig extends CommonKerberosConfig {

public LdapKerberosConfig(LdapMapConfig config) {
super(config.getConfig());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public String getFirst(String key) {
};
}

protected MultivaluedHashMap<String, String> getConfig() {
return config;
}

// from: RoleMapperConfig
public Collection<String> getRoleObjectClasses() {
String objectClasses = config.getFirst(LdapMapRoleMapperConfig.ROLE_OBJECT_CLASSES);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,22 @@ public void execute() {

@Override
public boolean delete(String key) {
if (deletedKeys.contains(key)) {
return true;
}
LdapMapRoleEntityFieldDelegate read = read(key);
if (read == null) {
throw new ModelException("unable to read entity with key " + key);
}
if (!deletedKeys.contains((key))) {
// avoid enlisting LDAP removal twice if client calls it twice
deletedKeys.add(key);
tasksOnCommit.add(new DeleteOperation() {
@Override
public void execute() {
identityStore.remove(read.getLdapMapObject());
// once removed from LDAP, avoid updating a modified entity in LDAP.
entities.remove(read.getId());
}
});
}
deletedKeys.add(key);
tasksOnCommit.add(new DeleteOperation() {
@Override
public void execute() {
identityStore.remove(read.getLdapMapObject());
// once removed from LDAP, avoid updating a modified entity in LDAP.
entities.remove(read.getId());
}
});
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,33 @@ public <EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapR
return consumer.apply(this);
}

@Override
public <K, EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> Object mapGet(EF field, K key) {
if (field == MapRoleEntityFields.ATTRIBUTES) {
return getAttribute((String) key);
} else {
throw new ModelException("unsupported field for mapGet " + field);
}
}

@Override
public <K, T, EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> void mapPut(EF field, K key, T value) {
if (field == MapRoleEntityFields.ATTRIBUTES) {
//noinspection unchecked
setAttribute((String) key, (List<String>) value);
} else {
throw new ModelException("unsupported field for mapGetPut " + field);
}
}

@Override
public <K, EF extends Enum<? extends EntityField<MapRoleEntity>> & EntityField<MapRoleEntity>> Object mapRemove(EF field, K key) {
if (field == MapRoleEntityFields.ATTRIBUTES) {
removeAttribute((String) key);
return null;
} else {
throw new ModelException("unsupported field for mapRemove " + field);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -531,6 +532,56 @@ protected String getEntryIdentifier(final LdapMapObject ldapObject) {
}
}

public void updatePassword(LdapMapObject user, String password, LdapMapOperationDecorator passwordUpdateDecorator) {
String userDN = user.getDn().toString();

if (logger.isDebugEnabled()) {
logger.debugf("Using DN [%s] for updating LDAP password of user", userDN);
}

if (getConfig().isActiveDirectory()) {
updateADPassword(userDN, password, passwordUpdateDecorator);
return;
}

try {
if (config.useExtendedPasswordModifyOp()) {
operationManager.passwordModifyExtended(userDN, password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
}
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
}

private void updateADPassword(String userDN, String password, LdapMapOperationDecorator passwordUpdateDecorator) {
try {
// Replace the "unicdodePwd" attribute with a new value
// Password must be both Unicode and a quoted string
String newQuotedPassword = "\"" + password + "\"";
byte[] newUnicodePassword = newQuotedPassword.getBytes(StandardCharsets.UTF_16LE);

BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);

List<ModificationItem> modItems = new ArrayList<>();
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));

operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}), passwordUpdateDecorator);
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException(e);
}
}



@Override
public void close() {
operationManager.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,4 +636,23 @@ private Set<String> getReturningAttributes(final Collection<String> returningAtt

return result;
}

/**
* Execute the LDAP Password Modify Extended Operation to update the password for the given DN.
*
* @param dn distinguished name of the entry.
* @param password the new password.
* @param decorator A decorator to apply to the ldap operation.
*/

public void passwordModifyExtended(String dn, String password, LdapMapOperationDecorator decorator) {
try {
execute(context -> {
LdapMapPasswordModifyRequest modifyRequest = new LdapMapPasswordModifyRequest(dn, null, password);
return context.extendedOperation(modifyRequest);
}, decorator);
} catch (NamingException e) {
throw new ModelException("Could not execute the password modify extended operation for DN [" + dn + "]", e);
}
}
}
Loading