Search

    Authorization and Access Control

    This guide explains how to restrict 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

    Authentication as Prerequisite

    In essence, authentication verifies the user’s identity and the presented claims such as granted roles and tenant membership. Briefly, authentication reveals who uses the service. In contrast, authorization controls how the user can interact with the application’s resources according to granted privileges. As the access control needs to rely on verified claims, authentication is a prerequisite to authorization.

    From perspective of CAP, the authentication method is freely configurable and typically uses central identity and authentication services of the underlying platform. For the local development scenario, CAP brings a built-in authentication on basis of mock users. Find instructions how to set up authentication in these runtime-specific guides:

    Static User Claims

    CDS authorization is model-driven. This basically means it binds access rules for CDS model elements to user claims. For instance, access to a service or entity is dependent from the role a user has been assigned to. Or you can even restrict access on instance level, for example, to the user who has created the instance.
    The generic CDS authorization is build on a CAP user concept, which is an _abstraction of a concrete user type determined by the platform’s identity service. This design decision makes different authentication strategies pluggable to generic CDS authorization.
    After successful authentication, a (CAP) user is represented by the following properties:

    • Unique (logon) name identifying the user. Unnamed users have a fixed name such as system or anonymous.
    • Tenant in case of multitenant applications.
    • Roles the user has been granted by an administrator (see User Roles) or which are derived by the authentication level (see Pseudo Roles).
    • Attributes the user has been assigned by an administrator.

    In the CDS model, some of the user properties can be referenced with $user prefix:

    User Property Reference
    Name $user
    Tenant $user.tenant
    Attribute (name <attribute>) $user.<attribute>

    A single user attribute can have several different values. For instance, attribute $user.language could contain ['DE','FR'].

    User Roles

    As basis for access control, you can design conceptual roles that are application-specific. Such a role should reflect how a user can interact with the application. For instance, the role Vendor could describe users who are allowed to read sales articles and update sales figures. In contrast, a ProcurementManager can have full access to sales articles. Users can have several roles, which are assigned by an administrative user in the platform’s authorization management solution.

    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

    Frequently, it’s required to define access rules that aren’t based on an application-specific user role, but rather on the authentication level of the request. For instance, a service could be accessible not only for identified, but also for anonymous (for example, unauthenticated) users. Such roles are called pseudo roles as they aren’t assigned by user administration, but are added at runtime automatically.

    The following predefined pseudo roles are currently supported by CAP:

    • authenticated-user refers to (named or unnamed) users who have presented a valid authentication claim such as a logon token.
    • system-user denotes an unnamed user used for technical communication.
    • any refers to all users including anonymous ones (that means, public access without authentication).

    The pseudo role system-user allows you to separate internal access by technical users from external access by business users. The technical user can come from a SaaS or the PaaS tenant. Such technical user requests typically run in a privileged mode without any restrictions on instance level. For example, an action that implements a data replication into another system needs to access all entities of subscribed SaaS tenants and can’t be exposed to any business user. Note that system-user also implies authenticated-user.

    In case of XSUAA authentication, the request user is attached with pseudo role system-user if the presented JWT token has been issued with grant type client_credentials or client_x509 for a trusted client application.

    Restrictions

    By default, CDS services have no access control. Hence, depending on the configured authentication, CDS services are initially open for anonymous users. To protect resources according to your business needs, you can define restrictions that make the runtime enforce proper access control. Alternatively, you can add custom authorization logic by means of authorization enforcement API.

    Restrictions can be defined on different CDS resources:

    • Services
    • Entities
    • (Un)bound actions and functions

    You can influence the scope of a restriction by choosing an adequate hierarchy level in the CDS model. For instance, a restriction on service level applies to all entities in the service. Additional restrictions on entities or actions can further limit authorized requests. See section combined restrictions for more details.

    Beside the scope, restrictions can limit access to resources with regards to different dimensions:

    • The event of the request, that is, the type of the operation (what?)
    • The roles of the user (who?)
    • Filter-condition on instances to operate on (which?)

    Restricting Events with @readonly and @insertonly

    Annotate entities with @readonly or @insertonly to statically restrict allowed operations for all users as demonstrated in the example:

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

    Note that both annotations introduce access control on entity level. In contrast, for sake of input validation, you can make use of @readonly also on property level.

    In addition, annotation @Capabilities from standard OData vocabulary is enforced by the runtimes analogously:

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

    Events to Auto-Exposed Entities

    In general, entities can be exposed in services in different ways: it can be explicitly exposed by the modeller (e.g. by a projection), or it is auto-exposed by the CDS compiler due to some reason. Access to auto-exposed entities needs to be controlled in a specific way. Consider the following example:

    context db {
      @cds.autoexpose
      entity Categories : cuid { // explicitly auto-exposed (by @cds.autoexpose)
        [...]
      }
      
      entity Issues : cuid { // implicitly auto-exposed (by composition)
        category: Association to Categories;
        [...]
      }
    
      entity Components : cuid { // explicitly exposed (by projection)
        issues: Composition of many Issues;
        [...]
      }
    }
    
    service IssuesService {
      entity Components as projection on db.Components;
    }
    

    As a result, service IssuesService actually exposes all three entities from db context:

    • db.Components is explicitly exposed due to the projection in the service.
    • db.Issues is implicitly auto-exposed by the compiler as it is a composition entity of Components.
    • db.Categories is explicitly auto-exposed due to the @cds.autoexpose annotation.

    In general, implicitly auto-exposed entities cannot be accessed directly, that means, only access via navigation path (starting from an explicitly exposed entity) is allowed.

    In contrast, explicitly auto-exposed entities can be accessed directly, but only as @readonly. The rational behind is, that entities representing value lists need to be readable at the service level, for instance to support value help lists.

    See details about @cds.autoexpose in section Auto-Exposed Entities.

    This results in the following access matrix:

    Request READ WRITE
    IssuesService.Components
    IssuesService.Issues x x
    IssuesService.Categories x
    IssuesService.Components[<id>].issues
    IssuesService.Components[<id>].issues[<id>].category x

    CodeLists such as Languages, Currencies, and Countries from sap.common are annotated with @cds.autoexpose and hence are explicitly auto-exposed.

    Restricting Roles with @requires

    You can use the @requires annotation to control which (pseudo-)role a user requires to access a resource:

    annotate BrowseBooksService with @(requires: 'authenticated-user');
    annotate ShopService.Books with @(requires: ['Vendor', 'ProcurementManager']);
    annotate ShopService.ReplicationAction with @(requires: 'system-user');
    

    In the example, service BrowseBooksService is open for authenticated, but not for anonymous users. A user who has role Vendor or ProcurementManager is allowed to access entity ShopService.Books. Unbound action ShopService.ReplicationAction can only be triggered by a technical user.

    When restricting service access through @requires, the service’s metadata endpoints (that means, /$metadata as well as the service root /) are restricted by default as well. If you require public metadata, you can disable the check trough config cds.env.runtime.protectMetadata = false (Node.js) or cds.security.openMetadataEndpoints = true (Java), respectively.

    Access Control with @restrict

    You can use the @restrict annotation to define authorizations on a fine-grained level. In essence, all kinds of restrictions that are based on static user roles, the request operation, and instance filters can be expressed by this annotation.
    The building block of such as restriction is a single privilege, which has the general form:

    { grant:<events>, to:<roles>, where:<filter-condition> }
    

    whereas the properties are:

    • grant: one or more events the privilege applies to
    • to: one or more user roles the privilege applies to (optional)
    • where: a filter condition that further restricts access on instance level (optional).

    Following values are supported:

    • grant accepts all standard CDS events (such as READ, CREATE, UPDATE, and DELETE) as well as action and function names. WRITE is a virtual event for all standard CDS events with write semantic (CREATE, DELETE, UPDATE, UPSERT) and * is a wildcard for all events.

    • The to property lists all user roles or pseudo roles the privilege applies to. Note that pseudo-role any does apply for all users and is the default if no value is provided.

    • The where-clause can contain a boolean expression in CQL-syntax that filters the instances the event applies to. As it allows user values (name, attributes etc.) and entity data as input, it’s suitable for dynamic authorizations based on the business domain. Supported expressions and typical use cases are presented in instance-based authorization.

    A privilege is met, if and only if all properties are fulfilled for the current request. In the following example, only orders can be read by an Auditor who meets AuditBy element of the instance:

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

    If a privilege contains several events, only one of them needs to match the request event to comply with the privilege. The same holds, if there are multiple roles defined in the to property:

    service ReviewService @(restrict: [ 
        { grant:['READ', 'WRITE'], to: ['Reviewer', 'Customer'] } 
      ]) {/*...*/}
    

    In this example, all users that have role Reviewer or Customer can read or write on ReviewService.

    You can build restrictions based on multiple privileges:

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

    A request passes such a restriction if at least one of the privileges is met. In this example, Admin users can read and write entity Orders. But also a user can read all orders, which have a buyer property that matches the request user.

    Similarly, the filter conditions of matched privileges are combined with logical OR:

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

    Here an Auditor user can read all orders with matching country or which has been created by him- or herself.

    Annotations such as @requires or @readonly are just convenience shortcuts for @restrict, for example:

    • @requires: 'Viewer' is equivalent to @restrict: [{grant:'*', to: 'Viewer'}]
    • @readonly is the same as @restrict: [{ grant:'READ' }]

    Currently, the security annotations are only evaluated on the target entity of the request. Restrictions on associated entities touched by the operation aren’t regarded. This has the following implications:

    • Restrictions of (recursively) expanded or inlined entities of a READ request aren’t checked.
    • Deep inserts and updates are checked on the root entity only.

    Find solution sketches how to deal with that.

    Supported Combinations with CDS Resources

    Restrictions can be defined on different types of CDS resources, but there are some limitations with regards to supported privileges:

    CDS Resource grant to where Remark
    service n/a n/a = @requires
    entity  
    action/function n/a n/a1 = @requires

    1 Node.js supports static expressions that don’t have any reference to the model such as where: $user.level = 2.

    Unsupported privilege properties are ignored by the runtime. Especially, in case of bound or unbound actions, the grant property is implicitly removed (assuming grant: '*' instead). The same also holds for functions:

    service CatalogService {
      entity Products as projection on db.Products { ... }
      actions {
        @(requires: 'Admin')
        action addRating (stars: Integer);
      }    
      function getViewsCount @(restrict: [{ to: 'Admin' }]) () returns Integer;
    }
    

    Combined Restrictions

    Restrictions can be defined on different levels in the CDS model hierarchy. Bound actions and functions refer to an entity, which in turn refers to a service. Unbound actions and functions directly refer to a service. As a general rule, all authorization checks of the hierarchy need to be passed (logical AND). This is illustrated in the following example:

    service CustomerService @(requires: 'authenticated-user') {
      entity Products @(restrict: [ 
        { grant: 'READ' },
        { grant: 'WRITE', to: 'Vendor' },
        { grant: 'addRating', to: 'Customer'}
      ]) {/*...*/}
      actions {
         action addRating (stars: Integer);
      }    
      entity Orders @(restrict: [
        { grant: '*', to: 'Customer', where: 'CreatedBy = $user' }
      ]) {/*...*/}
      action monthlyBalance @(requires: 'Vendor') ();
    }
    

    The privilege for action addRating is defined on entity level.

    The resulting authorizations are illustrated in the following access matrix:

    Operation Vendor Customer authenticated-user anonymous
    CustomerService.Products (READ) x
    CustomerService.Products (WRITE) x x x
    CustomerService.Products.addRating x x x
    CustomerService.Orders (*) x 1 x x
    CustomerService.monthlyBalance x x x

    1 A Vendor user can only access the instances created by him- or herself.

    The example models access rules for different roles in the same service. In general, this is not recommended due to the high complexity. Find best practices how to avoid this.

    Restrictions and Draft Mode

    Basically, the access control for entities in draft mode differs from the general restriction rules that apply to (active) entities. A user, who has created a draft, should also be able to edit (UPDATE) or cancel the draft (DELETE). The following rules apply:

    • If a user has the privilege to create an entity (CREATE), he or she also has the privilege to create a new draft entity and update, delete, and activate it.
    • If a user has the privilege to update an entity (UPDATE), he or she also has the privilege to put it into draft mode and update, delete, and activate it.
    • Draft entities can only be edited by the creator user.

    As a result of the derived authorization rules for draft entities, you don’t need to take care of draft events when designing the CDS authorization model.

    Restrictions of Auto-Exposed and Generated Entities

    In general, a service actually exposes more than the explicitly modeled entities from the CDS service model. This comes from the fact, that the compiler auto-exposes entities for sake of completeness, for example, adding composition entities. Another reason are generated entities for localization or draft support that need to appear in the service. Typically, such entities don’t have restrictions. The emerging question is, how can requests to these entities be authorized?

    For illustration, let’s extend the service IssuesService from section Events to Auto-Exposed Entities by adding a restriction to Components:

    annotate IssuesService.Components with @(restrict: [ 
      { grant: '*', to: 'Supporter' }, 
      { grant: 'READ', to: 'authenticated-user' } ]);
    

    Basically, users with role Supporter aren’t restricted, whereas authenticated users may only read the Components. But what about the auto-exposed entities such as IssuesService.Issues and IssuesService.Categories? They could be target of an (indirect) request as outlined in Events to Auto-Exposed Entities, but none of them is annotated with a concrete restriction. In general, the same also holds for service entities, which are generated by the compiler, for example, for localization or draft support.

    To close the gap with auto-exposed and generated entities, the authorization of such entities is delegated to a so-called authorization entity, which is the last entity in the request path, which bears authorization information, that means, which fulfills at least one of the following properties:

    • Explicitly exposed in the service
    • Annotated with a concrete restriction
    • Annotated with @cds.autoexpose

    Hence, the authorization for the requests in the example is delegated as follows:

    Request Target Authorization Entity
    IssuesService.Components n/a1
    IssuesService.Issues n/a1
    IssuesService.Categories IssuesService.Categories2
    IssuesService.Components[<id>].issues IssuesService.Components3
    IssuesService.Components[<id>].issues[<id>].category IssuesService.Categories2

    1 Request is rejected.

    2 @readonly due to @cds.autoexpose

    3 According to the restriction. <id> is relevant in case of instance-based filter.

    Inheritance of Restrictions

    Service entities inherit the restriction from the database entity, on which they define a projection. An explicit restriction defined on a service entity replaces inherited restrictions from the underlying entity.

    Entity Books on database level:

    namespace db;
    entity Books @(restrict: [
      { grant: 'READ', to: 'Buyer' },
    ]) {/*...*/}
    

    Services BuyerService and AdminService on service level:

    service BuyerService @(requires: 'authenticated-user'){
      entity Books as projection on db.Books; /* inherits */
    }
    
    service AdminService @(requires: 'authenticated-user'){
      entity Books @(restrict: [
        { grant: '*', to: 'Admin'} /* overrides */
      ]) as projection on db.Books;
    }
    
    Events Buyer Admin authenticated-user
    BuyerService.Books (READ) x x
    AdminService.Books (*) x x

    We recommend defining restrictions on database entity level only in exceptional cases. Inheritance and override mechanism can lead to an unclear situation.

    Warning
    A service level entity can’t inherit a restriction with a where condition that doesn’t match the projected entity. The restriction has to be overridden in this case.

    Instance-Based Authorization

    The restrict annotation for an entity allows to enforce authorization checks that statically depend on the event type and user roles. In addition, you can define a where-condition that further limits the set of accessible instances. This condition, which acts like a filter, establishes an instance-based authorization.
    The condition defined in the where-clause typically associates domain data with static user claims. Basically, it either filters the result set in queries or accepts only write operations on instances that meet the condition. Hence, the condition applies following standard CDS events only1:

    • READ (as result filter)
    • UPDATE (as reject condition)
    • DELETE (as reject condition)

    1 Node.js supports static expressions that don’t have any reference to the model such as where: $user.level = 2 for all events including action and functions.

    For instance, a user is allowed to read or edit Orders (defined with managed aspect) he or she’s created:

    annotate Orders with @(restrict: [ 
      { grant: ['READ', 'UPDATE', 'DELETE'], where: 'CreatedBy = $user' } ]);
    

    Or a Vendor can only edit articles on stock (that means Articles.stock positive):

    annotate Articles with @(restrict: [ 
      { grant: ['UPDATE'], to: 'Vendor',  where: 'stock > 0' } ]);
    

    You can define where-conditions in restrictions based on CQL-where-clauses.
    Supported features are:

    • Predicates with arithmetic operators.
    • Combining predicates to expressions with logical operators and and or.
    • Value references to constants, user attributes, and entity data (elements including paths)

    User Attribute Values

    To refer to attribute values from the user claim, prefix the attribute name with ‘$user.’ as outlined in static user claims. For instance, $user.country refers to the attribute with name country.

    In general, $user.<attribute-name> contains a list of attribute values that are assigned to the user. Following rules apply:

    • A predicate in the where clause evaluates to true if one of the attribute values from the list matches the condition.
    • An empty (or not defined) list means that the user is fully restricted with regards to this attribute (that means the predicate evaluates to false).
    • The special value $unrestricted in the list signals unrestricted access (that means the predicate evaluates to true).

    For example, the condition where: countryCode = $user.country will grant a user with attribute values country = ['DE', 'FR'] access to entity instances, which have countryCode = DE or countryCode = FR. A user with country = ['$unrestricted'] is authorized to access all instances, whereas country = [] (or country not defined at all) doesn’t allow to access any of the instances.

    Association Paths

    The where-condition in a restriction can also contain CQL path expressions that navigate to elements of associated entities:

    service SalesOrderService @(requires: 'authenticated-user') {
      entity SalesOrders @(restrict: [
         { grant: 'READ', 
           where: 'product.productType = $user.productType' } ]) {
        product: Association to one Products;
      }
      entity Products {
        productType: String(32); /*...*/
      }
    }
    

    Paths on 1:n associations (Association to many) are only supported, if the condition selects at most one associated instance.

    Be aware of increased execution time when modeling paths in the authorization check of frequently requested entities. Working with materialized views might be an option for performance improvement in this case.

    Warning
    In Node.js association paths in where-clauses are currently only supported when using SAP HANA.

    Best Practices

    CAP authorization allows you to control access to your business data on a fine granular level. But keep in mind that the high flexibility can end up in security vulnerabilities if not applied appropriately. In this perspective, lean and straightforward models are preferred. When modeling your access rules, the following recommendations can support you to design such models.

    Choose Conceptual Roles

    When defining user roles, one of the first ideas can be to align roles to the available operations on entities, which results in roles such as SalesOrders.Read, SalesOrders.Create, SalesOrders.Update, and SalesOrders.Delete etc. What is the problem with this approach? Think about the resulting number of roles the user administrator has to handle when assigning to business users. The administrator also would have to know the domain model precisely and understand the result of combining the roles. Similarly, assigning roles to operations only (Read, Create, Update, …) typically doesn’t fit your business needs.
    We strongly recommend defining roles that describe how a business user interacts with the system. Roles like Vendor, Customer, or Accountant can be appropriate. With this approach, the application developers define the set of accessible resources in the CDS model for each role - and not the user administrator.

    Prefer Single-Purposed, Use-Case Specific Services

    Have a closer look at this example:

    service CatalogService @(requires: 'authenticated-user') {
       entity Books @(restrict: [
        { grant: 'READ' },
        { grant: 'WRITE', to: 'Vendor', where: '$user.publishers = publisher' },
        { grant: 'WRITE', to: 'Admin' } ])
      as projection on db.Books;
      action doAccounting @(requires: ['Accountant', 'Admin']) ();
    }
    

    Four different roles (authenticated-user, Vendor, Accountant, Admin) share the same service CatalogService. As a result, it’s confusing how a user can use Books or doAccounting. Considering the complexity of this small example (4 roles, 1 service, 2 resources), this approach can introduce a security risk, especially in case the model is larger and subject to adaption. Moreover, UIs defined for this service will likely appear unclear as well.
    The fundamental purpose of services is to expose business data in a specific way. Hence, the more straightforward way is to spend a service for each of the roles:

    @path:'browse'
    service CatalogService @(requires: 'authenticated-user') {
      @readonly entity Books  
      as select from db.Books { title, publisher, price };
    }
    
    @path:'internal'
    service VendorService @(requires: 'Vendor') {
      entity Books @(restrict: [
        { grant: 'READ' },
        { grant: 'WRITE', to: 'vendor', where: '$user.publishers = publisher' } ]) 
      as projection on db.Books;
    }
    
    @path:'internal'
    service AccountantService @(requires: 'Accountant') {
      @readonly entity Books as projection on db.Books;
      action doAccounting();
    }
    /*...*/
    

    You can tailor the exposed data according to the corresponding role, even on level of entity elements like done in CatalogService.Books.

    Prefer Dedicated Actions for Specific Use-Cases

    In some cases it can be helpful to restrict entity access as far as possible and create actions with dedicated restrictions for specific use cases like in the following example:

    service GitHubRepositoryService @(requires: 'authenticated-user') {
      @readonly entity Organizations as projection on GitHub.Organizations actions {
        action rename @(requires: 'Admin') (newName : String);
        action delete @(requires: 'Admin') ();
      };
    }
    

    This service allows querying organizations for all authenticated users. In addition, Admin users are allowed to rename or delete. Granting UPDATE to Admin would allow administrators to change organization attributes that aren’t meant for adaption.

    Think About Domain-Driven Authorization

    In frequent cases, static roles don’t fit into an intuitive authorization model. Instead of making authorization dependent from static properties of the user, it’s often more appropriate to derive access rules from the business domain. For instance, all users assigned to a department (in the domain) are allowed to access the data of the organization comprising the department. Relationship in the entity model (for example, a department assignment to organization) influence authorization rules at runtime. In contrast to static user roles, dynamic roles are fully domain-driven.

    Advantages of dynamic roles are:

    • Most flexible way to define authorizations.
    • Induced authorizations according to business domain.
    • Application-specific authorization model and intuitive UIs.
    • Decentralized role management for application users (no central user administrator required).

    Drawbacks to be considered are:

    • Additional effort for modeling and designing application-specific role management (entities, services, UI).
    • Potentially higher security risk due to less usage of framework functionality.
    • Sharing authorization management with other (none-CAP) application is harder to achieve.
    • Dynamic role enforcement can introduce a performance penalty.

    Control Exposing of Associations and Compositions

    Note that exposed associations (and compositions) can disclose unauthorized data. Consider the following scenario:

    namespace db;
    entity Employees : cuid { // autoexposed!
      name: String(128);
      team: Association to Teams;
      contract: Composition of Contracts;
    }  
    entity Contracts @(requires:'Manager') : cuid { // autoexposed!
      salary: Decimal; 
    } 
    entity Teams : cuid {
      members: Composition of many Employees on members.team = $self;
    }
    
    
    service ManageTeamsService @(requires:'Manager') {
      entity Teams as projection on db.Teams;
    }
    
    service BrowseEmployeesService @(requires:'Employee') {
      @readonly entity Teams as projection on db.Teams; // navigate to Contracts!
    }
    

    A team (entity Teams) contains members of type Employees. An employee refers to a single contract (entity Contracts) which contains sensitive information that should be visible only to Manager users. Employee users should be able to browse the teams and their members, but aren’t allowed to read or even edit their contracts.
    As db.Employees and db.Contracts are auto-exposed, managers can navigate to all instances through service entity ManageTeamsService.Teams (for example, OData request /ManageTeamsService/Teams?$expand=members($expand=contract)).
    It’s important to notice, that this also holds for an Employee user, as only the target entity BrowseEmployeesService.Teams has to pass the authorization check in the generic handler, and not the associated entities.

    To solve this security issue, introduce a new service entity BrowseEmployeesService.Employees that removes the navigation to Contracts from the projection:

    service BrowseEmployeesService @(requires:'Employee') {
      @readonly entity Employees
      as projection on db.Employees excluding { contracts }; // hide contracts!
      
      @readonly entity Teams as projection on db.Teams;
    }
    

    Now, an Employee user can’t expand the contracts as the composition isn’t reachable anymore from the service.

    Associations without navigation links (for example, when an associated entity isn’t exposed) are still critical with regards to security.

    Design Authorization Model from Start

    As shown before, defining an adequate authorization strategy has a deep impact on the service model. Apart from the fundamental decision, if you want to build your authorization on dynamic roles, authorization requirements can result in rearranging service and entity definitions completely. In worst case, this means to rewrite huge parts of the application (including UI). Hence, it’s strongly recommended to take security design into consideration in early stage of your project.

    Keep as Simple as Possible

    • 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.

    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.

    Programmatic Enforcement

    The service provider frameworks automatically enforce restrictions in generic handlers. They evaluate the annotations in the CDS models and for example:

    • Reject incoming requests if static restrictions aren’t met.
    • Add corresponding filters to queries for instance-based authorization etc.

    In case generic enforcement doesn’t fit your needs, you can override or adapt it with programmatic enforcement in custom handlers:

    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:

    xs-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 > xs-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 can contain invalid characters from XSUAA perspective. See section Application Security Descriptor Configuration Syntax in the SAP HANA Platform documentation for the syntax of the xs-security.json. You can 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 xs-security.json file:

    mta.yml

    resources:
      name: my-uaa
      type: org.cloudfoundry.managed-service
      parameters:
        service: xsuaa
        service-plan: application
        path: ./xs-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 BTP.

    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 BTP Cockpit. See section Set Up the Roles for the Application for more details. If a user attribute isn’t set for a user in the IDP of the SAP BTP 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")
    


    See the following sections for more details:

    Show/Hide Beta Features