399 lines
27 KiB
Gherkin
399 lines
27 KiB
Gherkin
Feature: Roles and permissions enforcement: permission checks replace the owner and admin gates
|
|
|
|
As the owner of a RenewCMS site
|
|
I want every owner and admin gate replaced by a check of the caller's role permissions
|
|
So that built-in roles are enforced by what they hold, API keys follow their creator's live role,
|
|
and later slices can add custom roles without touching the routes again
|
|
|
|
Background:
|
|
Given the spec of record is docs/design/screens/roles.md (catalogue, "Built-in roles", "HTTP status to copy", section 7)
|
|
And features/roles_permissions_foundation.feature provides the catalogue, the Owner (34), Admin (29) and Writer (9) roles,
|
|
users.role_id, the legacy fallback and GetMyAccessUseCase
|
|
And "ok" in a table means the request passes the permission gate. A later 400, 404 or 409 from the handler is not a gate failure
|
|
And "403" in a table means status 403, body "missing_permission\n" (http.Error, plain text, like the other snake_case codes
|
|
in api/pkg/apperr) and the use case or handler body is not invoked
|
|
And the code is apperr.MissingPermissionErrorMessage = "missing_permission"
|
|
And the response carries the header X-Missing-Permission with the catalogue key that failed, so the SPA can word
|
|
"Your role doesn't include {permission name}"
|
|
And api/middleware gains RequirePermission(key) for on/off keys and for the minimum "not none" check of a scoped key. It uses
|
|
a use case that depends on domain interfaces only. It resolves the caller's role live on every request, with no cache
|
|
And a request that fails the gate answers only after the existing steps: authtoken.Authenticator, RequireCurrentToken (401),
|
|
RequireVerifiedUser (403 verification), RequireSession (403 session_required for an API key) and RejectAPIKey (401)
|
|
And RequireOwner, RequireOwnerOrAdmin, apperr.OwnerRequiredErrorMessage and apperr.OwnerOrAdminRequiredErrorMessage are removed
|
|
And no use case, handler or middleware reads user.Role for an authorization decision. user.Role stays only for the
|
|
role_id fallback, for CreateUser and AcceptInvite assigning it, for ListUsersAndInvites output and for migrations
|
|
And a scoped decision is taken from the caller's resolved grants. Use cases receive them, not user.Role
|
|
And the role is resolved as in GetMyAccessUseCase: role_id first, else the built-in named by the legacy string
|
|
And a role that cannot be resolved (role_id unknown, or no role_id and a legacy string other than "owner" or "admin")
|
|
is refused with 403 missing_permission on every gated route. A repository failure other than role.ErrRoleNotFound
|
|
answers 500 "Internal server error"
|
|
And enforcement honours any grant it finds, including an owner-only key placed on a custom role by hand. Refusing to
|
|
grant owner-only keys belongs to slice 3 (ValidateGrants at write time)
|
|
(superseded by roles_permissions_content_scope: owner-only keys are effective only on a locked role, so a hand-placed one is ignored)
|
|
And the users, tokens and cookies are the ones of the foundation feature: user 3 Owner, user 5 Admin, user 9 Writer,
|
|
all verified with a valid jwt cookie and token_version 0
|
|
And routes with no obvious catalogue key reuse existing keys: media rename = media.upload; purge settings GET = settings.view,
|
|
PATCH = settings.edit_general; purge preview and run = media.delete at scope all (own fails closed, images record no creator);
|
|
per-image variant mutations and image-variants configuration = settings.edit_general; media variants GET and image-variants
|
|
list = media.view; image-variants cache stats = settings.view; builtin types GET = schema.view
|
|
And GET /v1/site stays open to any verified user
|
|
And settings.manage_sites covers PATCH /v1/site/{id} including site SEO defaults, so Admin may edit them
|
|
|
|
Scenario: A refused request carries the missing_permission contract
|
|
Given user 9 (Writer)
|
|
When the client sends POST /v1/user/invite with body {"email": "new@example.com"} and user 9's cookie
|
|
Then the response status is 403 and the body is "missing_permission\n"
|
|
And the header X-Missing-Permission is "users.invite"
|
|
And no invite is created and no mail is sent
|
|
|
|
Scenario: Verification and token checks still come first
|
|
Given user 12 is not verified and holds the Admin role
|
|
When the client sends POST /v1/user/invite with user 12's cookie
|
|
Then the status is 403 and the body is apperr.VerificationRequiredErrorMessage
|
|
And a stale token_version gives 401 apperr.UnauthorizedErrorMessage and no cookie gives 401, as before
|
|
|
|
Scenario Outline: Users and invites
|
|
Given the caller is <role>
|
|
When the client sends <call> with that caller's cookie
|
|
Then the gate result is <result>
|
|
|
|
Examples:
|
|
| role | call | key | result |
|
|
| Owner | GET /v1/user | users.view | ok |
|
|
| Admin | GET /v1/user | users.view | ok |
|
|
| Writer | GET /v1/user | users.view | 403 |
|
|
| Owner | POST /v1/user/invite | users.invite | ok |
|
|
| Admin | POST /v1/user/invite | users.invite | ok |
|
|
| Writer | POST /v1/user/invite | users.invite | 403 |
|
|
| Admin | DELETE /v1/user/invite/1 | users.invite | ok |
|
|
| Writer | DELETE /v1/user/invite/1 | users.invite | 403 |
|
|
| Admin | POST /v1/user/invite/1/renew | users.invite | ok |
|
|
| Writer | POST /v1/user/invite/1/renew | users.invite | 403 |
|
|
|
|
Scenario Outline: Settings, sites and system
|
|
Given the caller is <role>
|
|
When the client sends <call> with that caller's cookie (and a real session, not an API key, where the route requires one)
|
|
Then the gate result is <result>
|
|
|
|
Examples:
|
|
| role | call | key | result |
|
|
| Owner | GET /v1/settings | settings.view | ok |
|
|
| Admin | GET /v1/settings | settings.view | ok |
|
|
| Writer | GET /v1/settings | settings.view | 403 |
|
|
| Owner | PATCH /v1/settings | system.edit_retention | ok |
|
|
| Admin | PATCH /v1/settings | system.edit_retention | 403 |
|
|
| Writer | PATCH /v1/settings | system.edit_retention | 403 |
|
|
| Owner | POST /v1/site | settings.manage_sites | ok |
|
|
| Admin | POST /v1/site | settings.manage_sites | ok |
|
|
| Writer | POST /v1/site | settings.manage_sites | 403 |
|
|
| Admin | PATCH /v1/site/2 | settings.manage_sites | ok |
|
|
| Admin | PATCH /v1/site/2/default | settings.manage_sites | ok |
|
|
| Writer | PATCH /v1/site/2 | settings.manage_sites | 403 |
|
|
| Owner | DELETE /v1/site/2 | settings.delete_site | ok |
|
|
| Admin | DELETE /v1/site/2 | settings.delete_site | 403 |
|
|
| Writer | DELETE /v1/site/2 | settings.delete_site | 403 |
|
|
| Writer | GET /v1/site | (none) | ok |
|
|
| Owner | GET /v1/system/update | settings.view | ok |
|
|
| Admin | GET /v1/system/update | settings.view | ok |
|
|
| Writer | GET /v1/system/update | settings.view | 403 |
|
|
| Owner | POST /v1/system/update/check | system.apply_updates | ok |
|
|
| Admin | POST /v1/system/update/check | system.apply_updates | 403 |
|
|
| Owner | POST /v1/system/update/download | system.apply_updates | ok |
|
|
| Admin | POST /v1/system/update/download | system.apply_updates | 403 |
|
|
| Owner | POST /v1/system/update/restart | system.apply_updates | ok |
|
|
| Admin | POST /v1/system/update/restart | system.apply_updates | 403 |
|
|
| Writer | POST /v1/system/update/restart | system.apply_updates | 403 |
|
|
|
|
Scenario: The permission gate runs before the update rate limiters
|
|
Given the update-check limiter allows 5 attempts per minute per user
|
|
When the Admin sends POST /v1/system/update/check 6 times
|
|
Then every response is 403 missing_permission and none is 429
|
|
|
|
Scenario: The permission gate runs before the invite rate limiter
|
|
Given the invite limiter is exhausted for user 9
|
|
When the Writer sends POST /v1/user/invite
|
|
Then the response is 403 missing_permission and none is 429
|
|
|
|
Scenario: An admin reads the update status but cannot apply it
|
|
Given the Admin and the Owner each send GET /v1/system/update
|
|
Then both get 200
|
|
And the Admin's body has can_apply false
|
|
And the Owner's body has can_apply true when the update helper is installed, false otherwise, as before
|
|
|
|
Scenario: An admin reads the activity retention but cannot change it
|
|
Given the Admin sends GET /v1/settings
|
|
Then the status is 200 and the body holds activity_retention_days and activity_retention_max_gb
|
|
And PATCH /v1/settings with {"activity_retention_days": 30} gives 403 missing_permission and nothing is stored
|
|
|
|
Scenario Outline: Authors, schema, media and API key creation
|
|
Given the caller is <role>
|
|
When the client sends <call> with that caller's cookie
|
|
Then the gate result is <result>
|
|
|
|
Examples:
|
|
| role | call | key | result |
|
|
| Owner | GET /v1/author | authors.view | ok |
|
|
| Writer | GET /v1/author | authors.view | ok |
|
|
| Admin | POST /v1/author | authors.create | ok |
|
|
| Writer | POST /v1/author | authors.create | 403 |
|
|
| Admin | DELETE /v1/author/4 | authors.delete | ok |
|
|
| Writer | DELETE /v1/author/4 | authors.delete | 403 |
|
|
| Writer | POST /v1/author/me | (none) | ok |
|
|
| Admin | GET /v1/content-type | schema.view | ok |
|
|
| Writer | GET /v1/content-type | schema.view | ok |
|
|
| Writer | GET /v1/content-type/events | schema.view | ok |
|
|
| Admin | POST /v1/content-type | schema.edit | ok |
|
|
| Writer | POST /v1/content-type | schema.edit | 403 |
|
|
| Writer | PUT /v1/content-type/events | schema.edit | 403 |
|
|
| Admin | DELETE /v1/content-type/events | schema.delete | ok |
|
|
| Writer | DELETE /v1/content-type/events | schema.delete | 403 |
|
|
| Writer | GET /v1/builtin-type | schema.view | ok |
|
|
| Writer | GET /v1/media | media.view | ok |
|
|
| Writer | GET /v1/media/1 | media.view | ok |
|
|
| Writer | GET /v1/media/1/usage | media.view | ok |
|
|
| Writer | POST /v1/media (multipart image) | media.upload | ok |
|
|
| Writer | PATCH /v1/media/1 | media.upload | ok |
|
|
| Writer | DELETE /v1/media/1 | media.delete | 403 |
|
|
| Writer | POST /v1/media/merge | media.delete | 403 |
|
|
| Admin | DELETE /v1/media/1 | media.delete | ok |
|
|
| Writer | GET /v1/media/purge-settings | settings.view | 403 |
|
|
| Writer | PATCH /v1/media/purge-settings | settings.edit_general| 403 |
|
|
| Writer | GET /v1/media/purge-preview | media.delete | 403 |
|
|
| Writer | POST /v1/media/purge | media.delete | 403 |
|
|
| Admin | POST /v1/media/purge | media.delete | ok |
|
|
| Writer | GET /v1/media/1/variants | media.view | ok |
|
|
| Writer | POST /v1/media/1/variants | settings.edit_general| 403 |
|
|
| Writer | DELETE /v1/media/1/variants/hero | settings.edit_general| 403 |
|
|
| Writer | GET /v1/image-variants | media.view | ok |
|
|
| Writer | GET /v1/image-variants/cache | settings.view | 403 |
|
|
| Writer | POST /v1/image-variants | settings.edit_general| 403 |
|
|
| Writer | POST /v1/image-variants/cache/purge | settings.edit_general| 403 |
|
|
| Admin | PUT /v1/image-variants/hero | settings.edit_general| ok |
|
|
| Admin | POST /v1/api-key | apikeys.create | ok |
|
|
| Writer | POST /v1/api-key | apikeys.create | 403 |
|
|
|
|
Scenario: Routes that keep their old behaviour for API keys
|
|
Given a valid API key whose creator is the Admin
|
|
Then POST /v1/api-key, GET /v1/activity and POST /v1/system/update/check answer 403 apperr.SessionRequiredErrorMessage
|
|
And GET /v1/media/1/variants answers 401 apperr.UnauthorizedErrorMessage (RejectAPIKey runs before the permission check)
|
|
|
|
Scenario Outline: Roles are judged by the keys they hold, with no implied keys
|
|
Given user 20 has a custom role that grants only <grants>
|
|
When the client sends <call> with user 20's cookie
|
|
Then the gate result is <result>
|
|
|
|
Examples:
|
|
| grants | call | result |
|
|
| schema.edit=all | POST /v1/content-type | ok |
|
|
| schema.edit=all | GET /v1/content-type | 403 |
|
|
| schema.view=all | PUT /v1/content-type/events | 403 |
|
|
| users.invite=all | GET /v1/user | 403 |
|
|
| users.view=all | POST /v1/user/invite | 403 |
|
|
| settings.manage_sites=all | DELETE /v1/site/2 | 403 |
|
|
| settings.delete_site=all | PATCH /v1/site/2 | 403 |
|
|
| media.upload=all | GET /v1/media | 403 |
|
|
| media.view=all | POST /v1/media | 403 |
|
|
| apikeys.create=all | GET /v1/api-key | 403 |
|
|
| settings.edit_general=all | PATCH /v1/media/purge-settings | ok |
|
|
| system.apply_updates=all | POST /v1/system/update/check | 403 system.apply_updates (superseded by roles_permissions_content_scope: owner-only keys count only on a locked role) |
|
|
| (no grants) | GET /v1/author | 403 |
|
|
|
|
Scenario: A user whose role cannot be resolved is refused everywhere that is gated
|
|
Given user 13 has role_id 99 (no such role) and legacy role ""
|
|
When user 13 sends GET /v1/user, GET /v1/content-type and POST /v1/media
|
|
Then every response is 403 missing_permission
|
|
|
|
Scenario: A repository failure while resolving the role is a server error
|
|
Given the role repository fails with a non-sentinel error
|
|
When user 3 sends GET /v1/user
|
|
Then the status is 500 and the body is "Internal server error"
|
|
|
|
Scenario Outline: The legacy role string is no longer read for authorization
|
|
Given user <id> has legacy role "<legacy>" and role_id <role_id>
|
|
When the client sends <call> with that user's cookie
|
|
Then the gate result is <result>
|
|
|
|
Examples:
|
|
| id | legacy | role_id | call | result |
|
|
| 14 | owner | Writer's id | POST /v1/user/invite | 403 |
|
|
| 15 | admin | Writer's id | POST /v1/content-type| 403 |
|
|
| 16 | "" | Admin's id | POST /v1/content-type| ok |
|
|
| 17 | owner | 0 | POST /v1/user/invite | ok |
|
|
| 18 | admin | 0 | POST /v1/content-type| ok |
|
|
| 19 | admin | 0 | PATCH /v1/settings | 403 |
|
|
|
|
Scenario: A session follows its role live
|
|
Given user 9 is on the Writer role and GET /v1/media/purge-settings gives 403
|
|
When Writer's grants are edited in the repository to add settings.view=all
|
|
Then the next GET /v1/settings with the same cookie answers ok, with no re-login and no cache
|
|
|
|
Scenario: Admin relaxations
|
|
Given the Admin
|
|
Then the Admin can now call POST /v1/user/invite, DELETE /v1/user/invite/{id} and POST /v1/user/invite/{id}/renew
|
|
And can now call POST /v1/site, PATCH /v1/site/{id} and PATCH /v1/site/{id}/default
|
|
And can now list every API key with created_by_name and revoke any key
|
|
And can read every setting: GET /v1/settings, GET /v1/system/update and the retention values
|
|
|
|
Scenario: Owner-only items stay owner-only
|
|
Given the Admin
|
|
Then DELETE /v1/site/{id}, PATCH /v1/settings, POST /v1/system/update/check, /download and /restart answer 403 missing_permission
|
|
And the Owner passes all of them
|
|
|
|
Scenario: Writer is effective
|
|
Given user 9 on the Writer role
|
|
Then GET /v1/content-type and GET /v1/content-type/events answer ok
|
|
And GET /v1/media, GET /v1/media/1 and POST /v1/media answer ok
|
|
And GET /v1/author answers ok and the author items carry no user_email
|
|
And GET /v1/user, GET /v1/settings, GET /v1/system/update, POST /v1/user/invite, POST /v1/site and POST /v1/api-key answer 403
|
|
And GET /v1/activity answers ok and returns only the events whose actor is user 9
|
|
And GET /v1/permission, GET /v1/auth/me, PATCH /v1/user/me and POST /v1/author/me answer ok
|
|
|
|
Scenario Outline: activity.view scope decides what the list returns
|
|
Given events by user 3, user 5 and user 9 exist
|
|
And the caller's activity.view grant is <scope>
|
|
When the client sends GET /v1/activity<query>
|
|
Then the result is <result>
|
|
|
|
Examples:
|
|
| scope | query | result |
|
|
| all | | 200, events of users 3, 5 and 9 |
|
|
| all | ?actor=user:5 | 200, events of user 5 only |
|
|
| own | | 200, only the caller's own events |
|
|
| own | ?actor=user:9 | 200, only the caller's own events (self is allowed) |
|
|
| own | ?actor=user:3 | 403 missing_permission |
|
|
| none | | 403 missing_permission |
|
|
|
|
Scenario Outline: Activity summary and export follow the scope
|
|
Given the caller holds <grants>
|
|
When the client sends <call>
|
|
Then the result is <result>
|
|
|
|
Examples:
|
|
| grants | call | result |
|
|
| activity.view=all | GET /v1/activity/summary| 200 |
|
|
| activity.view=own | GET /v1/activity/summary| 403 missing_permission |
|
|
| activity.view=all, activity.export=all | GET /v1/activity/export | 200 CSV of every event |
|
|
| activity.view=own, activity.export=all | GET /v1/activity/export | 200 CSV of the caller's own events only |
|
|
| activity.view=none, activity.export=all | GET /v1/activity/export | 403 missing_permission |
|
|
| activity.view=all | GET /v1/activity/export | 403 missing_permission |
|
|
|
|
Scenario Outline: apikeys.view scope decides which keys are listed
|
|
Given keys 1 (created by user 3), 2 (user 5) and 3 (user 9) exist
|
|
And the caller's apikeys.view grant is <scope>
|
|
When the client sends GET /v1/api-key
|
|
Then the result is <result>
|
|
|
|
Examples:
|
|
| scope | result |
|
|
| all | 200, keys 1, 2 and 3, each with created_by_name |
|
|
| own | 200, only the caller's own keys, without created_by_name |
|
|
| none | 403 missing_permission |
|
|
|
|
Scenario Outline: apikeys.revoke scope decides which keys can be revoked
|
|
Given key 2 was created by user 5
|
|
And the caller is user 9 and holds apikeys.revoke = <scope>
|
|
When the client sends DELETE /v1/api-key/2
|
|
Then the result is <result>
|
|
|
|
Examples:
|
|
| scope | result |
|
|
| all | 204 and key 2 is revoked |
|
|
| own | 404 apperr.APIKeyNotFoundErrorMessage and key 2 is not revoked |
|
|
| none | 403 missing_permission, key 2 is not revoked and no lookup occurs |
|
|
|
|
Scenario: The Admin lists and revokes every key
|
|
Given the Admin and a key created by the Owner
|
|
When the Admin sends GET /v1/api-key and DELETE /v1/api-key/<that id>
|
|
Then the list contains the Owner's key with created_by_name, and the delete returns 204
|
|
|
|
Scenario Outline: authors.edit scope decides who can edit an author
|
|
Given author 4 is linked to user 9 and author 5 is linked to user 3
|
|
And the caller is user 9 with authors.edit = <scope>
|
|
When the client sends PATCH <path> with body <body>
|
|
Then the result is <result>
|
|
|
|
Examples:
|
|
| scope | path | body | result |
|
|
| all | /v1/author/5 | {"bio": "x"} | 200 |
|
|
| all | /v1/author/5 | {"user_id": 9} | 200 or the existing link errors |
|
|
| own | /v1/author/4 | {"bio": "x"} | 200 |
|
|
| own | /v1/author/5 | {"bio": "x"} | 403 author_rights (existing contract) |
|
|
| own | /v1/author/4 | {"user_id": null} | 403 author_rights (existing contract) |
|
|
| none | /v1/author/4 | {"bio": "x"} | 403 missing_permission |
|
|
| none | /v1/author/999 | {"bio": "x"} | 403 missing_permission (before any lookup) |
|
|
|
|
Scenario Outline: user_email on authors needs users.view
|
|
Given author 4 is linked to a user with an email
|
|
And the caller holds authors.view and users.view = <users_view>
|
|
When the client sends GET /v1/author
|
|
Then user_email is <shown>
|
|
|
|
Examples:
|
|
| users_view | shown |
|
|
| all | present |
|
|
| none | absent |
|
|
|
|
Scenario: An API key resolves its creator's current role on every request
|
|
Given a valid API key created by user 5 (Admin)
|
|
And GET /v1/content-type with that key answers 200 and POST /v1/user/invite answers ok
|
|
When user 5's role_id is changed to the Writer role in the repository
|
|
Then the next POST /v1/user/invite with the same key answers 403 missing_permission
|
|
And GET /v1/content-type with the same key still answers 200
|
|
And POST /v1/content-type with the same key answers 403 missing_permission
|
|
And no new key and no new login is needed for the change to apply
|
|
|
|
Scenario: A key grows when its creator's role grows
|
|
Given a valid API key created by user 9 (Writer), and POST /v1/content-type with it gives 403
|
|
When user 9's role_id is changed to the Admin role
|
|
Then the next POST /v1/content-type with the same key passes the gate
|
|
|
|
Scenario: Editing the creator's role grants shrinks the key as well
|
|
Given a valid API key created by user 20 whose custom role grants media.view and media.upload
|
|
When the role's media.upload grant is removed in the repository
|
|
Then POST /v1/media with the key gives 403 missing_permission and GET /v1/media still passes
|
|
|
|
Scenario: A key whose creator's role cannot be resolved is refused by the gate
|
|
Given a valid API key created by user 13 (role_id 99)
|
|
When the client sends GET /v1/media with the key
|
|
Then the status is 403 missing_permission
|
|
|
|
Scenario: Key authentication failures are unchanged
|
|
Then an unknown, revoked or expired key, or one whose creator is deleted or unverified, still gives 401 apperr.UnauthorizedErrorMessage
|
|
|
|
Scenario: Public and self-service routes keep no permission gate
|
|
Given no cookie
|
|
Then GET /v1/article, GET /v1/article/{id}, GET /v1/article/{id}/seo and GET /v1/site/{key}/sitemap answer as before
|
|
And user 9 on a role with no grants still gets 200 on GET /v1/auth/me, GET /v1/permission, GET /v1/site and GET /v1/user/lookup?ids=1, and ok on PATCH /v1/user/me
|
|
|
|
Scenario: Articles and entries are not gated in this slice
|
|
Given user 20 has a custom role with no content grants
|
|
Then the article and entry routes answer exactly as before this slice (own/all, publish and draft are slice 2b)
|
|
And this scenario is superseded by roles_permissions_content_scope: articles and entries are gated by the content.* keys
|
|
|
|
Scenario Outline: Existing owner and admin behaviour does not regress
|
|
Given the caller is <role>
|
|
When the client sends <call>
|
|
Then the status is <status> exactly as before this slice
|
|
|
|
Examples:
|
|
| role | call | status |
|
|
| Owner | GET /v1/user | 200 |
|
|
| Admin | GET /v1/user | 200 |
|
|
| Owner | POST /v1/content-type | as before |
|
|
| Admin | POST /v1/content-type | as before |
|
|
| Admin | GET /v1/builtin-type | 200 |
|
|
| Admin | GET /v1/activity/summary | 200 |
|
|
| Admin | GET /v1/activity/export | 200 CSV |
|
|
| Admin | GET /v1/media/purge-settings | 200 |
|
|
| Admin | PATCH /v1/media/purge-settings | as before |
|
|
| Admin | GET /v1/image-variants | 200 |
|
|
| Admin | GET /v1/media/1/variants | as before |
|
|
| Owner | PATCH /v1/settings | as before |
|
|
| Admin | DELETE /v1/author/4 | as before |
|
|
|
|
Scenario: The owner and admin gates are gone from the code
|
|
Then api/router.go and api/middleware hold no RequireOwner or RequireOwnerOrAdmin
|
|
And no file under api/ or internal/infrastructure/useCases compares a user.Role value for authorization,
|
|
apart from the role_id fallback in GetMyAccessUseCase, CreateUser and AcceptInvite assigning a role, and the ListUsersAndInvites output
|
|
And the apperr constants OwnerRequiredErrorMessage and OwnerOrAdminRequiredErrorMessage no longer exist
|