drop users.role column: full RBAC through group membership
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m22s
CI/CD / test-sqlite (pull_request) Successful in 2m35s
CI/CD / build-and-deploy (pull_request) Successful in 1m18s
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m22s
CI/CD / test-sqlite (pull_request) Successful in 2m35s
CI/CD / build-and-deploy (pull_request) Successful in 1m18s
The role column was a pre-RBAC artifact. All authorization now flows through explicit group membership and permission grants: - Everyone group: all users added on creation (no implicit membership) - Admins group: grants surface.admin.access + all platform permissions - JWT claims, login response, profile: role field removed - OIDC: isIdPAdmin() maps IdP claims → Admins group (no role writes) - Admin UI: role dropdown removed, admin managed through groups - Middleware cache simplified to isActive only 28 files changed, -79 lines net. Zero magic roles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,9 +93,6 @@ type UserStore interface {
|
||||
|
||||
// ── CS2 additions (v0.29.0) ──
|
||||
|
||||
// CountByRole returns the number of users with a given role.
|
||||
CountByRole(ctx context.Context, role string) (int, error)
|
||||
|
||||
// CountAll returns the total number of users.
|
||||
CountAll(ctx context.Context) (int, error)
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ type UserStore struct{}
|
||||
func NewUserStore() *UserStore { return &UserStore{} }
|
||||
|
||||
const userCols = `id, username, email, password_hash, display_name, avatar_url,
|
||||
role, is_active, settings, created_at, updated_at, last_login_at,
|
||||
is_active, settings, created_at, updated_at, last_login_at,
|
||||
auth_source, external_id, handle`
|
||||
|
||||
const userListCols = `id, username, email, display_name, avatar_url, role, is_active,
|
||||
const userListCols = `id, username, email, display_name, avatar_url, is_active,
|
||||
settings, created_at, updated_at, last_login_at, auth_source, external_id, handle`
|
||||
|
||||
func (s *UserStore) Create(ctx context.Context, u *models.User) error {
|
||||
@@ -27,10 +27,10 @@ func (s *UserStore) Create(ctx context.Context, u *models.User) error {
|
||||
u.AuthSource = "builtin"
|
||||
}
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO users (username, email, password_hash, display_name, role, is_active, settings, auth_source, external_id, handle)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
INSERT INTO users (username, email, password_hash, display_name, is_active, settings, auth_source, external_id, handle)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
u.Username, u.Email, u.PasswordHash, u.DisplayName, u.Role, u.IsActive,
|
||||
u.Username, u.Email, u.PasswordHash, u.DisplayName, u.IsActive,
|
||||
ToJSON(u.Settings), u.AuthSource, u.ExternalID, u.Handle,
|
||||
).Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func (s *UserStore) List(ctx context.Context, opts store.ListOptions) ([]models.
|
||||
var extID, hdl sql.NullString
|
||||
var sj []byte
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Email, &dn, &av,
|
||||
&u.Role, &u.IsActive, &sj, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt,
|
||||
&u.IsActive, &sj, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt,
|
||||
&u.AuthSource, &extID, &hdl); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func scanOneUser(ctx context.Context, query string, args ...interface{}) (*model
|
||||
var sj []byte
|
||||
err := DB.QueryRowContext(ctx, query, args...).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &ph, &dn, &av,
|
||||
&u.Role, &u.IsActive, &sj, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt,
|
||||
&u.IsActive, &sj, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt,
|
||||
&u.AuthSource, &extID, &hdl,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -233,13 +233,6 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin
|
||||
|
||||
// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────
|
||||
|
||||
func (s *UserStore) CountByRole(ctx context.Context, role string) (int, error) {
|
||||
var count int
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT COUNT(*) FROM users WHERE role = $1`, role).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
UPDATE users SET settings = (
|
||||
|
||||
@@ -16,10 +16,10 @@ type UserStore struct{}
|
||||
func NewUserStore() *UserStore { return &UserStore{} }
|
||||
|
||||
const userCols = `id, username, email, password_hash, display_name, avatar_url,
|
||||
role, is_active, settings, created_at, updated_at, last_login_at,
|
||||
is_active, settings, created_at, updated_at, last_login_at,
|
||||
auth_source, external_id, handle`
|
||||
|
||||
const userListCols = `id, username, email, display_name, avatar_url, role, is_active,
|
||||
const userListCols = `id, username, email, display_name, avatar_url, is_active,
|
||||
settings, created_at, updated_at, last_login_at, auth_source, external_id, handle`
|
||||
|
||||
func (s *UserStore) Create(ctx context.Context, u *models.User) error {
|
||||
@@ -31,10 +31,10 @@ func (s *UserStore) Create(ctx context.Context, u *models.User) error {
|
||||
u.AuthSource = "builtin"
|
||||
}
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO users (id, username, email, password_hash, display_name, role, is_active,
|
||||
INSERT INTO users (id, username, email, password_hash, display_name, is_active,
|
||||
settings, created_at, updated_at, auth_source, external_id, handle)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
u.ID, u.Username, u.Email, u.PasswordHash, u.DisplayName, u.Role, u.IsActive,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
u.ID, u.Username, u.Email, u.PasswordHash, u.DisplayName, u.IsActive,
|
||||
ToJSON(u.Settings), now.Format(timeFmt), now.Format(timeFmt),
|
||||
u.AuthSource, u.ExternalID, u.Handle,
|
||||
)
|
||||
@@ -106,7 +106,7 @@ func (s *UserStore) List(ctx context.Context, opts store.ListOptions) ([]models.
|
||||
var extID, hdl sql.NullString
|
||||
var sj []byte
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Email, &dn, &av,
|
||||
&u.Role, &u.IsActive, &sj, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
&u.IsActive, &sj, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
&u.AuthSource, &extID, &hdl); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (s *UserStore) scanOne(ctx context.Context, query string, args ...interface
|
||||
var sj []byte
|
||||
err := DB.QueryRowContext(ctx, query, args...).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &ph, &dn, &av,
|
||||
&u.Role, &u.IsActive, &sj, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
&u.IsActive, &sj, st(&u.CreatedAt), st(&u.UpdatedAt), stN(&u.LastLoginAt),
|
||||
&u.AuthSource, &extID, &hdl,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -240,13 +240,6 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin
|
||||
|
||||
// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────
|
||||
|
||||
func (s *UserStore) CountByRole(ctx context.Context, role string) (int, error) {
|
||||
var count int
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT COUNT(*) FROM users WHERE role = ?`, role).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
UPDATE users SET settings = json_patch(
|
||||
|
||||
Reference in New Issue
Block a user