diff --git a/docs/Authorization/AuthorizationChecks.md b/docs/Authorization/AuthorizationChecks.md index a7fbeb0..da9142d 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] @@ -144,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 @@ -162,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; } } @@ -187,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; } } ``` @@ -206,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 @@ -217,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; } } ``` @@ -236,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 @@ -247,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; } } ``` @@ -264,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; } } ``` 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