GithubHelp home page GithubHelp logo

captain-p-goldfish / scim-sdk Goto Github PK

View Code? Open in Web Editor NEW
115.0 15.0 34.0 4.81 MB

a scim implementation as described in RFC7643 and RFC7644

Home Page: https://github.com/Captain-P-Goldfish/SCIM/wiki

License: BSD 3-Clause "New" or "Revised" License

ANTLR 0.04% Java 99.53% FreeMarker 0.43%
scim scim-specification sdk rfc-7643 rfc-7644 server client scim2 scim-2 restful

scim-sdk's People

Contributors

captain-p-goldfish avatar dependabot-preview[bot] avatar dependabot[bot] avatar dmoidl avatar hamiltont avatar haster avatar jessthrysoee avatar mberning avatar nielsvanzon avatar ondrejzivotsky avatar ozivotsky avatar psvo avatar stefan-wauters avatar temujinh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

scim-sdk's Issues

Handling of multiple HTTP headers

The class ResourceEndpoint defines methods to pass HTTP parameters as a map. As multiple HTTP header fields with the same field name appear in practice, the methods should use a multi map, e.g. javax.ws.rs.core.MultivaluedMap

Expose attributes and excludedAttributes in ResourceHandler#getResource

Would consider pull-request exposing supplied attributes and excludedAttributes to the ResourceHandler#getResource API call?

It would allow avoiding possibly expensive joins when retrieving the data from storage (e.g. large set of group members) in a similar way how listResources currently works.

I'm thinking about the following change in the ResourceHandler API:

// class ResourceHandler<T>

public T getResource(String id, Authorization auth) {
   // don't want it abstract, because then you would have to implement both getResource methods
   throw NotImplementedException(...); // or InternalServerError?
}

public T getResource(String id, List<SchemaAttribute> attributes, List<SchemaAttribute> excludedAttributes, Authorization auth) {
   return getResource(id, auth);
}

Call the getResource(String, List<SchemaAttribute>, List<SchemaAttribute>, Authorization) from the ResourceEndpointHandler#getResource(String, String, String, String, Map<String,String>, Supplier<String>, Authorization)

It seems it would be helpful to move attribute parsing outside of the SchemaValidator, because in case of the list request, it's done by the endpoint list method and then again by the SchemaValidator constructor. But that's just an optimization, that's not really required for this change.

What do you think? Thanks!

PATCH request returns in-memory resource instead of result from ResourceHandler

Hi,

Overview

When performing a PATCH request to add or replace an attribute for a resource, I've noticed that some attributes are mirrored from the PATCH request payload into the response payload, even when the mirrored properties aren't actually being used in the SCIM 2.0 server application.

Example

For example,
The SCIM server application uses the role property for a user. As apart of the implementation, the application code (sitting within the ResourceHandler implementation), only utilities the roles.display sub-attribute, and sets only the roles.display into response object.

Given a request that includes valid SCIM 2.0 attributes, but are attributes that are not explicitly set into the response, are appearing in the JSON response payload.

Request - A PATCH request to add a role, that includes the display, value, type and primary.

{
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:PatchOp"
    ],
    "Operations": [
        {
            "op": "add",
            "path": "roles",
            "value": [
                {
                    "primary": false,
                    "type": "WindowsAzureActiveDirectoryRole",
                    "display": "Role 2",
                    "value": "Something Irrelevant"
                },
            ]
        }
    ]
}

The returned User object, from the ResourceHandler does NOT set anything except the roles.display.

Response

{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "id": "100325",
    "userName": "scim.test13",
    "displayName": "Dave",
    "active": true,
    "emails": [
        {
            "value": "[email protected]",
            "type": "work",
            "primary": true
        }
    ],
    "phoneNumbers": [
        {
            "value": "551 1234",
            "type": "work"
        }
    ],
    "roles": [
        {
            "value": "Something Irrelevant",
            "display": "Role 1",
            "type": "WindowsAzureActiveDirectoryRole",
            "primary": false
        },
        {
            "value": "Something Irrelevant",
            "display": "Role 2",
            "type": "WindowsAzureActiveDirectoryRole",
            "primary": false
        }
    ],
    "meta": {
        "resourceType": "User",
        "created": "2021-04-23T02:15:38.378Z",
        "lastModified": "2021-07-21T17:45:13.223+10:00",
        "location": "https://********************/Users/100325",
        "version": "W/\"41BA1691F91FB9C9776DD2C18349540C051E59AD9E2339642B2CB0D32C7367D4FC7ABE6E9913B225A84B48E68AC2567BD2CF400149C6C9A93F5C84B1F5E98686\""
    },
}

Additional values are present in the response.

Code Investigation

  1. When looking at the current ResourceEndpointHandler.patchResource() method, I can see that the follow line
    Line 1010
ResourceNode patchedResourceNode = patchHandler.patchResource(resourceNode, patchOpRequest);

Is responsible for performing an in-memory update based on if the incoming node is different from the current state of the resource. The input reference of the resourceNode (that is originally retrieved from server application) has its structure/values modified based on the incoming PATCH request.

  1. The patchedResourceNode then becomes the result of the server applications update operation on the resource. The value that the patchedResourceNode has after this line is the desired response object.

Line 1032

 patchedResourceNode = resourceHandler.updateResource(patchedResourceNode, authorization);
  1. However, I noticed that return value for the ResourceEndpointHandler.patchResource() is the resourceNode after the in-memory update, as apposed to the intended patchedResourceNode.

Line 1049

      JsonNode responseResource = responseValidator.validateDocument(resourceNode);
      return new UpdateResponse(responseResource, location, meta);

I am wondering if I am interpreting this behavior incorrectly, or is this in-fact unintentional? The main issue I'd like to solve is to ensure that all attributes that are in the response are attributed to a resource found in the server application.

Currently with this behavior, attributes are appearing in the response body that are not being set by the server application.

Is it possible to look into this issue, or otherwise suggest a work-around?
Thanks in advance

Unable to use Basic Authorization with Keycloak

Hello, this is likely more a question than a bug, I'm using the issue tracking not having found other means.

I'm trying to use SCIM-SDK client against Keycloak by configuring it with BasicAuth:

private ScimRequestBuilder createScimRequestBuilder(Config config)
    {
        ScimClientConfig scimClientConfig = ScimClientConfig.builder()
                .basicAuth(BasicAuth.builder()
                        .username(config.clientId)
                        .password(config.clientSecret)
                        .build()
                )
                .build();

        return new ScimRequestBuilder(config.scimBaseUrl, scimClientConfig);
    }

In Keycloak the client is configured as
image

The SCIM extension has no explicitly authorized client, so it should allow them all.

How can I use client credentials just like I do with org.keycloak.admin.client.Keycloak, or org.keycloak.authorization.client.AuthzClient?

Filtering by meta attribute results in NPE

When automatic filtering is enabled and filter by meta attribute is used, for example meta.created eq "2000-10-10T00:00:00.000Z", NullPointerException is thrown when processing request.

It's caused by meta attribute being evaluated as extension attribute, but although it internally has it's own schema, it's not (and should not be) registered as an extension.

[...snip...]
Caused by: java.lang.NullPointerException
	at jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[?:?]
	at jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[?:?]
	at java.lang.reflect.Constructor.newInstance(Constructor.java:490) ~[?:?]
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:603) ~[?:?]
	at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:678) ~[?:?]
	at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:737) ~[?:?]
	at java.util.stream.ReduceOps$ReduceOp.evaluateParallel(ReduceOps.java:919) ~[?:?]
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233) ~[?:?]
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[?:?]
	at de.captaingoldfish.scim.sdk.server.filter.resources.FilterResourceResolver.filterResources(FilterResourceResolver.java:50) ~[scim-sdk-server-1.10.0.jar:?]
	at de.captaingoldfish.scim.sdk.server.endpoints.ResourceEndpointHandler.filterResources(ResourceEndpointHandler.java:654) ~[scim-sdk-server-1.10.0.jar:?]
	at de.captaingoldfish.scim.sdk.server.endpoints.ResourceEndpointHandler.listResources(ResourceEndpointHandler.java:544) ~[scim-sdk-server-1.10.0.jar:?]
	... 81 more
Caused by: java.lang.NullPointerException
	at de.captaingoldfish.scim.sdk.server.filter.resources.FilterResourceResolver.retrieveComplexAttributeNode(FilterResourceResolver.java:170) ~[scim-sdk-server-1.10.0.jar:?]
	at de.captaingoldfish.scim.sdk.server.filter.resources.FilterResourceResolver.visitAttributeExpressionLeaf(FilterResourceResolver.java:120) ~[scim-sdk-server-1.10.0.jar:?]
	at de.captaingoldfish.scim.sdk.server.filter.resources.FilterResourceResolver.isResourceMatchingFilter(FilterResourceResolver.java:92) ~[scim-sdk-server-1.10.0.jar:?]
	at de.captaingoldfish.scim.sdk.server.filter.resources.FilterResourceResolver.lambda$getResourcePredicate$0(FilterResourceResolver.java:61) ~[scim-sdk-server-1.10.0.jar:?]
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:176) ~[?:?]
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655) ~[?:?]
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[?:?]
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[?:?]
	at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:952) ~[?:?]
	at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:926) ~[?:?]
	at java.util.stream.AbstractTask.compute(AbstractTask.java:327) ~[?:?]
	at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:746) ~[?:?]
	at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:290) ~[?:?]
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java) ~[?:?]
	at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) ~[?:?]
	at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) ~[?:?]
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) ~[?:?]
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183) ~[?:?]

Incorrect auth scheme for oauthbearertoken

When oauthbearertoken authentication scheme is configured on ServiceProvider and Authorization header is not provided, SCIM-SDK returns HTTP authentication challenge WWW-Authenticate: oauthbearertoken realm="SCIM". That seems incorrect.

According to RFC 6750 Section 3, the challenge must be Bearer:

... the resource server MUST include the HTTP "WWW-Authenticate" response header field ...
All challenges defined by this specification MUST use the auth-scheme value "Bearer"

A similar issue is there for other schemes:

scim auth type http auth challenge notes
oauth OAuth
oauth2 ??? don't know enough about oauth2, it does not seem straightforward
oauthbearertoken Bearer
httpbasic Basic
httpdigest Digest also has a mandatory nonce parameter

Generally, it seems that a custom AuthenticationScheme implementation is needed for proper support of some schemas.

I believe, that adding a new challenge parameter to the AuthenticationScheme would solve the problem. I would skip WWW-Authenticate header value generation, when the challenge is not set to avoid broken value generation (e.g. for digest).

NPE when filtering by meta attribute

scim-sdk-server-1.9.2
scim-sdk-common-1.9.2

I'm trying to filter Users by meta.created attr in request query and NPE is thrown in FilterResourceResolver.retrieveComplexAttributeNode(JsonNode jsonNode, AttributeExpressionLeaf attributeExpressionLeaf),

In my case jsonNode is user:
{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"id":"usr-0000000000000002","externalId":"425ffs","meta":{"created":"2021-07-15T17:07:55.955Z","lastModified":"2021-07-16T15:07:56.433Z"},"userName":"frontman","name":{"familyName":"Driveshaft","givenName":"Charlie"},"active":false,"emails":[{"type":"work","value":"[email protected]"}]}

and attributeExpressionLeaf:
meta.created EQ "2021-07-15T17:07:55.955Z"

In other cases attributeExpressionLeaf.isMainSchemaNode() returns true and it works fine. In this case the leaf is not considered to be main schema node and in else branch extensionNode is set to null. Which makes sense since
jsonNode.get("urn:ietf:params:scim:schemas:core:2.0:Meta")
(in my case attributeExpressionLeaf.getSchemaAttribute().getSchema().getId().get() ~ urn:ietf:params:scim:schemas:core:2.0:Meta)

