User Management Modernized - sebiboga/testlink-upgraded GitHub Wiki
TestLink provides four screens for managing users, roles, and role assignments. All four are accessible from the System section of the left navigation sidebar and share a common tab bar for quick navigation between screens.
Prerequisite: You must be logged in as admin (or a user with the mgt_users and/or user_role_assignment rights).
- User Management
- Role Management
- Assign Test Project Roles
- Assign Test Plan Roles
- Role Hierarchy
- Common Workflows
Path: System > User Management
URL: gui/templates/usermanagement/usersView.html
Purpose: Create, edit, enable/disable, and delete user accounts.
| Element | Description |
|---|---|
| Create User button | Opens the create user modal |
| Export button | Redirects to the modern User Management Export screen |
| Manage user box | Login lookup (legacy manage_user form): type a login and click Manage user to jump straight to that user's edit modal; an unknown login shows a localized "login does not exist" message |
| Data Table | Lists all active and inactive users (soft-deleted users are hidden) |
| Search box | Filters the table by any column |
| Show entries | Controls how many rows are displayed per page |

Legacy gui/templates/dashio/usermanagement/usersView.tpl showed a "Login [...] Manage user"
form below the buttons that posted to usersEdit.php?doAction=edit&login=<login>, where
initializeGui() resolves the login to a user id via tlUser::doesUserExist() and opens
the edit screen β or shows login_does_not_exist for an unknown login.
The modern screen reimplements this in the toolbar:
- Type a full login into the lookup box (placeholder = Login, maxlength 30, required).
- Click Manage user β the BFF resolves it (
GET /api/users/index.php?login=<login>) and opens the Edit User modal for that user. - Unknown login β localized alert "Login ___ does not exist"
(
user.loginDoesNotExist, present in all 10 locale bundles); empty input is blocked client-side (HTML5required), and an empty parameter to the API returns 400login_required.
| Column | Description |
|---|---|
| # | Auto-incremented user ID |
| Login | The username used to log in |
| Name | The user's display name (first + last) |
| The user's email address | |
| Global Role | The system-wide role assigned to this user |
| Locale | The UI language preference |
| Active | Active (green badge) or Inactive (red badge) |
| Expiration Date | Localized expiration date of the account (server-side localize_dateOrTimeStamp with the user's date_format, e.g. 31/12/2026); empty cell when no expiry set (legacy parity, issue #883) |
| Actions | Edit, Enable/Disable, Delete icons |
The two legacy columns were re-added to the DataTable (same order as legacy
lib/usermanagement/usersView.php:168-180): Locale sits between Role and
Status, Expiration Date between Status and Actions. The BFF list route
(api/users/index.php) already returned locale and expirationDate for
every user and now additionally computes expirationDateFormatted with the
same call legacy uses (localize_dateOrTimeStamp(null,null,'date_format',β¦)),
so e.g. 2026-12-31 renders as 31/12/2026. Users without an expiration date
show an empty cell.

- Edit (pencil icon): Opens the edit modal to change user details and global role
-
Reset password (key icon, issue #884): Generates a new random password for the user, then either emails it (default
password_reset_send_method = send_password_by_mail) or returns it to be displayed on screen (display_on_screen). Shown only for users whose effective authentication method hasallowPasswordManagement = true(legacyusersEdit.tpl:169-178parity) - Enable/Disable (toggle icon): Toggles the user between Active (1) and Inactive (0) states
-
Delete (trash icon): Soft-deletes the user (sets
active = 2, hidden from UI but retained in database). Not available for the admin user
A second toolbar row sits under the Create/Export toolbar β the port of the legacy
ExtJS grid toolbar (lib/usermanagement/usersView.php:215-217 +
lib/functions/exttable.class.php:96-124). Five buttons:
| Button | Legacy flag | Behaviour |
|---|---|---|
| Expand/Collapse Groups | toolbarExpandCollapseGroupsButton |
Groups the grid by role (DataTables RowGroup, same pattern as the Test Plan with Custom Fields report). Clicking a group header collapses/expands that single group; the button collapses or expands all groups at once |
| Show all Columns | toolbarShowAllColumnsButton |
Reveals/hides the three hidden technical columns: Role ID, User ID, Is Special (in demoMode, Is Special = 1 for demoSpecialUsers, default admin β legacy usersView.php:295-303). Button label flips to "Hide technical columns" while shown; the choice survives a Refresh |
| Reset to Default State | toolbarDefaultStateButton |
Restores the default grid state: hides the technical columns, expands all groups, clears the search box, sorts by Login ascending, page size 25 |
| Refresh | toolbarRefreshButton |
Re-fetches the user list from the BFF (GET /api/users/index.php), keeping the current column-visibility choice |
| Reset Filters | toolbarResetFiltersButton |
Clears the DataTables search box, showing "Filters cleared" |
The BFF list route now returns the raw role_id, user_id and is_special
fields per user so the technical columns are populated client-side.

The legacy edit screen had a dedicated Reset password form
(gui/templates/dashio/usermanagement/usersEdit.tpl:339-356 β usersEdit.php:222-269
createNewPassword() β lib/functions/users.inc.php:167-225 resetPassword()). The
modern screen ports it to a row action backed by a new BFF route:
-
UI (
gui/templates/usermanagement/usersView.html): a key icon in the Actions column per row, gated bycanResetPassword(u)against the/meta/authenticationpayload (the user's ownauthenticationvalue, falling back to the configured default method β same rule as the legacy template). Clicking shows a confirm dialog, thenPOST /api/users/index.php/{id}/reset-password; the outcome is shown as an alert: the bundle message for sent-by-mail,user.passwordShownOnScreenwith the fresh password for display-on-screen, or the localized error. -
BFF (
api/users/index.php,POST /users/{id}/reset-password), mirroring the legacycreateNewPassword()flow step by step:-
tlUser::isPasswordMgtExternal($u->authentication)β refuse withpassword_mgmt_external(defense-in-depth; the UI hides the action anyway); -
config_get('demoMode')β refuse (demo_reset_password_disabled); - reads
password_reset_send_method; unless it isdisplay_on_screen, validatesconfig_get('smtp_host')withZend_Validate_Hostname(ALLOW_ALL)βinvalid_smtp_hostnameon failure; - calls the legacy
resetPassword($db,$userId,$method)(email send viaemail_send()when by-mail) and on success logslogAuditEvent(tls('audit_pwd_reset_requested',$login),'PWD_RESET',$id,'users').
-
-
Config:
config_get('password_reset_send_method')decides send-by-mail vs display-on-screen. With the default unconfigured SMTP host the by-mail path returns the same message as legacy: "Password Reset can not be done. Reason: SMTP Hostname seems to be invalid". - New i18n keys in all 10 locale bundles:
user.resetPassword,user.resetPasswordConfirm,user.passwordResetSent,user.passwordShownOnScreen,user.passwordResetInvalidSmtp,user.passwordResetDenied,user.resetFailed,user.passwordResetError.

The legacy edit screen offered a Generate a new key button
(gui/templates/dashio/usermanagement/usersEdit.tpl:351-355) gated on
$tlCfg->api->enabled and wired to usersEdit.php:274-318 createNewAPIKey().
The modern screen ports it to a row action backed by a new BFF route:
-
UI (
gui/templates/usermanagement/usersView.html): an id-card icon in the Actions column per row, rendered only when the/meta/authenticationpayload reportsapiEnabled = true(canGenerateApiKey()β legacy$tlCfg->api->enabledparity). Clicking shows a confirm dialog (user.generateApiKeyConfirm), thenPOST /api/users/index.php/{id}/generate-apikey; the result is shown as an alert: "New API key has been sent via mail" (user.apiKeySent) or the localized error. -
BFF (
api/users/index.php,POST /users/{id}/generate-apikey), mirroring the legacycreateNewAPIKey()flow step by step:- re-checks
$tlCfg->api->enabledserver-side β403 api_disabled(defense-in-depth; the UI hides the action anyway); - validates
config_get('smtp_host')withZend_Validate_Hostname(ALLOW_ALL)β400 invalid_smtp_hostnameon failure (same gate as legacy). The legacy lang keyapikey_cannot_be_reseted_invalid_smtp_hostnameis not defined in anystrings.txtbundle (lang_get would emit a "not localized" WARNING event), so the BFF returns a plain message and the UI maps thecodeto the localizeduser.apiKeyInvalidSmtp; - calls
APIKey::addKeyForUser($id)(lib/functions/APIKey.class.php:38-50UPDATE users SET script_key = md5(8Γmt_rand())) and reads the fresh key viagetAPIKey(); - emails the key via
@email_send(...)(subjectmail_apikey_subject, bodyyour_apikey_is+ key +contact_admin). A delivery failure is caught and written to the PHP error log only (error_log) β never to the Event Viewer (no new WARNING events), matching legacy's silent@suppression; - on success logs
logAuditEvent(tls('audit_user_apikey_set',$login),'CREATE',...).
- re-checks
-
Config:
config_get('smtp_host')+ the legacymail_apikey_subject/your_apikey_is/from_emailglobals. With the default unconfigured SMTP host the route returns400 invalid_smtp_hostname, same as legacy. - New i18n keys in all 10 locale bundles:
user.generateApiKey,user.generateApiKeyConfirm,user.apiKeySent,user.apiKeyFailed,user.apiKeyInvalidSmtp,user.apiKeyDisabled.

| Field | Required | Description |
|---|---|---|
| Login | Yes | Unique username (4-25 chars, alphanumeric + underscores) |
| Password | Yes (create only) | Minimum length enforced by system config |
| First Name | Yes | User's first name |
| Last Name | Yes | User's last name |
| Yes | Valid email address | |
| Global Role | Yes | Select from available roles (e.g., admin, test designer, tester, etc.) |
| Locale | No | UI language (e.g., en_GB, ro_RO) |
| Authentication method | No | Legacy parity usersEdit.php:440-451 (issue #882): Default (configured) β value '', follows config_get('authentication')['method'] β plus one option per authentication['domain'] key (DB/LDAP), fed by the BFF meta endpoint GET /api/users/index.php/meta/authentication. Persisted into users.auth_method on create/update. |
| Expiration Date | No | Date picker (native type=date) + Clear Date button. Empty or cleared β expiration_date = NULL; otherwise stored ISO Y-m-d via tlUser::setExpirationDate(). Hidden for users listed in config_get('noExpDateUsers') (default ['admin']) β legacy expDateEnabled usersEdit.php:462-467; the BFF enforces the same guard server-side. |



Legacy creates a user only with a non-empty password: the create form
blocks submit with warning_empty_pwd (usersEdit.tpl:153-155 sets
check_password=1, validateForm hook, password input required) and the
server rejects empty passwords β doCreate() checks setPassword() and treats
tlUser::E_PWDEMPTY (< 0) as a failure, so writeToDB() never runs
(usersEdit.php:153-171).
The modern screen now enforces the same rule on both layers:
-
BFF (
api/users/index.php, POST create): thesetPassword()return value is checked β anything< tl::OK(i.e.E_PWDEMPTY= "The password must not be empty!") returns HTTP 400{status:error, code:'warning_empty_pwd', message:<legacy string>}beforewriteToDB(), so no blank-password row can be persisted (not even via a direct API POST).S_PWDMGTEXTERNAL(2 >= OK) remains allowed, matching legacydoCreate, which validates the password against the configured default auth method. -
UI (
usersView.html): the password input carriesrequiredin the create modal (removed again on edit β "leave empty to keep"), andsaveUser()blocks an empty-password create client-side with the localizeduser.passwordRequiredmessage, focusing the field β mirroring legacyvalidateForm(f, check_password=1). -
i18n: new key
user.passwordRequiredin all 10 locale bundles, used for both the client-side block and the server 400 mapping.


When $tlCfg->demoMode = ON; in config.inc.php the whole User Management screen becomes read-only, mirroring legacy usersEdit.tpl:312-355 where the Save button is replaced by the demo_update_user_disabled note (on doUpdate) and the Reset password / Generate key form by demo_reset_password_disabled.
-
BFF enforcement (
api/users/index.php): a shareddemoModeBlockedWrite()helper (mirror ofapi/userinfo) returns HTTP 403{status:error, code:'demo_mode'}from every write route β POST create, PUT update, PUT/active, DELETE, POSTreset-passwordand POSTgenerate-apikey. The UI can never bypass it. -
UI gating (
usersView.html): with demo mode on a read-only amber banner appears under the header, the Create User toolbar button disappears, grid rows show only the Edit icon (no reset-password / generate-API-key / enable-disable / delete), and the create/edit modal replaces Save with auser.demoUpdateDisablednotice and disables every field. Cancel/close stay usable. -
Discovery:
GET /api/users/index.php/meta/authenticationnow returnsdemoModeso the front-end knows the state. - New i18n keys in all 10 locale bundles (values from the legacy
strings.txt):user.demoUpdateDisabled,user.demoResetPasswordDisabled.



Legacy usersEdit.tpl:189-193 showed a question/help icon (title
show_event_history) next to the "User Details" legend that drilled into the
Event Viewer pre-filtered to the edited user's audit activity via
showEventHistoryFor(user_id,'users'). The icon was gated on
grants->mgt_view_events (usersEdit.php:457-458, hasRight('mgt_view_events')).
The modern edit modal replicates it:
-
BFF (
api/users/index.phpGET /meta/grants): the grants payload now includesmgt_view_events('yes'/'no'), mirroring the legacy separate assignment inusersEdit.php:457-458(the value is deliberately NOT part ofgetGrantsForUserMgmt(), exactly like legacy). -
UI (
usersView.html): a Show event history button (#btnEventHistory,fa-history+data-i18n="user.showEventHistory") appears in the edit modal header only while editing an existing user and the current logged-in user holdsmgt_view_events. It is hidden in the create modal and for users without the right. Clicking it submits a hidden GET form (object_id=<user id>&object_type=users) which opens the modern Event Viewer ateventviewer.html?object_id=<id>&object_type=usersin a new tab, showing "Filtered by users #" β the same pattern as the user profile (userInfo.html) and milestones (planMilestones.html) screens. - The Event Viewer BFF (
api/eventviewer/index.php) independently enforcesmgt_view_events(403 without it), so the filter cannot be abused. - New i18n key in all 10 locale bundles:
user.showEventHistory(values match the existingprofile.showEventHistorytranslations).


Legacy lib/usermanagement/usersView.php:47 (disable) and
lib/usermanagement/usersEdit.php:169/214 (create/update) filled
$gui->user_feedback with localized messages β user_created ("User %s was
successfully created"), user_disabled ("User %s was successfully disabled"),
getUserErrorMessage() for failures β rendered as a banner by
inc_update.tpl. The modern screen used to close the modal / reload the grid
after every write with no success feedback at all.
The 2.0.1 screen now mirrors that feedback with a Dashio toast (bottom-right, auto-hides after ~3 s, teal for success / red for errors):
-
BFF (
api/users/index.php): every write-success route now returnsfeedback_keywith the same key namespace legacy used for$gui->user_feedback: POST/usersβuser_created, PUT/users/{id}βuser_updated, PUT/users/{id}/activeβuser_enabled|user_disabled, DELETE/users/{id}βuser_deleted. -
UI (
gui/templates/usermanagement/usersView.html): a fixed.toastelement (same pattern ascfieldsAssignView.html/planUpdateTC.html) plus auserFeedback(feedbackKey, login)mapper to the client-side i18n keys. Create/update save (saveUser()), enable/disable (toggleActive()) and delete (deleteUser()) all show the localized toast with the affected login; toggle/delete errors moved fromalert()to the red toast; the confirm dialogs (user.confirmDelete/Disable/Enable) and the status badges (user.active/user.inactive) are localized too. -
i18n: new keys in all 10 bundles β
user.feedback.created,user.feedback.updated,user.feedback.disabled,user.feedback.enabled,user.feedback.deleted,user.confirmDelete,user.confirmDisable,user.confirmEnableβ translations taken from the legacylocale/<XX>/strings.txtmessages.
Screenshot: docs/screenshots/issue-890-operation-feedback-toast.png (create
toast "User was successfully created").
- Only users with
active = 1(Active) appear in the table - Deleted users (
active = 2) are hidden but preserved for audit trail - The admin user cannot be deleted or disabled
- Changes to other users are logged in the Event Viewer as AUDIT events
Path: System > Role Management
URL: gui/templates/usermanagement/rolesView.html
Purpose: Create custom roles and manage their permission sets (rights).
| Element | Description |
|---|---|
| + Create Role button | Opens the create role modal |
| Data Table | Lists all roles (system + custom) |
| Search box | Filters roles by name or description |
| Show entries | Rows per page (10/25/50/100) |
| Column | Description |
|---|---|
| ID | Role ID |
| Name | Role display name |
| Description | Role description text |
| Rights | Color-coded permission tags (read/write/execute) |
| Type | System (grey badge) or Custom (teal badge) |
| Actions | Edit, Delete icons |
System roles cannot be edited or deleted. They are protected by the system:
| ID | Name | Description |
|---|---|---|
| 1 | <reserved system role 1> | Reserved for internal use |
| 2 | <reserved system role 2> | Reserved for internal use |
| 3 | <no rights> | Empty role with no permissions |
These roles ship with TestLink and can be edited (but not deleted):
| ID | Name | Typical Use |
|---|---|---|
| 4 | test designer | Design and manage test cases |
| 5 | guest | Read-only access to metrics |
| 6 | senior tester | Execute tests + create builds |
| 7 | tester | Basic test execution |
| 8 | admin | Full system administration |
| 9 | leader | Project and plan management |
| Field | Description |
|---|---|
| Name | Role name (required, must be unique) |
| Description | Optional description |
| Rights | Grid of checkboxes β select permissions for this role |
| Select All link | Toggles all checkboxes on/off |
Rights are color-coded in the table:
| Tag Color | Category | Examples |
|---|---|---|
| Blue | Read |
mgt_view_tc, mgt_view_req, testplan_metrics
|
| Red | Write |
mgt_modify_tc, mgt_modify_req, mgt_modify_product
|
| Green | Execute | testplan_execute |
- Only Custom roles can be deleted (roles with IDs > 3)
- Before deletion, the system shows how many users currently have this role assigned
- Users assigned to a deleted role are automatically reset to the configured
replacement role (
role_replace_for_deleted_roles, legacyrolesView.tpl:61warningwarning_users_will_be_reset)
Every create/update/duplicate/delete operation now surfaces a localized
feedback toast (bottom-right Dashio banner), mirroring the legacy
inc_update.tpl user_feedback/$sqlResult area of rolesEdit.tpl:53 and
rolesView.tpl:43:
-
BFF (
api/roles/index.php): write-success routes return the samefeedback_keynamespace #890 added for User Management βrole_created(POST),role_updated(PUT),role_deleted(DELETE),role_duplicated(POST/duplicate). Error routes returnmessageKeythat resolves the legacy localized messages viagetRoleErrorMessage()(roles.inc.php:440-466):error_duplicate_rolenameβrole.error.nameExists,error_role_no_rolenameβrole.error.noRoleName,error_role_no_rightsβrole.error.noRights,error_role_not_updatedβrole.error.notUpdated,error_role_deletionβrole.error.deleted; the system-role guards map torole.editLocked/role.systemDeleteDenied. -
UI (
rolesView.html): Dashio.toast(ok = teal, err = red, auto-hide ~3s),toast()+roleFeedback()helpers. Success β OK toast ("RoleXwas successfully created / saved / deleted / duplicated"); errors β red toast or the modal error line, always localized β noalert()dialogs remain. -
i18n: 9 keys added to all 10 bundles (
role.feedback.created/updated/ deleted/duplicated,role.error.deleted/nameExists/noRoleName/noRights/ notUpdated), translations taken from legacylocale/*/strings.txt.

- Create roles with only the permissions needed (principle of least privilege)
- Role rights apply globally β they are not project-specific
- Changes to roles immediately affect all users assigned to that role
Path: System > Assign Test Project Roles
URL: gui/templates/usermanagement/usersAssignProject.html
Purpose: Assign a role to each user at the test project level.
Test project roles control what a user can do within a specific test project. This overrides or supplements the user's global role for project-scoped activities.
| Element | Description |
|---|---|
| Test Project dropdown | Select which project to manage |
| User Table | Lists all active users with their current project role assignment |
| Save Changes button | Enabled only when changes are detected |
- Select a Test Project from the dropdown
- The table loads all active users and their current role assignment for that project
- Change the Assigned Role dropdown for any user
- Modified rows are highlighted in yellow with a modified badge
- Click Save Changes to persist
| Column | Description |
|---|---|
| # | User ID |
| Login | Username |
| Name | Display name |
| Assigned Role | Dropdown with available roles + "-- no role --" |
| Option | Meaning |
|---|---|
| -- no role -- | User has no project-specific role (inherits global role for public projects) |
| test designer | Can manage test cases within this project |
| guest | Read-only metrics access |
| senior tester | Execute + create builds |
| tester | Basic test execution |
| admin | Full project administration |
| leader | Project management |
| Test Manager | Custom role (if created) |
- Users see only projects they have access to (based on global role + project public/private settings)
- Admin users see all projects regardless of assignments
- A user with no project role on a private project will NOT have access to it
- Project roles do not override the global role β they supplement it
- For public projects, any user with a valid global role can participate unless explicitly denied
- For private projects, users must be explicitly assigned a role
Legacy opened this screen through usersAssign.php:23 testlinkInitPage(...), which ran
checkSessionValid() before the first render, so a session idle past
config_get("sessionInactivityTimeout") minutes never painted the assignment form β it went
straight to login.php?note=expired&destination=β¦, carrying the localized βSession expired.
Please log in again.β box.
The modern screen reproduces that behaviour on every request it makes:
| request | on 401 {"code":"session_expired"}
|
|---|---|
GET /api/roles/index.php/meta/tproject-roles (first request of the page) |
error toast + bounce to the login page |
project switch (loadUsers) |
same |
Save Changes (saveAssignments) |
same |
Between the toast and the redirect the page is put in the legacy βnothing to act onβ state: the
tab bar, the toolbar and the grid are hidden, the Test Project combo is emptied and disabled,
Save is disabled and the footer is cleared β no misleading βSelect a test project aboveβ¦β box is
shown and no dead control can be clicked. A 403 is unaffected: it still shows the βYou do not
have enough rightsβ¦β deny box. The helper keys on the HTTP status, so an absent session
(401 {"message":"Not authenticated"}, no code key) is handled too.
Because the Dashio shell never refreshes the session timer, this is the normal fate of a shell tab
left open past the timeout β a screen that handles the 401 itself bounces by itself. All four
user-management screens now do: the two assign-roles screens (this one, #1620, and
Assign Test Plan Roles, #1646), plus User Management and
Role Management themselves (#1760 β 12 and 8 request paths respectively, and
api/users/index.php never enforced the inactivity window at all, so a stale session could even
create users).
Details: Task-Issue-1760-User-Role-Management-Session-Expired-Bounce.md

Details: Task-Issue-1620-Assign-Project-Roles-Initial-Load-Session-Expired.md
Path: System > Assign Test Plan Roles
URL: gui/templates/usermanagement/usersAssignPlan.html
Purpose: Assign role overrides to users for a specific test plan.
Test plan roles allow you to narrow or change a user's permissions for a specific test plan, overriding their test project role. This is useful when a user needs different permissions for different test plans within the same project.
| Element | Description |
|---|---|
| Test Project dropdown | Select the parent project |
| Test Plan dropdown | Select the plan (loads after project is chosen) |
| User Table | Lists users with inherited role and plan override |
| Save Changes button | Enabled only when changes are detected |
- Select a Test Project from the first dropdown
- The Test Plan dropdown populates with active plans in that project
- Select a Test Plan
- The table loads all active users with:
- Their inherited role from the test project assignment
- Their current plan role override (if any)
- Change the Plan Role Override for any user
- Click Save Changes to persist
| Column | Description |
|---|---|
| # | User ID |
| Login | Username |
| Name | Display name |
| Inherited Role | The role inherited from the test project (read-only) |
| Plan Role Override | Dropdown to set a plan-specific override |
| Option | Meaning |
|---|---|
| -- no override -- | User uses their inherited project role (default) |
| test designer | Override to test designer for this plan only |
| guest | Override to read-only for this plan only |
| senior tester | Override to senior tester |
| tester | Override to tester |
| admin | Override to admin |
| leader | Override to leader |
| Test Manager | Custom role override |
When determining what a user can do in a test plan, TestLink checks (in order):
- Test plan role (highest priority) β if set, this overrides everything
- Test project role β the role assigned at the project level
- Global role (lowest priority) β the user's system-wide role
User
tester1has:
- Global role:
tester- Project role for P2:
tester- Plan role override for "smoke" plan in P2:
senior testerResult: On the "smoke" test plan,
tester1operates as asenior tester(can create builds, execute tests, view metrics). On all other plans in P2, they operate as atester.
- Use plan overrides sparingly β they add complexity to the permission model
- The Inherited Role column helps you understand what the user's effective role would be without an override
- Setting "-- no override --" removes the plan-specific override and falls back to the inherited project role
Global Role (system-wide, assigned in User Management)
βββ Test Project Role (assigned in Assign Test Project Roles)
βββ Test Plan Role Override (assigned in Assign Test Plan Roles)
- Global role applies everywhere unless overridden
- Project role applies within a specific project (supplements global)
- Plan role override applies within a specific test plan (overrides project role)
| Level | Role | Capabilities |
|---|---|---|
| Highest | admin | Full access to everything |
| High | leader | Project + plan management, user assignment |
| Medium | senior tester | Execute tests, create builds, view metrics |
| Low | test designer | Design and manage test cases |
| Lowest | tester | Execute tests only |
| Read-only | guest | View metrics only |
| None | <no rights> | No access (ID 3) |
- Go to User Management > click Create User
- Fill in login, name, email, password; select tester as global role
- Go to Assign Test Project Roles > select the project
- Set the user's role to tester for that project
- (Optional) Go to Assign Test Plan Roles > select project + plan
- Set a plan override if needed
- Go to Assign Test Project Roles > select the project
- Set the user's role to admin for that project only
- Their global role remains unchanged β admin access is limited to this project
- Go to Assign Test Plan Roles > select project + plan
- Set the user's Plan Role Override to guest (read-only)
- On all other plans they retain their normal project role
- Go to Role Management > click + Create Role
- Enter a name and description
- Use Select All to quickly check all permissions, then uncheck what you don't need
- Click Save
- The new role appears in all role assignment dropdowns across the system
Since issue #879, every User Management BFF endpoint enforces the legacy
mgt_users right (right id 13) on the session user β same check as legacy
lib/usermanagement/usersView.php checkRights(), also enforced in legacy
usersEdit.php and usersExport.php.
- A user with
mgt_users(e.g. admin): getsGET /api/users,POST,PUT,DELETEandGET /meta/grantsβ full management. - A user without
mgt_users(e.g. guest): any request to/api/users/β¦returns403 {"status":"error","message":"no_permissions_for_action","right":"mgt_users"}. No user data leaks; no mutation is possible. - The denial is also written to the audit trail as
audit_security_user_right_missing(AUTH event).
The modern User Management screen calls GET /api/users/meta/grants, which
mirrors legacy getGrantsForUserMgmt() (lib/functions/users.inc.php). The
resulting grant map controls which tabs the user sees:
| Tab | Required grant |
|---|---|
| User Management | user_mgmt |
| Role Management | role_mgmt |
| Assign Test Project Roles | tproject_user_role_assignment |
| Assign Test Plan Roles | tplan_user_role_assignment |
Legacy parity: when the user holds mgt_users, tproject/tplan assignment
grants are forced to "yes" (those tabs stay visible). A user without
mgt_users sees only the no-access notice (i18n user.noRights) instead of
the tab bar. This mirrors legacy gui/templates/dashio/usermanagement/menu.inc.tpl.