Give local users their own manager: custom passwords and role reassignment
Move user management out of Settings into its own /users page (nav-gated
to owners), let an owner type a specific password on reset instead of
always generating a random one, and add role reassignment via a new
PUT /auth/users/{id}/role endpoint. Role changes revoke the target's
existing sessions, same as a password reset, so a demoted user can't
keep acting under a stale, higher-privileged session.
This commit is contained in:
@@ -155,3 +155,69 @@ func TestIntegrationSetPasswordHashRevokesSessions(t *testing.T) {
|
||||
t.Errorf("GetSession after password reset: err = %v, want ErrNotFound (reset must revoke existing sessions)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationSetRoleRevokesSessions(t *testing.T) {
|
||||
store := integrationStore(t)
|
||||
ctx := context.Background()
|
||||
username := testUsername(t)
|
||||
|
||||
hash, _ := HashPassword("password1")
|
||||
user, err := store.CreateUser(ctx, username, hash, authz.RoleViewer)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.DeleteUser(ctx, user.ID) })
|
||||
|
||||
raw, err := store.CreateSession(ctx, user.ID, "default", authz.RoleViewer, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSession: %v", err)
|
||||
}
|
||||
|
||||
if err := store.SetRole(ctx, user.ID, authz.RoleAdmin); err != nil {
|
||||
t.Fatalf("SetRole: %v", err)
|
||||
}
|
||||
|
||||
got, _, err := store.GetUserForLogin(ctx, username)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserForLogin: %v", err)
|
||||
}
|
||||
if got.Role != authz.RoleAdmin {
|
||||
t.Errorf("role after SetRole = %q, want admin", got.Role)
|
||||
}
|
||||
|
||||
if _, err := store.GetSession(ctx, hashToken(raw)); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("GetSession after role change: err = %v, want ErrNotFound (role change must revoke existing sessions)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationSetRoleAcceptsEveryRole confirms tenant_memberships'
|
||||
// role CHECK constraint (0020_create_tenant_memberships.sql) accepts
|
||||
// all four roles via SetRole's UPDATE, not just CreateUser's INSERT --
|
||||
// the handler-level fake-store test already covers all sixteen ordered
|
||||
// transitions, but only a real Postgres run proves the constraint
|
||||
// itself doesn't reject any of them.
|
||||
func TestIntegrationSetRoleAcceptsEveryRole(t *testing.T) {
|
||||
store := integrationStore(t)
|
||||
ctx := context.Background()
|
||||
username := testUsername(t)
|
||||
|
||||
hash, _ := HashPassword("password1")
|
||||
user, err := store.CreateUser(ctx, username, hash, authz.RoleViewer)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.DeleteUser(ctx, user.ID) })
|
||||
|
||||
for _, role := range []authz.Role{authz.RoleEditor, authz.RoleAdmin, authz.RoleOwner, authz.RoleViewer} {
|
||||
if err := store.SetRole(ctx, user.ID, role); err != nil {
|
||||
t.Fatalf("SetRole(%s): %v", role, err)
|
||||
}
|
||||
got, _, err := store.GetUserForLogin(ctx, username)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserForLogin after SetRole(%s): %v", role, err)
|
||||
}
|
||||
if got.Role != role {
|
||||
t.Fatalf("role after SetRole(%s) = %q, want %q", role, got.Role, role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user