Feature a doAfterConsumer on ResourceEndpoint

Currently it is possible to specify a doBeforeConsumer (e.g. for authentication handling).
It would be nice to be able to specify a doAfterConsumer also to handle the SCIMResponse, e.g. for different transaction behaviour depending on the response.

Issue with Keycloak/Users version 11.0.3

I'm just having the first trial with SCIM/KC. Not sure whether a misconfiguration leads to this. I just ask ...

Requesting users using this powershell leads to a successful authentication and later a throws "{"detail":"sorry but an internal error has occurred.","schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"status":500,"scimType":"invalidValue"}":

$response = Invoke-RestMethod 'https://kc.domain.ch/auth/realms/beta/scim/v2/Users' -Method 'GET' -Headers $headers -Body $body

On Keycloak I observe this:
ERROR [de.captaingoldfish.scim.sdk.common.response.ErrorResponse] (default task-62) the attribute 'userName' must match the regular expression '[a-z-_ ]+' but value is '[email protected]': de.captaingoldfish.scim.sdk.common.exceptions.DocumentValidationException: the attribute 'userName' must match the regular expression '[a-z-_ ]+' but value is '[email protected]'

the user [email protected] is the first user in the list of my realm. it's not the user I use to login so I guess SCIM just wanted to send me a userlist and struggled with the first one. Is KC 11.0.3 already stable?

thanks!

User attribute userName is declared imutable in the schema

I have noticed that the schema for User resource declares the userName attribute as immutable. Is that intentional?

Would you consider changing it to readWrite?

  1. The RFC 7643 Section 8.7.1 declares the attribute as readWrite.
  2. At least Azure AD SCIM client needs the attribute to be readWrite, because Azure allows you to can change the userName.

I know I can have a custom schema easily, but it might be better to have the default schema compliant with the RFC.

No ResourceType object in scim-sdk-common

Hi, I am implementing a client and would like to first check /ResourceTypes in order to discover the types of resources available on the SCIM service provider. I did not find any ResourceType object in scim-sdk-common, but found an implementation in the scim-sdk-server. Is there a reason why this object is not in scim-sdk-common? I can of course add the server as a dependency or just supply my own implementation of ResourceType but I am curious about why the object only exists in the server module.

Meta Schema 'urn:ietf:params:scim:schemas:core:2.0:Schema' not correctly returned

The meta-schema for schemas is not correctly returned from the endpoint. This is caused by the more or less hardcoded schema validation to handle only one level of nesting as it was described in RFC7644.
In order to fix this smoothly a reimplementation of the SchemaValidator is necessary. The implementation of this class grew historically when I did not know what was coming at me.

Patch operation on multivalue attributes may fail to remove the correct values

When path in patch-remove operation is matching multiple values in multivalue attribute, it only removes correctly the first one.

The issue is in PatchTargetHandler#handleDirectMultiValuedComplexPathReference:

     for ( int i = 0 ; i < matchingComplexNodes.size() ; i++ )
      {
        multiValued.remove(matchingComplexNodes.get(i).getIndex());
        changeWasMade = true;
      }

After the first matching node is removed, the indexes in the multiValued shifts. Thus the next removal removes an incorrect node.

It seems the easiest fix would be to just iterate the matchingComplexNodes in a reverse order. I assume that the values returned by getIndex() are always increasing with position in the matchingComplexNodes list.

It's easy to reproduce on Group resource with multiple members and a patch operation like:

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "op": "remove",
      "path": "members[value eq \"member1\" or value eq \"member2\"]"
    }
  ]
}

Unfortunately, I won't be able to provide a patch with testcases in reasonable time.

Keycloak-Example patch group member returns 500

I try to patch an existing group with the following request

PATCH ..../auth/realms/Goldfish/scim/v2/Groups/d2ba0d0c-9536-4d61-beec-2b656170b0c8
{
	  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
	   "Operations": [   	
        {
          "op":"add",
        "path":"members",
        "value":[
        
        {
            "value": "1a463183-453d-467e-9ee5-602852754ba3",
            "type": "User"
        }
        ]
        }
    ]
 }

The user is assigned to the group, but the request returns an internal server error (500)

{
    "detail": "sorry but an internal error has occurred.",
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:Error"
    ],
    "status": 500
}

the stack-trace is:

15:00:11,077 ERROR [de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator] (default task-46) meta attribute validation failed for resource type: Group [urn:ietf:params:scim:schemas:core:2.0:Group]
15:00:11,078 ERROR [de.captaingoldfish.scim.sdk.common.response.ErrorResponse] (default task-46) the attribute 'urn:ietf:params:scim:schemas:core:2.0:Meta:meta.resourceType' is required on response.
                name: 'resourceType'
                type: 'STRING'
                description: 'The name of the resource type of the resource. This attribute has a mutability of "readOnly" and "caseExact" as "true".'
                mutability: 'READ_ONLY'
                returned: 'DEFAULT'
                uniqueness: 'NONE'
                multivalued: 'false'
                required: 'true'
                caseExact: 'true': de.captaingoldfish.scim.sdk.common.exceptions.DocumentValidationException: the attribute 'urn:ietf:params:scim:schemas:core:2.0:Meta:meta.resourceType' is required on response.
                name: 'resourceType'
                type: 'STRING'
                description: 'The name of the resource type of the resource. This attribute has a mutability of "readOnly" and "caseExact" as "true".'
                mutability: 'READ_ONLY'
                returned: 'DEFAULT'
                uniqueness: 'NONE'
                multivalued: 'false'
                required: 'true'
                caseExact: 'true'
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.getException(SchemaValidator.java:1287)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateIsRequiredForResponse(SchemaValidator.java:894)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateIsRequired(SchemaValidator.java:831)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.checkMetaAttributeOnDocument(SchemaValidator.java:577)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateAttributes(SchemaValidator.java:553)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.handleComplexNode(SchemaValidator.java:757)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.handleNode(SchemaValidator.java:736)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.checkMetaAttributeOnDocument(SchemaValidator.java:595)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateAttributes(SchemaValidator.java:553)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateDocument(SchemaValidator.java:524)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateExtensionForResponse(SchemaValidator.java:371)
        at de.captaingoldfish.scim.sdk.server.schemas.SchemaValidator.validateDocumentForResponse(SchemaValidator.java:261)
        at de.captaingoldfish.scim.sdk.server.endpoints.ResourceEndpointHandler.patchResource(ResourceEndpointHandler.java:947)
        at de.captaingoldfish.scim.sdk.server.endpoints.ResourceEndpoint.resolveRequest(ResourceEndpoint.java:178)
        at de.captaingoldfish.scim.sdk.server.endpoints.ResourceEndpoint.handleRequest(ResourceEndpoint.java:98)
        at de.captaingoldfish.scim.sdk.keycloak.scim.ScimEndpoint.get(ScimEndpoint.java:80)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)

