Search

Authorization and Access Control

Authorization means restricting access to data by adding respective declarations to CDS models, which are then enforced in service implementations. By adding such declarations, we essentially revoke all default access and then grant individual privileges.

Content

Restrictions

Restrict access to services by adding @requires or @restrict annotations to services, entities, or functions as shown in the following example:

services-with-auth.cds

service ReviewsService @(requires: 'identified-user'){
  /*...*/
} 

service CustomerService @(requires: 'authenticated-user'){
  entity Orders @(restrict: [ 
    { grant: ['READ','WRITE'], to: 'admin' },
    { grant: 'READ', where: 'buyer = $user' },
  ]){/*...*/}
  entity Approval @(restrict: [
    { grant: 'WRITE', where: '$user.level > 2' } 
  ]){/*...*/}
}

The annotation:

@requires: is just a convenience shortcut, which is more concise and comprehensible. For example: @requires: 'identified-user' is equivalent to: @restrict: [{ grant:'*', to: 'identified-user' }]

Privileges

You grant privileges within @restrict: annotations.

Supported properties:

Within where: conditions, use names prefixed with $ to refer to request attributes or attributes in the JWT token. For example, $user, $user.foo, or $foo.

Within where: conditions CQL syntax is allowed. Initially, and and or is supported (no subselects).

It’s possible to use where: and to: together in one grant: clause.

References to entity elements in where: conditions establish instance-based authorizations. The filter conditions usually relate attributes of protected entities to user or request attributes.

User Roles

User roles can be freely chosen and are local to the current application. For example, in the above sample, the role name admin reflects the assumed role of bookshop administrators.

Note: CDS-based authorization deliberately refrains from using technical concepts such as scopes as in OAuth in favor of user roles, which are closer to the conceptual domain of business applications. This also results in much smaller JWT tokens.

Pseudo Roles

The following are predefined pseudo roles:

identified-user is a superset of authenticated-user and any is a superset of all others.

System Users

The pseudo role system-user allows you to create specific authorizations for access through technical users. In general, technical users have different authorizations. The calls often run in a “privileged” mode without any restrictions on instance level. This mode allows you to create specific authorizations for service calls. These calls read some additional data and don’t have to be further restricted, as the call is already restricted to a certain entity by the caller. Also “authenticated-user” evaluates to “false” in case of a technical system call as there’s no named user available.

For example, an address for a business partner is required. In this scenario, there are two calls to the service:

  1. By the authenticated user to the business partner entity
  2. By a technical user reading additional data, for example, address

The important instance restriction can now be defined on the business partner, and not on the address. This instance restriction can be enforced using an authenticated user.

Using User Attributes

Use the @restrict: annotation to access user attributes that come along with the authenticated user. The JWT token has to transport the authorized scopes. This can be done through $user.attributename. $user.attributename contains a list of attribute values that are assigned to the user. It can be used to restrict access to instances having an attribute value that appears in the list. An empty list means that the user has no restriction for this attribute. Example $user.country = 'DE' can be used to restrict the users access to all records having country code ‘DE’.

If a user attribute used in a where clause is not provided in JWT, then the corresponding attribute comparison in the where clause is evaluated to TRUE. For example, the following where condition is always TRUE:

In the next example the first part of the AND statement is evaluated to TRUE and therefore the where condition is equivalent to $user.country = ‘‘DE’’.

Specific hard-coded attributes are:

Limitation for WHERE Clauses

CREATE Operation and Custom Operations

As of today WHERE clause restrictions are supported on data level for CREATE operations only with static checks. The same holds for actions and functions.

Example for supported statements

Example for NOT supported statements

Association Paths

Association path definitions in WHERE clauses aren’t yet supported but we’ll support “one step” paths in the future.

Example:

Examples and Best Practices

The following examples show how different combinations of restrictions and privileges behave.

General remarks for all examples:

Best Practices

1. List of Roles After @requires or @restrict

service ReviewService @(requires: [ 'Reviewer', 'Customer' ]){
  /*...*/
}

In this example, you can see that the access to the ReviewService is restricted to users with Reviewer or Customer roles.

2. Several GRANTS in @requires or @restrict

If there are multiple grants in @requires or @restrict, they’re combined logically using OR.

entity Orders @(restrict: [
    { grant: ['READ','WRITE'], to: 'admin' },
    { grant: 'READ', where: 'buyer = $user' },
  ]){/*...*/}

In this example, admin users are granted READ and WRITE access to the Orders entity. If the user is also the buyer, then READ access to the Orders entity is granted to the user.

3. GRANT in @requires or @restrict Together with a WHERE Clause

Both conditions, to: and where:, from this statement have to be true. For example:

entity Orders @(restrict: [
    { grant: ['WRITE'], to: 'admin', where: 'country = $user.country'},
  ]){/*...*/}

In this case WRITE access is allowed for ‘admin’ users who are in the same country as the country where the order was placed.

4. Additional Restricted Entities Within a Restricted Service

