SCIM & Syncope : OptimisticLockException on user groups membership update

classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

SCIM & Syncope : OptimisticLockException on user groups membership update

Adrian Gonzalez
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.
Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
From https://tools.ietf.org/html/rfc7643#page-24
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

Thanks,
Adrian

Sample stacktrace on client side :
2016-09-27 09:31:46.065 DEBUG 1 --- [tp1754926770-22] o.s.web.client.RestTemplate              : PUT request for "http://scim:7777/Groups/2564b3a7-0f5d-424d-a4b3-a70f5d624d80" resulted in 500 (Server Error); invoking error handler
     at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.server.Server.handle(Server.java:499) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:311) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:544) [jetty-io-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635) [jetty-util-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555) [jetty-util-9.2.14.v20151106.jar!/:9.2.14.v20151106]
     at java.lang.Thread.run(Thread.java:745) [na:1.8.0_92-internal]
 Caused by: org.apache.syncope.common.lib.SyncopeClientException: GenericPersistence [OptimisticLockException: An optimistic lock violation was detected when flushing object instance "org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-62f5f8bf-b390-4354-b5f8-bfb390a354e8" to the data store.  This indicates that the object was concurrently modified in another transaction.]
     at org.apache.syncope.common.lib.SyncopeClientException.build(SyncopeClientException.java:37) ~[syncope-common-lib-2.0.0-SNAPSHOT.jar!/:2.0.0-SNAPSHOT]
     at org.apache.syncope.client.lib.RestClientExceptionMapper.checkSyncopeClientCompositeException(RestClientExceptionMapper.java:147) ~[syncope-client-lib-2.0.0-SNAPSHOT.jar!/:2.0.0-SNAPSHOT]
     at org.apache.syncope.client.lib.RestClientExceptionMapper.fromResponse(RestClientExceptionMapper.java:58) ~[syncope-client-lib-2.0.0-SNAPSHOT.jar!/:2.0.0-SNAPSHOT]
     at org.apache.syncope.client.lib.RestClientExceptionMapper.fromResponse(RestClientExceptionMapper.java:42) ~[syncope-client-lib-2.0.0-SNAPSHOT.jar!/:2.0.0-SNAPSHOT]
     at org.apache.cxf.jaxrs.client.ClientProxyImpl.checkResponse(ClientProxyImpl.java:306) ~[cxf-rt-rs-client-3.1.7.jar!/:3.1.7]
2016-09-27 09:31:46.068 DEBUG 1 --- [tp1754926770-22] .m.m.a.ExceptionHandlerExceptionResolver : Resolving exception from handler [public void org.mycompany.iam.server.controller.GroupController.updateGroup(java.lang.String,org.mycompany.iam.common.model.Group) throws org.mycompany.iam.server.exception.IamServerException]: org.mycompany.iam.server.exception.IamServerException: Can't update group
     at org.apache.cxf.jaxrs.client.ClientProxyImpl.handleResponse(ClientProxyImpl.java:838) ~[cxf-rt-rs-client-3.1.7.jar!/:3.1.7]
2016-09-27 09:31:46.071 DEBUG 1 --- [tp1754926770-22] .m.m.a.ExceptionHandlerExceptionResolver : Invoking @ExceptionHandler method: public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.mycompany.iam.server.exception.IamServerExceptionHandler.handleIamException(org.mycompany.iam.server.exception.IamServerException)
     at org.apache.cxf.jaxrs.client.ClientProxyImpl.doChainedInvocation(ClientProxyImpl.java:752) ~[cxf-rt-rs-client-3.1.7.jar!/:3.1.7]
     at org.apache.cxf.jaxrs.client.ClientProxyImpl.invoke(ClientProxyImpl.java:231) ~[cxf-rt-rs-client-3.1.7.jar!/:3.1.7]
     at com.sun.proxy.$Proxy131.update(Unknown Source) ~[na:na]
     at org.mycompany.iam.scim.provisioning.SCIMGroupProvisioning.addUserToGroup(SCIMGroupProvisioning.java:201) ~[iam-scim_tpsvc-1011-update-group-in-user-1-20160927092425-g1ca9982.jar!/:na]
     ... 57 common frames omitted


Sample stacktrace on Syncope side :
09:31:45.914 ERROR org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper - Exception thrown
org.apache.openjpa.persistence.OptimisticLockException: Optimistic locking errors were detected when flushing to the data store.  The following objects may have been concurrently modified in another transaction: [org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-62f5f8bf-b390-4354-b5f8-bfb390a354e8, org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-314a1e53-0235-41ff-8a1e-53023571ff42, org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-c4c32ec3-0312-4f00-832e-c30312af0045, org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-9ce613a2-5b42-4bce-a613-a25b425bce86, org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-32fb4c5b-995f-45f2-bb4c-5b995f05f255, org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-7117ce5e-4f87-43da-97ce-5e4f8773da92]
    at org.apache.openjpa.kernel.BrokerImpl.newFlushException(BrokerImpl.java:2357) ~[openjpa-kernel-2.4.1.jar:2.4.1]
