Skip to content

Commit ed3ccbf

Browse files
committed
Add admin exemption for user write restriction tests
1 parent 3806b1d commit ed3ccbf

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
Feature: /users self-service write restriction (accountsInitializer veto)
2+
3+
# accountsInitializer registers a veto that unconditionally restricts what the generic
4+
# MongoDB REST resource at /users can be used for, regardless of any ACL permission:
5+
# - PUT/POST are always rejected.
6+
# - PATCH is only allowed if every touched field is under profile.* (dot notation and
7+
# update operators like $set/$push count too).
8+
# - Roles listed in accountsConfig.users-unrestricted-roles bypass the restriction
9+
# entirely — 'admin' is configured as exempt for this test env (conf-overrides.yml).
10+
#
11+
# The ACL rules seeded below grant broad access (no field restriction) to the caller's
12+
# own /users/{id} document, so any rejection observed here is caused by the veto, not by
13+
# the ACL itself.
14+
#
15+
# NOTE: the JWT's top-level `roles` claim is restheart-accounts's *system* ACL role,
16+
# which is "user" for every accounts-managed account regardless of team standing —
17+
# "owner"/"member" only exist in user.teams[].role, never in the JWT `roles` array. The
18+
# ACL rules below must therefore target role "user", not "member".
19+
20+
Background:
21+
* url baseUrl
22+
* configure followRedirects = false
23+
* def setupResult = karate.call('classpath:karate/accounts/helpers/setup-owner.feature')
24+
* def ownerJwt = setupResult.ownerJwt
25+
26+
# ACL: role "user" can PUT/POST/PATCH its own /users/{id} document, no field
27+
# restriction — isolates the veto as the only thing that can reject these calls.
28+
Given path '/restheart-test/acl'
29+
And param wm = "upsert"
30+
And header Authorization = adminAuth
31+
And request
32+
"""
33+
[
34+
{
35+
"_id": { "$oid": "000000000000000000000001" },
36+
"predicate": "path-template('/users/{userId}') and (method(PUT) or method(PATCH)) and (equals(@user._id, ${userId}) or equals(@user.sub, ${userId}))",
37+
"roles": ["user"],
38+
"priority": 1
39+
},
40+
{
41+
"_id": { "$oid": "000000000000000000000002" },
42+
"predicate": "path-template('/restheart-test/users/{userId}') and method(PATCH) and (equals(@user._id, ${userId}) or equals(@user.sub, ${userId}))",
43+
"roles": ["user"],
44+
"priority": 1
45+
},
46+
{
47+
"_id": { "$oid": "000000000000000000000003" },
48+
"predicate": "path('/users') and method(POST)",
49+
"roles": ["user"],
50+
"priority": 1
51+
}
52+
]
53+
"""
54+
When method POST
55+
Then status 200
56+
57+
# Register + activate a fresh "member" via invite, get a Bearer JWT for it
58+
* def memberEmail = 'uwr-' + java.util.UUID.randomUUID() + '@example.com'
59+
60+
Given path '/auth/invite'
61+
And header Authorization = 'Bearer ' + ownerJwt
62+
And request { "email": "#(memberEmail)", "role": "member" }
63+
When method POST
64+
Then status 201
65+
66+
* def tokenResult = karate.call('classpath:karate/accounts/helpers/get-invite-token.feature', { email: memberEmail })
67+
* def inviteToken = tokenResult.result
68+
69+
Given path '/auth/activate'
70+
And request { "email": "#(memberEmail)", "token": "#(inviteToken)", "password": "MemberPass1!" }
71+
When method PATCH
72+
Then status 200
73+
* def memberJwt = responseHeaders['Set-Cookie'][0].split('Bearer_')[1].split(';')[0]
74+
75+
# ---------------------------------------------------------------------------
76+
Scenario: PATCH profile.* is allowed
77+
# ---------------------------------------------------------------------------
78+
Given path '/users/' + memberEmail
79+
And header Authorization = 'Bearer ' + memberJwt
80+
And request { "profile.name": "Updated Name" }
81+
When method PATCH
82+
Then status 200
83+
84+
# ---------------------------------------------------------------------------
85+
Scenario: PATCH with dot notation on teams.*.role is rejected
86+
# ---------------------------------------------------------------------------
87+
Given path '/users/' + memberEmail
88+
And header Authorization = 'Bearer ' + memberJwt
89+
And request { "teams.0.role": "owner" }
90+
When method PATCH
91+
Then status 403
92+
93+
# ---------------------------------------------------------------------------
94+
Scenario: PATCH with $set update operator on teams.*.role is rejected
95+
# ---------------------------------------------------------------------------
96+
Given path '/users/' + memberEmail
97+
And header Authorization = 'Bearer ' + memberJwt
98+
And request { "$set": { "teams.0.role": "owner" } }
99+
When method PATCH
100+
Then status 403
101+
102+
# ---------------------------------------------------------------------------
103+
Scenario: PATCH with $push on teams is rejected
104+
# ---------------------------------------------------------------------------
105+
Given path '/users/' + memberEmail
106+
And header Authorization = 'Bearer ' + memberJwt
107+
And request { "$push": { "teams": { "id": { "$oid": "000000000000000000000099" }, "role": "member" } } }
108+
When method PATCH
109+
Then status 403
110+
111+
# ---------------------------------------------------------------------------
112+
Scenario: PATCH with roles field is rejected
113+
# ---------------------------------------------------------------------------
114+
Given path '/users/' + memberEmail
115+
And header Authorization = 'Bearer ' + memberJwt
116+
And request { "roles": ["admin"] }
117+
When method PATCH
118+
Then status 403
119+
120+
# ---------------------------------------------------------------------------
121+
Scenario: PUT is always rejected, even with only profile.* in the body
122+
# ---------------------------------------------------------------------------
123+
Given path '/users/' + memberEmail
124+
And header Authorization = 'Bearer ' + memberJwt
125+
And request { "profile": { "name": "Full Replace" } }
126+
When method PUT
127+
Then status 403
128+
129+
# ---------------------------------------------------------------------------
130+
Scenario: POST /users is always rejected
131+
# ---------------------------------------------------------------------------
132+
* def newEmail = 'uwr-post-' + java.util.UUID.randomUUID() + '@example.com'
133+
134+
Given path '/users'
135+
And header Authorization = 'Bearer ' + memberJwt
136+
And request { "_id": "#(newEmail)", "profile": { "name": "New" }, "roles": ["user"] }
137+
When method POST
138+
Then status 403
139+
140+
# ---------------------------------------------------------------------------
141+
# Same restriction applies when the users collection is reached through a
142+
# different mongo-mounts alias (/restheart-test/users instead of /users) —
143+
# the veto matches the resolved collection, not the request path.
144+
Scenario: PATCH via an alternate mongo-mounts alias for the same collection is also rejected
145+
# ---------------------------------------------------------------------------
146+
Given path '/restheart-test/users/' + memberEmail
147+
And header Authorization = 'Bearer ' + memberJwt
148+
And request { "$set": { "teams.0.role": "owner" } }
149+
When method PATCH
150+
Then status 403
151+
152+
# ---------------------------------------------------------------------------
153+
# 'admin' is configured as exempt in conf-overrides.yml — the same PATCH that
154+
# is rejected for "member" above succeeds here.
155+
Scenario: users-unrestricted-roles bypasses the restriction entirely
156+
# ---------------------------------------------------------------------------
157+
Given path '/users/' + memberEmail
158+
And header Authorization = adminAuth
159+
And request { "$set": { "teams.0.role": "owner" } }
160+
When method PATCH
161+
Then status 200

core/src/test/resources/etc/conf-overrides.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,10 @@
400400
/accountsConfig/frontend-app-url: http://localhost:4200/app
401401
/accountsConfig/terms-version: "1.0"
402402
/accountsConfig/privacy-version: "1.0"
403+
# 'admin' is exempt from accountsInitializer's /users self-service write restriction —
404+
# used by users-write-restriction.feature to test the bypass path.
405+
/accountsConfig/users-unrestricted-roles:
406+
- admin
403407

404408
/accountsService/enabled: true
405409
/accountsInitializer/enabled: true

0 commit comments

Comments
 (0)