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 http://localhost:9091/groups/2564b3a7-0f5d-424d-a4b3-a70f5d624d80 => gives a 200
PUT 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]http://localhost:9091/groups/fad94db3-9245-449e-994d-b39245449e


------------------------------------------------------------------------
*De :* Francesco Chicchiriccò <ilgro...@apache.org>
*À :* user@syncope.apache.org
*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).
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) ?

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 via email to