From 95ab407633364b77cb197f45680224e8422c1356 Mon Sep 17 00:00:00 2001 From: Manuel Fink Date: Mon, 27 Jul 2026 17:13:19 +0200 Subject: [PATCH 1/3] Java 4.4.1 CHANGELOG --- docs/Libraries/java/changelog.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/Libraries/java/changelog.md b/docs/Libraries/java/changelog.md index 9704b83..ab2e26e 100644 --- a/docs/Libraries/java/changelog.md +++ b/docs/Libraries/java/changelog.md @@ -2,6 +2,11 @@ ## Version 4 +### 4.4.1 + +- `SciAuthorizationsProvider` now populates any String/String[] `$user` attributes from the AMS schema based on token claims with the same name (e.g. `department` -> `$user.department`), not just the default `$user` attributes. +- The `AmsBundleLoader` now sets a timeout of 30s for all requests for improved resilience. + ### 4.4.0 - Feature: Synchronous startup readiness check in Spring Boot starters. The `AuthorizationManagementService` bean now blocks during application startup until the initial authorization bundle has been loaded. This is enabled by default; see [Startup Check](/Authorization/AuthorizationBundle#startup-check) for details, including how to adjust the timeout or opt out. The new utility method `AuthorizationManagementService.awaitReady(Duration)` simplifies synchronously awaiting the first bundle outside Spring. @@ -71,7 +76,7 @@ The CAP Spring Boot starter already wraps the standard `Authorizations` in a `Cd - Improved [Spring Security beans](/Libraries/java/spring-boot-ams#auto-configuration) for custom authorization checks - New [event logging API](/Libraries/java/ams-core#events-logging) for logging authorization events - Configuration options for [technical communication](/Authorization/TechnicalCommunication) scenarios via SAP Identity Service -- Customization of authorization strategy via `AuthorizationsProvider` interface, e.g. [custom user attribute injection](/Authorization/AuthorizationChecks#overriding-methods) +- Customization of authorization strategy via `AuthorizationsProvider` interface, e.g. [granting additional policies based on token attributes](/Authorization/AuthorizationChecks#overriding-methods) - JUnit 5+ extension for unit testing policy semantics without a full-blown integration test using [`ams-test`](/Libraries/java/ams-test). - Detailed [**DEBUG**](/Troubleshooting) logging about construction of `Authorizations` from token - **TRACE** logging of authorization bundle content and logic engine evaluations, showing how conditions are built and grounded with attribute input and how the predicates were evaluated From 40a99a7b563c15396198a1da0e491dd40b802725 Mon Sep 17 00:00:00 2001 From: Manuel Fink Date: Mon, 27 Jul 2026 17:19:33 +0200 Subject: [PATCH 2/3] document new $user string population --- docs/Authorization/AuthorizationChecks.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/Authorization/AuthorizationChecks.md b/docs/Authorization/AuthorizationChecks.md index a7fbeb0..55f4ee2 100644 --- a/docs/Authorization/AuthorizationChecks.md +++ b/docs/Authorization/AuthorizationChecks.md @@ -87,6 +87,12 @@ The `SciAuthorizationsProvider` (Node.js: `IdentityServiceAuthProvider`) is the using SAP Cloud Identity Services for authentication. It derives authorizations from SAP Identity Service token principals. +::: tip Custom `$user` attributes are populated automatically +Any `String` or `String[]` attribute declared in your AMS schema under `$user` is automatically populated from the token claim of the +same name (e.g. the `department` claim populates `$user.department`). You only need to +[customize the default input](#overriding-methods) for attributes whose claim has a different name or type, or if it is a computed value. +::: + ::: code-group ```js [Node.js] From 702b14c2bd41951c59a19dc92bb7e53a4a98e6dc Mon Sep 17 00:00:00 2001 From: Manuel Fink Date: Mon, 27 Jul 2026 17:19:48 +0200 Subject: [PATCH 3/3] adapt AuthorizationProvider example --- docs/Authorization/AuthorizationChecks.md | 111 +++++++++++++--------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/docs/Authorization/AuthorizationChecks.md b/docs/Authorization/AuthorizationChecks.md index 55f4ee2..da9142d 100644 --- a/docs/Authorization/AuthorizationChecks.md +++ b/docs/Authorization/AuthorizationChecks.md @@ -150,13 +150,15 @@ described on that page in detail. ##### Overriding Methods -You can override the `getUserAuthorizations`, -`getClientAuthorizations` and the methods for building default input for authorization checks to derive a [custom implementation](#custom-implementation) from the class if necessary. +You can override `getUserAuthorizations` and `getClientAuthorizations` to customize which policies apply for a +principal, or override the methods for building default input to add custom attribute values for authorization checks. +This lets you derive a [custom implementation](#custom-implementation) from the standard class while reusing its token +handling. -**Example: Customizing Default Input for Authorization Checks** +**Example: Granting Additional Policies Based on Token Attributes** -In this example, we override `getDefaultInput` to include a custom user attribute (`$user.division`) from a token -claim that is not included by default: +In this example, we override `getUserAuthorizations` to grant an additional policy based on a token claim: users from +the `Sales` department receive the `shopping.SalesDashboard` policy on top of their assigned policies. ::: code-group @@ -168,15 +170,15 @@ class CustomAuthProvider extends IdentityServiceAuthProvider { /** * @param {import("@sap/xssec").IdentityServiceSecurityContext} securityContext */ - getInput(securityContext) { - const defaultInput = super.getInput(securityContext); + async getUserAuthorizations(securityContext) { + const userAuthorizations = await super.getUserAuthorizations(securityContext); - const division = securityContext.token.payload.division; - if (division) { - defaultInput["$user.division"] = division; + // grant an additional policy to users from the Sales department + if (userAuthorizations && securityContext.token.payload.department === 'Sales') { + userAuthorizations.policySet.policies.push('shopping.SalesDashboard'); } - return defaultInput; + return userAuthorizations; } } @@ -193,15 +195,15 @@ class CustomAuthProvider extends IdentityServiceAuthProvider { /** * @param {import("@sap/xssec").IdentityServiceSecurityContext} securityContext */ - getInput(securityContext) { - const defaultInput = super.getInput(securityContext); + async getUserAuthorizations(securityContext) { + const userAuthorizations = await super.getUserAuthorizations(securityContext); - const division = securityContext.token.payload.division; - if (division) { - defaultInput["$user.division"] = division; + // grant an additional policy to users from the Sales department + if (userAuthorizations && securityContext.token.payload.department === 'Sales') { + userAuthorizations.policySet.policies.push('shopping.SalesDashboard'); } - return defaultInput; + return userAuthorizations; } } ``` @@ -212,9 +214,10 @@ import org.springframework.context.annotation.Bean; import com.sap.cloud.security.ams.core.SciAuthorizationsProvider; import com.sap.cloud.security.ams.api.*; import com.sap.cloud.security.ams.cap.api.*; -import com.sap.cloud.security.ams.api.expression.AttributeName; +import com.sap.cloud.security.ams.api.PolicyName; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; // Define in a @Configuration class @Bean @@ -223,16 +226,22 @@ public AuthorizationsProvider customAmsAuthProvider(Authoriza } public class CustomAuthorizationsProvider extends SciAuthorizationsProvider { - private static final AttributeName $USER_DIVISION = AttributeName.of("$user.division"); + private static final PolicyName SALES_DASHBOARD = PolicyName.of("shopping.SalesDashboard"); @Override - protected Map getDefaultInput(Principal principal) { - Map defaultInput = super.getDefaultInput(principal); - - principal.getClaimAsString("division") - .ifPresent(division -> defaultInput.put($USER_DIVISION, division)); + protected Authorizations getUserAuthorizations(Principal principal) { + Authorizations userAuthorizations = super.getUserAuthorizations(principal); + + // grant an additional policy to users from the Sales department + boolean isSales = principal.getClaimAsString("department") + .filter("Sales"::equals).isPresent(); + if (userAuthorizations != null && isSales) { + Set policies = new HashSet<>(userAuthorizations.getPolicies()); + policies.add(SALES_DASHBOARD); + userAuthorizations.setPolicies(policies); + } - return defaultInput; + return userAuthorizations; } } ``` @@ -242,9 +251,10 @@ import org.springframework.context.annotation.Bean; import com.sap.cloud.security.ams.core.SciAuthorizationsProvider; import com.sap.cloud.security.ams.api.*; -import com.sap.cloud.security.ams.api.expression.AttributeName; +import com.sap.cloud.security.ams.api.PolicyName; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; // Define in a @Configuration class @Bean @@ -253,16 +263,22 @@ public AuthorizationsProvider customAmsAuthProvider(Authorizatio } public class CustomAuthorizationsProvider extends SciAuthorizationsProvider { - private static final AttributeName $USER_DIVISION = AttributeName.of("$user.division"); + private static final PolicyName SALES_DASHBOARD = PolicyName.of("shopping.SalesDashboard"); @Override - protected Map getDefaultInput(Principal principal) { - Map defaultInput = super.getDefaultInput(principal); - - principal.getClaimAsString("division") - .ifPresent(division -> defaultInput.put($USER_DIVISION, division)); + protected Authorizations getUserAuthorizations(Principal principal) { + Authorizations userAuthorizations = super.getUserAuthorizations(principal); + + // grant an additional policy to users from the Sales department + boolean isSales = principal.getClaimAsString("department") + .filter("Sales"::equals).isPresent(); + if (userAuthorizations != null && isSales) { + Set policies = new HashSet<>(userAuthorizations.getPolicies()); + policies.add(SALES_DASHBOARD); + userAuthorizations.setPolicies(policies); + } - return defaultInput; + return userAuthorizations; } } ``` @@ -270,21 +286,28 @@ public class CustomAuthorizationsProvider extends SciAuthorizationsProvider { - private static final AttributeName $USER_DIVISION = AttributeName.of("$user.division"); + private static final PolicyName SALES_DASHBOARD = PolicyName.of("shopping.SalesDashboard"); @Override - protected Map getDefaultInput(Principal principal) { - Map defaultInput = super.getDefaultInput(principal); - - principal.getClaimAsString("division") - .ifPresent(division -> defaultInput.put($USER_DIVISION, division)); + protected Authorizations getUserAuthorizations(Principal principal) { + Authorizations userAuthorizations = super.getUserAuthorizations(principal); + + // grant an additional policy to users from the Sales department + boolean isSales = principal.getClaimAsString("department") + .filter("Sales"::equals).isPresent(); + if (userAuthorizations != null && isSales) { + Set policies = new HashSet<>(userAuthorizations.getPolicies()); + policies.add(SALES_DASHBOARD); + userAuthorizations.setPolicies(policies); + } - return defaultInput; + return userAuthorizations; } } ```