Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/CAP/Basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ POLICY SalesRepresentative {
}
```

::: tip Policy Generation
The AMS Node.js module [`@sap/ams`](https://www.npmjs.com/package/@sap/ams) can be used to [generate](/CAP/cds-Plugin#base-policy-generation) base policies from a cds model.
::: tip CAP User Object
The AMS CAP library modules automatically **extend** the current user's roles with the roles assigned through AMS policies during authentication. Afterwards, these roles are visible to the CDS framework, which then enforces the role requirements from cds `@requires` and `@restrict` annotations as usual. They are also available for programmatic role checks on the user object like any other role.
:::

### Role Policy Guidelines
### Naming Guidelines
There is no technical requirement for the policy name to match the role name. For example, policies can also be used to define higher-level business roles that assign lower-level technical roles from the cds model:
```dcl
POLICY SalesRepresentative {
Expand All @@ -33,6 +33,10 @@ POLICY SalesRepresentative {

If a policy assigns exactly one role, it is a good practice to use the same name of the role as policy name.

::: tip Policy Generation
The AMS Node.js module [`@sap/ams`](https://www.npmjs.com/package/@sap/ams) can be used to [generate](/CAP/cds-Plugin#base-policy-generation) 1 base policy per cds role from the cds model.
:::

## ASSIGN ROLE keyword
The `ASSIGN ROLE` keyword is CAP-specific syntactic sugar of DCL to abstract away from the underlying *action*/*resource* model of AMS and to allow a more intuitive way of defining policies that assign cds roles to users.

Expand Down
94 changes: 82 additions & 12 deletions docs/CAP/InstanceBasedAuthorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ This derived policy is equivalent to the policy defined in the [Motivation](#mot

## Annotating the CDS Model

Finally, via `@ams.attributes` annotations, the AMS attributes are mapped to elements (or association paths) in the cds model using compile-safe cds expressions. Whenever requests access the annotated resources, the result is filtered based on the attribute conditions computed by AMS.
Finally, via `@ams.attributes` annotations, the AMS attributes are mapped to elements (or association paths) in the cds model using compile-safe cds expressions. Whenever requests access the annotated resources, the result is filtered based on attribute conditions dynamically injected by the AMS CAP library module for the current user's roles.

```js
annotate Product with @ams.attributes: { // [!code ++:8]
Expand Down Expand Up @@ -125,11 +125,11 @@ annotate SalesOrder with @restrict: [
`ams.attributes` annotations are supported on *aspects*, *entities*, and *actions/functions bound to a single entity*. They are the [cds resources that support *where* conditions](https://cap.cloud.sap/docs/guides/security/authorization#supported-combinations-with-cds-resources).
:::

## Effect of Attribute Filters
## Dynamic Filter Injection

When the `SalesRepresentativeEUElectronics` policy is assigned to a user, the CAP modules for AMS dynamically adjusts the cds `where` condition to inject the attribute conditions from authorization policies.
When the `SalesRepresentativeEUElectronics` policy is assigned to a user, the AMS CAP library modules dynamically adjust the cds `where` conditions of the privileges during each request to inject the attribute conditions from authorization policies. The AMS module adjusts privileges temporarily for the current authorization check only; the cds model itself isn't changed for other contexts.

For example, when accessing the `SalesOrder` entity with this policy, the AMS module will add a `where` condition to the privilege for the `SalesRepresentative` role that looks like this:
For example, when accessing the `SalesOrder` entity above with the `SalesRepresentativeEUElectronics` policy, the AMS module will add a `where` condition to the second privilege (for the `SalesRepresentative` role) that looks like this:

```js
@restrict: [
Expand All @@ -141,18 +141,88 @@ For example, when accessing the `SalesOrder` entity with this policy, the AMS mo
]
```

If a user has more than one cds role that grants access to a resource, the AMS module combines the attribute conditions of all roles using `OR`. For example, if the user also has the `SalesManager` role assigned with unfiltered access, the resulting `where` condition on `Product` would effectively look like this (before being simplified by the AMS module):
::: tip
After adjusting the `where` condition, the CAP framework will enforce the attribute-based restrictions as usual. In particular, the AMS plugin is not directly responsible for `403` response codes.
:::

### Details

The AMS module computes the filter separately for each privilege whose role requirements are met and applies the filters with the following strategy.

1. **Collect the required roles.** The AMS module determines the roles required on
- *service level* (`@requires` or `@restrict` of the service)

and on
- *privilege level* (`@restrict` or `@requires` on an entity, action, or function). If the entity, action, or function has multiple privileges, each is considered separately.

On each level, only the roles that the user actually got from AMS policies are considered. Other roles of the user are ignored for the filter computation. If a level (implicitly or explicitly) grants access to the pseudo-roles `any` or `authenticated-user`, filters are only computed for the other level because this one is already fully accessible without an AMS policy.

2. **Compute a condition for each role.** For each of these roles, the AMS module computes an individual condition based on the user's policies that assign that role. The condition is either `true` (unrestricted access), `false` (no access), or an attribute condition on the elements mapped via `@ams.attributes`.

3. **Combine the roles within a level with `OR`.** One of the roles listed on each level is sufficient to satisfy the role requirement, so the conditions for these roles are combined with `OR`. For example, if the second privilege for `SalesOrder` also allowed `READ` with the `SalesManager` role and the user in our example additionally had a policy with unfiltered access for `SalesManager` assigned, the resulting `where` condition would effectively look like this (before being simplified by the AMS module):

```js
@restrict: [
{
grant: ['READ'],
to: [ 'SalesManager', 'SalesRepresentative' ],
where: "true OR (region = 'EU' AND product.category = 'Electronics')", // [!code ++]
}
]
```

4. **Combine the levels with `AND`.** The user must pass both the service level and the privilege level role annotations, so the conditions of the two levels are combined with `AND`.

5. **Combine with the static `where` condition using `AND`.** When there is already a static `where` condition on the privilege, it's combined with the AMS filter by using `AND`. If the AMS filter is `true`, the privilege remains unchanged. If it's `false`, the privilege no longer grants access.

#### Example

In the following model, the service requires the `SalesUser` role, and the privilege on `SalesOrder` has a static `where` condition:

```js
@restrict: [
annotate SalesService with @requires: 'SalesUser';

annotate SalesOrder with @restrict: [
{
grant: ['READ'],
grant: [ 'READ' ],
to: [ 'SalesManager', 'SalesRepresentative' ],
where: "true OR (region = 'EU' AND product.category = 'Electronics')", // [!code ++]
}
]
where: 'archived = false'
},
];
```

::: tip
When there is already a static *where* condition on a cds privilege, the AMS module combines the static *where* condition with the attribute conditions from authorization policies by using `AND`.
For a user who has all three roles assigned via AMS policies, the `where` condition of the privilege effectively becomes:

```js
where: "(archived = false) AND (<SalesUser filter>) AND (<SalesManager filter> OR <SalesRepresentative filter>)"
```

```mermaid
graph TD
Result["Effective where condition"] --> AndNode["AND"]
AndNode --> Static["Static where: archived = false"]
AndNode --> ServiceLevel["Service level: OR over AMS roles"]
AndNode --> PrivilegeLevel["Privilege level: OR over AMS roles"]
ServiceLevel --> SalesUser["SalesUser filter"]
PrivilegeLevel --> SalesManager["SalesManager filter"]
PrivilegeLevel --> SalesRep["SalesRepresentative filter"]
```

::: tip Mixing AMS with Non-AMS Roles
If the application makes use of additional roles that are not granted **by AMS policies**, e.g. because they are assigned by custom application logic, you should not mix them with AMS roles in cds annotations. Otherwise, the filters for the AMS role will be applied to the privilege which may not be desired if the application intended unfiltered access with the other role.

```js
// Auditor is NOT assigned via AMS policies (e.g., it comes from a custom role mapping)

// Avoid: users with both Auditor and SalesRepresentative role get the SalesRepresentative filter
annotate SalesOrder with @restrict: [
{ grant: 'READ', to: [ 'SalesRepresentative', 'Auditor' ] },
];

// Prefer: Auditor grants full access, while SalesRepresentative role on its own is still filtered by AMS
annotate SalesOrder with @restrict: [
{ grant: 'READ', to: 'SalesRepresentative' },
{ grant: 'READ', to: 'Auditor' },
];
```
:::
Loading