mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-08 09:00:22 +09:00
124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
// Package scope models the OAuth capability scopes the Headscale v2 API enforces
|
|
// and the rule for whether a granted set of scopes satisfies a required one.
|
|
//
|
|
// The vocabulary is taken from Tailscale's OpenAPI spec (the same scope names
|
|
// the Terraform provider and Kubernetes operator request), so a client written
|
|
// against Tailscale's scopes works unchanged against Headscale. The grant
|
|
// predicate is kept here, separate from the HTTP/huma layer in
|
|
// hscontrol/api/v2, so it can be tested exhaustively on its own.
|
|
package scope
|
|
|
|
import "strings"
|
|
|
|
// Scope is an OAuth capability an operation requires and a token grants. The names
|
|
// mirror Tailscale's API scopes; a "...:read" scope is the read-only subset of its
|
|
// write scope.
|
|
type Scope string
|
|
|
|
const (
|
|
// All and AllRead are Tailscale's forward-compatible super-scopes: "all"
|
|
// grants every other scope, "all:read" grants every :read subset.
|
|
All Scope = "all"
|
|
AllRead Scope = "all:read"
|
|
|
|
AuthKeys Scope = "auth_keys"
|
|
AuthKeysRead Scope = "auth_keys:read"
|
|
|
|
// OAuthKeys gates managing OAuth clients (keyType:"client" on the keys
|
|
// resource).
|
|
OAuthKeys Scope = "oauth_keys"
|
|
OAuthKeysRead Scope = "oauth_keys:read"
|
|
|
|
DevicesCore Scope = "devices:core"
|
|
DevicesCoreRead Scope = "devices:core:read"
|
|
|
|
DevicesRoutes Scope = "devices:routes"
|
|
DevicesRoutesRead Scope = "devices:routes:read"
|
|
|
|
PolicyFile Scope = "policy_file"
|
|
PolicyFileRead Scope = "policy_file:read"
|
|
|
|
FeatureSettings Scope = "feature_settings"
|
|
FeatureSettingsRead Scope = "feature_settings:read"
|
|
|
|
Users Scope = "users"
|
|
UsersRead Scope = "users:read"
|
|
)
|
|
|
|
const readSuffix = ":read"
|
|
|
|
// Known returns every scope in the vocabulary, in a stable order. Useful for
|
|
// exhaustive iteration in tests and documentation.
|
|
func Known() []Scope {
|
|
return []Scope{
|
|
All, AllRead,
|
|
AuthKeys, AuthKeysRead,
|
|
OAuthKeys, OAuthKeysRead,
|
|
DevicesCore, DevicesCoreRead,
|
|
DevicesRoutes, DevicesRoutesRead,
|
|
PolicyFile, PolicyFileRead,
|
|
FeatureSettings, FeatureSettingsRead,
|
|
Users, UsersRead,
|
|
}
|
|
}
|
|
|
|
// IsRead reports whether s is a read-only scope (its name ends with ":read").
|
|
func (s Scope) IsRead() bool {
|
|
return strings.HasSuffix(string(s), readSuffix)
|
|
}
|
|
|
|
// IsWrite reports whether s is a non-empty write scope.
|
|
func (s Scope) IsWrite() bool {
|
|
return s != "" && !s.IsRead()
|
|
}
|
|
|
|
// Parse converts scope strings (as stored on a token or client) into Scope values.
|
|
// Unknown strings are kept as-is; they simply never satisfy any required scope.
|
|
func Parse(ss []string) []Scope {
|
|
out := make([]Scope, len(ss))
|
|
for i, s := range ss {
|
|
out[i] = Scope(s)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// Grants reports whether the granted scopes satisfy the required want scope.
|
|
func Grants(granted []Scope, want Scope) bool {
|
|
for _, g := range granted {
|
|
if satisfies(g, want) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// satisfies reports whether a single held scope satisfies want: exact match; a
|
|
// write scope grants its own :read subset; "all" grants everything; "all:read"
|
|
// grants any :read scope.
|
|
func satisfies(have, want Scope) bool {
|
|
if have == want || have == All {
|
|
return true
|
|
}
|
|
|
|
if have == AllRead {
|
|
return want.IsRead()
|
|
}
|
|
|
|
// A write scope grants its own read subset, e.g. auth_keys ⊇ auth_keys:read.
|
|
return string(want) == string(have)+readSuffix
|
|
}
|
|
|
|
// RequiresTags reports whether any scope obliges a credential to carry tags:
|
|
// devices:core and auth_keys mint tagged, tailnet-owned credentials.
|
|
func RequiresTags(scopes []Scope) bool {
|
|
for _, s := range scopes {
|
|
if s == DevicesCore || s == AuthKeys {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|