mirror of
https://github.com/OliveTin/OliveTin
synced 2025-12-12 00:55:34 +00:00
368 lines
9.3 KiB
Go
368 lines
9.3 KiB
Go
package acl
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/OliveTin/OliveTin/internal/auth"
|
|
config "github.com/OliveTin/OliveTin/internal/config"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
type PermissionBits int
|
|
|
|
const (
|
|
View PermissionBits = 1 << iota
|
|
Exec
|
|
Logs
|
|
Kill
|
|
)
|
|
|
|
func (p PermissionBits) Has(permission PermissionBits) bool {
|
|
return p&permission != 0
|
|
}
|
|
|
|
// User respresents a person.
|
|
type AuthenticatedUser struct {
|
|
Username string
|
|
UsergroupLine string
|
|
|
|
Provider string
|
|
SID string
|
|
|
|
Acls []string
|
|
|
|
EffectivePolicy *config.ConfigurationPolicy
|
|
}
|
|
|
|
func (u *AuthenticatedUser) IsGuest() bool {
|
|
return u.Username == "guest" && u.Provider == "system"
|
|
}
|
|
|
|
func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
|
|
ret := []string{}
|
|
|
|
if sep != "" {
|
|
for _, v := range strings.Split(u.UsergroupLine, sep) {
|
|
trimmed := strings.TrimSpace(v)
|
|
|
|
if trimmed != "" {
|
|
ret = append(ret, trimmed)
|
|
}
|
|
}
|
|
} else {
|
|
ret = strings.Fields(u.UsergroupLine)
|
|
}
|
|
|
|
log.Debugf("parseUsergroupLine: %v, %v, sep:%v", u.UsergroupLine, ret, sep)
|
|
|
|
return ret
|
|
}
|
|
|
|
func (u *AuthenticatedUser) matchesUsergroupAcl(matchUsergroups []string, sep string) bool {
|
|
groupList := u.parseUsergroupLine(sep)
|
|
|
|
for _, group := range groupList {
|
|
if slices.Contains(matchUsergroups, group) {
|
|
log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
|
if cfg.LogDebugOptions.AclNotMatched {
|
|
log.WithFields(log.Fields{
|
|
"User": user.Username,
|
|
"Action": action.Title,
|
|
"ACL": acl.Name,
|
|
}).Debugf("%v - ACL Not Matched", aclFunction)
|
|
}
|
|
}
|
|
|
|
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
|
actionTitle := "N/A"
|
|
|
|
if action != nil {
|
|
actionTitle = action.Title
|
|
}
|
|
|
|
if cfg.LogDebugOptions.AclMatched {
|
|
log.WithFields(log.Fields{
|
|
"User": user.Username,
|
|
"Action": actionTitle,
|
|
"ACL": acl.Name,
|
|
}).Debugf("%v - Matched ACL", aclFunction)
|
|
}
|
|
}
|
|
|
|
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
|
if cfg.LogDebugOptions.AclNoneMatched {
|
|
log.WithFields(log.Fields{
|
|
"User": user.Username,
|
|
"Action": action.Title,
|
|
"Default": defaultPermission,
|
|
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
|
|
}
|
|
}
|
|
|
|
func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits {
|
|
type permPair struct {
|
|
enabled bool
|
|
bit PermissionBits
|
|
}
|
|
|
|
permMap := []permPair{
|
|
{permissions.View, View},
|
|
{permissions.Exec, Exec},
|
|
{permissions.Logs, Logs},
|
|
{permissions.Kill, Kill},
|
|
}
|
|
|
|
var ret PermissionBits
|
|
|
|
for _, perm := range permMap {
|
|
if perm.enabled {
|
|
ret |= perm.bit
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) bool {
|
|
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
|
|
|
if cfg.LogDebugOptions.AclCheckStarted {
|
|
log.WithFields(log.Fields{
|
|
"actionTitle": action.Title,
|
|
"username": user.Username,
|
|
"usergroupLine": user.UsergroupLine,
|
|
"relevantAcls": len(relevantAcls),
|
|
"requiredPermission": requiredPermission,
|
|
}).Debugf("ACL check - %v", aclFunction)
|
|
}
|
|
|
|
for _, acl := range relevantAcls {
|
|
permissionBits := permissionsConfigToBits(acl.Permissions)
|
|
|
|
if permissionBits.Has(requiredPermission) {
|
|
logAclMatched(cfg, aclFunction, user, action, acl)
|
|
|
|
return true
|
|
} else {
|
|
logAclNotMatched(cfg, aclFunction, user, action, acl)
|
|
}
|
|
}
|
|
|
|
logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)
|
|
|
|
return defaultValue
|
|
}
|
|
|
|
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
|
func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
|
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
|
|
}
|
|
|
|
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
|
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
|
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
|
|
}
|
|
|
|
// IsAllowedView checks if a User is allowed to view an Action
|
|
func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
|
if action.Hidden {
|
|
return false
|
|
}
|
|
|
|
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
|
}
|
|
|
|
func IsAllowedKill(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
|
return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
|
|
}
|
|
|
|
func getHeaderKeyOrEmpty(headers http.Header, key string) string {
|
|
values := headers.Values(key)
|
|
if len(values) > 0 {
|
|
return values[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// UserFromContext tries to find a user from a Connect RPC context
|
|
func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *config.Config) *AuthenticatedUser {
|
|
user := userFromHeaders(req, cfg)
|
|
if user.Username == "" {
|
|
user = userFromLocalSession(req, cfg, user)
|
|
}
|
|
if user.Username == "" {
|
|
user = *UserGuest(cfg)
|
|
} else {
|
|
buildUserAcls(cfg, &user)
|
|
}
|
|
|
|
path := ""
|
|
if req != nil {
|
|
path = req.Spec().Procedure
|
|
}
|
|
|
|
log.WithFields(log.Fields{
|
|
"username": user.Username,
|
|
"usergroupLine": user.UsergroupLine,
|
|
"provider": user.Provider,
|
|
"acls": user.Acls,
|
|
"path": path,
|
|
}).Debugf("Authenticated API request")
|
|
return &user
|
|
}
|
|
|
|
//gocyclo:ignore
|
|
func userFromHeaders[T any](req *connect.Request[T], cfg *config.Config) AuthenticatedUser {
|
|
var u AuthenticatedUser
|
|
if req == nil {
|
|
return u
|
|
}
|
|
if cfg.AuthHttpHeaderUsername != "" {
|
|
u.Username = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUsername)
|
|
}
|
|
if cfg.AuthHttpHeaderUserGroup != "" {
|
|
u.UsergroupLine = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUserGroup)
|
|
}
|
|
if prov := getHeaderKeyOrEmpty(req.Header(), "provider"); prov != "" {
|
|
u.Provider = prov
|
|
}
|
|
return u
|
|
}
|
|
|
|
//gocyclo:ignore
|
|
func userFromLocalSession[T any](req *connect.Request[T], cfg *config.Config, u AuthenticatedUser) AuthenticatedUser {
|
|
if req == nil || u.Username != "" {
|
|
return u
|
|
}
|
|
dummy := &http.Request{Header: req.Header()}
|
|
c, err := dummy.Cookie("olivetin-sid-local")
|
|
if err != nil || c == nil || c.Value == "" {
|
|
return u
|
|
}
|
|
sess := auth.GetUserSession("local", c.Value)
|
|
if sess == nil {
|
|
log.WithFields(log.Fields{"sid": c.Value, "provider": "local"}).Warn("UserFromContext: stale local session")
|
|
return u
|
|
}
|
|
if cfgUser := cfg.FindUserByUsername(sess.Username); cfgUser != nil {
|
|
u.Username = cfgUser.Username
|
|
u.UsergroupLine = cfgUser.Usergroup
|
|
u.Provider = "local"
|
|
u.SID = c.Value
|
|
return u
|
|
}
|
|
log.WithFields(log.Fields{"username": sess.Username}).Warn("UserFromContext: local session user not in config")
|
|
return u
|
|
}
|
|
|
|
func UserGuest(cfg *config.Config) *AuthenticatedUser {
|
|
ret := &AuthenticatedUser{}
|
|
ret.Username = "guest"
|
|
ret.UsergroupLine = "guest"
|
|
ret.Provider = "system"
|
|
|
|
buildUserAcls(cfg, ret)
|
|
|
|
return ret
|
|
}
|
|
|
|
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
|
|
ret := &AuthenticatedUser{
|
|
Username: username,
|
|
UsergroupLine: "system",
|
|
Provider: "system",
|
|
}
|
|
|
|
buildUserAcls(cfg, ret)
|
|
|
|
return ret
|
|
}
|
|
|
|
func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
|
|
for _, acl := range cfg.AccessControlLists {
|
|
if slices.Contains(acl.MatchUsernames, user.Username) {
|
|
user.Acls = append(user.Acls, acl.Name)
|
|
continue
|
|
}
|
|
|
|
if user.matchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
|
|
user.Acls = append(user.Acls, acl.Name)
|
|
continue
|
|
}
|
|
}
|
|
|
|
user.EffectivePolicy = getEffectivePolicy(cfg, user)
|
|
}
|
|
|
|
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
|
|
if !slices.Contains(user.Acls, acl.Name) {
|
|
// If the user does not have this ACL, then it is not relevant
|
|
|
|
return false
|
|
}
|
|
|
|
if acl.AddToEveryAction {
|
|
return true
|
|
}
|
|
|
|
if slices.Contains(actionAcls, acl.Name) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
|
|
var ret []*config.AccessControlList
|
|
|
|
for _, acl := range cfg.AccessControlLists {
|
|
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
|
|
ret = append(ret, acl)
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func getEffectivePolicy(cfg *config.Config, user *AuthenticatedUser) *config.ConfigurationPolicy {
|
|
ret := &config.ConfigurationPolicy{
|
|
ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
|
|
ShowLogList: cfg.DefaultPolicy.ShowLogList,
|
|
}
|
|
|
|
for _, acl := range cfg.AccessControlLists {
|
|
if slices.Contains(user.Acls, acl.Name) {
|
|
logAclMatched(cfg, "GetEffectivePolicy", user, nil, acl)
|
|
|
|
ret = buildConfigurationPolicy(ret, acl.Policy)
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
|
|
if policy.ShowDiagnostics {
|
|
ret.ShowDiagnostics = policy.ShowDiagnostics
|
|
}
|
|
|
|
if policy.ShowLogList {
|
|
ret.ShowLogList = policy.ShowLogList
|
|
}
|
|
|
|
return ret
|
|
}
|