Primary reference: https://cap.cloud.sap/docs/guides/security/overview Authorization: https://cap.cloud.sap/docs/guides/authorization
Service-level access control
// Restrict entire service to authenticated users
@requires: 'authenticated-user'
service CatalogService { ... }
// Multiple roles (OR logic)
@requires: ['admin', 'manager']
service AdminService { ... }
Entity-level @restrict
service OrderService @(requires: 'authenticated-user') {
entity Orders @(restrict: [
{ grant: 'READ', to: ['viewer', 'editor', 'admin'] },
{ grant: 'CREATE', to: ['editor', 'admin'] },
{ grant: 'UPDATE', to: ['editor', 'admin'], where: 'createdBy = $user' },
{ grant: 'DELETE', to: 'admin' },
]) as projection on db.Orders;
}
Instance-based authorization (row-level security)
❌ Wrong vs ✅ Correct syntax
// ❌ WRONG — operations instead of grant, roles instead of to, where as array
entity Orders @(restrict: [
{ operations: ['READ'], roles: ['authenticated-user'] },
{ operations: ['UPDATE'], where: ['owner = $user.id'] }
])
// ✅ CORRECT
entity Orders @(restrict: [
{ grant: 'READ', to: 'authenticated-user' },
{ grant: 'UPDATE', to: 'editor', where: 'createdBy = $user' }
])
// User can only update their own records
{ grant: 'UPDATE', to: 'editor', where: 'createdBy = $user' }
// Filter by user attribute (e.g. cost center)
{ grant: 'READ', to: 'employee', where: 'costCenter = $user.attr.costCenter' }
Expose user attributes from XSUAA/IAS in xs-security.json:
{
"role-templates": [{
"name": "Employee",
"attribute-references": ["costCenter"]
}]
}
Checking authorization in handlers
async init() {
this.before('UPDATE', Orders, this.checkOwner)
}
async checkOwner(req) {
const { user } = req
if (user.is('admin')) return // admins bypass
const order = await SELECT.one(Orders, req.params[0]).columns('createdBy')
if (!order) req.reject(404)
if (order.createdBy !== user.id) req.reject(403, 'You can only edit your own orders')
}
Programmatic role checks
const { user } = req
user.is('admin') // role check (boolean)
user.id // 'alice@example.com'
user.tenant // tenant ID (multitenancy)
user.attr.costCenter // custom attribute from XSUAA
user.locale // 'en', 'de', etc.
xs-security.json — XSUAA scopes and roles
{
"xsappname": "my-cap-app",
"tenant-mode": "dedicated",
"scopes": [
{ "name": "$XSAPPNAME.admin", "description": "Administrative access" },
{ "name": "$XSAPPNAME.editor", "description": "Create and edit records" },
{ "name": "$XSAPPNAME.viewer", "description": "Read-only access" }
],
"role-templates": [
{
"name": "Admin",
"scope-references": ["$XSAPPNAME.admin", "$XSAPPNAME.editor", "$XSAPPNAME.viewer"]
},
{
"name": "Editor",
"scope-references": ["$XSAPPNAME.editor", "$XSAPPNAME.viewer"]
},
{
"name": "Viewer",
"scope-references": ["$XSAPPNAME.viewer"]
}
]
}
Local development mock users
In .cdsrc.json or package.json:
{
"cds": {
"requires": {
"auth": {
"kind": "mocked",
"users": {
"alice": { "roles": ["admin"] },
"bob": { "roles": ["editor"] },
"carol": { "roles": ["viewer"], "attr": { "costCenter": "CC100" } }
}
}
}
}
}
Test with: http://localhost:4004 --user alice
IAS (Identity Authentication Service) setup
{
"cds": {
"requires": {
"auth": "ias"
}
}
}
Bind the identity service in mta.yaml:
- name: my-app-srv
requires:
- name: my-ias-instance
Common mistakes to avoid
- ❌ Using
@requires: 'any'when you mean'authenticated-user'(they differ!) - ❌ Relying only on client-side checks — always enforce in the service layer
- ❌ Hardcoding user IDs in
whereclauses instead of using$user - ❌ Forgetting to add scopes to
xs-security.json— roles without scopes don't work - ❌ Not testing with different mock users locally before deploying
- ❌ Using
@restrictwithout@requireson the service — annotations don't cascade automatically