Gate entire services with @requires and filter entity rows with @restrict. Combine operation-level and entity-level controls.
@requires — coarse-grained role-based access control@restrict — fine-grained instance-based authorization with WHERE@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.
// 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
});
}
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''' }
]);
}
// @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.
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']);
}
entity SalesOrders as projection on my.SalesOrders
@(restrict: [{
grant: 'READ',
to: 'SalesPerson',
where: 'region = $user.attr.region' // resolved from JWT attribute
}]);
| 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) |
// .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
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)
})
@restrict overrides entity-level@restrict always wins over @requires when both present at the same level@requires: 'RoleName' is a gate — 403 if the user doesn't have the role@restrict with where auto-applies a filter to every query for that role$user = req.user.id; $user.attr.region = JWT attribute value.cdsrc.json with kind: mocked enable local auth testing