If restrictions are defined at both, the service and entity level, they’re logically combined using AND. The restrictions on the entity are valid only within this specific service.

Service CustomerService @(requires: 'authenticated-user'){
  entity Orders @(restrict: [
    { grant: ['READ','WRITE'], to: 'admin' },
  entity Approval @(restrict: [
    { grant: 'WRITE', to: 'approve' }
  ]){/*...*/}

This results in the following access matrix:

Role
gives access to
‘admin’
‘approve’
‘authenticated-user’
‘admin’
‘authenticated-user’
‘approve’
‘authenticated-user’
‘authenticated-user’
‘orders’ YES YES NO NO
‘approval’ YES NO YES NO

5. Entities Restricted Generally (Outside of a Specific Service)

Restrictions that are defined globally at the entity level are valid in all services where the entity appears. It’s possible to overwrite the restrictions on the same entity within a service. The restriction on service level then completely replaces the inherited restriction from the entity.

Entity “books”

define entity Books @(restrict: [
    { grant: 'READ', to: 'buyer' },
  ])
{
  key ID : Integer;
  title  : String;
  author : Association to Authors;
  stock  : Integer;
}

Buyer Service:

service BuyerService @(requires: 'authenticated-user'){
  entity Books
         {/*...*/}
}

Customer Service:

service CustomerService @(requires: 'authenticated-user'){
  entity Books @(restrict: [
    { grant: ['READ'], to: 'admin'},
  ]){/*...*/}
}
Role has access to… buyer
admin
authenticated-user
buyer
authenticated-user
admin
authenticated-user
authenticated-user
Books in BuyerService YES YES NO NO
Books in CustomerService YES NO YES NO

6. Restrictions on ACTIONS and FUNCTIONS

(Restrictions on ACTIONS and FUNCTIONS are currently only supported in the Node.js runtime.)

Authorizations can be defined on bound and unbound actions / functions. Both @restrict: and @requires: are supported. However, WHERE clauses are only supported with simple static checks - see Limitation for WHERE clauses.

Bound Actions and Functions

service CatalogService {
  entity Products as projection on data.Products { ... }
    actions {
      // bound actions/functions
      @(restrict: [{ to: 'admin', where: '$user.level = 2' }])
      action addRating (stars: Integer);
      function getViewsCount @(requires: ['admin']) () returns Integer;
    }
}

Unbound Actions and Functions

For unbound actions and functions, the grant: property isn’t needed as always exactly this action or function is meant. We either skip it here or use ‘*’.

service MyOrders {
  entity Order ...;
  // unbound actions / functions
  type cancelOrderRet {
    acknowledge: String enum { succeeded; failed; };
    message: String;
  }
  @(restrict: [{ grant: '*', to: 'buyer', where: '$user.status > 1' }])
  action cancelOrder (orderID:Integer, reason:String) returns cancelOrderRet;
  function countOrders @(requires: 'admin') () returns Integer;
}

Enforcement

The service provider frameworks automatically enforce restrictions in generic handlers. They evaluate the annotations in the CDS models and depend on the operations:

Alternatively, you can programmatically enforce restrictions in custom request handlers as in this sample implementation for the services defined above:

const cds = require('@sap/cds')

cds.serve ('ReviewsService') .with ((srv)=>{
  srv.before ('*', req =>
    req.user.is('identified') || req.reject(403)
  )
})

cds.serve ('CustomerService') .with ((srv)=>{
  srv.before ('*', req =>
   req.user.is('authenticated') || req.reject(403)
  )
  srv.before (['READ', 'CREATE'], 'Orders', req =>
    req.user.is('admin') || req.reject(403)
  )
  srv.before ('READ', 'Orders', req =>
    req.query.where('buyer =',req.user)
  )
  srv.before ('*', 'Approval', req =>
    req.user.level > 2 || req.reject(403)
  )
})

Enforcement API

Node.js

Following are the Node.js APIs you can use for implementing your enforcement (swift has corresponding ones):

API Description
req.user Shortcut for req.user.id in queries
req.user.id Access the current user’s unique ID, an arbitrary string
req.user.is(<role>) Check whether the user has assigned the given role
req.user.<attr> Access user-related attributes from JWT tokens
req.reject(403, ...) Reject a request due to missing authorizations
req.query.where(...) Add a filter to the query’s where clause

Note: The function req.user.is(<role>) accepts role names introduced in the current application’s service models.

Remark regarding implementations: req.user should be implemented as getters or Proxy objects to only decrypt JWT tokens lazily on demand.

Java

Use the following Java APIs to programmatically enforce restrictions in your custom request handlers:

API Description
isAuthenticatedUser(String serviceName) Checks if the current user is authenticated to access the service. If there is no restriction defined for the service in the model, it always returns true.
isRegisteredUser(String serviceName) Checks if the current user is registered to access the service. If there is no restriction defined for the service in the model, it always returns true.
hasEntityAccess(String entityName, String operation) Checks if the current user has the rights to perform the specified operation on an entity. If there is no restriction defined on the entity in the service model, it always returns true.
getWhereCondition() Gets the where condition specified in the service model for granting entity-level access.
getUserName() Gets the current user’s name.
getUserId() Gets the current user’s ID.
hasUserRole(String roleName) Checks if the current user is associated with the specified user role.
getUserAttribute(String attributeName) Gets the value of the specified attribute which is a part of the JSON Web Token.
isContainerSecurityEnabled() Indicates whether container-based security is enabled.

For more information, see Authorization and Access Control Using Java.

Role Assignments

Information about roles and attributes has to be made available to the UAA platform service. This information enables the respective JWT tokens to be constructed and sent with the requests for authenticated users. In particular, the following happens automatically behind-the-scenes upon build:

1. Roles and Attributes are Filled into XSUAA Configuration

Scopes, attributes, and role templates are derived out of the CDS model. This results in:

cds-security.json

{
  "xsappname": "bookshop", "tenant-mode": "dedicated",
  "scopes": [
    { "name": "$XSAPPNAME.admin", "description": "admin" }
  ],
  "attributes": [
    { "name": "level", "description": "level", "valueType": "s" }
  ],
  "role-templates": [
    { "name": "admin", "scope-references": [ "$XSAPPNAME.admin" ], "description": "generated" }
  ]
}

You have such a file generated through cds compile service.cds --to xsuaa,json > cds-security.json

2. XSUAA Configuration is Completed and Published

Depending on whether MTA deployment is used, choose one approach:

Through MTA Build

This merges any inline configuration from mta.yaml (see the config block) and the cds-security.json file:

mta.yml

resources:
  name: my-uaa
  type: org.cloudfoundry.managed-service
  parameters:
    service: xsuaa
    service-plan: application
    path: ./cds-security.json  # include cds managed scopes and role templates
    config:
      xsappname: my-uaa-${space}
      tenant-mode: dedicated   # use 'shared' for multi-tenant deployments
      scopes: []   # more scopes

If there are conflicts, the MTA security configuration has priority.

Deployment of such an MTA uploads the XSUAA configuration to SAP Cloud Platform.

Learn more about building and deploying MTA applications

Manual

Invoke cds manually, which merges a custom ‘base’ file with the derived configuration from the model:

cds compile srv/ --to xsuaa  > xs-security.json

Use cf create-service xsuaa application <servicename> -c xs-security.json to create the XSUAA service with the XSUAA configuration, or cf update-service <servicename> -c xs-security.json to update the configuration.

3. Assembling Roles and Assigning Roles to Users

This is a manual step an administrator would do in SAP Cloud Platform Cockpit. For more information, see here. If a user attribute isn’t set for a user in the IDP of the SAP Cloud Platform Cockpit the semantic for authorizations is that the user has no restriction for this attribute. For example, if a user has no value set for an attribute “Country” he’s allowed to see data records for all countries. In the xs-security.json, the entity attribute has a property valueRequired where the developer can specify whether an unrestricted access is possible by not assigning a value to the attribute.

4. Scopes Are Narrowed to Local Roles

Based on this, the JWT token for an administrator contains a scope my.app.admin. From within service implementations of my.app you can reference the scope:

req.user.is ("admin")

… and, if required, from others by:

req.user.is ("my.app.admin")


For more information, see Maintaining Application Security in XS Advanced.

Separating Concerns

Consider using CDS Aspects to separate the actual service definitions from authorization annotations as follows:

services.cds

service ReviewsService {
  /*...*/
} 

service CustomerService {
  entity Orders {/*...*/}
  entity Approval {/*...*/}
}

services-auth.cds

using { ReviewsService, CustomerService } from './services';

annotate ReviewsService with @(requires: 'identified-user');
annotate CustomerService with @(requires: 'authenticated-user');

annotate CustomerService.Orders with @(restrict: [
  { grant: ['READ','WRITE'], to: 'admin' },
  { grant: 'READ', where: 'buyer = $user' },
]);

annotate CustomerService.Approval with @(restrict: [
  { grant: 'WRITE', where: '$user.level > 2' }
]);

This keeps your actual service definitions concise and focused on structure only. It also allows giving authorization models a separate ownership and lifecycle.

Add Authentication

To run our application with these security restrictions different authentication strategies can be used: mocked authentication, or authentication based on a remote XSUAA instance.

Run with Mocks

This approach swaps the relevant authorization information that would normally come from the JWT token (that is, user ID, scopes, and attributes) by fake information. This way, no development roundtrips to UAA nor logon is necessary, which is convenient for fast development turnarounds or testing.

Note that authorization mocks are available for Node.js only.

→ See Authentication guide to enable mocked authentication.

Run with XSUAA

In this mode, we let our local application run against a remote XSUAA instance. This allows for testing authentication and authorization as it happens in production.

→ Follow steps in Authentication guide to configure and use the JWT strategy.