enjpa-kernel-2.4.1.jar:2.4.1].kernel.BrokerImpl.flush(BrokerImpl.java:2205) ~[op--More--
    at org.apache.openjpa.kernel.BrokerImpl.flushSafe(BrokerImpl.java:2103) ~[openjpa-kernel-2.4.1.jar:2.4.1]
    at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:1874) ~[openjpa-kernel-2.4.1.jar:2.4.1]
    at org.apache.openjpa.kernel.DelegatingBroker.flush(DelegatingBroker.java:1044) ~[openjpa-kernel-2.4.1.jar:2.4.1]
    at org.apache.openjpa.persistence.EntityManagerImpl.flush(EntityManagerImpl.java:664) ~[openjpa-persistence-2.4.1.jar:2.4.1]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.save(JPAUserDAO.java:399) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.save(JPAUserDAO.java:83) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy153.save(Unknown Source) ~[?:?]
    at org.apache.syncope.core.workflow.java.DefaultUserWorkflowAdapter.doUpdate(DefaultUserWorkflowAdapter.java:115) ~[syncope-core-workflow-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.workflow.java.AbstractUserWorkflowAdapter.update(AbstractUserWorkflowAdapter.java:78) ~[syncope-core-workflow-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy164.update(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:121) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:57) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.doUpdate(UserLogic.java:232) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:213) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:68) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic$$FastClassBySpringCGLIB$$67b1988f.invoke(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.LogicInvocationHandler.around(LogicInvocationHandler.java:72) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor130.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.UserLogic$$EnhancerBySpringCGLIB$$134f19c3.update(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.rest.cxf.service.AbstractAnyService.update(AbstractAnyService.java:200) ~[syncope-core-rest-cxf-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
        at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
Caused by: org.apache.openjpa.persistence.OptimisticLockException: An optimistic lock violation was detected when flushing object instance "org.apache.syncope.core.persistence.jpa.entity.user.JPAUPlainAttrValue-62f5f8bf-b390-4354-b5f8-bfb390a354e8" to the data store.  This indicates that the object was concurrently modified in another transaction.
    at org.apache.openjpa.jdbc.kernel.PreparedStatementManagerImpl.flushAndUpdate(PreparedStatementManagerImpl.java:124) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.BatchingPreparedStatementManagerImpl.flushAndUpdate(BatchingPreparedStatementManagerImpl.java:79) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.PreparedStatementManagerImpl.flushInternal(PreparedStatementManagerImpl.java:100) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.PreparedStatementManagerImpl.flush(PreparedStatementManagerImpl.java:88) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.ConstraintUpdateManager.flush(ConstraintUpdateManager.java:550) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.ConstraintUpdateManager.flush(ConstraintUpdateManager.java:120) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.BatchingConstraintUpdateManager.flush(BatchingConstraintUpdateManager.java:59) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.flush(AbstractUpdateManager.java:104) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.flush(AbstractUpdateManager.java:77) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.jdbc.kernel.JDBCStoreManager.flush(JDBCStoreManager.java:731) ~[openjpa-jdbc-2.4.1.jar:2.4.1]
    at org.apache.openjpa.kernel.DelegatingStoreManager.flush(DelegatingStoreManager.java:131) ~[openjpa-kernel-2.4.1.jar:2.4.1]
    at org.apache.openjpa.datacache.DataCacheStoreManager.flush(DataCacheStoreManager.java:668) ~[openjpa-kernel-2.4.1.jar:2.4.1]
    at org.apache.openjpa.kernel.DelegatingStoreManager.flush(DelegatingStoreManager.java:131) ~[openjpa-kernel-2.4.1.jar:2.4.1]
    ... 131 more

Reply | Threaded
Open this post in threaded view
|

Re: SCIM & Syncope : OptimisticLockException on user groups membership update

ilgrosso
Administrator
On 27/09/2016 15:00, Adrian Gonzalez wrote:
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.

That's very good to hear: looks it is for SCIM 2.0, correct?
Looking forward to take a look at it!

Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

You are right about the fact that Syncope manages groups memberships by modifying users.

I am a bit confused about your snippet above, so let me rephrase it a bit.
Assuming your scenario:

 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2

the Group endpoint should perform as following:

UserPatch userPatch = new UserPatch();
userPatch.setKey("the key of user1");
userPatch.getMemberships().add(new MembershipPatch.Builder().
        operation(PatchOperation.DELETE).
        group("the key of the group passed to the endpoint").
        build());
userLogic.update(userPatch, true);

As you can see, I am rather using the logic layer (e.g. UserLogic as done in UserServiceImpl) to interact with Syncope core, rather than the external service layer (e.g. how REST clients do).

HTH
Regards.
-- 
Francesco Chicchiriccò

Tirasa - Open Source Excellence
http://www.tirasa.net/

Member at The Apache Software Foundation
Syncope, Cocoon, Olingo, CXF, OpenJPA, PonyMail
http://home.apache.org/~ilgrosso/
Reply | Threaded
Open this post in threaded view
|

Re: SCIM & Syncope : OptimisticLockException on user groups membership update

Adrian Gonzalez
Thanks Francesco !

I've modified my code, but now I'm getting NullPointerException (on the second PUT request on /Group endpoint).
This error isn't generated when we execute the request serially.
It's generated when HTTP request are executed in parallel.

i.e.
=> gives a 500

My code is available in [1]
The stacktrace is available in [2]

The error in syncope is from JPAUserDAO (input is null) :
public Collection<Group> findAllGroups(final User user) {
return CollectionUtils.union(
CollectionUtils.collect(user.getMemberships(), new Transformer<UMembership, Group>() {

@Override
public Group transform(final UMembership input) {
return input.getRightEnd();
}
}, new ArrayList<Group>()),
findDynGroupMemberships(user));
}


[1] Here's exactly the code from the SCIM Provider side (calling Syncope REST API) :
 
    // This is the method called on PUT for Group endpoint
    public Group update(String id, Group group) {

        // 1. General group update
        GroupTO existingGroupTO = getGroupTOByKey(id);
        List<UserTO> usersOfThisGroup = getUsersForGroup(id);
        GroupTO groupTO = groupConverter.fromScim(group);
        groupTO.setKey(existingGroupTO.getKey());
        GroupService groupService = syncopeClient.getService(GroupService.class);
        Response response;
        try {
            response = groupService.update(groupTO);
        } catch (SyncopeClientException ex) {
            throw syncopeExHandler.convertSyncopeGroupClientException(ex, group.getDisplayName(), ProvisioningOperation.UPDATING);
        }
        if (response == null || Response.Status.OK.getStatusCode() != response.getStatus()) {
            throw syncopeExHandler.convertSyncopeGroupErrorResponse(response, group.getDisplayName(),
                    ProvisioningOperation.UPDATING);
        }

        // 2. General membership update
        List<String> idsOfExistingUsers = usersOfThisGroup.stream().map(user -> user.getKey()).collect(Collectors.toList());
        List<String> idsOfNewUsers = group.getMembers().stream().map(member -> member.getValue()).collect(Collectors.toList());
        for (MemberRef memberRef : group.getMembers()) {
            if (!idsOfExistingUsers.contains(memberRef.getValue())) {
                addUserToGroup(groupTO, memberRef.getValue());
            }
        }
        for (String userId : idsOfExistingUsers) {
            if (!idsOfNewUsers.contains(userId)) {
                removeUserFromGroup(groupTO, userId);
            }
        }
        return getById(id);
    }

    private void addUserToGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.ADD_REPLACE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }
    }

    private void removeUserFromGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.DELETE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not removed from the group %s", userId, groupTO.getName()), e);
        }
    }

