51 · @requires & @restrict

Gate entire services with @requires and filter entity rows with @restrict. Combine operation-level and entity-level controls.

Episode 51 — @requires & @restrict Annotations

What you'll learn

Why this matters

@requires and @restrict are declarative security — they express authorization rules in CDS, not in handler code. This keeps security concerns out of business logic, makes the rules auditable in the model, and lets CAP enforce them automatically before any handler runs.

@requires — role check

// Service level — all operations require this role
service AdminService @(requires: 'admin') {
  entity Users as projection on my.Users;
}

// Entity level — override service-level for one entity
service CatalogService @(requires: 'authenticated-user') {
  entity Books   as projection on my.Books;   // inherits authenticated-user
  entity Reports as projection on my.Reports  // stricter: requires Analyst role
    @(requires: 'Analyst');
}

// Operation level — different roles for different operations
service OrdersService @(requires: 'authenticated-user') {
  entity Orders as projection on my.Orders
    @(requires: {
      READ:   'authenticated-user',    // anyone logged in can read
      CREATE: 'Buyer',                  // only Buyers can create
      UPDATE: 'Buyer',
      DELETE: 'Admin'                   // only Admin can delete
    });
}

@restrict — role + WHERE clause

service OrdersService {
  entity Orders as projection on my.Orders
    @(restrict: [
      // Admins: full access, no WHERE
      { grant: '*',      to: 'Admin' },

      // Buyers: can only read/write their own orders
      { grant: ['READ','WRITE'], to: 'Buyer', where: 'buyer_ID = $user' },

      // Auditors: read-only, closed orders only
      { grant: 'READ',   to: 'Auditor', where: 'status = ''CLOSED''' }
    ]);
}

Combining @requires and @restrict

// @requires = gate: who can enter the service at all
// @restrict = filter: what they see/can do once inside

service OrdersService @(requires: 'authenticated-user') {
  entity Orders as projection on my.Orders
    @(restrict: [
      { grant: '*',    to: 'Admin'   },
      { grant: 'READ', to: 'Auditor', where: 'status = ''CLOSED''' },
      { grant: ['READ','WRITE'], to: 'Buyer', where: 'buyer_ID = $user' }
    ]);
}

@requires: 'authenticated-user' rejects anonymous; @restrict filters within the authenticated population.

Actions and functions

service OrdersService @(requires: 'authenticated-user') {
  entity Orders as projection on my.Orders
    @(restrict: [{ grant: 'READ', to: 'Buyer', where: 'buyer_ID = $user' }]);

  // Action requires specific role
  action  cancelOrder (ID: UUID) @(requires: 'Admin');
  function getStats ()           @(requires: ['Admin','Analyst']);
}

User attributes in WHERE clause

entity SalesOrders as projection on my.SalesOrders
  @(restrict: [{
    grant: 'READ',
    to:    'SalesPerson',
    where: 'region = $user.attr.region'   // resolved from JWT attribute
  }]);

Built-in pseudo-roles

Pseudo-role Meaning
'any' No authentication required
'authenticated-user' Any logged-in user
'identified-user' User with an identity (may include service users)
'system-user' Service-to-service calls (client credentials JWT)

Mock users for testing

// .cdsrc.json
{
  "requires": {
    "auth": {
      "[development]": {
        "kind": "mocked",
        "users": {
          "alice": {
            "password": "alice",
            "roles":    ["Buyer"],
            "attr":     { "region": "EMEA" }
          },
          "bob-admin": {
            "password": "admin",
            "roles":    ["Admin"]
          },
          "guest": {
            "password":      "guest",
            "roles":         [],
            "authenticated": false
          }
        }
      }
    }
  }
}
### Test as Alice (Buyer)
GET http://localhost:4004/orders/Orders
Authorization: Basic alice:alice

### Test as Bob (Admin)
GET http://localhost:4004/orders/Orders
Authorization: Basic bob-admin:admin

Testing with cds.test

const { GET, DELETE } = cds.test('.').in(__dirname + '/..')

it('buyer can only see own orders', async () => {
  const { data } = await GET('/orders/Orders', {
    headers: { Authorization: 'Basic alice:alice' }
  })
  expect(data.value.every(o => o.buyer_ID === 'alice')).toBe(true)
})

it('buyer cannot delete orders', async () => {
  const { status } = await DELETE('/orders/Orders(order-id)', {
    headers: { Authorization: 'Basic alice:alice' }
  })
  expect(status).toBe(403)
})

Priority rules

  1. More specific annotations override less specific
  2. Operation-level @restrict overrides entity-level
  3. Entity-level overrides service-level
  4. @restrict always wins over @requires when both present at the same level

Checkpoint ✓

Sign in for progress tracking & Q&A →