Repository navigation
Bugfix Issue 1412 usersView CreateUser Reserved Role1
Issue 1412 — usersView.html: Create User assigns reserved role 1 (no rights) instead of default_roleid on create
Issue: #1412
Branch: fix/issue-1412
Status: VERIFIED-FIXED (2026-09-12)
Creating a user from the modernized User Management screen
(gui/templates/usermanagement/usersView.html) assigns the reserved role
<reserved system role 1> (id 1, zero rights) instead of the configured
$tlCfg->default_roleid (guest, id 5). The new user cannot see any project,
and re-saving such a user (or a user whose stored role is 0/unknown) can strip
rights on save.
- Login admin/admin →
http://localhost:8082/gui/templates/usermanagement/usersView.html?tproject_id=0&tplan_id=0 - Click Create User; leave the Global Role dropdown on its default selection
(first, unnamed option =
<reserved system role 1>); fill the rest; Save. -
mysql testlink -e "SELECT id, login, role_id FROM users"→ new user hasrole_id = 1.

Expected: new users get the legacy default role
(gui/templates/dashio/usermanagement/usersEdit.tpl:240-243 +
usersEdit.php use $tlCfg->default_roleid, config.inc.php:1968 =
TL_ROLES_GUEST, cfg/const.inc.php:521 → role 5) whenever the created
user's globalRoleID is 0.
gui/templates/usermanagement/usersView.html:371 (showCreateModal()):
$('#editRole').val($('#editRole option:first').val());The dropdown is populated from api/users/index.php:190:
tlRole::getAll($db, null, null, null, tlRole::TLOBJ_O_GET_DETAIL_MINIMUM).
lib/functions/tlRole.class.php:517 sorts ORDER BY id ASC, so the first
option is DB row id 1 = <reserved system role 1> (zero rights) — the least
privilege trap.
Chain:
-
/api/users/meta/rolesreturns roles id-ascending (role 1 first) and never exposeddefault_roleid. -
showCreateModal()preselectsoption:first→ role 1. -
POST /api/users(api/users/index.php:273) storedintval($body['globalRoleID'] ?? 0)verbatim →role_id = 1persisted with no fallback for role 0/absent. - Edit path had the same invariant hole:
$('#editRole').val(u.globalRoleID)silently fails when the stored role is not among the options (0 / stripped), leaving the first option (role 1) selected so a re-save rewrites the user onto role 1.
Blast radius:
- Only
usersView.htmlconsumes/api/users/meta/roles; the change is additive. - Server
tlUser::writeToDB(lib/functions/tlUser.class.php:434,453) has no role validation — 0/junk roles are stored as-is.
BFF api/users/index.php
-
/meta/roles: exposedefaultRoleID=intval(config_get('default_roleid'))(legacyusersEdit.tpl:240-243parity) and skipid <= 0entries (mirror of legacyusersEdit.php:85unset($roles[TL_ROLES_UNDEFINED])which drops the<inherited>pseudo-role from the dropdown). -
POST /users:globalRoleID <= 0(missing/zero) now maps tointval(config_get('default_roleid'))instead of persisting 0 — role-by-omission can no longer happen through the API.
Frontend gui/templates/usermanagement/usersView.html
New presetRole(value) helper used by both create and edit:
function presetRole(value) {
var v = parseInt(value, 10) || 0;
if (v === 0) { v = defaultRoleID; }
var sel = $('#editRole');
if (v > 0 && sel.find('option[value="' + v + '"]').length) {
sel.val(String(v));
} else {
sel.val(sel.find('option:first').val());
}
}-
showCreateModal()→presetRole(defaultRoleID)(legacy: create form preselectsdefault_roleidbecauseglobalRoleID eq 0). -
editUser()→presetRole(u.globalRoleID)— a stored valid role is preserved exactly; a 0/unknown stored role falls back to the default instead of silently landing on reserved role 1.
Method / alternatives
- Method: mirror the legacy invariant "globalRoleID 0 ⇒ default role" in the BFF (single source of truth) plus fix the modal preselection in the UI.
- Rejected: hardcoding role
5in the HTML/JS — the default is config-driven ($tlCfg->default_roleid), a hardcode would diverge if an admin changesdefault_roleid. - Rejected: silently guessing the "lowest real role" — that is exactly what produced role 1; the correct default is the configured one.
- No real role id 0 is ever a valid
users.role_id(the<inherited>id-0 entry is only a transient pseudo-object added bytlRole::getAll), so coercing 0→default is safe.


-
primary repro (UI create, default role): pre-fix
reprouserstoredrole_id=1; post-fixfixuserstoredrole_id=5(guest). -
explicit role preserved: UI create picking "test designer" →
role_id=4. -
API POST without
globalRoleID→role_id=5; withglobalRoleID:0→role_id=5. -
edit existing user with valid stored role (reprouser, role 1): modal shows
<reserved system role 1>, save preserves role 1. -
edit user with legacy
role_id=0: modal preselects guest (5), save persistsrole_id=5. -
dropdown hygiene:
/meta/rolesreturns ids 1..9, no<inherited>(id 0). -
hygiene:
php -lclean;node --checkclean;eventstable only log_level 16 (INFO) rows — no new Error/Warning; no i18n bundle touched (no user-facing string changed).
-
api/users/index.php—/meta/roles:defaultRoleID+ drop id≤0;POST /users: role 0/absent → default. -
gui/templates/usermanagement/usersView.html—presetRole()for create/edit. -
CHANGELOG— KEY BUGFIX entry (#1412).
Commit range (fix), (regression suite), (docs + wiki) on branch
fix/issue-1412 (fix commit c35db8c8d).
Regression suite: tmp/TLU_Test_Cases.md → "Regression — Issue #1412".
Screenshots: issue-1412-create-modal-preselect-role1.png (pre-fix),
issue-1412-create-modal-preselect-guest-fixed.png (post-fix),
issue-1412-grid-after-create-fix.png (post-fix grid).