What this is about
Some data belongs to a single person: notes, drafts, personal settings. That is what user models (level USER) are for. Every row of a user model has a system field _userId, the ID of the person it belongs to.
CDMS adds a condition to every request on a user model: _userId = signed-in person. This condition is called the owner filter. You do not see it in your request and cannot switch it off.
When to put a model on the USER level is described in Model levels: system, tenant, user.
Two people, one table
flowchart LR
subgraph T["Table note in the tenant's database"]
R1["Shopping list<br/>_userId = anna"]
R2["Holiday plan<br/>_userId = anna"]
R3["Project ideas<br/>_userId = ben"]
end
A(["Anna: POST /note/query"]) --> F1{"_userId = anna"}
B(["Ben: POST /note/query"]) --> F2{"_userId = ben"}
F1 --> R1
F1 --> R2
F2 --> R3
Both send the same search. Anna gets two hits, Ben gets one. totalCount counts only their own rows in each case.
Where the ID comes from
The person’s ID is in the token, by default in the claim sub. CIAS reads it on every request and passes it on to CDMS. The client never sends it itself.
Variants
When: POST /note/create
-
1Client→CDMSsends the note, with or without
_userId -
2CDMSsets
_userIdto the ID of the signed-in person; a value sent along does not count -
3CDMS→Databasestores the note
Result: The note always belongs to the person who creates it.
When: POST /note/read/{id}
-
1CDMS→Databasereads the row with
idand_userId = signed-in person -
2CDMSno row found → 404
not-found
Result: Someone else's note looks like one that does not exist.
When: POST /note/query
CDMS combines your filters with AND with _userId = signed-in person. Other people's rows are missing from data and do not count in totalCount.
Result: Lists in a response, e.g. the notes on another object, also show only your own rows.
When: PUT, PATCH, DELETE, rollback
-
1CDMS→Databasecounts the row with
idand_userId = signed-in person -
2CDMS0 → 404
not-found|<Dto>|<id>, even before the role check -
3CDMS1 → continues as usual;
_userIdstays unchanged, even if the client sends a different value
Result: Nobody can take over another person's row or push a row onto someone.
When: A create also creates objects of a user model through a relation.
Every object of a user model created along with it also gets the ID of the signed-in person as _userId.
Result: Everything a request creates belongs to the same person.
Decision table
| _userId of the note | role for the operation | Result |
|---|---|---|
| anna | yes | allowed |
| anna | no | 403 missing-permission|<role> |
| ben | – | read, change, delete: 404; search: missing from the list |
Technical clients
A server that signs in with client credentials has no real person. The token then contains the ID of its service account. For the owner filter, the service account is a person like any other:
- What the server creates belongs to the service account.
- The server sees only rows the service account created itself, no rows of real people.
If a service is supposed to work on data of real people, a user model is usually the wrong choice. Use a tenant model instead and restrict it with an attribute filter or a custom filter.
On behalf of another person
CDMS has no role that lifts the owner filter, not even for administrators. If someone has to see the data of a specific person, they use the user switch of CIAS: whoever has the realm role allowed-user-context-switch sends the other person’s ID in the header user. The person must exist, must belong to the tenant if the request runs under one, and must have consented to the switch. Otherwise CIAS answers with 403. The owner filter then works with their ID and shows exactly their rows. Whether your roles or theirs apply is chosen with the header user-roles. More under User switch by header.
Pitfalls
Where to go next
- Visibility by a profile attribute instead of by person: Attribute filter
- Why other people’s rows return 404: Why invisible objects return 404
- All filters of a search at a glance: Filters that always run along