[2] Stacktrace Sorry for the super long stack :
15:05:47.623 ERROR org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper - Exception thrown
java.lang.NullPointerException
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:500) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:496) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1077) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1049) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllGroups(JPAUserDAO.java:495) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllResources(JPAUserDAO.java:529) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor124.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
]       at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE--More--
    at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy172.findAllResources(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.createTasks(PropagationManagerImpl.java:345) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUpdateTasks(PropagationManagerImpl.java:275) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:204) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:224) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
_91]    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0--More--
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy209.getUserUpdateTasks(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:123) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:57) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.doUpdate(UserLogic.java:232) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:213) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:68) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]0-SNAPSHOT.jar:2.0.0-SNAPSHOT]--More--
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.LogicInvocationHandler.around(LogicInvocationHandler.java:72) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor133.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.UserLogic$$EnhancerBySpringCGLIB$$f6ff0886.update(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
stractAnyService.java:159) ~[syncope-core-rest-cxf-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]e--
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[cxf-core-3.1.7.jar:3.1.7]
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:254) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:180) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:299) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:276) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[catalina.jar:8.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-websocket.jar:8.0.35]
icationFilterChain.java:240) ~[catalina.jar:8.0.35]erChain.internalDoFilter(Appl--More--
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.apache.syncope.core.spring.security.MustChangePasswordFilter.doFilter(MustChangePasswordFilter.java:77) ~[syncope-core-spring-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
jar:4.1.3.RELEASE]ingframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.3.RELEASE.--More--
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:215) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e



De : Francesco Chicchiriccò <[hidden email]>
À : [hidden email]
Envoyé le : Mardi 27 septembre 2016 15h20
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

On 27/09/2016 15:00, Adrian Gonzalez wrote:
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.

That's very good to hear: looks it is for SCIM 2.0, correct?
Looking forward to take a look at it!

Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

You are right about the fact that Syncope manages groups memberships by modifying users.

I am a bit confused about your snippet above, so let me rephrase it a bit.
Assuming your scenario:

 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2

the Group endpoint should perform as following:

UserPatch userPatch = new UserPatch();
userPatch.setKey("the key of user1");
userPatch.getMemberships().add(new MembershipPatch.Builder().
        operation(PatchOperation.DELETE).
        group("the key of the group passed to the endpoint").

        build());

userLogic.update(userPatch, true);

As you can see, I am rather using the logic layer (e.g. UserLogic as done in UserServiceImpl) to interact with Syncope core, rather than the external service layer (e.g. how REST clients do).

HTH
Regards.
-- 
Francesco Chicchiriccò

Tirasa - Open Source Excellence
http://www.tirasa.net/

Member at The Apache Software Foundation
Syncope, Cocoon, Olingo, CXF, OpenJPA, PonyMail
http://home.apache.org/~ilgrosso/


Reply | Threaded
Open this post in threaded view
|

Re: SCIM & Syncope : OptimisticLockException on user groups membership update

ilgrosso
Administrator
On 27/09/2016 17:24, Adrian Gonzalez wrote:
Thanks Francesco !

I've modified my code, but now I'm getting NullPointerException (on the second PUT request on /Group endpoint).
This error isn't generated when we execute the request serially.
It's generated when HTTP request are executed in parallel.

I suppose that the issue might come from the fact that you are essentially performing concurrent modifications to the same entity (e.g. user).

i.e.
=> gives a 500

My code is available in [1]
The stacktrace is available in [2]

The error in syncope is from JPAUserDAO (input is null) :

This NPE is quite strange: two possible causes come to my mind:

1. there is some nasty transactional issue - even though I would have expected user.getMemberships() to be null rather than such collection containing a null membership

2. there is some null element in user.getMemberships(), possibly coming by the fact that the first user modification is not committed yet

In order to better investigate this behaviour I would suggest to debug the Syncope core application and to break in the findAllGroups() method to check if user.getMemberships() effectively contains null (and possibly non-null) elements.

Regards.

public Collection<Group> findAllGroups(final User user) {
    return CollectionUtils.union(
            CollectionUtils.collect(user.getMemberships(), new Transformer<UMembership, Group>() {

                @Override
                public Group transform(final UMembership input) {
                    return input.getRightEnd();
                }
            }, new ArrayList<Group>()),
            findDynGroupMemberships(user));
}


[1] Here's exactly the code from the SCIM Provider side (calling Syncope REST API) :
 
    // This is the method called on PUT for Group endpoint
    public Group update(String id, Group group) {

        // 1. General group update
        GroupTO existingGroupTO = getGroupTOByKey(id);
        List<UserTO> usersOfThisGroup = getUsersForGroup(id);
        GroupTO groupTO = groupConverter.fromScim(group);
        groupTO.setKey(existingGroupTO.getKey());
        GroupService groupService = syncopeClient.getService(GroupService.class);
        Response response;
        try {
            response = groupService.update(groupTO);
        } catch (SyncopeClientException ex) {
            throw syncopeExHandler.convertSyncopeGroupClientException(ex, group.getDisplayName(), ProvisioningOperation.UPDATING);
        }
        if (response == null || Response.Status.OK.getStatusCode() != response.getStatus()) {
            throw syncopeExHandler.convertSyncopeGroupErrorResponse(response, group.getDisplayName(),
                    ProvisioningOperation.UPDATING);
        }

        // 2. General membership update
        List<String> idsOfExistingUsers = usersOfThisGroup.stream().map(user -> user.getKey()).collect(Collectors.toList());
        List<String> idsOfNewUsers = group.getMembers().stream().map(member -> member.getValue()).collect(Collectors.toList());
        for (MemberRef memberRef : group.getMembers()) {
            if (!idsOfExistingUsers.contains(memberRef.getValue())) {
                addUserToGroup(groupTO, memberRef.getValue());
            }
        }
        for (String userId : idsOfExistingUsers) {
            if (!idsOfNewUsers.contains(userId)) {
                removeUserFromGroup(groupTO, userId);
            }
        }
        return getById(id);
    }

    private void addUserToGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.ADD_REPLACE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }
    }

    private void removeUserFromGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.DELETE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not removed from the group %s", userId, groupTO.getName()), e);
        }
    }

[2] Stacktrace Sorry for the super long stack :
15:05:47.623 ERROR org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper - Exception thrown
java.lang.NullPointerException
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:500) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:496) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1077) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1049) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllGroups(JPAUserDAO.java:495) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllResources(JPAUserDAO.java:529) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor124.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
]       at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE--More--
    at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy172.findAllResources(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.createTasks(PropagationManagerImpl.java:345) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUpdateTasks(PropagationManagerImpl.java:275) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:204) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:224) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
_91]    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0--More--
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy209.getUserUpdateTasks(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:123) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:57) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.doUpdate(UserLogic.java:232) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:213) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:68) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]0-SNAPSHOT.jar:2.0.0-SNAPSHOT]--More--
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.LogicInvocationHandler.around(LogicInvocationHandler.java:72) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor133.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.UserLogic$$EnhancerBySpringCGLIB$$f6ff0886.update(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
stractAnyService.java:159) ~[syncope-core-rest-cxf-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]e--
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[cxf-core-3.1.7.jar:3.1.7]
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:254) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:180) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:299) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:276) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[catalina.jar:8.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-websocket.jar:8.0.35]
icationFilterChain.java:240) ~[catalina.jar:8.0.35]erChain.internalDoFilter(Appl--More--
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.apache.syncope.core.spring.security.MustChangePasswordFilter.doFilter(MustChangePasswordFilter.java:77) ~[syncope-core-spring-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
jar:4.1.3.RELEASE]ingframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.3.RELEASE.--More--
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:215) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e



De : Francesco Chicchiriccò [hidden email]
À : [hidden email]
Envoyé le : Mardi 27 septembre 2016 15h20
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

On 27/09/2016 15:00, Adrian Gonzalez wrote:
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.

That's very good to hear: looks it is for SCIM 2.0, correct?
Looking forward to take a look at it!

Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

You are right about the fact that Syncope manages groups memberships by modifying users.

I am a bit confused about your snippet above, so let me rephrase it a bit.
Assuming your scenario:

 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2

the Group endpoint should perform as following:

UserPatch userPatch = new UserPatch();
userPatch.setKey("the key of user1");
userPatch.getMemberships().add(new MembershipPatch.Builder().
        operation(PatchOperation.DELETE).
        group("the key of the group passed to the endpoint").

        build());

userLogic.update(userPatch, true);

As you can see, I am rather using the logic layer (e.g. UserLogic as done in UserServiceImpl) to interact with Syncope core, rather than the external service layer (e.g. how REST clients do).

HTH
Regards.
-- 
Francesco Chicchiriccò

Tirasa - Open Source Excellence
http://www.tirasa.net/

Member at The Apache Software Foundation
Syncope, Cocoon, Olingo, CXF, OpenJPA, PonyMail
http://home.apache.org/~ilgrosso/
Reply | Threaded
Open this post in threaded view
|

Re: SCIM & Syncope : OptimisticLockException on user groups membership update

ilgrosso
Administrator
Hi Adrian,
I was wondering if you made any progress on this.

Regards.

On 28/09/2016 08:02, Francesco Chicchiriccò wrote:
On 27/09/2016 17:24, Adrian Gonzalez wrote:
Thanks Francesco !

I've modified my code, but now I'm getting NullPointerException (on the second PUT request on /Group endpoint).
This error isn't generated when we execute the request serially.
It's generated when HTTP request are executed in parallel.

I suppose that the issue might come from the fact that you are essentially performing concurrent modifications to the same entity (e.g. user).

i.e.
=> gives a 500

My code is available in [1]
The stacktrace is available in [2]

The error in syncope is from JPAUserDAO (input is null) :

This NPE is quite strange: two possible causes come to my mind:

1. there is some nasty transactional issue - even though I would have expected user.getMemberships() to be null rather than such collection containing a null membership

2. there is some null element in user.getMemberships(), possibly coming by the fact that the first user modification is not committed yet

In order to better investigate this behaviour I would suggest to debug the Syncope core application and to break in the findAllGroups() method to check if user.getMemberships() effectively contains null (and possibly non-null) elements.

Regards.

public Collection<Group> findAllGroups(final User user) {
    return CollectionUtils.union(
            CollectionUtils.collect(user.getMemberships(), new Transformer<UMembership, Group>() {

                @Override
                public Group transform(final UMembership input) {
                    return input.getRightEnd();
                }
            }, new ArrayList<Group>()),
            findDynGroupMemberships(user));
}