Patch Request for Group does not remove Member

A PATCH Request on a Group does not remove the member
Sample

**PATCH** request Url: https://keycloa/auth/realms/Demo/scim/v2/Groups/e0d1016a-26dc-446f-8dab-2fd29aa4f779
Parameters : {
  "schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations":[
     {"op":"Remove","path":"members[value eq \"fb945029-265c-4c41-9ada-459f013ed126\"]"}
  ]
}

A Fix could be:


private GroupModel groupToModel(KeycloakSession keycloakSession, Group group, GroupModel groupModel)
  {
    RealmModel realmModel = keycloakSession.getContext().getRealm();
    group.getDisplayName().ifPresent(groupModel::setName);
    if (group.getExternalId().isPresent())
    {
      groupModel.setSingleAttribute(AttributeNames.RFC7643.EXTERNAL_ID, group.getExternalId().get());
    }
    List<Member> groupMembers = group.getMembers();
    keycloakSession.users().getGroupMembers(realmModel, groupModel).stream().forEach(modelMember -> {
      boolean found = false;
      for ( Member groupMember : groupMembers )
      {
        if (groupMember.getType().isPresent() && groupMember.getType().get().equalsIgnoreCase("User"))
        {
          if (groupMember.getValue().get().equals(modelMember.getId()))
          {
            found = false;
          }
        }
      }
      if (!found)
      {
        modelMember.leaveGroup(groupModel);
      }
    });

Patching Extension Attributes without an Operation Path property

Hi Pascal,

I have identified an issue in version 1.11.1 where any extension property (for example anything in the enterprise user schema, or a custom schema) that is updated via PATCH and is supplied in the below format will not map correctly into the User object after the incoming patch object is merged with the existing object is completed.

For example, if I perform the following PATCH request on a User to update their employeeNumber attribute in the enterprise extension schema:

Incoming PATCH Request

{
    "Operations": [
        {
            "op": "replace",
            "value": {
                ...
                "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber": "1111"
            }
        }
    ],
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:PatchOp"
    ]
}

From what I can see, in this specific scenario, the way which the "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber" is resolved in PatchResourceHandler.addResourceValues() seems to be incorrectly mapped as a "root" level property, instead of an "extension" property.
After the incoming patch object is merged with the existing object. The User object in ResourceHandler<User>.updateResource(User updatedUser, Authorization authorization) looks like the following:

Current Response

{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
        "employeeNumber": "2222" // Old employee ID
    },
    "employeeNumber": "1111", // New Employee ID
    "id": "1",
    "userName": "user name",
    ...
    "meta": {
        ...
    }
}

Expected Response

I would expect the User object would look like this instead:

{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
        "employeeNumber": "1111" // Updated extension property
    },
    "id": "1",
    "userName": "user name",
    ...
    "meta": {
        ...
    }
}

Findings

I believe a fix could be put in around the method PatchResourceHandler.addResourceValues() specifically around line 77 to better determine whether the incoming key is an extension property.
E.g. The key would be "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber", but the schema would be "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" which does not match.

Furthermore, there maybe some changes required to update the key value before it is passed into the recursive call at line 87.

In my investigation this issue is not just limited to the User resource but is also effecting Group too. But I think a fix in this handler would resolve it for all.

Thank you very much for your time, taking a look at this issue.
Please let me know if you need any further information or clarification.

Kyle

Query and path on extension property seems to not work

I'm trying to filter by manager property from the enterprise user extension. But I don't get any results. Also, if I try to remove the manager property with a PATCH request, it doesn't work for me either.

The filter query:

/Users/?filter=urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value eq "managerid"

The PATCH request:

PATCH /Users/userid
Content-Type: application/scim+json

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp",
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
  ],
  "Operations": [
    {
      "op": "remove",
      "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager"
    }
  ]
}

Although I believe the queries are ok, I'm not totally sure it's not error on my side.

Removing attributes from User schema

Just had a quick question regarding the default User schema and the best practices section in the wiki.

If we want to remove attributes from the default User schema, is the proper way to create a new schema entirely? I tried modifying the schema by removing attributes at runtime (based on the demonstration of modifying schema in the wiki) but it didn't appear to work.

Thanks!

Inconsistent handling of string attributes set to an empty or blank string

I'm not completely sure how empty/blank strings should be treated in SCIM, but the SCIM-SDK API seems to treat them a bit inconsistently.

  1. Empty/blank string is accepted as a valid value of required string attribute.
  2. Empty/blank string can be obtained by ScimObjectNode#getStringAttribute(java.lang.String).
  3. Empty/blank string cannot be set by ScimObjectNode#setAttribute(java.lang.String, java.lang.String), It will be normalized to a missing value.

Example request:

