Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/ruscker-admin/assets/i18n/en/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ admin-groups-kpi-total = Groups
admin-groups-kpi-members = Unique members
admin-groups-kpi-apps = Apps covered
admin-groups-subtitle = Create and edit the groups derived from apps and users.
admin-groups-editor-subtitle = Add or remove members in your groups. Creating, renaming, and deleting groups requires an Admin.
admin-groups-members = Members
admin-groups-apps = Apps
admin-groups-public-title = Public apps
Expand All @@ -917,6 +918,7 @@ admin-groups-pick-user = Choose a user…
admin-groups-create = Create group
admin-groups-new-name = Group name
admin-groups-new-group-title = New group:
admin-groups-flash-created = Group created.
admin-groups-flash-renamed = Group renamed.
admin-groups-flash-deleted = Group deleted.
admin-groups-flash-member-added = Member added.
Expand Down
2 changes: 2 additions & 0 deletions crates/ruscker-admin/assets/i18n/es/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ admin-groups-kpi-total = Grupos
admin-groups-kpi-members = Miembros únicos
admin-groups-kpi-apps = Apps cubiertas
admin-groups-subtitle = Creación y edición de los grupos derivados de los apps y usuarios.
admin-groups-editor-subtitle = Añade o quita miembros de tus grupos. Crear, renombrar y eliminar grupos requiere un Admin.
admin-groups-members = Miembros
admin-groups-apps = Apps
admin-groups-public-title = Apps públicas
Expand All @@ -917,6 +918,7 @@ admin-groups-pick-user = Elegir usuario…
admin-groups-create = Crear grupo
admin-groups-new-name = Nombre del grupo
admin-groups-new-group-title = Nuevo grupo:
admin-groups-flash-created = Grupo creado.
admin-groups-flash-renamed = Grupo renombrado.
admin-groups-flash-deleted = Grupo eliminado.
admin-groups-flash-member-added = Miembro añadido.
Expand Down
2 changes: 2 additions & 0 deletions crates/ruscker-admin/assets/i18n/fr/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ admin-groups-kpi-total = Groupes
admin-groups-kpi-members = Membres uniques
admin-groups-kpi-apps = Applications couvertes
admin-groups-subtitle = Création et édition des groupes dérivés des apps et des utilisateurs.
admin-groups-editor-subtitle = Ajoutez ou retirez des membres de vos groupes. La création, le renommage et la suppression nécessitent un Admin.
admin-groups-members = Membres
admin-groups-apps = Apps
admin-groups-public-title = Apps publiques
Expand All @@ -917,6 +918,7 @@ admin-groups-pick-user = Choisir un utilisateur…
admin-groups-create = Créer un groupe
admin-groups-new-name = Nom du groupe
admin-groups-new-group-title = Nouveau groupe :
admin-groups-flash-created = Groupe créé.
admin-groups-flash-renamed = Groupe renommé.
admin-groups-flash-deleted = Groupe supprimé.
admin-groups-flash-member-added = Membre ajouté.
Expand Down
2 changes: 2 additions & 0 deletions crates/ruscker-admin/assets/i18n/pt/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ admin-groups-kpi-total = Grupos
admin-groups-kpi-members = Membros únicos
admin-groups-kpi-apps = Apps cobertas
admin-groups-subtitle = Criação e edição dos grupos derivados dos apps e usuários.
admin-groups-editor-subtitle = Adicione ou remova membros dos seus grupos. Criar, renomear e excluir grupos exige um Admin.
admin-groups-members = Membros
admin-groups-apps = Apps
admin-groups-public-title = Apps públicos
Expand All @@ -921,6 +922,7 @@ admin-groups-pick-user = Escolher usuário…
admin-groups-create = Criar grupo
admin-groups-new-name = Nome do grupo
admin-groups-new-group-title = Novo grupo:
admin-groups-flash-created = Grupo criado.
admin-groups-flash-renamed = Grupo renomeado.
admin-groups-flash-deleted = Grupo excluído.
admin-groups-flash-member-added = Membro adicionado.
Expand Down
36 changes: 21 additions & 15 deletions crates/ruscker-admin/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,17 @@ pub enum Role {
impl Role {
/// Whether this role may reach the given admin nav section.
/// `section` matches the `nav_section` strings used by the
/// templates (`dashboard`, `specs`, `images`, `credentials`,
/// `landing`, `blocks`, `audit`).
/// templates (`dashboard`, `specs`, `images`, `users`, `groups`,
/// `credentials`, `landing`, `blocks`, `audit`).
pub fn can_access_section(&self, section: &str) -> bool {
match section {
// Dashboard + apps + media + scoped users: Editor and up. A
// Dashboard + apps + media + scoped users/groups: Editor and up. A
// Viewer is NOT an admin-panel operator (#857) — it's the portal's
// authenticated-end-user role (group-based card visibility,
// #155), so it reaches NO admin section.
"dashboard" | "specs" | "images" | "users" => *self >= Role::Editor,
// Everything else — credentials, landing, groups, logs, disk,
// audit, system, schedules — is admin-only.
"dashboard" | "specs" | "images" | "users" | "groups" => *self >= Role::Editor,
// Everything else — credentials, landing, logs, disk, audit,
// system, schedules — is admin-only.
_ => *self == Role::Admin,
}
}
Expand Down Expand Up @@ -114,13 +114,19 @@ impl Role {
}
}

/// Parse the lowercase form back to a [`Role`]. Unknown ⇒ `None`.
/// Parse a wire/DB role case-insensitively. Unknown ⇒ `None`.
///
/// Writes stay canonical lowercase through [`Self::as_str`], but the
/// column has no CHECK and external identity provisioners may vary casing.
pub fn parse(s: &str) -> Option<Role> {
match s {
"viewer" => Some(Role::Viewer),
"editor" => Some(Role::Editor),
"admin" => Some(Role::Admin),
_ => None,
if s.eq_ignore_ascii_case("viewer") {
Some(Role::Viewer)
} else if s.eq_ignore_ascii_case("editor") {
Some(Role::Editor)
} else if s.eq_ignore_ascii_case("admin") {
Some(Role::Admin)
} else {
None
}
}
}
Expand Down Expand Up @@ -741,9 +747,9 @@ mod tests {
assert!(Role::Admin > Role::Editor);
assert!(Role::Editor > Role::Viewer);

// Dashboard + apps + media + scoped users: Editor and up — a Viewer
// reaches no admin section (#857).
for sec in ["dashboard", "specs", "images", "users"] {
// Dashboard + apps + media + scoped users/groups: Editor and up — a
// Viewer reaches no admin section (#857).
for sec in ["dashboard", "specs", "images", "users", "groups"] {
assert!(!Role::Viewer.can_access_section(sec));
assert!(Role::Editor.can_access_section(sec));
assert!(Role::Admin.can_access_section(sec));
Expand Down
19 changes: 18 additions & 1 deletion crates/ruscker-admin/src/db/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,27 @@ fn row_from(
// Treat a stored empty string the same as NULL (the form may submit
// blanks); keep only non-empty trimmed values.
let clean = |s: Option<String>| s.map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
// An unparseable stored role falls back to the LEAST privilege, which is
// the right default — but it used to do so silently, and a row that
// suddenly reads as Viewer is exactly the "why did my admin lose access?"
// mystery nobody can debug from the UI. `role` is plain TEXT with no
// CHECK, so a hand-edited row or an external provisioner (#934) can put
// anything there. Say so once per read; an operator needs the row's name.
let role = match Role::parse(&role) {
Some(parsed) => parsed,
None => {
tracing::warn!(
username = %username,
stored_role = %role,
"stored role does not parse; treating the account as Viewer"
);
Role::Viewer
}
};
UserRow {
id,
username,
role: Role::parse(&role).unwrap_or(Role::Viewer),
role,
// `must_change_password` reads as `bool` on both backends: sqlx
// decodes SQLite's INTEGER 0/1 and Postgres' native BOOLEAN.
must_change_password: must_change,
Expand Down
2 changes: 2 additions & 0 deletions crates/ruscker-admin/src/routes/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,8 @@ fn section_for_admin_path(path: &str) -> &'static str {
"schedules"
} else if path.starts_with("/admin/users") {
"users"
} else if path.starts_with("/admin/groups") {
"groups"
} else {
// /admin root and anything unrecognised → dashboard (every
// role can reach it).
Expand Down
Loading