[1] Here's exactly the code from the SCIM Provider side (calling Syncope REST API) :
 
    // This is the method called on PUT for Group endpoint
    public Group update(String id, Group group) {

        // 1. General group update
        GroupTO existingGroupTO = getGroupTOByKey(id);
        List<UserTO> usersOfThisGroup = getUsersForGroup(id);
        GroupTO groupTO = groupConverter.fromScim(group);
        groupTO.setKey(existingGroupTO.getKey());
        GroupService groupService = syncopeClient.getService(GroupService.class);
        Response response;
        try {
            response = groupService.update(groupTO);
        } catch (SyncopeClientException ex) {
            throw syncopeExHandler.convertSyncopeGroupClientException(ex, group.getDisplayName(), ProvisioningOperation.UPDATING);
        }
        if (response == null || Response.Status.OK.getStatusCode() != response.getStatus()) {
            throw syncopeExHandler.convertSyncopeGroupErrorResponse(response, group.getDisplayName(),
                    ProvisioningOperation.UPDATING);
        }

        // 2. General membership update
        List<String> idsOfExistingUsers = usersOfThisGroup.stream().map(user -> user.getKey()).collect(Collectors.toList());
        List<String> idsOfNewUsers = group.getMembers().stream().map(member -> member.getValue()).collect(Collectors.toList());
        for (MemberRef memberRef : group.getMembers()) {
            if (!idsOfExistingUsers.contains(memberRef.getValue())) {
                addUserToGroup(groupTO, memberRef.getValue());
            }
        }
        for (String userId : idsOfExistingUsers) {
            if (!idsOfNewUsers.contains(userId)) {
                removeUserFromGroup(groupTO, userId);
            }
        }
        return getById(id);
    }

    private void addUserToGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.ADD_REPLACE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }
    }

    private void removeUserFromGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.DELETE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not removed from the group %s", userId, groupTO.getName()), e);
        }
    }

[2] Stacktrace Sorry for the super long stack :
15:05:47.623 ERROR org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper - Exception thrown
java.lang.NullPointerException
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:500) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:496) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1077) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1049) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllGroups(JPAUserDAO.java:495) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllResources(JPAUserDAO.java:529) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor124.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
]       at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE--More--
    at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy172.findAllResources(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.createTasks(PropagationManagerImpl.java:345) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUpdateTasks(PropagationManagerImpl.java:275) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:204) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:224) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
_91]    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0--More--
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy209.getUserUpdateTasks(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:123) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:57) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.doUpdate(UserLogic.java:232) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:213) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:68) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]0-SNAPSHOT.jar:2.0.0-SNAPSHOT]--More--
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.LogicInvocationHandler.around(LogicInvocationHandler.java:72) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor133.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.UserLogic$$EnhancerBySpringCGLIB$$f6ff0886.update(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
stractAnyService.java:159) ~[syncope-core-rest-cxf-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]e--
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[cxf-core-3.1.7.jar:3.1.7]
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:254) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:180) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:299) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:276) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[catalina.jar:8.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-websocket.jar:8.0.35]
icationFilterChain.java:240) ~[catalina.jar:8.0.35]erChain.internalDoFilter(Appl--More--
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.apache.syncope.core.spring.security.MustChangePasswordFilter.doFilter(MustChangePasswordFilter.java:77) ~[syncope-core-spring-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
jar:4.1.3.RELEASE]ingframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.3.RELEASE.--More--
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:215) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e



De : Francesco Chicchiriccò [hidden email]
À : [hidden email]
Envoyé le : Mardi 27 septembre 2016 15h20
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

On 27/09/2016 15:00, Adrian Gonzalez wrote:
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.

That's very good to hear: looks it is for SCIM 2.0, correct?
Looking forward to take a look at it!

Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

You are right about the fact that Syncope manages groups memberships by modifying users.

I am a bit confused about your snippet above, so let me rephrase it a bit.
Assuming your scenario:

 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2

the Group endpoint should perform as following:

UserPatch userPatch = new UserPatch();
userPatch.setKey("the key of user1");
userPatch.getMemberships().add(new MembershipPatch.Builder().
        operation(PatchOperation.DELETE).
        group("the key of the group passed to the endpoint").

        build());

userLogic.update(userPatch, true);

As you can see, I am rather using the logic layer (e.g. UserLogic as done in UserServiceImpl) to interact with Syncope core, rather than the external service layer (e.g. how REST clients do).

HTH
Regards.
-- 
Francesco Chicchiriccò

Tirasa - Open Source Excellence
http://www.tirasa.net/

Member at The Apache Software Foundation
Syncope, Cocoon, Olingo, CXF, OpenJPA, PonyMail
http://home.apache.org/~ilgrosso/
Reply | Threaded
Open this post in threaded view
|

Re: SCIM & Syncope : OptimisticLockException on user groups membership update

Adrian Gonzalez
Hello Francesco,

I'm really really sorry, I switched to another subject and was quite busy. 
It's always a pending task on my side, and I'll need to handle it in a very near future.



De : Francesco Chicchiriccò <[hidden email]>
À : [hidden email]
Envoyé le : Mardi 8 novembre 2016 9h22
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

Hi Adrian,
I was wondering if you made any progress on this.

Regards.

On 28/09/2016 08:02, Francesco Chicchiriccò wrote:
On 27/09/2016 17:24, Adrian Gonzalez wrote:
Thanks Francesco !

I've modified my code, but now I'm getting NullPointerException (on the second PUT request on /Group endpoint).
This error isn't generated when we execute the request serially.
It's generated when HTTP request are executed in parallel.

I suppose that the issue might come from the fact that you are essentially performing concurrent modifications to the same entity (e.g. user).

