481359ddb2
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
140 lines
4.7 KiB
Go
140 lines
4.7 KiB
Go
package authz
|
|
|
|
// OrgRole is the instance-wide role carried on the user record. It grants org
|
|
// scopes outright and supplies the default workspace role used on workspaces
|
|
// that are visible to the whole organization.
|
|
//
|
|
// The `owner` role and the `org.delete` scope described in
|
|
// docs/specs/auth/requirements/roles-and-scopes.md arrive with multi-tenancy: without an
|
|
// org entity there is nothing to own or delete, and shipping an unreachable
|
|
// role would only clutter the role picker.
|
|
type OrgRole string
|
|
|
|
const (
|
|
// OrgRoleAdmin manages users, org settings, and shared configuration. It
|
|
// is NOT a reach role: an admin reaches a workspace the same way anyone
|
|
// else does, through the unit tree, and never reaches one sitting in
|
|
// someone's personal unit.
|
|
OrgRoleAdmin OrgRole = "admin"
|
|
// OrgRoleMember is a regular contributor.
|
|
OrgRoleMember OrgRole = "member"
|
|
// OrgRoleGuest holds no org scopes and reaches only workspaces they are an
|
|
// explicit member of, never one reached through the unit tree.
|
|
OrgRoleGuest OrgRole = "guest"
|
|
)
|
|
|
|
// WorkspaceRole is the per-workspace role, held either by an explicit
|
|
// workspace_members row or inherited from membership of a unit above the one
|
|
// the workspace sits in. Where both apply the stronger of the two wins.
|
|
type WorkspaceRole string
|
|
|
|
const (
|
|
WorkspaceRoleOwner WorkspaceRole = "owner"
|
|
WorkspaceRoleCollaborator WorkspaceRole = "collaborator"
|
|
WorkspaceRoleViewer WorkspaceRole = "viewer"
|
|
// WorkspaceRoleNone means the workspace is unreachable.
|
|
WorkspaceRoleNone WorkspaceRole = ""
|
|
)
|
|
|
|
// NormalizeOrgRole coerces an unknown or empty stored value to the least
|
|
// privileged real role. Unknown input must never widen access.
|
|
func NormalizeOrgRole(value string) OrgRole {
|
|
switch OrgRole(value) {
|
|
case OrgRoleAdmin:
|
|
return OrgRoleAdmin
|
|
case OrgRoleMember:
|
|
return OrgRoleMember
|
|
case OrgRoleGuest:
|
|
return OrgRoleGuest
|
|
default:
|
|
return OrgRoleGuest
|
|
}
|
|
}
|
|
|
|
// NormalizeWorkspaceRole coerces an unknown stored value to no access.
|
|
func NormalizeWorkspaceRole(value string) WorkspaceRole {
|
|
switch WorkspaceRole(value) {
|
|
case WorkspaceRoleOwner:
|
|
return WorkspaceRoleOwner
|
|
case WorkspaceRoleCollaborator:
|
|
return WorkspaceRoleCollaborator
|
|
case WorkspaceRoleViewer:
|
|
return WorkspaceRoleViewer
|
|
default:
|
|
return WorkspaceRoleNone
|
|
}
|
|
}
|
|
|
|
// IsAssignableWorkspaceRole reports whether a role may be set through the
|
|
// member API. Owner is reached by transfer, not by assignment.
|
|
func IsAssignableWorkspaceRole(role WorkspaceRole) bool {
|
|
return role == WorkspaceRoleCollaborator || role == WorkspaceRoleViewer
|
|
}
|
|
|
|
// orgRoleScopes maps an org role to the org scopes it grants.
|
|
var orgRoleScopes = map[OrgRole]Set{
|
|
OrgRoleAdmin: NewSet(ScopeOrgMembersManage, ScopeOrgSettingsManage, ScopeOrgConfigManage, ScopeUnitManage),
|
|
OrgRoleMember: NewSet(),
|
|
OrgRoleGuest: NewSet(),
|
|
}
|
|
|
|
// workspaceRoleScopes maps a workspace role to the workspace scopes it grants.
|
|
//
|
|
// session.exec is deliberately absent from viewer and present on collaborator:
|
|
// prompting an agent is bounded by that agent's own permissions, a shell in the
|
|
// worktree is not, so reading a transcript must never imply a shell.
|
|
var workspaceRoleScopes = map[WorkspaceRole]Set{
|
|
WorkspaceRoleOwner: NewSet(
|
|
ScopeWorkspaceRead, ScopeWorkspaceManage, ScopeTaskWrite,
|
|
ScopeSessionPrompt, ScopeSessionControl, ScopeSessionExec,
|
|
ScopeRepositoryManage, ScopeSecretManage, ScopeMemberManage,
|
|
),
|
|
WorkspaceRoleCollaborator: NewSet(
|
|
ScopeWorkspaceRead, ScopeTaskWrite,
|
|
ScopeSessionPrompt, ScopeSessionControl, ScopeSessionExec,
|
|
),
|
|
WorkspaceRoleViewer: NewSet(ScopeWorkspaceRead),
|
|
WorkspaceRoleNone: NewSet(),
|
|
}
|
|
|
|
// OrgScopes returns the org scopes granted by an org role.
|
|
func OrgScopes(role OrgRole) Set { return copySet(orgRoleScopes[role]) }
|
|
|
|
// WorkspaceScopes returns the workspace scopes granted by a workspace role.
|
|
func WorkspaceScopes(role WorkspaceRole) Set { return copySet(workspaceRoleScopes[role]) }
|
|
|
|
// AllScopes returns every scope both role tables can grant. Used by the
|
|
// synthetic (auth-disabled) identity, which must behave exactly as the
|
|
// pre-auth single user did.
|
|
func AllScopes() Set {
|
|
all := NewSet()
|
|
for _, def := range registry {
|
|
all[def.Scope] = struct{}{}
|
|
}
|
|
return all
|
|
}
|
|
|
|
func copySet(src Set) Set {
|
|
out := make(Set, len(src))
|
|
for scope := range src {
|
|
out[scope] = struct{}{}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// workspaceRoleRank orders the workspace roles so they can be combined by
|
|
// maximum. The zero value is the weakest, so an unknown role never outranks a
|
|
// real one.
|
|
func workspaceRoleRank(role WorkspaceRole) int {
|
|
switch role {
|
|
case WorkspaceRoleOwner:
|
|
return 3
|
|
case WorkspaceRoleCollaborator:
|
|
return 2
|
|
case WorkspaceRoleViewer:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|