Skip to content

spring-ams

Authorization Management Service Client Library for Spring Boot Applications

The client library for the Authorization Management Service (AMS) enables Spring Boot 3 applications to make authorization decisions based on user policies. Users are identified using JWT bearer tokens provided by either the SAP Business Technology Platform xsuaa or identity service. To specify these policies, the application must use a Data Control Language (DCL). Administrators can customize and combine policies and assign them to users. The library also supports controlling access to specific actions and resources, and allows for easy implementation of instance-based access control by specifying resource-specific attributes such as "cost center" or "country code".

Supported environments

  • Cloud Foundry
  • Kyma

Disclaimer on API usage

This documentation provides information that might be useful when you use the Authorization Management Service. We try to ensure that future versions of the APIs are backwards compatible with the immediately preceding version. This is also true for the API that is exposed with com.sap.cloud.security.ams.dcl.client package and its subpackages.
Check the release notes or the release notes for updates about new features and API modifications.

Requirements

  • Java 17
  • Spring Boot 3.4.0-SNAPSHOT or later
  • Spring Security 6.0.0 or later

Setup

Maven dependencies

When using Spring Security OAuth 2.0 Resource Server for authentication in your Spring Boot application, you can leverage the provided Spring Boot Starter:

xml
<dependency>
    <groupId>com.sap.cloud.security.ams.client</groupId>
    <artifactId>spring-boot-starter-ams-resourceserver</artifactId>
    <version>${sap.cloud.security.ams.client.version}</version>
</dependency>

It's possible to disable all auto configurations using com.sap.cloud.security.ams.auto=false.

Base DCL

Every business application must describe its security model in dcl language.
To describe the names and types of attributes, a file named schema.dcl and must be located in the root folder, for example, in src/main/resources.

SCHEMA {
    salesOrder: {
            type: number
    },
    CountryCode: String
}

Furthermore, the security model comprises rules and policies that can be used or enriched by the administrator to grant users permissions. They can be organized in packages.

POLICY readAll {
    GRANT read ON * WHERE CountryCode IS NOT RESTRICTED;
}

POLICY readAll_Europe {
    USE readAll RESTRICT CountryCode IN ('AT', 'BE', 'BG', ...);
}

POLICY adminAllSales {
    GRANT read, write, delete, activate ON salesOrders, salesOrderItems;
}

POLICY anyActionOnSales {
    GRANT * ON salesOrders, salesOrderItems;
}

POLICY readSalesOrders_Type {
    GRANT read ON salesOrders WHERE salesOrder.type BETWEEN 100 AND 500;
}

Usage

This section explains how to configure the health check and how to enforce authorization decisions by integrating the Authorization Management Service with Spring Security.

Authorization decision point readiness evaluation

Initial application startup check

Initial startup check checks whether the bundle containing the defined policies is accessible by the application during the application initialization step. This functionality is provided by PolicyDecisionPoint and upon application startup will perform a periodical health checks until OK state is reached or until health check times out with the max default timeout of 30s, then it will throw an PolicyEngineCommunicationException error. Startup check behavior can be configured by the following parameters:

  • com.sap.dcl.client.startupHealthCheckTimeout - maximum waiting time for startup check in seconds
  • com.sap.dcl.client.failOnStartupCheck - Boolean whether the application should start, if policies are not available. This means that PolicyEngineCommunicationException won't be thrown

These parameters can be configured as:

  • system properties: System.setProperty("com.sap.dcl.client.startupHealthCheckTimeout", "0")
  • spring properties: com.sap.dcl.client.startupHealthCheckTimeout:0 💡 spring properties only works with autoconfiguration enabled
  • maven command line argument: -Dcom.sap.dcl.client.startupHealthCheckTimeout=0
  • environment variable

and they take the following precedence:

  1. spring properties
  2. system properties
  3. maven command line argument
  4. environment variable

Configure endpoint with health check

For web applications, Cloud Foundry recommends setting the health check type to http instead of a simple port check (default). That means you can configure an endpoint that returns http status code HTTP 200 if application is healthy. This check should include an availability check of the policy decision runtime:

java
import com.sap.cloud.security.ams.dcl.client.pdp.HealthState;

@SpringBootApplication
@RestController
public class DemoApplication {

    @Autowired
    PolicyDecisionPoint policyDecisionPoint;