i.e.
PUT <a rel="nofollow" shape="rect" id="yiv1714786121yui_3_16_0_ym19_1_1474667136328_342622" target="_blank" onclick="return theMainWindow.showLinkWarning(this)" href="http://localhost:9091/groups/2564b3a7-0f5d-424d-a4b3-a70f5d624d80">http://localhost:9091/groups/2564b3a7-0f5d-424d-a4b3-a70f5d624d80 => gives a 200
PUT <a rel="nofollow" shape="rect" id="yiv1714786121yui_3_16_0_ym19_1_1474667136328_340501" target="_blank" onclick="return theMainWindow.showLinkWarning(this)" href="http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e6a">http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e6a
=> gives a 500

My code is available in [1]
The stacktrace is available in [2]

The error in syncope is from JPAUserDAO (input is null) :

This NPE is quite strange: two possible causes come to my mind:

1. there is some nasty transactional issue - even though I would have expected user.getMemberships() to be null rather than such collection containing a null membership

2. there is some null element in user.getMemberships(), possibly coming by the fact that the first user modification is not committed yet

In order to better investigate this behaviour I would suggest to debug the Syncope core application and to break in the findAllGroups() method to check if user.getMemberships() effectively contains null (and possibly non-null) elements.

Regards.

public Collection<Group> findAllGroups(final User user) {
    return CollectionUtils.union(
            CollectionUtils.collect(user.getMemberships(), new Transformer<UMembership, Group>() {

                @Override
                public Group transform(final UMembership input) {
                    return input.getRightEnd();
                }
            }, new ArrayList<Group>()),
            findDynGroupMemberships(user));
}


[1] Here's exactly the code from the SCIM Provider side (calling Syncope REST API) :
 
    // This is the method called on PUT for Group endpoint
    public Group update(String id, Group group) {

        // 1. General group update
        GroupTO existingGroupTO = getGroupTOByKey(id);
        List<UserTO> usersOfThisGroup = getUsersForGroup(id);
        GroupTO groupTO = groupConverter.fromScim(group);
        groupTO.setKey(existingGroupTO.getKey());
        GroupService groupService = syncopeClient.getService(GroupService.class);
        Response response;
        try {
            response = groupService.update(groupTO);
        } catch (SyncopeClientException ex) {
            throw syncopeExHandler.convertSyncopeGroupClientException(ex, group.getDisplayName(), ProvisioningOperation.UPDATING);
        }
        if (response == null || Response.Status.OK.getStatusCode() != response.getStatus()) {
            throw syncopeExHandler.convertSyncopeGroupErrorResponse(response, group.getDisplayName(),
                    ProvisioningOperation.UPDATING);
        }

        // 2. General membership update
        List<String> idsOfExistingUsers = usersOfThisGroup.stream().map(user -> user.getKey()).collect(Collectors.toList());
        List<String> idsOfNewUsers = group.getMembers().stream().map(member -> member.getValue()).collect(Collectors.toList());
        for (MemberRef memberRef : group.getMembers()) {
            if (!idsOfExistingUsers.contains(memberRef.getValue())) {
                addUserToGroup(groupTO, memberRef.getValue());
            }
        }
        for (String userId : idsOfExistingUsers) {
            if (!idsOfNewUsers.contains(userId)) {
                removeUserFromGroup(groupTO, userId);
            }
        }
        return getById(id);
    }

    private void addUserToGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.ADD_REPLACE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }
    }

    private void removeUserFromGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.DELETE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not removed from the group %s", userId, groupTO.getName()), e);
        }
    }

[2] Stacktrace Sorry for the super long stack :
15:05:47.623 ERROR org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper - Exception thrown
java.lang.NullPointerException
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:500) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:496) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1077) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1049) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllGroups(JPAUserDAO.java:495) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllResources(JPAUserDAO.java:529) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor124.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
]       at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE--More--
    at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy172.findAllResources(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.createTasks(PropagationManagerImpl.java:345) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUpdateTasks(PropagationManagerImpl.java:275) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:204) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:224) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
_91]    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0--More--
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy209.getUserUpdateTasks(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:123) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:57) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.doUpdate(UserLogic.java:232) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:213) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:68) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]0-SNAPSHOT.jar:2.0.0-SNAPSHOT]--More--
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.LogicInvocationHandler.around(LogicInvocationHandler.java:72) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor133.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.UserLogic$$EnhancerBySpringCGLIB$$f6ff0886.update(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
stractAnyService.java:159) ~[syncope-core-rest-cxf-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]e--
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[cxf-core-3.1.7.jar:3.1.7]
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:254) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:180) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:299) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:276) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[catalina.jar:8.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-websocket.jar:8.0.35]
icationFilterChain.java:240) ~[catalina.jar:8.0.35]erChain.internalDoFilter(Appl--More--
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.apache.syncope.core.spring.security.MustChangePasswordFilter.doFilter(MustChangePasswordFilter.java:77) ~[syncope-core-spring-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
jar:4.1.3.RELEASE]ingframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.3.RELEASE.--More--
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:215) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]<a rel="nofollow" shape="rect" target="_blank" onclick="return theMainWindow.showLinkWarning(this)" href="http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e">http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e



De : Francesco Chicchiriccò [hidden email]
À : [hidden email]
Envoyé le : Mardi 27 septembre 2016 15h20
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

On 27/09/2016 15:00, Adrian Gonzalez wrote:
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.

That's very good to hear: looks it is for SCIM 2.0, correct?
Looking forward to take a look at it!

Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

You are right about the fact that Syncope manages groups memberships by modifying users.

I am a bit confused about your snippet above, so let me rephrase it a bit.
Assuming your scenario:

 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2

the Group endpoint should perform as following:

UserPatch userPatch = new UserPatch();
userPatch.setKey("the key of user1");
userPatch.getMemberships().add(new MembershipPatch.Builder().
        operation(PatchOperation.DELETE).
        group("the key of the group passed to the endpoint").

        build());