POST http://localhost:8180/ssc/api/scim/v2/Groups
Content-Type: application/scim+json

{
  "displayName": "",
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:Group"
  ]
}

It seems it would be better if empty/blank strings would be treated consistently in SCIM-SDK API. Or is there some rationale I'm missing for this behavior?

I haven't found any mentions about empty/blank strings in the SCIM RFCs, so probably the setAttribute should not attempt string normalization?

scim-for-keycloak-deployment.ear incompatible with keycloak 9.0.0

When trying to deploy the .ear file into keycloak 9.0.0 server deployments, encountered the following exception - the class PasswordUserCredentialModel comes from Keycloak version 11

2020-12-17 11:32:27,792 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-11) Uncaught server error: java.lang.NoClassDefFoundError: org/keycloak/models/credential/PasswordUserCredentialModel
	at deployment.scim-for-keycloak-deployment.ear.scim-for-keycloak-server.jar//de.captaingoldfish.scim.sdk.keycloak.scim.ScimConfiguration.createNewResourceEndpoint(ScimConfiguration.java:72)
	at deployment.scim-for-keycloak-deployment.ear.scim-for-keycloak-server.jar//de.captaingoldfish.scim.sdk.keycloak.scim.ScimConfiguration.getScimEndpoint(ScimConfiguration.java:55)
	at deployment.scim-for-keycloak-deployment.ear.scim-for-keycloak-server.jar//de.captaingoldfish.scim.sdk.keycloak.scim.AbstractEndpoint.<init>(AbstractEndpoint.java:34)
	at deployment.scim-for-keycloak-deployment.ear.scim-for-keycloak-server.jar//de.captaingoldfish.scim.sdk.keycloak.scim.ScimEndpoint.<init>(ScimEndpoint.java:60)
	at deployment.scim-for-keycloak-deployment.ear.scim-for-keycloak-server.jar//de.captaingoldfish.scim.sdk.keycloak.provider.ScimEndpointProvider.getResource(ScimEndpointProvider.java:40)
	at [email protected]//org.keycloak.services.resources.RealmsResource.resolveRealmExtension(RealmsResource.java:280)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at [email protected]//org.jboss.resteasy.core.ResourceLocatorInvoker.createResource(ResourceLocatorInvoker.java:69)
	at [email protected]//org.jboss.resteasy.core.ResourceLocatorInvoker.createResource(ResourceLocatorInvoker.java:48)
	at [email protected]//org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:99)
	at [email protected]//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:440)
	at [email protected]//org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:229)
	at [email protected]//org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:135)
	at [email protected]//org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:356)
	at [email protected]//org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:138)
	at [email protected]//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:215)
	at [email protected]//org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:227)
	at [email protected]//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
	at [email protected]//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
	at [email protected]//javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
	at [email protected]//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
	at [email protected]//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
	at [email protected]//org.keycloak.services.filters.KeycloakSessionServletFilter.doFilter(KeycloakSessionServletFilter.java:91)
	at [email protected]//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
	at [email protected]//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
	at [email protected]//io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
	at [email protected]//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
	at [email protected]//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
	at [email protected]//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
	at [email protected]//org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
	at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
	at [email protected]//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
	at [email protected]//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
	at [email protected]//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
	at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
	at [email protected]//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
	at [email protected]//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
	at [email protected]//io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
	at [email protected]//io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
	at [email protected]//io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
	at [email protected]//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
	at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
	at [email protected]//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
	at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
	at [email protected]//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)
	at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
	at [email protected]//io.undertow.server.handlers.MetricsHandler.handleRequest(MetricsHandler.java:64)
	at [email protected]//io.undertow.servlet.core.MetricsChainHandler.handleRequest(MetricsChainHandler.java:59)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:269)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:78)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:133)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:130)
	at [email protected]//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
	at [email protected]//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
	at [email protected]//org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1504)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1504)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1504)
	at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1504)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:249)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:78)
	at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:99)
	at [email protected]//io.undertow.server.Connectors.executeRootHandler(Connectors.java:376)
	at [email protected]//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
	at [email protected]//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1982)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
	at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ClassNotFoundException: org.keycloak.models.credential.PasswordUserCredentialModel from [Module "deployment.scim-for-keycloak-deployment.ear.scim-for-keycloak-server.jar" from Service Module Loader]
	at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:255)
	at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:410)
	at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:398)
	at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:116)```

Any chance of a new release soon?

I'm running into #131 which I see was fixed a while back. Any chance of a new release?

PS - thanks for the nice library! I quite appreciate the auto-filtering, it got me started really quickly, but I'm ready to turn it off and try out direct sql for better performance

Rollback after failed operation

According to the SCIM RFC, it mentions that if there are multiple operations, and one fails, all operations should be rolled back to the start state.

A PATCH request, regardless of the number of operations, SHALL be
treated as atomic.  If a single operation encounters an error
condition, the original SCIM resource MUST be restored, and a failure
status SHALL be returned.

And I noticed that this SCIM-SDK doesn't do that. Is this something you are aware of?
Below you can find the JSON Payload of the Patch Operation, which is trying to update the same emails.value attribute.
The first operation is fine, but the second operation fails due to a bad path, and then the third operation is not executed.
What I would expect to happen is when the second operation fails, the first one is rolled back.

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "path": "emails[value eq \"[email protected]\"].value",
      "op": "replace",
      "value": "[email protected]"
    },
    {
      "path": "email[value eq \"[email protected]\"].value",
      "op": "replace",
      "value": "[email protected]"
    },
    {
      "path": "emails[value eq \"[email protected]\"].value",
      "op": "replace",
      "value": "[email protected]"
    }
  ]
}

response.isSuccess returns false but HTTP Status is 200

Hey! This is likely more a question than a issue..

Im using the ScimClient to retrieve users and groups from an cloud based IDP (Federated Directory) with a bearer token authentication.

However, the isSucess () check returns false, although the HTTP status is 200 and data is returned in the body.
I have the same problem with the node.js based scim gateway. Here, however, only with Basic Auth.

Information from the response is printed here:

HEADER: {X-Cloud-Trace-Context=a6129f6f41bc3cae194165aa24bdd7f5, Transfer-Encoding=chunked, Server=Google Frontend, vary=origin,accept-encoding, cache-control=no-cache, Date=Thu, 25 Mar 2021 10:07:55 GMT, Content-Type=application/json; charset=utf-8}

HTTP STATUS: 200

IS SUCCESS: false

BODY: {"totalResults":6,"itemsPerPage":50,"startIndex":1,"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],"Resources":[{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"id":"a1ec9b70-8d45-11eb-8092-6b8b09f2c982","userName":"[email protected]","displayName":"Armando Pearson","photos":[{"type":"photo","primary":true,"value":"https://cdn.federated.directory/images/users/demo/m26.jpg"},{"type":"thumbnail","value":"https://cdn.federated.directory/images/users/demo/thumb/thumb_m26.jpg"}],"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"division":"Getting Started","department":"Bulk"}},{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"id":"8eefc470-8d45-11eb-8092-6b8b09f2c982","userName":"[email protected]","displayName":"Babs Jensen","photos":[{"type":"photo","primary":true,"value":"https://cdn.federated.directory/images/users/demo/w1.jpg"},{"type":"thumbnail","value":"https://cdn.federated.directory/images/users/demo/thumb/thumb_w1.jpg"}],"title":"Tour Guide","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"division":"Getting Started"}},{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"id":"a1ed10a0-8d45-11eb-8092-6b8b09f2c982","userName":"[email protected]","displayName":"Deborah Watson","photos":[{"type":"photo","primary":true,"value":"https://cdn.federated.directory/images/users/demo/w18.jpg"},{"type":"thumbnail","value":"https://cdn.federated.directory/images/users/demo/thumb/thumb_w18.jpg"}],"title":"Project Manager","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"division":"Getting Started","department":"Bulk"}},,{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"id":"a1e0dba0-8d45-11eb-8092-6b8b09f2c982","userName":"[email protected]","displayName":"Mae Thomas","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"division":"Getting Started","department":"Bulk"}},{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"id":"a1ec2640-8d45-11eb-8092-6b8b09f2c982","userName":"[email protected]","displayName":"Rói Da Rosa","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"division":"Getting Started","department":"Bulk"}}]}```

