From 4f211c2f9f0948b88f610c8f4e68d5c02526b9d6 Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 21 Jul 2026 12:32:58 +0200 Subject: [PATCH 1/8] Node.js: clarify instance-based auth --- guides/security/authorization.md | 42 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index 3b64d1331..2f8722c1e 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -257,14 +257,22 @@ Here, users can read and write orders they've created, and `Auditor` users can r Restrictions can be defined on different types of CDS resources, but there are some limitations with regards to supported privileges: -| CDS Resource | `grant` | `to` | `where` | Remark | -|-----------------|:-------:|:----:|:-----------------:|---------------| -| service | | | | = `@requires` | -| entity | | | 1 | | -| action/function | | | 2 | = `@requires` | - -> 1For bound actions and functions that are not bound against a collection, Node.js supports instance-based authorization at the entity level. For example, you can use `where` clauses that *contain references to the model*, such as `where: CreatedBy = $user`. For all bound actions and functions, Node.js supports simple static expressions at the entity level that *don't have any reference to the model*, such as `where: $user.level = 2`. -> 2 For unbound actions and functions, Node.js supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. +| CDS Resource | `grant` | `to` | `where` | Remark | +|-----------------------|:-------:|:----:|:-----------------:|---------------| +| service | | | | = `@requires` | +| entity | | | | | +| bound action/function | | | 1 | = `@requires` | +| action/function | | | 2 | = `@requires` | + +> 1 For [bound actions and functions](../../cds/cdl#bound-actions) that *are not bound to a collection of instances*, Node.js supports instance-based authorization. +> Example: +> ```cds +> entity Orders @(restrict: [ +> { grant: 'cancel', where: (CreatedBy = $user) }, +> ]) {/*...*/} +> ``` + +> 2 For (unbound) actions and functions, Node.js supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. Unsupported privilege properties are ignored by the runtime. Especially, for bound or unbound actions, the `grant` property is implicitly removed (assuming `grant: '*'` instead). The same also holds for functions: @@ -314,7 +322,7 @@ The resulting authorizations are illustrated in the following access matrix: | `CustomerService.Orders` (*) | | 1 | | | | `CustomerService.monthlyBalance` | | | | | -> 1 A `Vendor` user can only access the instances that they created.
+> 1 A `Customer` user can only access the instances that they created.
The example models access rules for different roles in the same service. In general, this is _not recommended_ due to the high complexity. See [best practices](#dedicated-services) for information about how to avoid this. @@ -440,13 +448,11 @@ This means that, the condition applies to following standard CDS events only: - `UPDATE` (as reject condition) - `DELETE` (as reject condition) -
- -In addition, the runtime [checks the filter condition of the input data](#input-data-auth) for following standard CDS events: +The Java runtime additionally [checks the filter condition of the input data](#input-data-auth) for following standard CDS events: - `CREATE` (input filter) - `UPDATE` (input filer) -
+The Node.js runtime, on the other hand, supports simple static expressions that *don't have any reference to the model* (e.g., `where: $user.level = 2`) for `CREATE` as well as unbound actions and functions. You can define filter conditions in the `where`-clause of restrictions based on [CQL](/cds/cql)-predicates, declared as [compiler expressions](../../cds/cdl#expressions-as-annotation-values): @@ -609,12 +615,13 @@ Paths on 1:n associations (`Association to many`) evaluate to `true`, _if the co
+// TODO: Node.js does not support his?
-### Checking Input Data { #input-data-auth .java} +### Checking Input Data (Java only) { #input-data-auth } Input data of `CREATE` and `UPDATE` events is also validated with regards to instance-based authorization conditions. Invalid input that does not meet the condition is rejected with response code `400`. @@ -633,7 +640,12 @@ Starting with CAP Java `4.0`, deep authorization is active by default. It can be disabled by setting cds.security.authorization.instanceBased.checkInputData: false. -### Rejected Entity Selection { #reject-403 .java} +### Simple Static Checks (Node.js only) { #simple-static-checks } + +TODO + + +### Rejected Entity Selection { #reject-403 } //> TODO: Node.js? Entities that have an instance-based authorization condition, that is [`@restrict.where`](/guides/security/authorization#restrict-annotation), are guarded by the CAP Java runtime by adding a filter condition to the DB query **excluding not matching instances from the result**. From cf490c17128cc6a93a07f5d2bfd1cea8f40cb466 Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 21 Jul 2026 13:03:43 +0200 Subject: [PATCH 2/8] restore toggle --- guides/security/authorization.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index 2f8722c1e..ce9294856 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -448,11 +448,19 @@ This means that, the condition applies to following standard CDS events only: - `UPDATE` (as reject condition) - `DELETE` (as reject condition) -The Java runtime additionally [checks the filter condition of the input data](#input-data-auth) for following standard CDS events: +
+ +In addition, the Java runtime [checks the filter condition of the input data](#input-data-auth) for following standard CDS events: - `CREATE` (input filter) - `UPDATE` (input filer) -The Node.js runtime, on the other hand, supports simple static expressions that *don't have any reference to the model* (e.g., `where: $user.level = 2`) for `CREATE` as well as unbound actions and functions. +
+ +
+ +In addition, for `CREATE` as well as unbound actions and functions, the Node.js runtime supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. + +
You can define filter conditions in the `where`-clause of restrictions based on [CQL](/cds/cql)-predicates, declared as [compiler expressions](../../cds/cdl#expressions-as-annotation-values): From c110fde24f28cf299fa63a30c95ccfed50b517be Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 21 Jul 2026 13:05:48 +0200 Subject: [PATCH 3/8] more toggles --- guides/security/authorization.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index ce9294856..1e57b709d 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -629,7 +629,7 @@ Paths on 1:n associations (`Association to many`) evaluate to `true`, _if the co
-### Checking Input Data (Java only) { #input-data-auth } +### Checking Input Data { #input-data-auth .java} Input data of `CREATE` and `UPDATE` events is also validated with regards to instance-based authorization conditions. Invalid input that does not meet the condition is rejected with response code `400`. @@ -648,12 +648,13 @@ Starting with CAP Java `4.0`, deep authorization is active by default. It can be disabled by setting cds.security.authorization.instanceBased.checkInputData: false. -### Simple Static Checks (Node.js only) { #simple-static-checks } +### Simple Static Checks { #simple-static-checks .node} TODO -### Rejected Entity Selection { #reject-403 } //> TODO: Node.js? +//> TODO: Node.js? +### Rejected Entity Selection { #reject-403 .java} Entities that have an instance-based authorization condition, that is [`@restrict.where`](/guides/security/authorization#restrict-annotation), are guarded by the CAP Java runtime by adding a filter condition to the DB query **excluding not matching instances from the result**. From 9ebc788f9bf762da109b842b8b41c68edeae9f17 Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Tue, 21 Jul 2026 13:23:39 +0200 Subject: [PATCH 4/8] Simple Static Checks --- guides/security/authorization.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index 1e57b709d..c78099e6d 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -650,7 +650,16 @@ It can be disabled by setting cds.security.authorization.instanceBa ### Simple Static Checks { #simple-static-checks .node} -TODO +Most instance-based [`@restrict.where`](#restrict-annotation) conditions reference business data (for example, `where: 'createdBy = $user'`) and can only be enforced against persisted data — pushed into the query for `READ`, or verified with a `COUNT` for `UPDATE`/`DELETE`. + +Some conditions, though, reduce to a plain comparison of literals once [user attributes](#user-attrs) are resolved: + +```cds +entity Reviews @(restrict: [ + { grant: 'CREATE', where: '$user.level >= 2' } ]); +``` + +For a user with `level = 3`, this becomes `3 >= 2`, which the runtime evaluates in memory — granting or rejecting with `403` without any database access. Such _simple static checks_ apply to `CREATE` (and its draft variant `NEW`) and to unbound actions and functions, where there's no persisted instance to query. They're only recognized for a single binary comparison (`=`, `!=`, `<`, `<=`, `>`, `>=`) with no reference to entity elements. //> TODO: Node.js? From 2b26bee4ea33fc47f2812eff13b4d3e6241c821a Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:26:10 +0200 Subject: [PATCH 5/8] Apply suggestion from @sjvans --- guides/security/authorization.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index c78099e6d..fdd7021d0 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -662,8 +662,7 @@ entity Reviews @(restrict: [ For a user with `level = 3`, this becomes `3 >= 2`, which the runtime evaluates in memory — granting or rejecting with `403` without any database access. Such _simple static checks_ apply to `CREATE` (and its draft variant `NEW`) and to unbound actions and functions, where there's no persisted instance to query. They're only recognized for a single binary comparison (`=`, `!=`, `<`, `<=`, `>`, `>=`) with no reference to entity elements. -//> TODO: Node.js? -### Rejected Entity Selection { #reject-403 .java} +### Rejected Entity Selection { #reject-403 } Entities that have an instance-based authorization condition, that is [`@restrict.where`](/guides/security/authorization#restrict-annotation), are guarded by the CAP Java runtime by adding a filter condition to the DB query **excluding not matching instances from the result**. From 2da4a155847cdcdf6331a0f9a99aea5de2b27b35 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:26:35 +0200 Subject: [PATCH 6/8] Apply suggestion from @sjvans --- guides/security/authorization.md | 1 - 1 file changed, 1 deletion(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index fdd7021d0..7de78ef34 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -623,7 +623,6 @@ Paths on 1:n associations (`Association to many`) evaluate to `true`, _if the co
-// TODO: Node.js does not support his?
From b125dd6989371595069d7e4d5435e3a20c5da76f Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Fri, 14 Aug 2026 02:25:29 +0200 Subject: [PATCH 7/8] docs(auth): static @restrict.where on collection-bound actions + Node reject-403 - Simple static checks now also apply to collection-bound actions in Node.js (cap/cds#6570), alongside CREATE/NEW and unbound actions/functions. - Rejected Entity Selection (single-entity READ -> 404, UPDATE/DELETE -> 403) is no longer Java-only; Node.js aligns (verified in cds-compliance#46). The CAP Java version/config note is scoped to an impl-java block. --- guides/security/authorization.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index 7de78ef34..151ef800a 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -272,7 +272,7 @@ Restrictions can be defined on different types of CDS resources, but there are s > ]) {/*...*/} > ``` -> 2 For (unbound) actions and functions, Node.js supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. +> 2 For (unbound) actions and functions — as well as actions and functions bound to a *collection* of instances — Node.js supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. Unsupported privilege properties are ignored by the runtime. Especially, for bound or unbound actions, the `grant` property is implicitly removed (assuming `grant: '*'` instead). The same also holds for functions: @@ -458,7 +458,7 @@ In addition, the Java runtime [checks the filter condition of the input data](#i
-In addition, for `CREATE` as well as unbound actions and functions, the Node.js runtime supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. +In addition, for `CREATE` as well as unbound actions and functions and actions and functions bound to a *collection* of instances, the Node.js runtime supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`.
@@ -658,25 +658,29 @@ entity Reviews @(restrict: [ { grant: 'CREATE', where: '$user.level >= 2' } ]); ``` -For a user with `level = 3`, this becomes `3 >= 2`, which the runtime evaluates in memory — granting or rejecting with `403` without any database access. Such _simple static checks_ apply to `CREATE` (and its draft variant `NEW`) and to unbound actions and functions, where there's no persisted instance to query. They're only recognized for a single binary comparison (`=`, `!=`, `<`, `<=`, `>`, `>=`) with no reference to entity elements. +For a user with `level = 3`, this becomes `3 >= 2`, which the runtime evaluates in memory — granting or rejecting with `403` without any database access. Such _simple static checks_ apply to `CREATE` (and its draft variant `NEW`), to unbound actions and functions, and to actions and functions bound to a *collection* of instances — everywhere there's no single persisted instance to query. They're only recognized for a single binary comparison (`=`, `!=`, `<`, `<=`, `>`, `>=`) with no reference to entity elements. ### Rejected Entity Selection { #reject-403 } Entities that have an instance-based authorization condition, that is [`@restrict.where`](/guides/security/authorization#restrict-annotation), -are guarded by the CAP Java runtime by adding a filter condition to the DB query **excluding not matching instances from the result**. +are guarded by the runtime by adding a filter condition to the DB query **excluding not matching instances from the result**. Hence, if the user isn't authorized to query an entity, requests targeting a *single* entity return *404 - Not Found* response and not *403 - Forbidden*. -To allow the UI to distinguish between *not found* and *forbidden*, CAP Java can detect this situation and rejects `UPDATE` and `DELETE` requests to single entities with forbidden accordingly. +To allow the UI to distinguish between *not found* and *forbidden*, the runtime detects this situation and rejects `UPDATE` and `DELETE` requests to single entities with forbidden accordingly. The additional authorization check might affect performance. ::: warning Avoid enumerable keys To avoid disclosure of the existence of such entities to unauthorized users, make sure that the key is not efficiently enumerable or add custom code to overrule the default behavior otherwise. ::: +
+ Starting with CAP Java `4.0`, the reject behaviour is active by default. It can be disabled by setting cds.security.authorization.instance-based.reject-selected-unauthorized-entity.enabled: false. +
+ ## Limitations {.node} From 812fc7a080dc7062da549589a5ccc16b528ee32c Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:41:42 +0200 Subject: [PATCH 8/8] Apply suggestion from @sjvans --- guides/security/authorization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/security/authorization.md b/guides/security/authorization.md index 151ef800a..82710d9d2 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -272,7 +272,7 @@ Restrictions can be defined on different types of CDS resources, but there are s > ]) {/*...*/} > ``` -> 2 For (unbound) actions and functions — as well as actions and functions bound to a *collection* of instances — Node.js supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. +> 2 For actions and functions that are either unbound or bound to a *collection* of instances, Node.js supports simple static expressions that *don't have any reference to the model*, such as `where: $user.level = 2`. Unsupported privilege properties are ignored by the runtime. Especially, for bound or unbound actions, the `grant` property is implicitly removed (assuming `grant: '*'` instead). The same also holds for functions: