Skip to content

Commit

Permalink
Merge pull request #2769 from cloudfoundry/feature/alias-handler-for-…
Browse files Browse the repository at this point in the history
…scim-users

Alias Handler for SCIM Users
  • Loading branch information
torsten-sap committed Jun 5, 2024
2 parents ac9602c + f451e8e commit 3164d36
Show file tree
Hide file tree
Showing 7 changed files with 880 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.cloudfoundry.identity.uaa.alias;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AliasEntitiesConfig {
@Bean
public boolean aliasEntitiesEnabled(@Value("${login.aliasEntitiesEnabled:false}") final boolean enabled) {
return enabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA;
import static org.springframework.util.StringUtils.hasText;

import java.util.Objects;
import java.util.Optional;

import org.cloudfoundry.identity.uaa.EntityWithAlias;
Expand Down Expand Up @@ -154,6 +155,7 @@ public final T ensureConsistencyOfAliasEntity(
// update the existing alias entity
if (existingAliasEntity != null) {
setId(aliasEntity, existingAliasEntity.getId());
setPropertiesFromExistingAliasEntity(aliasEntity, existingAliasEntity);
updateEntity(aliasEntity, originalEntity.getAliasZid());
return originalEntity;
}
Expand All @@ -177,6 +179,12 @@ public final T ensureConsistencyOfAliasEntity(
return updateEntity(originalEntity, originalEntity.getZoneId());
}

/**
* Set properties from the existing alias entity in the new alias entity before it is updated. Can be used if
* certain properties should differ between the original and the alias entity.
*/
protected abstract void setPropertiesFromExistingAliasEntity(final T newAliasEntity, final T existingAliasEntity);

private T buildAliasEntity(final T originalEntity) {
final T aliasEntity = cloneEntity(originalEntity);
aliasEntity.setAliasId(originalEntity.getId());
Expand Down Expand Up @@ -208,4 +216,23 @@ public final Optional<T> retrieveAliasEntity(final T originalEntity) {
protected abstract T updateEntity(final T entity, final String zoneId);

protected abstract T createEntity(final T entity, final String zoneId) throws EntityAliasFailedException;

protected static <T extends EntityWithAlias> boolean isValidAliasPair(
@NonNull final T entity1,
@NonNull final T entity2
) {
// check if both entities have an alias
final boolean entity1HasAlias = hasText(entity1.getAliasId()) && hasText(entity1.getAliasZid());
final boolean entity2HasAlias = hasText(entity2.getAliasId()) && hasText(entity2.getAliasZid());
if (!entity1HasAlias || !entity2HasAlias) {
return false;
}

// check if they reference each other
final boolean entity1ReferencesEntity2 = Objects.equals(entity1.getAliasId(), entity2.getId()) &&
Objects.equals(entity1.getAliasZid(), entity2.getZoneId());
final boolean entity2ReferencesEntity1 = Objects.equals(entity2.getAliasId(), entity1.getId()) &&
Objects.equals(entity2.getAliasZid(), entity1.getZoneId());
return entity1ReferencesEntity2 && entity2ReferencesEntity1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
Expand All @@ -33,7 +32,7 @@ public class IdentityProviderAliasHandler extends EntityAliasHandler<IdentityPro
public IdentityProviderAliasHandler(
@Qualifier("identityZoneProvisioning") final IdentityZoneProvisioning identityZoneProvisioning,
final IdentityProviderProvisioning identityProviderProvisioning,
@Value("${login.aliasEntitiesEnabled:false}") final boolean aliasEntitiesEnabled
@Qualifier("aliasEntitiesEnabled") final boolean aliasEntitiesEnabled
) {
super(identityZoneProvisioning, aliasEntitiesEnabled);
this.identityProviderProvisioning = identityProviderProvisioning;
Expand All @@ -45,6 +44,14 @@ protected boolean additionalValidationChecksForNewAlias(@NonNull final IdentityP
return IDP_TYPES_ALIAS_SUPPORTED.contains(requestBody.getType());
}

@Override
protected void setPropertiesFromExistingAliasEntity(
final IdentityProvider<?> newAliasEntity,
final IdentityProvider<?> existingAliasEntity
) {
// not required for identity providers
}

@Override
protected void setId(final IdentityProvider<?> entity, final String id) {
entity.setId(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.dao.EmptyResultDataAccessException;
Expand Down Expand Up @@ -84,7 +83,7 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware

protected static Logger logger = LoggerFactory.getLogger(IdentityProviderEndpoints.class);

@Value("${login.aliasEntitiesEnabled:false}")
@Qualifier("aliasEntitiesEnabled")
private boolean aliasEntitiesEnabled;
private final IdentityProviderProvisioning identityProviderProvisioning;
private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package org.cloudfoundry.identity.uaa.scim;

import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.EMPTY_STRING;

import java.util.Optional;

import org.cloudfoundry.identity.uaa.alias.EntityAliasFailedException;
import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler;
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
public class ScimUserAliasHandler extends EntityAliasHandler<ScimUser> {
private final ScimUserProvisioning scimUserProvisioning;
private final IdentityProviderProvisioning identityProviderProvisioning;
private final IdentityZoneManager identityZoneManager;

protected ScimUserAliasHandler(
@Qualifier("identityZoneProvisioning") final IdentityZoneProvisioning identityZoneProvisioning,
final ScimUserProvisioning scimUserProvisioning,
final IdentityProviderProvisioning identityProviderProvisioning,
final IdentityZoneManager identityZoneManager,
@Qualifier("aliasEntitiesEnabled") final boolean aliasEntitiesEnabled
) {
super(identityZoneProvisioning, aliasEntitiesEnabled);
this.scimUserProvisioning = scimUserProvisioning;
this.identityProviderProvisioning = identityProviderProvisioning;
this.identityZoneManager = identityZoneManager;
}

@Override
protected boolean additionalValidationChecksForNewAlias(final ScimUser requestBody) {
/* check if an IdP with the user's origin exists in both the current and the alias zone and that they are
* aliases of each other */
final String origin = requestBody.getOrigin();
final Optional<IdentityProvider<?>> idpInAliasZone = retrieveIdpByOrigin(origin, requestBody.getAliasZid());
if (idpInAliasZone.isEmpty()) {
return false;
}
final Optional<IdentityProvider<?>> idpInCurrentZone = retrieveIdpByOrigin(origin, identityZoneManager.getCurrentIdentityZoneId());
if (idpInCurrentZone.isEmpty()) {
return false;
}
return EntityAliasHandler.isValidAliasPair(idpInCurrentZone.get(), idpInAliasZone.get());
}

@Override
protected void setPropertiesFromExistingAliasEntity(
final ScimUser newAliasEntity,
final ScimUser existingAliasEntity
) {
// these three timestamps should not be overwritten by the timestamps of the original user
newAliasEntity.setPasswordLastModified(existingAliasEntity.getPasswordLastModified());
newAliasEntity.setLastLogonTime(existingAliasEntity.getLastLogonTime());
newAliasEntity.setPreviousLogonTime(existingAliasEntity.getPreviousLogonTime());
}

private Optional<IdentityProvider<?>> retrieveIdpByOrigin(final String originKey, final String zoneId) {
final IdentityProvider<?> idpInAliasZone;
try {
idpInAliasZone = identityProviderProvisioning.retrieveByOrigin(originKey, zoneId);
} catch (final EmptyResultDataAccessException e) {
return Optional.empty();
}
return Optional.ofNullable(idpInAliasZone);
}

@Override
protected void setId(final ScimUser entity, final String id) {
entity.setId(id);
}

@Override
protected void setZoneId(final ScimUser entity, final String zoneId) {
entity.setZoneId(zoneId);
}

@Override
protected ScimUser cloneEntity(final ScimUser originalEntity) {
final ScimUser aliasUser = new ScimUser();

aliasUser.setExternalId(originalEntity.getExternalId());

/* we only allow alias users to be created if their origin IdP has an alias to the same zone, therefore, an IdP
* with the same origin key will exist in the alias zone */
aliasUser.setOrigin(originalEntity.getOrigin());

aliasUser.setUserName(originalEntity.getUserName());
aliasUser.setName(new ScimUser.Name(originalEntity.getGivenName(), originalEntity.getFamilyName()));

aliasUser.setEmails(originalEntity.getEmails());
aliasUser.setPhoneNumbers(originalEntity.getPhoneNumbers());

aliasUser.setActive(originalEntity.isActive());
aliasUser.setVerified(originalEntity.isVerified());

/* password: empty string
* - alias users are only allowed for IdPs that also have an alias
* - IdPs can only have an alias if they are of type SAML, OIDC or OAuth 2.0
* - users with such IdPs as their origin always have an empty password
*/
aliasUser.setPassword(EMPTY_STRING);
aliasUser.setSalt(null);

/* The following fields will be overwritten later and are therefore not set here:
* - id and identityZoneId
* - aliasId and aliasZid
* - timestamp fields (password last modified, last logon, previous logon):
* - creation: with current timestamp during persistence (JdbcScimUserProvisioning)
* - update: with values from existing alias entity
*/

return aliasUser;
}

@Override
protected Optional<ScimUser> retrieveEntity(final String id, final String zoneId) {
final ScimUser user;
try {
user = scimUserProvisioning.retrieve(id, zoneId);
} catch (final ScimResourceNotFoundException e) {
return Optional.empty();
}
return Optional.ofNullable(user);
}

@Override
protected ScimUser updateEntity(final ScimUser entity, final String zoneId) {
return scimUserProvisioning.update(entity.getId(), entity, zoneId);
}

@Override
protected ScimUser createEntity(final ScimUser entity, final String zoneId) {
try {
return scimUserProvisioning.createUser(entity, entity.getPassword(), zoneId);
} catch (final ScimResourceAlreadyExistsException e) {
final String errorMessage = String.format(
"Could not create %s. A user with the same username already exists in the alias zone.",
entity.getAliasDescription()
);
throw new EntityAliasFailedException(errorMessage, HttpStatus.CONFLICT.value(), e);
}
}
}
Loading

0 comments on commit 3164d36

Please sign in to comment.