diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AnyUtils.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AnyUtils.java index 0ceb427afe..92d65ba86d 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AnyUtils.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AnyUtils.java @@ -60,4 +60,6 @@ public interface AnyUtils { Set getAllResources(Any any); void addAttr(PlainAttrValidationManager validator, String key, PlainSchema schema, String value); + + void removeAttr(String key, PlainSchema schema); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java index 2f3cf23387..33f1e234c1 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java @@ -282,9 +282,11 @@ public AnyUtilsFactory anyUtilsFactory( final @Lazy UserDAO userDAO, final @Lazy GroupDAO groupDAO, final @Lazy AnyObjectDAO anyObjectDAO, + final @Lazy PlainAttrDAO plainAttrDAO, + final @Lazy PlainAttrValueDAO plainAttrValueDAO, final @Lazy EntityFactory entityFactory) { - return new JPAAnyUtilsFactory(userDAO, groupDAO, anyObjectDAO, entityFactory); + return new JPAAnyUtilsFactory(userDAO, groupDAO, anyObjectDAO, plainAttrDAO, plainAttrValueDAO, entityFactory); } @ConditionalOnMissingBean diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtils.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtils.java index 786709523b..fa7aca61e9 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtils.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtils.java @@ -45,12 +45,15 @@ import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; +import org.apache.syncope.core.persistence.api.dao.PlainAttrDAO; +import org.apache.syncope.core.persistence.api.dao.PlainAttrValueDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.AnyTypeClass; import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.ExternalResource; +import org.apache.syncope.core.persistence.api.entity.GroupablePlainAttr; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; @@ -125,6 +128,10 @@ public static boolean matchesFieldName(final String candidate) { protected final AnyObjectDAO anyObjectDAO; + protected final PlainAttrDAO plainAttrDAO; + + protected final PlainAttrValueDAO plainAttrValueDAO; + protected final EntityFactory entityFactory; protected final AnyTypeKind anyTypeKind; @@ -135,6 +142,8 @@ protected JPAAnyUtils( final UserDAO userDAO, final GroupDAO groupDAO, final AnyObjectDAO anyObjectDAO, + final PlainAttrDAO plainAttrDAO, + final PlainAttrValueDAO plainAttrValueDAO, final EntityFactory entityFactory, final AnyTypeKind anyTypeKind, final boolean linkedAccount) { @@ -142,6 +151,8 @@ protected JPAAnyUtils( this.userDAO = userDAO; this.groupDAO = groupDAO; this.anyObjectDAO = anyObjectDAO; + this.plainAttrDAO = plainAttrDAO; + this.plainAttrValueDAO = plainAttrValueDAO; this.entityFactory = entityFactory; this.anyTypeKind = anyTypeKind; this.linkedAccount = linkedAccount; @@ -454,4 +465,23 @@ public void addAttr( LOG.debug("{} has already {} set: {}", any, schema.getKey(), attr.getValuesAsStrings()); } } + + @Transactional + @Override + public void removeAttr(final String key, final PlainSchema schema) { + Any any = dao().find(key); + + any.getPlainAttr(schema.getKey()).ifPresentOrElse(attr -> { + PlainAttr plainAttr = (PlainAttr) attr; + any.remove(plainAttr); + plainAttr.setOwner(null); + if (plainAttr instanceof GroupablePlainAttr) { + ((GroupablePlainAttr) plainAttr).setMembership(null); + } + plainAttrValueDAO.deleteAll(plainAttr, this); + plainAttrDAO.delete(plainAttr); + + dao().save(any); + }, () -> LOG.warn("Any {} does not contain {} PLAIN attribute", key, schema.getKey())); + } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtilsFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtilsFactory.java index e48d0934b2..5e50979ba6 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtilsFactory.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAnyUtilsFactory.java @@ -23,6 +23,8 @@ import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; +import org.apache.syncope.core.persistence.api.dao.PlainAttrDAO; +import org.apache.syncope.core.persistence.api.dao.PlainAttrValueDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.AnyUtils; @@ -41,6 +43,10 @@ public class JPAAnyUtilsFactory implements AnyUtilsFactory { protected final AnyObjectDAO anyObjectDAO; + protected final PlainAttrDAO plainAttrDAO; + + protected final PlainAttrValueDAO plainAttrValueDAO; + protected final EntityFactory entityFactory; protected final Map instances = new HashMap<>(3); @@ -51,11 +57,15 @@ public JPAAnyUtilsFactory( final UserDAO userDAO, final GroupDAO groupDAO, final AnyObjectDAO anyObjectDAO, + final PlainAttrDAO plainAttrDAO, + final PlainAttrValueDAO plainAttrValueDAO, final EntityFactory entityFactory) { this.userDAO = userDAO; this.groupDAO = groupDAO; this.anyObjectDAO = anyObjectDAO; + this.plainAttrDAO = plainAttrDAO; + this.plainAttrValueDAO = plainAttrValueDAO; this.entityFactory = entityFactory; } @@ -65,7 +75,14 @@ public AnyUtils getInstance(final AnyTypeKind anyTypeKind) { synchronized (instances) { instance = instances.get(anyTypeKind); if (instance == null) { - instance = new JPAAnyUtils(userDAO, groupDAO, anyObjectDAO, entityFactory, anyTypeKind, false); + instance = new JPAAnyUtils(userDAO, + groupDAO, + anyObjectDAO, + plainAttrDAO, + plainAttrValueDAO, + entityFactory, + anyTypeKind, + false); ApplicationContextProvider.getBeanFactory().autowireBean(instance); instances.put(anyTypeKind, instance); } @@ -96,8 +113,14 @@ public AnyUtils getInstance(final Any any) { public AnyUtils getLinkedAccountInstance() { synchronized (this) { if (linkedAccountInstance == null) { - linkedAccountInstance = new JPAAnyUtils( - userDAO, groupDAO, anyObjectDAO, entityFactory, AnyTypeKind.USER, true); + linkedAccountInstance = new JPAAnyUtils(userDAO, + groupDAO, + anyObjectDAO, + plainAttrDAO, + plainAttrValueDAO, + entityFactory, + AnyTypeKind.USER, + true); } } return linkedAccountInstance; diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java index a20aa19cc3..5ff7933f82 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java @@ -230,6 +230,8 @@ protected Uid doCreate( taskInfo.getResource().getProvisionByAnyType(taskInfo.getAnyType()). filter(provision -> provision.getUidOnCreate() != null). ifPresent(provision -> { + LOG.debug("Adding uidOnCreate [{}] attribute to [{}] on create", provision.getUidOnCreate(), + taskInfo.getEntityKey()); AnyUtils anyUtils = anyUtilsFactory.getInstance(taskInfo.getAnyTypeKind()); anyUtils.addAttr( validator, @@ -383,6 +385,19 @@ protected Uid delete( connector.delete(objectClass, uid, null, propagationAttempted); result = uid; + taskInfo.getResource() + .getProvisionByAnyType(taskInfo.getAnyType()) + .filter(provision -> provision.getUidOnCreate() != null) + .ifPresent(provision -> { + LOG.debug("Removing uidOnCreate [{}] attribute from [{}] on delete", + provision.getUidOnCreate(), taskInfo.getEntityKey()); + AnyUtils anyUtils = anyUtilsFactory.getInstance(taskInfo.getAnyTypeKind()); + anyUtils.removeAttr(taskInfo.getEntityKey(), plainSchemaDAO.find(provision.getUidOnCreate())); + publisher.publishEvent(new EntityLifecycleEvent<>(this, + SyncDeltaType.UPDATE, + anyUtils.dao().find(taskInfo.getEntityKey()), + AuthContextUtils.getDomain())); + }); } return result; diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java index f72436fe17..41619d3fc6 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java @@ -55,16 +55,19 @@ import org.apache.syncope.common.lib.request.MembershipUR; import org.apache.syncope.common.lib.request.PasswordPatch; import org.apache.syncope.common.lib.request.ResourceAR; +import org.apache.syncope.common.lib.request.ResourceDR; import org.apache.syncope.common.lib.request.StringPatchItem; import org.apache.syncope.common.lib.request.StringReplacePatchItem; import org.apache.syncope.common.lib.request.UserCR; import org.apache.syncope.common.lib.request.UserUR; +import org.apache.syncope.common.lib.to.AnyTypeClassTO; import org.apache.syncope.common.lib.to.ConnObject; import org.apache.syncope.common.lib.to.GroupTO; import org.apache.syncope.common.lib.to.ImplementationTO; import org.apache.syncope.common.lib.to.Item; import org.apache.syncope.common.lib.to.Mapping; import org.apache.syncope.common.lib.to.MembershipTO; +import org.apache.syncope.common.lib.to.PlainSchemaTO; import org.apache.syncope.common.lib.to.PropagationStatus; import org.apache.syncope.common.lib.to.ProvisioningResult; import org.apache.syncope.common.lib.to.RealmTO; @@ -72,6 +75,7 @@ import org.apache.syncope.common.lib.to.RoleTO; import org.apache.syncope.common.lib.to.UserTO; import org.apache.syncope.common.lib.types.AnyTypeKind; +import org.apache.syncope.common.lib.types.AttrSchemaType; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.common.lib.types.ClientExceptionType; import org.apache.syncope.common.lib.types.ExecStatus; @@ -83,6 +87,8 @@ import org.apache.syncope.common.lib.types.PatchOperation; import org.apache.syncope.common.lib.types.PolicyType; import org.apache.syncope.common.lib.types.ResourceAssociationAction; +import org.apache.syncope.common.lib.types.ResourceDeassociationAction; +import org.apache.syncope.common.lib.types.SchemaType; import org.apache.syncope.common.rest.api.RESTHeaders; import org.apache.syncope.common.rest.api.beans.RealmQuery; import org.apache.syncope.common.rest.api.service.UserService; @@ -1634,4 +1640,72 @@ public void issueSYNCOPE1793() { resource(RESOURCE_NAME_NOPROPAGATION).action(ResourceAssociationAction.ASSIGN).build()); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); } + + @Test + public void issueSYNCOPE1809() throws IOException { + // 1. add a new schema externalKey and update provision accordingly + PlainSchemaTO externalKeySchemaTO = new PlainSchemaTO(); + externalKeySchemaTO.setKey("externalKey"); + externalKeySchemaTO.setType(AttrSchemaType.String); + externalKeySchemaTO.setReadonly(true); + SCHEMA_SERVICE.create(SchemaType.PLAIN, externalKeySchemaTO); + try { + AnyTypeClassTO minimalUser = ANY_TYPE_CLASS_SERVICE.read("minimal user"); + minimalUser.getPlainSchemas().add(externalKeySchemaTO.getKey()); + ANY_TYPE_CLASS_SERVICE.update(minimalUser); + ResourceTO restResourceTO = RESOURCE_SERVICE.read(RESOURCE_NAME_REST); + restResourceTO.getProvision(AnyTypeKind.USER.name()) + .ifPresent(p -> p.setUidOnCreate(externalKeySchemaTO.getKey())); + RESOURCE_SERVICE.update(restResourceTO); + UserCR userCR = UserITCase.getUniqueSample("rest@syncope.apache.org"); + userCR.getResources().clear(); + userCR.getResources().add(RESOURCE_NAME_REST); + + // 2. create + ProvisioningResult result = createUser(userCR); + assertEquals(1, result.getPropagationStatuses().size()); + assertEquals(ExecStatus.SUCCESS, result.getPropagationStatuses().get(0).getStatus()); + assertEquals(RESOURCE_NAME_REST, result.getPropagationStatuses().get(0).getResource()); + assertEquals("surname", result.getEntity().getPlainAttr("surname").get().getValues().get(0)); + // externalKey is going to be populated on create + assertTrue(result.getEntity().getPlainAttr("externalKey").isPresent()); + assertEquals(result.getEntity().getKey(), + result.getEntity().getPlainAttr("externalKey").get().getValues().get(0)); + // 3. remove resource from the user + result = updateUser(new UserUR.Builder(result.getEntity() + .getKey()).resource(new StringPatchItem.Builder().value(RESOURCE_NAME_REST) + .operation(PatchOperation.DELETE) + .build()).build()); + assertEquals(ExecStatus.SUCCESS, result.getPropagationStatuses().get(0).getStatus()); + // externalKey is going to be removed on resource unassignment + assertFalse(result.getEntity().getPlainAttr("externalKey").isPresent()); + + // 4. create a new user and deprovision, attribute is cleared + userCR = UserITCase.getUniqueSample("rest@syncope.apache.org"); + userCR.getResources().clear(); + userCR.getResources().add(RESOURCE_NAME_REST); + result = createUser(userCR); + assertEquals(ExecStatus.SUCCESS, result.getPropagationStatuses().get(0).getStatus()); + // this time fire a deprovision + assertNotNull(parseBatchResponse(USER_SERVICE.deassociate(new ResourceDR.Builder().key(result.getEntity() + .getKey()).action(ResourceDeassociationAction.DEPROVISION).resource(RESOURCE_NAME_REST).build()))); + UserTO restUserTO = USER_SERVICE.read(result.getEntity().getKey()); + assertFalse(restUserTO.getPlainAttr("externalKey").isPresent()); + + // 5. create a new user and unlink, attribute is not cleared since provisioning hasn't been fired + userCR = UserITCase.getUniqueSample("rest@syncope.apache.org"); + userCR.getResources().clear(); + userCR.getResources().add(RESOURCE_NAME_REST); + result = createUser(userCR); + assertEquals(ExecStatus.SUCCESS, result.getPropagationStatuses().get(0).getStatus()); + // this time deprovision + assertNotNull(parseBatchResponse(USER_SERVICE.deassociate(new ResourceDR.Builder().key(result.getEntity() + .getKey()).action(ResourceDeassociationAction.UNLINK).resource(RESOURCE_NAME_REST).build()))); + restUserTO = USER_SERVICE.read(result.getEntity().getKey()); + assertTrue(restUserTO.getPlainAttr("externalKey").isPresent()); + } finally { + // remove additional externalKey schema + SCHEMA_SERVICE.delete(SchemaType.PLAIN, externalKeySchemaTO.getKey()); + } + } }