It is not possible to create a group with a name that is prefix of an existing group

Create a group "Group1"
When you try to create "Group" you get an error that a group with this name allready exists
That is because
searchForGroupByName()
returns all matching groups
to go around this one can:

 if (keycloakSession.realms()
                       .searchForGroupByName(keycloakSession.getContext().getRealm(), groupName, null, null)
                       .stream()
                       .filter(g -> ((GroupModel)g).getName().equals(groupName))
                       .findFirst()
                       .orElse(null) != null)
    {
      throw new ConflictException("a group with name '" + groupName + "' does already exist");
    }

Empty ref node calculation does not work for canonical 'User.groups[].type' value

Running version 1.10.0.

Issue

The feature of adding a $ref value is super handy. Unfortunately it does not seem to work with the canonical values for listing groups a user belongs to, because it uses the RFC7643.TYPE field to try and lookup a resource type. This fails if you use the RFC7643 recommended values of "direct" or "indirect". Seems like a simple fix is possible - add a special case to check if the TYPE field is one of the canonical values of "direct" or "indirect", and map that to "Group".

For example:

    "groups": [
        {
            "value": "9d66400a-149e-40da-b55a-96d9bb6100aa",
            "display": "test group",
            "type": "indirect"
        }
    ],

Workaround

Use non-canonical resource name e.g. GroupNode.builder().type("Group") instead of GroupNode.builder().type("direct"). This works as expected:

    "groups": [
        {
            "value": "9d66400a-149e-40da-b55a-96d9bb6100aa",
            "$ref": "http://localhost:8089/scim/v2/Groups/9d66400a-149e-40da-b55a-96d9bb6100aa",
            "display": "test group",
            "type": "Group"
        }
    ],

RFC7643 section 4.1.2

groups
A list of groups to which the user belongs, either through direct
membership, through nested groups, or dynamically calculated. The
values are meant to enable expression of common group-based or
role-based access control models, although no explicit
authorization model is defined. It is intended that the semantics
of group membership and any behavior or authorization granted as a
result of membership are defined by the service provider. The
canonical types "direct" and "indirect" are defined to describe
how the group membership was derived. Direct group membership
indicates that the user is directly associated with the group and
SHOULD indicate that clients may modify membership through the
"Group" resource. Indirect membership indicates that user
membership is transitive or dynamic and implies that clients
cannot modify indirect group membership through the "Group"
resource but MAY modify direct group membership through the
"Group" resource, which may influence indirect memberships. If
the SCIM service provider exposes a "Group" resource, the "value"
sub-attribute MUST be the "id", and the "$ref" sub-attribute must
be the URI of the corresponding "Group" resources 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".

Relevant code

See ResponseAttributeValidator#overrideEmptyReferenceNodeInComplex:

Error when removing group members via MS Azure

Hi all,

we're trying to implement a SCIM solution for our application with MS Azure as a user directory and are currently using SCIM SDK 1.9.2 (also tried 1.10.0). So far I'm really enthusiastic about the SCIM SDK (though OTOH the MS SCIM implementation often seems quite strange).

However, when I remove a user from a group on the Azure side the following SCIM message is sent:

PATCH /scim/Groups/2752513
{
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:PatchOp"
    ],
    "Operations": [
        {
            "op": "Remove",
            "path": "members",
            "value": [
                {
                    "value": "2392066"
                }
            ]
        }
    ]
}

(2752513 is my group id, 2392066 the id of the removed user). SCIM-SDK answers with

