Make the Guest role composable #66

Merged
sjg merged 1 commits from agent/composable-guest-role into main 2026-08-13 02:20:58 +02:00
4 changed files with 33 additions and 13 deletions
@@ -1852,10 +1852,9 @@ function buildRoleChoices(selected) {
input.addEventListener("change", () => {
if (!input.checked) return;
if (value === "guest") {
inputs.forEach((choice) => {
if (choice.value !== "guest") choice.input.checked = false;
});
} else {
const administrator = inputs.find((choice) => choice.value === "administrator");
if (administrator) administrator.input.checked = false;
} else if (value === "administrator") {
const guest = inputs.find((choice) => choice.value === "guest");
if (guest) guest.input.checked = false;
}
@@ -447,8 +447,9 @@ function buildRoleChoices(selected: readonly AuthRole[]) {
input.addEventListener("change", () => {
if (!input.checked) return;
if (value === "guest") {
inputs.forEach(choice => { if (choice.value !== "guest") choice.input.checked = false; });
} else {
const administrator = inputs.find(choice => choice.value === "administrator");
if (administrator) administrator.input.checked = false;
} else if (value === "administrator") {
const guest = inputs.find(choice => choice.value === "guest");
if (guest) guest.input.checked = false;
}
@@ -69,7 +69,11 @@ try {
const listenerPassword = listenerRow.locator('input[type="password"]');
await listenerRow.locator('input[value="guest"]').check();
assert.equal(await listenerPassword.isDisabled(), true);
await listenerRow.locator('input[value="read"]').check();
assert.equal(await listenerRow.locator('input[value="read"]').isChecked(), true);
await listenerRow.locator('input[value="control"]').check();
assert.equal(await listenerRow.locator('input[value="guest"]').isChecked(), true);
assert.equal(await listenerPassword.isDisabled(), true);
await listenerRow.locator('input[value="guest"]').uncheck();
assert.equal(await listenerPassword.isDisabled(), false);
assert.deepEqual(runtimeErrors, []);
} finally {
@@ -646,8 +646,8 @@ fn validate_roles(roles: AuthRoles) -> Result<AuthRoles, String> {
if roles.is_empty() {
return Err("at least one role is required".to_string());
}
if roles.contains(&AuthRole::Guest) && roles.len() != 1 {
return Err("guest cannot be combined with other roles".to_string());
if roles.contains(&AuthRole::Guest) && roles.contains(&AuthRole::Administrator) {
return Err("guest cannot be combined with administrator".to_string());
}
Ok(roles)
}
@@ -1428,6 +1428,7 @@ mod tests {
#[test]
fn test_route_access_allows() {
let guest = roles(&[AuthRole::Guest]);
let guest_control = roles(&[AuthRole::Guest, AuthRole::Control]);
let read = roles(&[AuthRole::Read]);
let control = roles(&[AuthRole::Control]);
let transmit = roles(&[AuthRole::Transmit]);
@@ -1438,10 +1439,12 @@ mod tests {
assert!(RouteAccess::Public.allows(Some(&administrator)));
assert!(!RouteAccess::Account.allows(None));
assert!(!RouteAccess::Account.allows(Some(&guest)));
assert!(!RouteAccess::Account.allows(Some(&guest_control)));
assert!(RouteAccess::Account.allows(Some(&write)));
assert!(!RouteAccess::Read.allows(None));
assert!(RouteAccess::Read.allows(Some(&guest)));
assert!(RouteAccess::Control.allows(Some(&guest_control)));
assert!(RouteAccess::Read.allows(Some(&read)));
assert!(RouteAccess::Read.allows(Some(&control)));
assert!(RouteAccess::Read.allows(Some(&transmit)));
@@ -1604,12 +1607,25 @@ mod tests {
}
#[test]
fn guest_is_an_exclusive_role() {
fn guest_can_be_combined_with_non_administrator_roles() {
assert_eq!(
validate_roles(roles(&[AuthRole::Guest])).unwrap(),
roles(&[AuthRole::Guest])
validate_roles(roles(&[
AuthRole::Guest,
AuthRole::Read,
AuthRole::Control,
AuthRole::Transmit,
AuthRole::Write,
]))
.unwrap(),
roles(&[
AuthRole::Guest,
AuthRole::Read,
AuthRole::Control,
AuthRole::Transmit,
AuthRole::Write,
])
);
assert!(validate_roles(roles(&[AuthRole::Guest, AuthRole::Read])).is_err());
assert!(validate_roles(roles(&[AuthRole::Guest, AuthRole::Administrator])).is_err());
}
#[test]