fix: registration fills only an explicit allow-list and never takes the IP columns from the body
User.RegisterFillable lists what a registration body may set: name, surname, email, password, preferred_locale and marketing_consent. RegisterUser fills from it instead of Fillable, so organisation_id, organisation_role, is_onboarded and must_change_password can no longer be mass-assigned through POST /register, and created_ip_address and last_ip_address come only from RegisterOptions.IP. Fillable is unchanged for the admin form and trusted application code.
This commit is contained in:
@@ -16,7 +16,7 @@ It owns the `users` table extensions, `user_throttle`, `jwt_blacklist`, `user_ap
|
||||
|
||||
## Features
|
||||
|
||||
- **Registration and activation.** `POST /register`, `POST /activate`, `POST /activate-by-code`. Registration can be switched off and throttled; `activate_mode` is `auto`, `user` (a mailed code) or `admin`, with codes from `IssueActivationCode` and `VerifyActivationCode`.
|
||||
- **Registration and activation.** `POST /register`, `POST /activate`, `POST /activate-by-code`. Registration can be switched off and throttled; `activate_mode` is `auto`, `user` (a mailed code) or `admin`, with codes from `IssueActivationCode` and `VerifyActivationCode`. A `/register` body sets only the `User.RegisterFillable` columns: `name`, `surname`, `email`, `password` (stored as its hash), `preferred_locale` and `marketing_consent`. It cannot set `organisation_id` or `organisation_role` (membership is the administrator's or the application's), `is_onboarded`, `must_change_password`, or the activation, reset, address proof, handle and permission columns; other keys are ignored (they still reach `RegisterEvent.Payload`). `created_ip_address` and `last_ip_address` are server-set from the client address only.
|
||||
- **JWT sessions.** `POST /login`, `GET /fetch`, `POST /refresh`, `POST /logout`. Boot registers the `jwt` guard with the framework's `bouncer.Registry` and exposes it as the `jwt.auth` middleware for other plugins. Logged-out tokens go to the Postgres `jwt_blacklist`, which a background sweep prunes.
|
||||
- **Password reset and change.** `POST /forgot-password` answers the same body for known and unknown addresses; `POST /reset-password` consumes a code from `IssueResetCode`. `POST /change-password` requires the current password.
|
||||
- **Must-change-password lock.** The `user:require-password-change` console command flags an account. The flag rides on the authenticated principal (`bouncer.Principal.MustChangePassword`) for guards and other plugins to act on, and a successful `POST /change-password` clears it.
|
||||
@@ -168,7 +168,7 @@ Migrations added for address proof, all additive:
|
||||
Packages:
|
||||
|
||||
- `controllers`: the route handlers above, `AdminControllers` (the admin controller list), `PermissionAccessUsers` and `PermissionAccessGroups`, plus the registration steps `POST /register` runs, for other plugins:
|
||||
- `RegisterUser(ctx, app, db, fields, RegisterOptions)` validates `fields` against the user model's rules, hashes the password with `golem15.user.password.bcrypt_cost` and creates the user on `db` (which may be a transaction). It returns the validation errors instead of writing a response and never modifies `fields`. `RegisterOptions.Activate` creates the account activated; `RegisterOptions.IP` records the client IP; `RegisterOptions.PasswordLess` stores `has_self_set_password=false` for an account whose random password the user never knew (a social sign-in), while the zero value keeps `true`.
|
||||
- `RegisterUser(ctx, app, db, fields, RegisterOptions)` validates `fields` against the user model's rules, hashes the password with `golem15.user.password.bcrypt_cost` and creates the user on `db` (which may be a transaction). It fills only the `User.RegisterFillable` columns and ignores every other key of `fields`: an organisation membership and role are set with an explicit update after creation, as an application's onboarding does. It returns the validation errors instead of writing a response and never modifies `fields`. `RegisterOptions.Activate` creates the account activated; `RegisterOptions.IP` records the client IP and is the only source of `created_ip_address` and `last_ip_address` (both stay NULL without it, whatever `fields` says); `RegisterOptions.PasswordLess` stores `has_self_set_password=false` for an account whose random password the user never knew (a social sign-in), while the zero value keeps `true`.
|
||||
- `FireRegisterEvent(ctx, app, user, fields)` fires `RegisterEvent` with `Payload` set to a copy of `fields` without `password` and `password_confirmation`, and returns the listeners' error (`/register` ignores it). Fire it after the user row is committed.
|
||||
- `FireCreatedEvent(ctx, app, user)` fires `CreatedEvent` (`user.Created`). `/register` ignores that error.
|
||||
- `FireDeletingEvent(ctx, app, user, tx)` fires `DeletingEvent` (`user.Deleting`) inside the delete transaction.
|
||||
@@ -188,7 +188,7 @@ Packages:
|
||||
- `AvatarURL(ctx, app, user, size)`, the display picture rule of the user payload at any size: the avatar thumbnail (size by size, mode auto) when the user has an avatar and a file bucket is published, the file path when the thumbnail fails, otherwise the gravatar URL. The URL may be protocol-relative or relative. The payload's `avatar_url` comes from it at 128.
|
||||
- `HandleGenerator`, `AssignHandle(ctx, db, user)`, `CreatedEvent` / `CreatedEventName`, `DeletingEvent` / `DeletingEventName`, `DeactivatingEvent` / `DeactivatingEventName`, `AuthorizeUser` and `RegisterUserAuthorizer`.
|
||||
- `middleware`: `APITokenGuard`, `Scope`, `Permission` and `BearerToken`.
|
||||
- `models`: `User`, `Throttle`, `Organisation`, `UserGroup`, `UsersGroup` (the `users_groups` pivot), `APIToken`, `OAuthIdentity`, `FrontendPermission` and `PermissionSet`. `PermissionSet` reads a stored permission column tolerantly (NULL, an empty string, `[]`, numeric strings) and writes a JSON object of integers; `ParsePermissionSet` and `PermissionSet.Text` do the same for a column that is a plain string on its model. `UserGroup.UsersCount` is filled only by a query that selects `users_count` (the admin list does). `Organisation.Members` is the relation behind the members manager, `Organisation.AttachRelations` declares its avatar (`OrganisationAvatarField`), and `Slugify` is the slug rule. `User.FilterScope` answers the admin list scope `FilterByGroup`, and `User.AttachRelations` declares the avatar (`AvatarField`).
|
||||
- `models`: `User`, `Throttle`, `Organisation`, `UserGroup`, `UsersGroup` (the `users_groups` pivot), `APIToken`, `OAuthIdentity`, `FrontendPermission` and `PermissionSet`. `PermissionSet` reads a stored permission column tolerantly (NULL, an empty string, `[]`, numeric strings) and writes a JSON object of integers; `ParsePermissionSet` and `PermissionSet.Text` do the same for a column that is a plain string on its model. `UserGroup.UsersCount` is filled only by a query that selects `users_count` (the admin list does). `Organisation.Members` is the relation behind the members manager, `Organisation.AttachRelations` declares its avatar (`OrganisationAvatarField`), and `Slugify` is the slug rule. `User.FilterScope` answers the admin list scope `FilterByGroup`, and `User.AttachRelations` declares the avatar (`AvatarField`). `User.RegisterFillable` is the registration allow-list; `User.Fillable` is the wider mass-assignment backstop of the admin Users form (cabana intersects it with the form's fields) and of trusted application code.
|
||||
- `updates`: the gormigrate migrations, returned by `updates.All()`.
|
||||
- `console`: `RequirePasswordChange`, `BackfillHandles` and `APITokenCommands`.
|
||||
|
||||
@@ -248,4 +248,6 @@ go test ./... # full run, starts Postgres with testcontainers (needs D
|
||||
|
||||
`TestCommandsDocumented` (in `-short`) logs, without failing, each command whose help lacks a description, long help, examples or an option description (`bonfire.CheckDocs`).
|
||||
|
||||
`TestRegisterIgnoresProtectedFields` posts organisation, IP and account-flag keys to `/register` and `RegisterUser` and checks that only the allow-listed columns and the server-side IP are stored.
|
||||
|
||||
The admin tests (`TestAdmin*`) boot the plugin with the admin API mounted on a database of their own and sign in backend administrators with a chosen permission set; the helpers are in `admin_harness_test.go`.
|
||||
|
||||
Reference in New Issue
Block a user