Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class ScimRequestContext {

Expand Down Expand Up @@ -163,4 +164,55 @@ public String toString() {
public static ScimRequestContext empty() {
return new ScimRequestContext();
}

/**
* Returns the fully qualified SCIM attribute names the client explicitly requested
* via the {@code attributes} query parameter, or an empty set if no specific
* attributes were requested (meaning all attributes should be returned).
*
* <p>All attribute names are returned in their fully qualified form
* (e.g., {@code urn:ietf:params:scim:schemas:core:2.0:User:userName}).
* The server layer normalizes unqualified attribute references before they
* reach the repository, so all references in this context carry their schema URN.</p>
*
* <p>Repository implementations MAY use this to optimize backend queries
* (e.g., selecting specific LDAP attributes or database columns). The SCIM server
* layer always applies attribute filtering post-retrieval as a safety net, so
* repositories that ignore this hint will still produce correct results.</p>
*
* @return set of fully qualified SCIM attribute names, or empty for "all"
* @see #getIncludedAttributes()
*/
public Set<String> getIncludedAttributeNames() {
if (includedAttributes == null) {
return Set.of();
}
return includedAttributes.stream()
.map(AttributeReference::getFullyQualifiedAttributeName)
.collect(Collectors.toUnmodifiableSet());
}

/**
* Returns the fully qualified SCIM attribute names the client explicitly excluded
* via the {@code excludedAttributes} query parameter, or an empty set if none
* were excluded.
*
* <p>Attribute names follow the same fully qualified rules as
* {@link #getIncludedAttributeNames()}.</p>
*
* <p>Repository implementations MAY use this to optimize backend queries by
* omitting excluded attributes from the fetch. The SCIM server layer always
* applies attribute filtering post-retrieval as a safety net.</p>
*
* @return set of fully qualified excluded SCIM attribute names, or empty for "none excluded"
* @see #getExcludedAttributes()
*/
public Set<String> getExcludedAttributeNames() {
if (excludedAttributes == null) {
return Set.of();
}
return excludedAttributes.stream()
.map(AttributeReference::getFullyQualifiedAttributeName)
.collect(Collectors.toUnmodifiableSet());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at

* http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.directory.scim.core.repository;

import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
import org.junit.jupiter.api.Test;

import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class ScimRequestContextTest {

static final String USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
static final String ENTERPRISE_SCHEMA = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";

// --- getIncludedAttributeNames ---

@Test
void getIncludedAttributeNames_returnsFullyQualifiedNames() {
// Server layer normalizes references before they reach the repository,
// so all references should have URNs by this point
AttributeReference ref1 = new AttributeReference(USER_SCHEMA + ":userName");
AttributeReference ref2 = new AttributeReference(USER_SCHEMA + ":emails");
ScimRequestContext ctx = new ScimRequestContext(Set.of(ref1, ref2), Set.of());

Set<String> names = ctx.getIncludedAttributeNames();

assertThat(names).containsExactlyInAnyOrder(
USER_SCHEMA + ":userName",
USER_SCHEMA + ":emails"
);
}

@Test
void getIncludedAttributeNames_extensionAttributesPreserveTheirUrn() {
AttributeReference coreAttr = new AttributeReference(USER_SCHEMA + ":userName");
AttributeReference extensionAttr = new AttributeReference(ENTERPRISE_SCHEMA + ":department");
ScimRequestContext ctx = new ScimRequestContext(Set.of(coreAttr, extensionAttr), Set.of());

Set<String> names = ctx.getIncludedAttributeNames();

assertThat(names).containsExactlyInAnyOrder(
USER_SCHEMA + ":userName",
ENTERPRISE_SCHEMA + ":department"
);
}

@Test
void getIncludedAttributeNames_subAttributeIncluded() {
AttributeReference ref = new AttributeReference(USER_SCHEMA + ":name.givenName");
ScimRequestContext ctx = new ScimRequestContext(Set.of(ref), Set.of());

assertThat(ctx.getIncludedAttributeNames())
.containsExactly(USER_SCHEMA + ":name.givenName");
}

@Test
void getIncludedAttributeNames_emptyReturnsEmptySet() {
ScimRequestContext ctx = new ScimRequestContext(Set.of(), Set.of());
assertThat(ctx.getIncludedAttributeNames()).isEmpty();
}

@Test
void getIncludedAttributeNames_nullReturnsEmptySet() {
ScimRequestContext ctx = new ScimRequestContext();
ctx.setIncludedAttributes(null);
assertThat(ctx.getIncludedAttributeNames()).isEmpty();
}

// --- getExcludedAttributeNames ---

@Test
void getExcludedAttributeNames_returnsFullyQualifiedNames() {
AttributeReference ref = new AttributeReference(USER_SCHEMA + ":password");
ScimRequestContext ctx = new ScimRequestContext(Set.of(), Set.of(ref));

assertThat(ctx.getExcludedAttributeNames())
.containsExactly(USER_SCHEMA + ":password");
}

@Test
void getExcludedAttributeNames_emptyReturnsEmptySet() {
ScimRequestContext ctx = new ScimRequestContext(Set.of(), Set.of());
assertThat(ctx.getExcludedAttributeNames()).isEmpty();
}

// --- empty() ---

@Test
void empty_returnsEmptySetsForBothIncludedAndExcluded() {
ScimRequestContext ctx = ScimRequestContext.empty();
assertThat(ctx.getIncludedAttributeNames()).isEmpty();
assertThat(ctx.getExcludedAttributeNames()).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Optional;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.enterprise.inject.spi.CDI;
import jakarta.ws.rs.core.*;
Expand Down Expand Up @@ -57,6 +58,8 @@
import org.apache.directory.scim.protocol.data.SearchRequest;
import org.apache.directory.scim.spec.filter.FilterResponse;
import org.apache.directory.scim.spec.filter.Filter;
import org.apache.directory.scim.spec.schema.ResourceType;
import org.apache.directory.scim.spec.schema.Schema;
import org.apache.directory.scim.spec.filter.SortOrder;
import org.apache.directory.scim.core.repository.ScimRequestContext;
import org.apache.directory.scim.spec.resources.ScimResource;
Expand All @@ -67,6 +70,8 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple

private final RepositoryRegistry repositoryRegistry;

private final SchemaRegistry schemaRegistry;

private final AttributeUtil attributeUtil;

private final Class<T> resourceClass;
Expand All @@ -83,6 +88,7 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
HttpHeaders headers;

public BaseResourceTypeResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry, Class<T> resourceClass) {
this.schemaRegistry = schemaRegistry;
this.repositoryRegistry = repositoryRegistry;
this.resourceClass = resourceClass;
this.attributeUtil = new AttributeUtil(schemaRegistry);
Expand All @@ -109,6 +115,8 @@ public Response getById(String id, AttributeReferenceListWrapper attributes, Att
Set<AttributeReference> attributeReferences = AttributeReferenceListWrapper.getAttributeReferences(attributes);
Set<AttributeReference> excludedAttributeReferences = AttributeReferenceListWrapper.getAttributeReferences(excludedAttributes);
validateAttributes(attributeReferences, excludedAttributeReferences);
attributeReferences = qualifyReferences(attributeReferences);
excludedAttributeReferences = qualifyReferences(excludedAttributeReferences);

Repository<T> repository = getRepositoryInternal();

Expand Down Expand Up @@ -172,6 +180,8 @@ public Response create(T resource, AttributeReferenceListWrapper attributes, Att
Set<AttributeReference> attributeReferences = AttributeReferenceListWrapper.getAttributeReferences(attributes);
Set<AttributeReference> excludedAttributeReferences = AttributeReferenceListWrapper.getAttributeReferences(excludedAttributes);
validateAttributes(attributeReferences, excludedAttributeReferences);
attributeReferences = qualifyReferences(attributeReferences);
excludedAttributeReferences = qualifyReferences(excludedAttributeReferences);

T created = repository.create(resource, new ScimRequestContext(attributeReferences, excludedAttributeReferences, null, null, null));

Expand Down Expand Up @@ -207,6 +217,8 @@ public Response find(SearchRequest request) throws ScimException, ResourceExcept
Set<AttributeReference> excludedAttributeReferences = Optional.ofNullable(request.getExcludedAttributes())
.orElse(Collections.emptySet());
validateAttributes(attributeReferences, excludedAttributeReferences);
attributeReferences = qualifyReferences(attributeReferences);
excludedAttributeReferences = qualifyReferences(excludedAttributeReferences);

Filter filter = request.getFilter();
ScimRequestContext requestContext = new ScimRequestContext(attributeReferences, excludedAttributeReferences, request.getPageRequest(), request.getSortRequest(), null);
Expand Down Expand Up @@ -273,6 +285,8 @@ private Response update(AttributeReferenceListWrapper attributes, AttributeRefer
Set<AttributeReference> attributeReferences = AttributeReferenceListWrapper.getAttributeReferences(attributes);
Set<AttributeReference> excludedAttributeReferences = AttributeReferenceListWrapper.getAttributeReferences(excludedAttributes);
validateAttributes(attributeReferences, excludedAttributeReferences);
attributeReferences = qualifyReferences(attributeReferences);
excludedAttributeReferences = qualifyReferences(excludedAttributeReferences);

String requestEtag = headers.getHeaderString("If-Match");
Set<ETag> etags = EtagParser.parseETag(requestEtag);
Expand Down Expand Up @@ -368,6 +382,55 @@ private EntityTag fromVersion(ScimResource resource) {
return null;
}

/**
* Normalizes {@link AttributeReference} objects to their fully qualified form by
* resolving unqualified attribute names against the base schema and extension schemas
* for this resource type. References that already have a URN are returned as-is.
*/
private Set<AttributeReference> qualifyReferences(Set<AttributeReference> refs) {
if (refs == null || refs.isEmpty()) {
return refs;
}

org.apache.directory.scim.spec.annotation.ScimResourceType annotation =
resourceClass.getAnnotation(org.apache.directory.scim.spec.annotation.ScimResourceType.class);
if (annotation == null) {
return refs;
}

ResourceType resourceType = schemaRegistry.getResourceType(annotation.name());
if (resourceType == null) {
return refs;
}

Schema baseSchema = schemaRegistry.getSchema(resourceType.getSchemaUrn());

return refs.stream()
.map(ref -> {
if (ref.isFullyQualified()) {
return ref;
}

// Try base schema first
if (baseSchema != null && baseSchema.getAttribute(ref.getAttributeName()) != null) {
return new AttributeReference(baseSchema.getId(), ref.getFullAttributeName());
}

// Try extension schemas
if (resourceType.getSchemaExtensions() != null) {
for (ResourceType.SchemaExtensionConfiguration ext : resourceType.getSchemaExtensions()) {
Schema extSchema = schemaRegistry.getSchema(ext.getSchemaUrn());
if (extSchema != null && extSchema.getAttribute(ref.getAttributeName()) != null) {
return new AttributeReference(extSchema.getId(), ref.getFullAttributeName());
}
}
}

return ref; // best-effort: leave unresolved
})
.collect(Collectors.toUnmodifiableSet());
}

@FunctionalInterface
private interface UpdateFunction<T extends ScimResource> {
T update(ScimRequestContext requestContext, Repository<T> repository) throws ResourceException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,28 @@ public boolean hasSubAttribute() {
return subAttributeName != null;
}

public boolean hasUrn() {
/**
* Returns {@code true} if this reference includes a schema URN prefix,
* making it unambiguous across schemas.
*
* <p>Extension attributes are always fully qualified since their names
* are only meaningful with the schema URN. Core attributes may or may
* not be, depending on how the client sent them.</p>
*
* @return {@code true} if the URN is present
*/
public boolean isFullyQualified() {
return urn != null;
}

/**
* @deprecated Use {@link #isFullyQualified()} instead.
*/
@Deprecated
public boolean hasUrn() {
return isFullyQualified();
}

public String toString() {
return (this.urn != null ? this.urn + ":" : "") + this.attributeName + (this.subAttributeName != null ? "." + this.subAttributeName : "");
}
Expand Down
Loading