mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-07-26 07:48:13 +00:00
0d804fbd39
* feat(access-control): fine grain tokens * fix(docs): clean wording * test(access-control): add tests for team extraction from access tokens and bucket info permissions * test(vocabulary): add test for ExpandGlob function to reject non-glob patterns * feat(helm): add multi-user access control documentation and schema support
137 lines
6.5 KiB
Go
137 lines
6.5 KiB
Go
// Package authz implements the UI-layer access-control policy for garage-ui.
|
|
//
|
|
// IMPORTANT: this is UI-layer policy, not a security boundary. garage-ui talks
|
|
// to Garage with a single admin token and a single S3 credential set; anyone
|
|
// holding those bypasses everything here.
|
|
package authz
|
|
|
|
import "strings"
|
|
|
|
// ScopeKind says what a permission applies to.
|
|
type ScopeKind int
|
|
|
|
const (
|
|
// ScopeGlobal permissions are all-or-nothing per team (cluster_permissions).
|
|
ScopeGlobal ScopeKind = iota
|
|
// ScopePrefix permissions are gated by a binding's bucket_prefixes.
|
|
ScopePrefix
|
|
)
|
|
|
|
// PermSpec describes one abstract permission. Endpoints lists the Garage admin
|
|
// API endpoint names (or S3 data-plane operations) the permission maps to.
|
|
// This registry is the only place those names appear in authz.
|
|
type PermSpec struct {
|
|
Scope ScopeKind
|
|
AdminOnly bool // not grantable to teams in v1; only the synthetic admin subject holds it
|
|
Endpoints []string
|
|
}
|
|
|
|
// Permission constants for every permission referenced from route wiring.
|
|
const (
|
|
PermBucketList = "bucket.list"
|
|
PermBucketRead = "bucket.read"
|
|
PermBucketCreate = "bucket.create"
|
|
PermBucketUpdate = "bucket.update"
|
|
PermBucketDelete = "bucket.delete"
|
|
PermObjectList = "object.list"
|
|
PermObjectRead = "object.read"
|
|
PermObjectWrite = "object.write"
|
|
PermObjectDelete = "object.delete"
|
|
PermAllowBucketKey = "permission.allow_bucket_key"
|
|
PermDenyBucketKey = "permission.deny_bucket_key"
|
|
PermKeyList = "key.list"
|
|
PermKeyRead = "key.read"
|
|
PermKeyReadSecret = "key.read_secret"
|
|
PermKeyCreate = "key.create"
|
|
PermKeyUpdate = "key.update"
|
|
PermKeyDelete = "key.delete"
|
|
PermClusterStatus = "cluster.status"
|
|
PermClusterHealth = "cluster.health"
|
|
PermClusterStatistics = "cluster.statistics"
|
|
PermNodeInfo = "node.info"
|
|
PermNodeStatistics = "node.statistics"
|
|
)
|
|
|
|
// Vocabulary is the full v1 permission registry, ratified in issue #33.
|
|
// admin_token.* is deliberately absent (admin-only implicitly, not modeled).
|
|
var Vocabulary = map[string]PermSpec{
|
|
"bucket.list": {Scope: ScopePrefix, Endpoints: []string{"ListBuckets"}},
|
|
"bucket.read": {Scope: ScopePrefix, Endpoints: []string{"GetBucketInfo"}},
|
|
"bucket.create": {Scope: ScopePrefix, Endpoints: []string{"CreateBucket"}},
|
|
"bucket.update": {Scope: ScopePrefix, Endpoints: []string{"UpdateBucket"}},
|
|
"bucket.delete": {Scope: ScopePrefix, Endpoints: []string{"DeleteBucket"}},
|
|
"bucket.cleanup_uploads": {Scope: ScopePrefix, Endpoints: []string{"CleanupIncompleteUploads"}},
|
|
"bucket.inspect_object": {Scope: ScopePrefix, Endpoints: []string{"InspectObject"}},
|
|
|
|
"bucket_alias.add": {Scope: ScopePrefix, Endpoints: []string{"AddBucketAlias"}},
|
|
"bucket_alias.remove": {Scope: ScopePrefix, Endpoints: []string{"RemoveBucketAlias"}},
|
|
|
|
// S3 data plane (object browser), no Garage admin endpoint.
|
|
"object.list": {Scope: ScopePrefix, Endpoints: []string{"S3:ListObjectsV2"}},
|
|
"object.read": {Scope: ScopePrefix, Endpoints: []string{"S3:GetObject", "S3:HeadObject", "S3:PresignGet"}},
|
|
"object.write": {Scope: ScopePrefix, Endpoints: []string{"S3:PutObject"}},
|
|
"object.delete": {Scope: ScopePrefix, Endpoints: []string{"S3:DeleteObject", "S3:DeleteObjects"}},
|
|
|
|
"permission.allow_bucket_key": {Scope: ScopePrefix, Endpoints: []string{"AllowBucketKey"}},
|
|
"permission.deny_bucket_key": {Scope: ScopePrefix, Endpoints: []string{"DenyBucketKey"}},
|
|
|
|
"key.list": {Scope: ScopeGlobal, Endpoints: []string{"ListKeys"}},
|
|
"key.read": {Scope: ScopeGlobal, Endpoints: []string{"GetKeyInfo"}},
|
|
"key.read_secret": {Scope: ScopeGlobal, AdminOnly: true, Endpoints: []string{"GetKeyInfo(showSecretKey)"}},
|
|
"key.create": {Scope: ScopeGlobal, AdminOnly: true, Endpoints: []string{"CreateKey"}},
|
|
"key.import": {Scope: ScopeGlobal, AdminOnly: true, Endpoints: []string{"ImportKey"}},
|
|
"key.update": {Scope: ScopeGlobal, AdminOnly: true, Endpoints: []string{"UpdateKey"}},
|
|
"key.delete": {Scope: ScopeGlobal, AdminOnly: true, Endpoints: []string{"DeleteKey"}},
|
|
|
|
"cluster.status": {Scope: ScopeGlobal, Endpoints: []string{"GetClusterStatus"}},
|
|
"cluster.health": {Scope: ScopeGlobal, Endpoints: []string{"GetClusterHealth"}},
|
|
"cluster.statistics": {Scope: ScopeGlobal, Endpoints: []string{"GetClusterStatistics"}},
|
|
"cluster.connect_nodes": {Scope: ScopeGlobal, Endpoints: []string{"ConnectClusterNodes"}},
|
|
|
|
"cluster.layout.read": {Scope: ScopeGlobal, Endpoints: []string{"GetClusterLayout"}},
|
|
"cluster.layout.history": {Scope: ScopeGlobal, Endpoints: []string{"GetClusterLayoutHistory"}},
|
|
"cluster.layout.apply": {Scope: ScopeGlobal, Endpoints: []string{"ApplyClusterLayout"}},
|
|
"cluster.layout.skip_dead_nodes": {Scope: ScopeGlobal, Endpoints: []string{"ClusterLayoutSkipDeadNodes"}},
|
|
|
|
"node.info": {Scope: ScopeGlobal, Endpoints: []string{"GetNodeInfo"}},
|
|
"node.statistics": {Scope: ScopeGlobal, Endpoints: []string{"GetNodeStatistics"}},
|
|
"node.snapshot": {Scope: ScopeGlobal, Endpoints: []string{"CreateMetadataSnapshot"}},
|
|
"node.repair": {Scope: ScopeGlobal, Endpoints: []string{"LaunchRepairOperation"}},
|
|
|
|
"worker.list": {Scope: ScopeGlobal, Endpoints: []string{"ListWorkers"}},
|
|
"worker.info": {Scope: ScopeGlobal, Endpoints: []string{"GetWorkerInfo"}},
|
|
"worker.get_variable": {Scope: ScopeGlobal, Endpoints: []string{"GetWorkerVariable"}},
|
|
"worker.set_variable": {Scope: ScopeGlobal, Endpoints: []string{"SetWorkerVariable"}},
|
|
|
|
"block.list_errors": {Scope: ScopeGlobal, Endpoints: []string{"ListBlockErrors"}},
|
|
"block.info": {Scope: ScopeGlobal, Endpoints: []string{"GetBlockInfo"}},
|
|
}
|
|
|
|
// IsValidPermission reports whether p names a concrete vocabulary entry.
|
|
// Globs are patterns, not permissions, and return false.
|
|
func IsValidPermission(p string) bool {
|
|
_, ok := Vocabulary[p]
|
|
return ok
|
|
}
|
|
|
|
// ExpandGlob expands a trailing-star pattern ("bucket.*", "cluster.layout.*",
|
|
// bare "*") against the vocabulary. Admin-only permissions are never matched
|
|
// by globs; they must be held via the synthetic admin subject. Returns nil
|
|
// when the pattern matches nothing or has no trailing star.
|
|
func ExpandGlob(pattern string) []string {
|
|
if !strings.HasSuffix(pattern, "*") {
|
|
return nil
|
|
}
|
|
prefix := strings.TrimSuffix(pattern, "*")
|
|
var out []string
|
|
for name, spec := range Vocabulary {
|
|
if spec.AdminOnly {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(name, prefix) {
|
|
out = append(out, name)
|
|
}
|
|
}
|
|
return out
|
|
}
|