Skip to content

Commit a96f757

Browse files
committed
Exclude mapped body fields from user document
1 parent a46e0ad commit a96f757

2 files changed

Lines changed: 92 additions & 6 deletions

File tree

accounts/src/main/java/org/restheart/accounts/RegisterService.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@
101101
* {@code required: [...]} mandates them. Service-managed fields
102102
* ({@code _id}, {@code password}, {@code roles}, {@code profile.name},
103103
* {@code profile.surname}, {@code emailVerificationToken},
104-
* {@code emailVerificationCreatedAt}) are never overwritten by the body.
104+
* {@code emailVerificationCreatedAt}) are never overwritten by the body, and the
105+
* body fields of the mapping table above ({@code firstName}, {@code lastName},
106+
* {@code email}, {@code teamName}) are not carried over either — they already
107+
* reach the document, or the team, through that mapping.
105108
*
106109
* <p><strong>The document is validated as it is inserted, which is not its final shape.</strong>
107110
* {@code createInitialTeam} runs after the insert and adds {@code teams} and {@code team}
@@ -279,8 +282,8 @@ public void handle(JsonRequest req, JsonResponse res) throws Exception {
279282
// remaining body fields are carried into the document as-is; the schema
280283
// becomes the contract — additionalProperties:false rejects extensions,
281284
// required:[...] mandates them.
282-
// Service-managed fields (_id, password, roles, profile.name, profile.surname,
283-
// emailVerificationToken, emailVerificationCreatedAt) are never overwritten.
285+
// Service-managed fields are never overwritten, and the mapped body fields
286+
// (firstName, lastName, email, teamName) are not carried over as extras.
284287
if (db(req).hasSchema()) {
285288
mergeExtraBodyProperties(body, userDoc);
286289
}
@@ -357,12 +360,24 @@ private static String extractString(JsonObject obj, String key) {
357360
}
358361

359362
/**
360-
* Top-level fields managed by the registration service. These are never
361-
* overwritten by additional body properties.
363+
* Body keys that must not be carried into the user document as additional
364+
* properties.
365+
*
366+
* <p>Two groups: the document fields the service owns ({@code _id},
367+
* {@code password}, {@code roles}, {@code profile},
368+
* {@code emailVerificationToken}, {@code emailVerificationCreatedAt}), and the
369+
* body fields the mapping table sends somewhere else ({@code firstName} and
370+
* {@code lastName} to {@code profile.name}/{@code profile.surname},
371+
* {@code email} to {@code _id}, {@code teamName} to the team document). Without
372+
* the second group the stored document would duplicate its own mapped fields
373+
* and carry the team name, and a schema with {@code additionalProperties: false}
374+
* would reject every registration over properties the client never sent as
375+
* extras.
362376
*/
363377
private static final java.util.Set<String> SERVICE_MANAGED_FIELDS = java.util.Set.of(
364378
"_id", "password", "roles", "profile",
365-
"emailVerificationToken", "emailVerificationCreatedAt");
379+
"emailVerificationToken", "emailVerificationCreatedAt",
380+
"firstName", "lastName", "email", "teamName");
366381

367382
/**
368383
* Fields inside {@code profile} managed by the registration service.

core/src/test/java/karate/accounts/register-schema-validation.feature

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,77 @@ Feature: Apply JSON Schema on user registration
342342
When method PATCH
343343
Then assert responseStatus == 200
344344

345+
# ---------------------------------------------------------------------------
346+
Scenario: mapped body fields are not carried over as additional properties
347+
# ---------------------------------------------------------------------------
348+
# firstName/lastName/email/teamName already reach the document through the
349+
# mapping table (profile.name, profile.surname, _id) or the team document, so
350+
# they must not also land as top-level properties. additionalProperties:false
351+
# is what makes this observable: the tenant declares the document it expects,
352+
# and a leaked mapped field fails the registration it never asked for.
353+
* header Authorization = adminAuth
354+
Given path schemas
355+
And request
356+
"""
357+
{
358+
"_id": "user-no-extras",
359+
"$schema": "http://json-schema.org/draft-07/schema#",
360+
"type": "object",
361+
"additionalProperties": false,
362+
"required": ["_id", "password", "roles", "profile"],
363+
"properties": {
364+
"_id": { "type": "string" },
365+
"_etag": { "type": "object" },
366+
"password": { "type": "string" },
367+
"roles": { "type": "array" },
368+
"profile": {
369+
"type": "object",
370+
"properties": {
371+
"name": { "type": "string" },
372+
"surname": { "type": "string" }
373+
}
374+
},
375+
"consents": { "type": "object" },
376+
"emailVerificationToken": { "type": "string" },
377+
"emailVerificationCreatedAt": { "type": "object" }
378+
}
379+
}
380+
"""
381+
When method POST
382+
Then assert responseStatus == 201 || responseStatus == 409
383+
384+
* header Authorization = adminAuth
385+
Given path usersColl
386+
And request { "jsonSchema": { "schemaId": "user-no-extras" } }
387+
When method PATCH
388+
Then assert responseStatus == 200
389+
390+
* def email = 'reg-no-extras-' + java.util.UUID.randomUUID() + '@example.com'
391+
392+
Given path '/auth/register'
393+
And request { "firstName": "Schema", "lastName": "Test", "teamName": "ST Corp", "email": "#(email)", "password": "Password123!", "consents": { "terms": true } }
394+
When method POST
395+
Then status 201
396+
397+
* header Authorization = adminAuth
398+
Given path usersColl + '/' + email
399+
When method GET
400+
Then status 200
401+
# the mapped fields landed where the mapping table says, and nowhere else
402+
And match response.profile.name == 'Schema'
403+
And match response.profile.surname == 'Test'
404+
And match response.consents.terms == true
405+
And match response !contains { firstName: '#notnull' }
406+
And match response !contains { lastName: '#notnull' }
407+
And match response !contains { teamName: '#notnull' }
408+
And match response !contains { email: '#notnull' }
409+
410+
* header Authorization = adminAuth
411+
Given path usersColl
412+
And request { "jsonSchema": null }
413+
When method PATCH
414+
Then assert responseStatus == 200
415+
345416
# ---------------------------------------------------------------------------
346417
Scenario: extra body properties are dropped when no schema is configured
347418
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)