Search

Authorization and Access Control

About restricting access to data by adding respective declarations to CDS models, which are then enforced in service implementations.

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

Prerequisite: Authentication

Authentication, that is the ability to authenticate users’ identities, ia a prerequisite to enforce access control checks at runtime. Find instructions how to set up authentication in these runtime-specific guides:

Restrictions

Static Restrictions with @readonly, @insertonly, etc.

Annotate entities with @readonly or @insertonly to statically restrict allowed operations as follows:

service BookshopService {
  @readonly entity Books {...}
  @insertonly entity Orders {...}
}

In addition, these standard OData Annotations are similarly enforced automatically:

service SomeService {
  @Capabilities: { Insertable:true, Updatable:true, Deletable:false }
  entity Foo { key ID : UUID }
}

Learn more about OData Annotations

Finally, all direct write access requests to entities annotated with @cds.autoexposed are automatically rejected.

Learn more about Auto-exposed Entities

User-Specific Restrictions with @requires and @restrict

Restrict access to services specific to individual users and data by adding @requires or @restrict annotations to services, entities, or actions and 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' } 
  ]){/*...*/}
}
  • @restrict: allows fine-grained control through an array of privileges given as grant statements in the form {grant:<operation>, to:<roles>, ...}. For more details, see Privileges.

  • @requires: allows specifying one or more user roles (as a single string or an array of strings) that the current user must be assigned.

@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:

  • grant: one or more operations (as a string or an array of strings)
  • to: (optional) one or more user roles the privilege is granted to
  • where: (optional) a WHERE condition that further restricts access

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

Predicated Privileges in where: Clauses

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.

Instance-Based Authorization through where: Clauses

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.

Within WHERE conditions CQL syntax is allowed. The logical operators and and or are supported.

User Roles

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

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:

  • authenticated-user refers to users identified by logon
  • identified-user is identified by weaker mechanisms such as cookies
  • system-user is for client systems calling in with unnamed, technical users
  • any refers to all users including nonidentified ones

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” if 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 isn’t provided in the JWT, then the corresponding attribute comparison in the WHERE clause is evaluated to FALSE. For example, the following WHERE condition is always FALSE:

  • @restrict: [ { grant: 'READ', where: '$user.not_existing_attribute = COLUMN' } ]

Specific hard-coded attributes are:

  • $user.tenant, which contains the tenant ID the user has the authentication from.

The external attributes from the JWT can be referenced generically. For example:

  • $user.ext_attr.serviceinstanceid, which contains the service instance id from the “external attributes” section in the JWT. The list of these attributes isn’t stable and can change. Be careful when using them.

Limitation for where: Clauses

CREATE / WRITE Operation and Custom Operations

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

Example for supported statements

@restrict: [ 
  { grant: 'CREATE', to: 'authenticated-user', 
    where: '$user.country = ''DE''' } 
] 

or:

@restrict: [ 
  { grant: 'CREATE', to: 'authenticated-user', 
    where: '$user = ''testUser'''  } 
]

Example for NOT supported statements

@restrict: [ 
  { grant: 'CREATE', to: 'authenticated-user', 
    where: '$user = NAME'  } 
]

NAME in here is a database column or:

@restrict: [ 
  { grant: 'CREATE', to: 'authenticated-user', 
    where: '$user = ''testUser'' AND $user.country = ''DE'''  } 
]

Association Paths

Association paths in WHERE clauses are currently only supported if using SAP HANA.

Example:

  • restrict: [ { grant: 'READ', to: 'authenticated-user', where: salesOrderItem.productType = '$user.productType' } ]

Enforcement

Out-of-the-box Generic Enforcement

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

  • Reject incoming requests if the conditions aren’t met (static authorizations)

  • Add corresponding filters to the queries’ WHERE clauses if there are read queries and instance-based authorizations

In case certain use cases aren’t covered by the generic enforcement, you can always complement that with programmatic enforcement in custom handlers in Node.js and Java using the APIs outlined in the next sections.

Programmatic Enforcement in 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

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

For example, the following sample would use this API to programmatically enforce the restrictions as 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)
  )
})

Programmatic Enforcement in 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.

Best Practices

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

Current Limitations:

  • The Java runtime currently supports @requires: only at service level, and @restrict: only on entity level.
  • The where: clause in @restrict: is only supported at the entity level.

Best Practices

  • If different authorizations are needed for different operations, it’s easier to have them defined at service level. If you start defining them at entity level all possible operations must be specified otherwise the not mentioned ones are automatically forbidden.
  • If possible, try to define your authorizations either on service or on entity level. Mixing both variants increases complexity and not all combinations are supported either.
  • If you already check for a nonpseudo role, it isn’t necessary to check for role authenticated-user or system-user in addition as all users having roles are authenticated.

Separation of 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.

Privileges Combine with OR

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.

Using to: with Multiple Roles

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.

Combining to: and where:

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

entity Orders @(restrict: [
    { grant: ['READ'], 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.

Combining service-level and entity-level Restrictions

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

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. For more details, 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;
}

Inheritance of Restrictions

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

Role Assignments with XSUAA

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 can have such a file generated through cds compile service.cds --to xsuaa,json > cds-security.json.

For every role name in the CDS model one scope and one role template are generated having exactly the name of the CDS role name. The modeled role and scope names in the CDS files might contain invalid characters from XSUAA perspective. Please refer to the SAP Help documentation for the syntax of the xs-security.json. You’ll also find hints there how to complete this file manually for the complete setup of your XSUAA instance besides the authorization aspect. If you create the xs-security.json manually or you already have an existing file, make sure that the scope names in the file exactly match the role names in the CDS Model as these scope names will be checked at runtime.

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, this means that the user has no restriction for this attribute. For example, if a user has no value set for an attribute “Country”, they’re 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 necessary, from others by:

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


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