userLogic.update(userPatch, true);

As you can see, I am rather using the logic layer (e.g. UserLogic as done in UserServiceImpl) to interact with Syncope core, rather than the external service layer (e.g. how REST clients do).

HTH
Regards.
-- 
Francesco Chicchiriccò

Tirasa - Open Source Excellence
http://www.tirasa.net/

Member at The Apache Software Foundation
Syncope, Cocoon, Olingo, CXF, OpenJPA, PonyMail
http://home.apache.org/~ilgrosso/


Reply | Threaded
Open this post in threaded view
|

Re: SCIM & Syncope : OptimisticLockException on user groups membership update

ilgrosso
Administrator
On 08/11/2016 09:47, Adrian Gonzalez wrote:
Hello Francesco,

I'm really really sorry, I switched to another subject and was quite busy. 
It's always a pending task on my side, and I'll need to handle it in a very near future.

Hi Adrian,
no problems at all.

Whenever you'll start again working on it, please update here: I am interested in your work as it pretty much matches

https://issues.apache.org/jira/browse/SYNCOPE-152

and, if you are interested, we might be able to consider it for building a new extension, as

https://syncope.apache.org/docs/reference-guide.html#extensions

Regards.


De : Francesco Chicchiriccò [hidden email]
À : [hidden email]
Envoyé le : Mardi 8 novembre 2016 9h22
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

Hi Adrian,
I was wondering if you made any progress on this.

Regards.

On 28/09/2016 08:02, Francesco Chicchiriccò wrote:
On 27/09/2016 17:24, Adrian Gonzalez wrote:
Thanks Francesco !

I've modified my code, but now I'm getting NullPointerException (on the second PUT request on /Group endpoint).
This error isn't generated when we execute the request serially.
It's generated when HTTP request are executed in parallel.

I suppose that the issue might come from the fact that you are essentially performing concurrent modifications to the same entity (e.g. user).

i.e.
PUT <a moz-do-not-send="true" rel="nofollow" shape="rect" id="yiv1714786121yui_3_16_0_ym19_1_1474667136328_342622" target="_blank" onclick="return theMainWindow.showLinkWarning(this)" href="http://localhost:9091/groups/2564b3a7-0f5d-424d-a4b3-a70f5d624d80">http://localhost:9091/groups/2564b3a7-0f5d-424d-a4b3-a70f5d624d80 => gives a 200
PUT <a moz-do-not-send="true" rel="nofollow" shape="rect" id="yiv1714786121yui_3_16_0_ym19_1_1474667136328_340501" target="_blank" onclick="return theMainWindow.showLinkWarning(this)" href="http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e6a">http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e6a
=> gives a 500

My code is available in [1]
The stacktrace is available in [2]

The error in syncope is from JPAUserDAO (input is null) :

This NPE is quite strange: two possible causes come to my mind:

1. there is some nasty transactional issue - even though I would have expected user.getMemberships() to be null rather than such collection containing a null membership

2. there is some null element in user.getMemberships(), possibly coming by the fact that the first user modification is not committed yet

In order to better investigate this behaviour I would suggest to debug the Syncope core application and to break in the findAllGroups() method to check if user.getMemberships() effectively contains null (and possibly non-null) elements.

Regards.

public Collection<Group> findAllGroups(final User user) {
    return CollectionUtils.union(
            CollectionUtils.collect(user.getMemberships(), new Transformer<UMembership, Group>() {

                @Override
                public Group transform(final UMembership input) {
                    return input.getRightEnd();
                }
            }, new ArrayList<Group>()),
            findDynGroupMemberships(user));
}


[1] Here's exactly the code from the SCIM Provider side (calling Syncope REST API) :
 
    // This is the method called on PUT for Group endpoint
    public Group update(String id, Group group) {

        // 1. General group update
        GroupTO existingGroupTO = getGroupTOByKey(id);
        List<UserTO> usersOfThisGroup = getUsersForGroup(id);
        GroupTO groupTO = groupConverter.fromScim(group);
        groupTO.setKey(existingGroupTO.getKey());
        GroupService groupService = syncopeClient.getService(GroupService.class);
        Response response;
        try {
            response = groupService.update(groupTO);
        } catch (SyncopeClientException ex) {
            throw syncopeExHandler.convertSyncopeGroupClientException(ex, group.getDisplayName(), ProvisioningOperation.UPDATING);
        }
        if (response == null || Response.Status.OK.getStatusCode() != response.getStatus()) {
            throw syncopeExHandler.convertSyncopeGroupErrorResponse(response, group.getDisplayName(),
                    ProvisioningOperation.UPDATING);
        }

        // 2. General membership update
        List<String> idsOfExistingUsers = usersOfThisGroup.stream().map(user -> user.getKey()).collect(Collectors.toList());
        List<String> idsOfNewUsers = group.getMembers().stream().map(member -> member.getValue()).collect(Collectors.toList());
        for (MemberRef memberRef : group.getMembers()) {
            if (!idsOfExistingUsers.contains(memberRef.getValue())) {
                addUserToGroup(groupTO, memberRef.getValue());
            }
        }
        for (String userId : idsOfExistingUsers) {
            if (!idsOfNewUsers.contains(userId)) {
                removeUserFromGroup(groupTO, userId);
            }
        }
        return getById(id);
    }

    private void addUserToGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.ADD_REPLACE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }
    }

    private void removeUserFromGroup(GroupTO groupTO, String userId) {
        UserService userService = syncopeClient.getService(UserService.class);
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(userId);
        userPatch.getMemberships().add(
                new MembershipPatch.Builder().operation(PatchOperation.DELETE).group(groupTO.getKey()).build());
        try {
            userService.update(userPatch);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not removed from the group %s", userId, groupTO.getName()), e);
        }
    }