    @GetMapping(value = "/health")
    @ResponseStatus(OK)
    public String healthCheck() {
        if (policyDecisionPoint.getHealthStatus.getHealthState == HealthState.OK){
            return "{\"status\":\"UP\"}";
        }
        throw new HttpServerErrorException(SERVICE_UNAVAILABLE, "Policy engine isn't reachable.");
    }
}

Find further documentation on Cloud Foundry Documentation: Using App Health Checks.

Advanced health status check

PolicyDecisionPoint exposes a getHealthStatus() method that provides fine granular policy decision point health status information together with information of each policy bundle status.

java
import com.sap.cloud.security.ams.dcl.client.pdp.HealthState;

public class DemoApplication {
    @Autowired
    PolicyDecisionPoint policyDecisionPoint;

    public String healthCheck() {
        PolicyDecisionPointHealthStatus pdpStatus = policyDecisionPoint.getHealthStatus();
        if (pdpStatus.getHealthState() == HealthState.UPDATE_FAILED) { //Checks whether bundle update was successful
            Map<String, BundleStatus> bundles = pdpStatus.getBundleStatus();
            bundles.entrySet().forEach(b -> 
            {
                if (b.getValue().hasBundleError()) {
                    //Process the failing bundle update ...
                    LOG.error("{} bundle doesn't contain latest updates The last successful bundle activation was at {}", b.getKey, b.getValue().getLastSuccessfulActivation());
                }
            });
        }
    }
}

Access control on request level

This section explains how to configure authentication and authorization checks for REST APIs exposed by your servlet based Spring application. You are going to configure the SecurityFilterChain for HttpServletRequest.

By default, Spring Security’s authorization requires all requests to be authenticated. We can configure Spring Security to have different rules by adding more rules in order of precedence.

In addition to the common built-in Spring Security authorization rules like hasAuthority(String authority) this library provides another one, namely: hasBaseAuthority(String action, String resource). With hasBaseAuthority security expression you can easily specify "start-stop-conditions". For instance, you can validate if the user's principal has the necessary authorization to execute a specific action on a certain resource. It's also allowed to use a '*' to be less restrictive. In case the authorization check fails, an exception of type AccessDeniedException is thrown.

ℹ️ If a user only has limited access to a resource, for example, the user can only 'read' 'sales orders' within their own country, this restriction isn't accounted for with hasBaseAuthority('read', 'salesOrders').

ℹ️ .access("hasBaseAuthority('read', '*')") is different from the .hasAuthority("read") policy. While both allow 'read' access, the .hasAuthority("read") policy may currently contain or could in the future have attribute-level constraints.

Example Security Configuration

java
@Configuration
@PropertySource(factory = IdentityServicesPropertySourceFactory.class, ignoreResourceNotFound = true, value = { "" })
public class SecurityConfiguration {

    @Autowired
    SecurityExpressionHandler<FilterInvocation> amsWebExpressionHandler;

    @Autowired
  SecurityExpressionHandler<RequestAuthorizationContext> amsHttpExpressionHandler;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    WebExpressionAuthorizationManager hasBaseAuthority = new WebExpressionAuthorizationManager(
        "hasBaseAuthority('read', 'salesOrders')");
    hasBaseAuthority.setExpressionHandler(amsHttpExpressionHandler);
    http.authorizeHttpRequests(authz -> {
          authz.requestMatchers(GET, "/health", "/", "/uiurl")
              .permitAll();
          authz.requestMatchers(GET, "/salesOrders/**")
              .access(hasBaseAuthority);
          authz.requestMatchers(GET, "/authorized")
              .hasAuthority("view").anyRequest().authenticated();
        })
        .oauth2ResourceServer(oauth2 ->
            oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(amsAuthenticationConverter)));
    return http.build();
  }

}

See Spring Authorization Documentation for in-depth understanding and comprehensive details about its usage and implementation.

Access control on method level

PolicyDecisionPointSecurityExpression extends the common built-in Spring Security expressions. This can be used to control access on method level using Method Security.

❗ In Spring Boot versions >= 6.1, the Java compiler must be configured with the -parameters flag to use the expressions below that refer to method parameters.

Overview of Spring Security expressions for Authorization Management

