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

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:
2026-03-26 17:23:12 +00:00
parent e8e45184f7
commit b10f5bee05
28 changed files with 206 additions and 285 deletions

View File

@@ -33,7 +33,6 @@ func NewProfileBootstrapHandler(s store.Stores) *ProfileBootstrapHandler {
// GET /api/v1/profile/bootstrap
func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
userID := getUserID(c)
role, _ := c.Get("role")
ctx := c.Request.Context()
// ── User profile ────────────────────────
@@ -48,27 +47,20 @@ func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
"username": user.Username,
"display_name": user.DisplayName,
"email": user.Email,
"role": user.Role,
}
if user.AvatarURL != "" {
userPayload["avatar"] = user.AvatarURL
}
// ── Permissions ─────────────────────────
var permList []string
if role == "admin" {
permList = make([]string, len(auth.AllPermissions))
copy(permList, auth.AllPermissions)
} else {
perms, err := auth.ResolvePermissions(ctx, h.stores, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve permissions"})
return
}
permList = make([]string, 0, len(perms))
for p := range perms {
permList = append(permList, p)
}
perms, err := auth.ResolvePermissions(ctx, h.stores, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve permissions"})
return
}
permList := make([]string, 0, len(perms))
for p := range perms {
permList = append(permList, p)
}
sort.Strings(permList)
@@ -77,7 +69,6 @@ func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
if groupIDs == nil {
groupIDs = []string{}
}
groupIDs = append(groupIDs, auth.EveryoneGroupID)
// ── Teams ───────────────────────────────
teams, _ := h.stores.Teams.ListForUser(ctx, userID)