[2] Stacktrace Sorry for the super long stack :
15:05:47.623 ERROR org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper - Exception thrown
java.lang.NullPointerException
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:500) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO$2.transform(JPAUserDAO.java:496) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1077) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.commons.collections4.CollectionUtils.collect(CollectionUtils.java:1049) ~[commons-collections4-4.1.jar:4.1]
    at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllGroups(JPAUserDAO.java:495) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.apache.syncope.core.persistence.jpa.dao.JPAUserDAO.findAllResources(JPAUserDAO.java:529) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor124.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
]       at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE--More--
    at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy172.findAllResources(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.createTasks(PropagationManagerImpl.java:345) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUpdateTasks(PropagationManagerImpl.java:275) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:204) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.propagation.PropagationManagerImpl.getUserUpdateTasks(PropagationManagerImpl.java:224) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
_91]    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0--More--
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:280) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.apache.syncope.core.persistence.jpa.spring.DomainTransactionInterceptor.invoke(DomainTransactionInterceptor.java:64) ~[syncope-core-persistence-jpa-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at com.sun.proxy.$Proxy209.getUserUpdateTasks(Unknown Source) ~[?:?]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:123) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.provisioning.java.DefaultUserProvisioningManager.update(DefaultUserProvisioningManager.java:57) ~[syncope-core-provisioning-java-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.doUpdate(UserLogic.java:232) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:213) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.apache.syncope.core.logic.UserLogic.update(UserLogic.java:68) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]0-SNAPSHOT.jar:2.0.0-SNAPSHOT]--More--
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.LogicInvocationHandler.around(LogicInvocationHandler.java:72) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at sun.reflect.GeneratedMethodAccessor133.invoke(Unknown Source) ~[?:?]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655) ~[spring-aop-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.apache.syncope.core.logic.UserLogic$$EnhancerBySpringCGLIB$$f6ff0886.update(<generated>) ~[syncope-core-logic-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
stractAnyService.java:159) ~[syncope-core-rest-cxf-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]e--
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
    at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99) ~[cxf-rt-frontend-jaxrs-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[cxf-core-3.1.7.jar:3.1.7]
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[cxf-core-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:254) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:180) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:299) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:276) ~[cxf-rt-transports-http-3.1.7.jar:3.1.7]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[catalina.jar:8.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-websocket.jar:8.0.35]
icationFilterChain.java:240) ~[catalina.jar:8.0.35]erChain.internalDoFilter(Appl--More--
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.35]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.apache.syncope.core.spring.security.MustChangePasswordFilter.doFilter(MustChangePasswordFilter.java:77) ~[syncope-core-spring-2.0.0-SNAPSHOT.jar:2.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
jar:4.1.3.RELEASE]ingframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.3.RELEASE.--More--
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:215) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]<a moz-do-not-send="true" rel="nofollow" shape="rect" target="_blank" onclick="return theMainWindow.showLinkWarning(this)" href="http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e">http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e



De : Francesco Chicchiriccò [hidden email]
À : [hidden email]
Envoyé le : Mardi 27 septembre 2016 15h20
Objet : Re: SCIM & Syncope : OptimisticLockException on user groups membership update

On 27/09/2016 15:00, Adrian Gonzalez wrote:
Hello,

We're trying to build a POC on SCIM APIs on top of Syncope.

That's very good to hear: looks it is for SCIM 2.0, correct?
Looking forward to take a look at it!

Problem is when we're using some basic SCIM APIs to update the groups membership of a given user, we got a OptimisticLockException.

This is due to the fact that SCIM group membership can be updated only from the Group endpoint (not from the User endpoint).
groups
A list of groups to which the user belongs,...
Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the
"Group" Resource (Section 4.2).  This attribute has a mutability of "readOnly".

So, we have the following scenario :
 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2
 * we get a OptimisticLockException since both calls are made for a relation on the same user - because on the SCIM side for the Group endpoint, we must call
   userService.update(userTO) to update a user <-> group relation.

i.e.
        MembershipTO membershipTO =
                new MembershipTO.Builder().group(userTO.getKey(), "USER").group(groupTO.getKey(), groupTO.getName()).build();
        userTO.getMemberships().add(membershipTO);
        try {
            userService.update(userTO);
        } catch (SyncopeClientException e) {
            throw new SCIMException(String.format("User %s was not added to the group %s", userId, groupTO.getName()), e);
        }

Is there an API to update user or group membership without testing @Version field ? (i.e like a syncope REST API on top of a jpql update ?)
Perhaps we're not using the good API here (is there an API to handle membership from the group's side ?
Do you see another possible solution (besides updating membership from the Group side/screen) ?

You are right about the fact that Syncope manages groups memberships by modifying users.

I am a bit confused about your snippet above, so let me rephrase it a bit.
Assuming your scenario:

 * we have a user1 member of 2 groups (group1 and group2).
 * we want to remove the user1 from both groups from a UI console.
 * the UI then needs to send 2 HTTP PUT on SCIM /Groups endpoint (one for each group).
    /PUT Groups/group1
    /PUT Groups/group2

the Group endpoint should perform as following:

UserPatch userPatch = new UserPatch();
userPatch.setKey("the key of user1");
userPatch.getMemberships().add(new MembershipPatch.Builder().
        operation(PatchOperation.DELETE).
        group("the key of the group passed to the endpoint").

        build());

userLogic.update(userPatch, true);

As you can see, I am rather using the logic layer (e.g. UserLogic as done in UserServiceImpl) to interact with Syncope core, rather than the external service layer (e.g. how REST clients do).

HTH
Regards.
-- 
Francesco Chicchiriccò

Tirasa - Open Source Excellence
http://www.tirasa.net/

Member at The Apache Software Foundation
Syncope, Cocoon, Olingo, CXF, OpenJPA, PonyMail
http://home.apache.org/~ilgrosso/