Trace the full scope → role template → role → role collection → user assignment chain in BTP Cockpit.
$XSAPPNAME expansionYou can write perfect CDS security annotations and still have 403 errors because the role isn't assigned in BTP Cockpit. Understanding the assignment chain — scope → role template → role → role collection → user — lets you diagnose any "access denied" quickly.
xs-security.json BTP Cockpit
────────────────── ───────────────────────
scope: bookshop.Admin ←── role-template "Admin" references it
│
──────────┘
role "Bookshop Admin" (instance of template)
│ (with optional attribute values)
role collection "Bookshop Administrators"
│
User: alice@company.com ← assigned here
+ to create a role from a template → set attribute values if anyalice@company.comFor CI/CD:
# Using BTP CLI
btp assign security/role-collection "Bookshop Admins" \
--to-user alice@company.com \
--subaccount <id>
// Alice's JWT after role collection assignment
{
"scope": [
"bookshop.Admin", // from xs-security.json scope name
"openid", // standard OIDC scope
"uaa.user" // XSUAA internal scope
]
}
// CAP strips the app prefix automatically
req.user.is('Admin') // true — checks scope includes 'bookshop.Admin'
req.user.roles // ['Admin'] — stripped of 'bookshop.' prefix
# Get Alice's JWT (password flow — dev only)
curl -s -X POST "https://<subdomain>.authentication.eu10.hana.ondemand.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=password&username=alice&password=<pass>&scope=openid" \
| jq .access_token -r \
| cut -d'.' -f2 | base64 -d 2>/dev/null | jq .scope
Or use jwt.io — paste the token, check scope array.
When CAP calls an external service:
// xs-security.json — declare what this app can grant to callers
{
"scopes": [
{
"name": "$XSAPPNAME.Integrate",
"description": "Allow external services to call this app",
"granted-apps": ["<other-app-xsappname>"] // only this app can get the scope
}
]
}
// External CAP service calling this app
const externalClient = await cds.connect.to('BookshopAPI')
// Uses client credentials with 'bookshop.Integrate' scope
// No user in req.user.id — it's a service call
req.user.is('system-user') // true for service-to-service calls
# mta.yaml
modules:
- name: bookshop-srv
parameters:
role-collections:
- name: "Bookshop Admins"
description: "Full admin access to Bookshop"
role-template-references:
- "$XSAPPNAME.Admin"
- name: "Bookshop Buyers"
description: "Purchase books"
role-template-references:
- "$XSAPPNAME.Buyer"
These role collections are created automatically on cf deploy.
bookshop.Admin@requires: 'Buyer' protected endpoint — verify you get 403 without the Buyer scopereq.user.is('RoleName') checks the JWT scope for xsappname.RoleNamereq.user has no personal infomta.yaml are created automatically on cf deploy