400 Bad Request
{
    "detail": "values must not be set for remove operation but was: {\"value\":\"2392066\"}",
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:Error"
    ],
    "status": 400,
    "scimType": "invalidValue"
}

Is this to be considered a bug on the Azure side? If yes, how is SCIM supposed to remove a group member (without sending the whole group)? Or am I missing something (or is there some kind of workaround)?

If this is an issue in SCIM SDK, of course I'm willing to help resolving this. So if you need some more data, just let me know.

Cheers

Thomas

Create/Patch Group with none-existing member causes NPE

Sample Request
POST request Url: https://keycloak/auth/realms/REALM/scim/v2/Groups
Parameters :
{
"displayName":"NeGroup",
"schemas":["urn:ietf:params:scim:schemas:core:2.0:Group"],
"members":[
{
"type":"User",
"value":"badid-265c-4c41-9ada-459f013ed126"
}],
"active":true}

A fix may be


 group.getMembers()
         .stream()
         .filter(groupMember -> groupMember.getType().isPresent()
                                && groupMember.getType().get().equalsIgnoreCase("User"))
         .forEach(groupMember -> {
           UserModel userMember = keycloakSession.users().getUserById(groupMember.getValue().get(), realmModel);
           // ADDED
           if (userMember == null)
           {
             throw new ResourceNotFoundException(String.format("An user with Id %s was not found",
                                                               groupMember.getValue().get()));
           }
           userMember.joinGroup(groupModel);
         });

Member class may have an unneeded subattribute

This might be me misreading 7643, but....

In 7643 section 2.4 we see this:

Multi-valued attributes contain a list of elements using the JSON
array format defined in Section 5 of [RFC7159]. Elements can be
either of the following:
o primitive values, or
o objects with a set of sub-attributes and values, using the JSON
object format defined in Section 4 of [RFC7159], in which case
they SHALL be considered to be complex attributes. As with
complex attributes, the order of sub-attributes is not
significant. The predefined sub-attributes listed in this section
can be used with multi-valued attribute objects, but these
sub-attributes MUST be used with the meanings defined here.

If not otherwise defined, the default set of sub-attributes for a
multi-valued attribute is as follows:

I'm focused on that last line "If not otherwise defined". However, 7643's urn:ietf:params:scim:schemas:core:2.0:Group does define the set of sub-attributes for the members, and it does not include primary. So it seems like the Java class Member should possibly not have a primary attribute?

Noticed this while trying to use the builder for member and got confused about what was meant by primary group, that led me to looking up the schema and noticing this was (possibly) a bit off

No authentication checking when authenticated is true but Authorization object is not present

With the below code snippet, I can go straight to the endpoints without providing any Authorization.

private void authenticateClient(UriInfos uriInfos, Authorization authorization)
{
ResourceType resourceType = uriInfos.getResourceType();
if (!resourceType.getFeatures().getAuthorization().isAuthenticated())
{
// no authentication required for this endpoint
return;
}
Optional.ofNullable(authorization).ifPresent(auth -> {
boolean isAuthenticated = auth.authenticate(uriInfos.getHttpHeaders(), uriInfos.getQueryParameters());
if (!isAuthenticated)
{
log.error("authentication has failed");
throw new UnauthenticatedException("not authenticated", getServiceProvider().getAuthenticationSchemes(),
auth.getRealm());
}
});
}

addtionalProperties is missing

I tried the following json schema and used "addtionalProperties" keyword to control the handling of extra stuff, but it seems no support for it

Couldn't find it in SchemaValidator.java

.
.
.
,
  "additionalProperties": false,
  "meta": {
    "resourceType": "Schema",
    "created": "2019-10-18T14:51:11+02:00",
    "lastModified": "2019-10-18T14:51:11+02:00",
    "location": "/Schemas/User"
  }
}

Using "active" flag on Azure

The Microsoft Azure SCIM implementation sends patch requests for users in the following form:

{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations":[{"op":"Add","path":"name.formatted","value":"My display name"}]}

Please note the "active" flag is not sent. This is translated to a call to updateResource(). When I'm asking for

if (user.isActive().isPresent() && !user.isActive().get()) {
    deactivateUser(...);
}

the call is executed (probably because in the User constructor active is not Optional and so has to have some value).

When (soft-)deleting a user on the Azure side, a similar call is sent with active=false. My application needs this information to deactivate users on the app side.

Naively, I'd expect user.isActive().isPresent() to return false for the first call. Am I getting this wrong? Or is there a better way to distinguish these cases?

Regards

Thomas

Request validation of string attributes set to null fails

If a string attribute is set to null, the request fails with type mismatch error. But it seems the same issue affects other attribute types too.

For example:

POST /scim/v2/Groups
Content-Type: application/scim+json

{
  "displayName": "my group",
  "externalId": null,
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:Group"
  ]
}

Fails with:

{
  "detail": "value of field with name 'urn:ietf:params:scim:schemas:core:2.0:Group:externalId' is not of type 'string' but of type: null",
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:Error"
  ],
  "status": 400
}

According to RFC 7643 it seems like an incorrect behavior and the request should be treated the same way as when the externalId attribute is omitted:

2.5. Unassigned and Null Values
Unassigned attributes, the null value, or an empty array (in the case of a multi-valued attribute) SHALL be considered to be equivalent in "state". Assigning an attribute with the value "null" or an empty array (in the case of multi-valued attributes) has the effect of making the attribute "unassigned". When a resource is expressed in JSON format, unassigned attributes, although they are defined in schema, MAY be omitted for compactness.

I believe the safest/easiest fix would to replace NullNode with null at the beginning of SchemaValidator#checkMetaAttributeOnDocument method. I tried that and it fixes the issue.

Would you be interested in a pull-request?

Disabling or configuring parallel stream processing

Do think it would be possible to expose a knob to disable using parallel stream processing using ForkJoinPool? Or maybe allow specifying a custom ForkJoinPool instead of using the default common pool? The reason is it may cause unpredictable load on system.