methodDescriptionExample
boolean hasAuthority(String action)returns true if the authenticated user has the permission to perform the given action. This check can only be applied for very trivial policies, for example, POLICY myPolicy { GRANT action ON *; }.
boolean hasBaseAuthority(String action, String resource)returns true if the authenticated user has in principal the permission to perform a given action on a given resource. It's also allowed to use a '*' to be less restrictive.
boolean forAction(String action, String... attributes)returns true if the user is authorized to perform a dedicated action. The resource in the DCL rules must be declared as * for this case. If the DCL rules depend on attributes that aren't automatically filled (either by default or an AttributesProvider) then their values need to be provided as attributes arguments.*Has user read access to any resources?
@PreAuthorize("forAction('read')")
boolean forResource(String resource, String... attributes)returns true if the user is authorized to access the given resource. The action in the DCL rules must be declared as * for this case. If the DCL rules depend on attributes that aren't automatically filled (either by default or an AttributesProvider) then their values must be provided as attributes arguments.*Has user any access to salesOrder resource?
@PreAuthorize("forResource('salesOrders')")
boolean forResourceAction(String resource, String action, String... attributes)returns true if the user is authorized to perform a dedicated action on the given resource. If the DCL rules depend on attributes that aren't automatically filled (either by default or an AttributesProvider), their values must be provided as attributes arguments. *Has user read access to salesOrders resource?
@PreAuthorize("forResourceAction('salesOrders', 'read')")

Has user read access to salesOrders resource with CountryCode = "DE" and salesOrder.type = 247?

Consider a RESTful application that looks up a salesOrder by country and type from the URL path in the format, for example, /readByCountryAndType/{countryCode}/{type}.
You are able to refer to path variables within a URL:
@PreAuthorize("forResourceAction('salesOrders', 'read', 'CountryCode:string=' + #countryCode, 'salesOrder.type:number=' + #type)")
@GetMapping(value = "/readByCountryAndType/{countryCode}/{type}")
public String readSelectedSalesOrder(@PathVariable String countryCode, @PathVariable String type){}

💡 * the attributes varargs shall have the format: <attribute name>:<string|number| boolean>=<attribute value>. Currently, these data types are supported: string, number and boolean.

Authorization hybrid mode - XSUAA and Authorization Management authorization checks

The hybrid authorization mode provides a way to use XSUAA tokens and their scopes, as well as SAP Cloud Identity Services tokens with authorization policies.

There are 2 ways to enforce authorization in the hybrid mode:

  1. Xsuaa scope check: this option performs as before for XSUAA issued tokens and authorizations are defined with a help of hasAuthority("Role") method provided by Spring Security. This option is enabled by default within an autoconfiguration ResourceServerAuthConverterAutoConfiguration.java , if the service configuration has an XSUAA binding.
  2. AMS Policy Authorization: this option disables the XSUAA scope check, requiring the Spring property sap.security.hybrid.xsuaa.scope-check.enabled to be set to false. Authorization is carried out through PolicyDecisionPoint and enforced by AMS Spring Security Expressions. However, for this mode to work, XSUAA Scopes must be converted into authorization policies. This can be done with the help of AttributesProccessor, but please note this conversion process is not provided by this library.

Additional Information

How to use PolicyDecisionPoint API

java
@Autowired
private PolicyDecisionPoint policyDecisionPoint;

    ...
    Principal principal=Principal.create();
    Attributes attributes=principal.getAttributes().setAction("read");
    boolean isReadAllowed=policyDecisionPoint.allow(attributes);

See here for PolicyDecisionPoint API documentation.

Consider also limitation of api liability.

Pre-fill user-attributes from OIDC token

You can simply create a Principal instance within the same thread. Use Principal.create() to derive the principal information from the OIDC token, which is stored in SecurityContextHolder.

Alternatively, you can also build the Principal using the PrincipalBuilder.
Example: PrincipalBuilder.create("the-zone-id", "the-user-id").build();

Testing

You can find the test utilities spring-ams-test-starter module.

Audit Logging

Please check out this documentation.

Troubleshooting

For troubleshooting purposes check here.

Set DEBUG log level

First, configure the Debug log level for Spring Framework Web and all Security related libs. This can be done as part of your application.yml or application.properties file.

yaml
logging.level:
  com.sap.cloud.security: DEBUG       # set SAP-class loggers to DEBUG; set to ERROR for production setup
  org.springframework: ERROR          # set to DEBUG to see all beans loaded and auto-config conditions met
  org.springframework.security: DEBUG # set to ERROR for production setup
  org.springframework.web: DEBUG      # set to ERROR for production setup

Next, if you like to see what different filters are applied to particular request, set the debug flag to true in @EnableWebSecurity annotation:

java
@Configuration
@EnableWebSecurity(debug = true) // TODO "debug" may include sensitive information. Don't use it in a production system!
public class SecurityConfiguration {
   ...
}

💡 Remember to restage your application for the changes to take effect.

Sample