151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
// Package access implements uopi's global user-access policy: every user is
|
|
// trusted (full write) by default, while a configured blacklist can downgrade
|
|
// specific users to read-only or no access. It also resolves the per-request
|
|
// user identity and the user→group memberships defined in config.
|
|
//
|
|
// This is the foundation layer (Phase 1). Per-panel ownership/ACL evaluation is
|
|
// layered on top of this in a later phase.
|
|
package access
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Level is a global access level. Higher levels include lower ones.
|
|
type Level int
|
|
|
|
const (
|
|
// LevelNone denies all access.
|
|
LevelNone Level = iota
|
|
// LevelRead permits reads only (no create/update/delete/share, no signal writes).
|
|
LevelRead
|
|
// LevelWrite permits full access. This is the default for any user not blacklisted.
|
|
LevelWrite
|
|
)
|
|
|
|
// String renders the level using the same tokens accepted by ParseLevel and
|
|
// surfaced to the frontend via /api/v1/me.
|
|
func (l Level) String() string {
|
|
switch l {
|
|
case LevelNone:
|
|
return "none"
|
|
case LevelRead:
|
|
return "readonly"
|
|
default:
|
|
return "write"
|
|
}
|
|
}
|
|
|
|
// ParseLevel maps a config string to a Level. Unknown values restrict to
|
|
// read-only, since the only reason to list a user is to limit them.
|
|
func ParseLevel(s string) Level {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "noaccess", "none", "no":
|
|
return LevelNone
|
|
case "readonly", "read", "ro":
|
|
return LevelRead
|
|
case "write", "readwrite", "rw", "full":
|
|
return LevelWrite
|
|
default:
|
|
return LevelRead
|
|
}
|
|
}
|
|
|
|
// Policy holds the resolved global access configuration. It is immutable after
|
|
// construction and safe for concurrent use.
|
|
type Policy struct {
|
|
defaultUser string
|
|
blacklist map[string]Level // user → downgraded level
|
|
userGroups map[string][]string // user → groups they belong to
|
|
groupNames []string // all configured group names (sorted)
|
|
}
|
|
|
|
// New builds a Policy. blacklist maps a username to a config level string;
|
|
// groups maps a group name to its member usernames.
|
|
func New(defaultUser string, blacklist map[string]string, groups map[string][]string) *Policy {
|
|
p := &Policy{
|
|
defaultUser: strings.TrimSpace(defaultUser),
|
|
blacklist: make(map[string]Level),
|
|
userGroups: make(map[string][]string),
|
|
}
|
|
for user, lvl := range blacklist {
|
|
u := strings.TrimSpace(user)
|
|
if u == "" {
|
|
continue
|
|
}
|
|
p.blacklist[u] = ParseLevel(lvl)
|
|
}
|
|
for g, members := range groups {
|
|
g = strings.TrimSpace(g)
|
|
if g == "" {
|
|
continue
|
|
}
|
|
p.groupNames = append(p.groupNames, g)
|
|
for _, m := range members {
|
|
m = strings.TrimSpace(m)
|
|
if m == "" {
|
|
continue
|
|
}
|
|
p.userGroups[m] = append(p.userGroups[m], g)
|
|
}
|
|
}
|
|
sort.Strings(p.groupNames)
|
|
return p
|
|
}
|
|
|
|
// GroupNames returns a copy of every configured user-group name, sorted.
|
|
func (p *Policy) GroupNames() []string {
|
|
out := make([]string, len(p.groupNames))
|
|
copy(out, p.groupNames)
|
|
return out
|
|
}
|
|
|
|
// ResolveUser trims the proxy-provided header value and falls back to the
|
|
// configured default_user when it is empty (e.g. unproxied/dev deployments).
|
|
func (p *Policy) ResolveUser(headerValue string) string {
|
|
u := strings.TrimSpace(headerValue)
|
|
if u == "" {
|
|
return p.defaultUser
|
|
}
|
|
return u
|
|
}
|
|
|
|
// Level returns the global access level for a user. Users that are neither
|
|
// blacklisted nor anonymous get full write access.
|
|
func (p *Policy) Level(user string) Level {
|
|
user = strings.TrimSpace(user)
|
|
if user == "" {
|
|
// No identity at all (no proxy header, no default_user): trusted LAN.
|
|
return LevelWrite
|
|
}
|
|
if lvl, ok := p.blacklist[user]; ok {
|
|
return lvl
|
|
}
|
|
return LevelWrite
|
|
}
|
|
|
|
// GroupsOf returns a copy of the groups a user belongs to.
|
|
func (p *Policy) GroupsOf(user string) []string {
|
|
src := p.userGroups[strings.TrimSpace(user)]
|
|
out := make([]string, len(src))
|
|
copy(out, src)
|
|
return out
|
|
}
|
|
|
|
// ── request-scoped user identity ────────────────────────────────────────────
|
|
|
|
type ctxKey struct{}
|
|
|
|
// WithUser returns a copy of ctx carrying the resolved end-user identity.
|
|
func WithUser(ctx context.Context, user string) context.Context {
|
|
return context.WithValue(ctx, ctxKey{}, user)
|
|
}
|
|
|
|
// UserFrom returns the identity stored by WithUser, or "" if none.
|
|
func UserFrom(ctx context.Context) string {
|
|
u, _ := ctx.Value(ctxKey{}).(string)
|
|
return u
|
|
}
|