It seems to be only used in 3 places: parsing, filtering and sorting.

Feature: Resource Validation

I am going to add a new feature that should be an imitation of the java enterprise bean validation. Currently there is already a validation feature present but this shall be extended to add custom validations and it should go hand in hand with the schema-validation to create unified error messages that are pretty easily parseable on client side.

  • Reimplementation of SchemaValidator
    • The SchemaValidtor has some weeknesses beside its messy structure. It cannot validate more than a single nested attribute layer and thus returns erroneous results for the /Schemas endpoint. @see #143
    • In order to prevent the SchemaValidtor from getting even more complicated and unreadable when linking this feature to the SchemaValidator it needs a reimplementation

Group patch remove operation is not working

I am trying to perform patch operation on a group and request is standard request as given in https://docs.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
The payload is something like.

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{ "op": "Remove", "path": "members", "value": [{ "$ref": null, "value": "f648f8d5ea4e4cd38e9c" }] }] }
But the response is always some validation error
values must not be set for remove operation but was: {"$ref":null,"value":"f648f8d5ea4e4cd38e9c"}"
Please guide me here.

REPLACE with a filter doesn't behave as expected

Given a User with an emails payload of:

"emails": [
    {
      "value": "[email protected]",
      "type": "work",
      "primary": true
    },
    {
      "value": "[email protected]",
      "type": "home"
    },
    {
      "value": "[email protected]",
      "type": "private"
    },
    {
      "value": "[email protected]"
    }
  ],

When patching with the following request

{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ],
  "Operations": [
    {
      "path": "emails[value eq \"[email protected]\"]",
      "op": "replace",
      "value": "{\n  \"type\" : \"business\",\n  \"primary\" : false,\n  \"display\" : \"testEmail\",\n  \"value\" : \"[email protected]\",\n  \"$ref\" : \"ref\"\n}"
    }
  ]
}

I expect the outcome to be

"emails": [
    {
      "value": "[email protected]",
      "type": "business",
      "display": "testEmail",
      "primary": true
    },
    {
      "value": "[email protected]",
      "type": "home"
    },
    {
      "value": "[email protected]",
      "type": "private"
    },
    {
      "value": "[email protected]"
    }
  ],

But the actual outcome is

"emails": [
    {
      "value": "[email protected]",
      "type": "business",
      "primary": false
    }
  ],

According to the wiki, it says that this should only update matching nodes from the filter https://github.com/Captain-P-Goldfish/SCIM-SDK/wiki/Patching-resources#add-3

When using add with the same filter, a new item is just added to the array, they aren't "merged" together

Query parameter parsing may fail with unexpected error

If request query parameters contains just parameter name (without an =), exception is thrown due to negative index passed to substring method, resulting in HTTP 500 error.

There might be clients requiring such query parameter flags. For example Azure AD is using value-less query parameters to manage client behavior.

Patch on boolean attributes fails

When patching a boolean attribute, for example User:active, the request fails due to PatchOp validation. For example:

PATCH /Users/id
Content-Type: application/scim+json

{
  "Operations": [
    {
      "op": "replace",
      "path": "active",
      "value": true
    }
  ],
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ]
}

Fails with:

{
  "detail": "value of field with name 'urn:ietf:params:scim:api:messages:2.0:PatchOp:Operations.value' is not of type 'string' but of type: boolean",
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:Error"
  ],
  "status": 400
}

The PathOp declares the value attribute as string/multivalue. On the other hand, a json object in the value is (incorrectly?) accepted due the this check:

// class SchemaValidator
//   private JsonNode validateComplexAndArrayTypeAttribute(JsonNode document, SchemaAttribute schemaAttribute)
      if (isSimpleMultiValuedExpected && !isNodeSimpleMultiValued && !isNodeMultiValuedComplex)
      {
        ArrayNode arrayNode = new ScimArrayNode(schemaAttribute);
        arrayNode.add(document);
        return arrayNode;
      }

It seems for PatchOp value validation, the SCIM type system is not sufficient and some custom extension will be needed, because the value can have any type. Surprisingly, the following request succeeds, although the active has type boolean:

PATCH /Users/id
Content-Type: application/scim+json

{
  "Operations": [
    {
      "op": "replace",
      "path": "active",
      "value": "true"
    }
  ],
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:PatchOp"
  ]
}

Lots of allocations inside JsonHelper

Just reporting in case it slipped notice, feel free to close if the current approach is intentional

Inside JsonHelper, almost every method creates a new ObjectMapper. As JsonHelper is used for every API call, this seems to be a fair number of allocations. OM is not just a simple DTO object it has a few internal classes with non-trivial internal structures.

I have not tested this at all, but suspect you could see better performance (both req/sec and memory thrashing) by using a pool of OMs. Ideally, callers would be able to configure the pool size to match the server worker thread pool size.

Having a OM pool would prevent the repeated allocations. While (IIUC) OM is a thread-safe API (so you could just use a single OM), having a pool also prevents slowdowns from thread synchronization

Schema for displayName and Patch Operation

When I try to patch an user with the request

PATCH /auth/realms/REALM/scim/v2/Users/03146c7f-acab-4edd-a6b2-0648ad405eb9
 {
     "schemas":
       ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
     "Operations": [{
       "op":"replace",
       "path": "displayName",
       "value":     "Erna Elvira Ente"      		
     }]
}

I get the error

{
    "detail": "the attribute 'Operations.value' does not apply to its defined type. The received document node is of type 'STRING' but the schema defintion is as follows: \n\tmultivalued: true\n\ttype: STRING\nfor schema with id urn:ietf:params:scim:api:messages:2.0:PatchOp\n\"Erna Elvira Ente\"",
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:Error"
    ],
    "status": 400
}

whereas
"value": ["Erna Elvira Ente"]

works, but as far as I understand from rfc7643 displayName is a Singular Attribute.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.