From defcf6d26ee2bc3f9c47ff98e6d0ea6c9ec9f382 Mon Sep 17 00:00:00 2001 From: James Read Date: Fri, 30 Aug 2024 21:30:44 +0100 Subject: [PATCH] fmt: Rebuilt ACL code to have less boilerplate (#397) --- internal/acl/acl.go | 128 +++++++++++++++++++++++--------- internal/config/config.go | 1 + internal/httpservers/restapi.go | 8 +- 3 files changed, 96 insertions(+), 41 deletions(-) diff --git a/internal/acl/acl.go b/internal/acl/acl.go index a8c7b3c..6fb6c6c 100644 --- a/internal/acl/acl.go +++ b/internal/acl/acl.go @@ -9,6 +9,18 @@ import ( "google.golang.org/grpc/metadata" ) +type PermissionBits int + +const ( + View PermissionBits = 1 << iota + Exec + Logs +) + +func (p PermissionBits) Has(permission PermissionBits) bool { + return p&permission != 0 +} + // User respresents a person. type AuthenticatedUser struct { Username string @@ -17,7 +29,7 @@ type AuthenticatedUser struct { acls []string } -func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) { +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, @@ -36,34 +48,70 @@ func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUs } } -// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs -func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool { - for _, acl := range getRelevantAcls(cfg, action.Acls, user) { - if acl.Permissions.Logs { - logAclMatched(cfg, "isAllowedLogs", user, action, acl) +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 { + var ret PermissionBits + + if permissions.View { + ret |= View + } + + if permissions.Exec { + ret |= Exec + } + + if permissions.Logs { + ret |= Logs + } + + 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) + + log.WithFields(log.Fields{ + "actionTitle": action.Title, + "username": user.Username, + "usergroup": user.Usergroup, + "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) } } - logAclNotMatched(cfg, "isAllowedLogs", user, action) + logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs) - return 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 { - for _, acl := range getRelevantAcls(cfg, action.Acls, user) { - if acl.Permissions.Exec { - logAclMatched(cfg, "isAllowedExec", user, action, acl) - - return true - } - } - - logAclNotMatched(cfg, "isAllowedExec", user, action) - - return cfg.DefaultPermissions.Exec + return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action) } // IsAllowedView checks if a User is allowed to view an Action @@ -72,20 +120,10 @@ func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.A return false } - for _, acl := range getRelevantAcls(cfg, action.Acls, user) { - if acl.Permissions.View { - logAclMatched(cfg, "isAllowedView", user, action, acl) - - return true - } - } - - logAclNotMatched(cfg, "isAllowedView", user, action) - - return cfg.DefaultPermissions.View + return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action) } -func getMetdataKeyOrEmpty(md metadata.MD, key string) string { +func getMetadataKeyOrEmpty(md metadata.MD, key string) string { mdValues := md.Get(key) if len(mdValues) > 0 { @@ -97,16 +135,21 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string { // UserFromContext tries to find a user from a grpc context func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser { + ret := &AuthenticatedUser{} + md, ok := metadata.FromIncomingContext(ctx) - ret := &AuthenticatedUser{ - Username: "guest", - Usergroup: "guest", + if ok { + ret.Username = getMetadataKeyOrEmpty(md, "username") + ret.Usergroup = getMetadataKeyOrEmpty(md, "usergroup") } - if ok { - ret.Username = getMetdataKeyOrEmpty(md, "username") - ret.Usergroup = getMetdataKeyOrEmpty(md, "usergroup") + if ret.Username == "" { + ret.Username = "guest" + } + + if ret.Usergroup == "" { + ret.Usergroup = "guest" } buildUserAcls(cfg, ret) @@ -119,6 +162,17 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser return ret } +func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser { + ret := &AuthenticatedUser{ + Username: username, + Usergroup: "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) { diff --git a/internal/config/config.go b/internal/config/config.go index a2bd1ea..9c21115 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -144,6 +144,7 @@ type LogDebugOptions struct { SingleFrontendRequestHeaders bool AclMatched bool AclNotMatched bool + AclNoneMatched bool } type DashboardComponent struct { diff --git a/internal/httpservers/restapi.go b/internal/httpservers/restapi.go index 616f8c9..0d91ec0 100644 --- a/internal/httpservers/restapi.go +++ b/internal/httpservers/restapi.go @@ -57,10 +57,10 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD { username, usergroup = parseHttpHeaderForAuth(req) } - md := metadata.Pairs( - "username", username, - "usergroup", usergroup, - ) + md := metadata.New(map[string]string { + "username": username, + "usergroup": usergroup, + }) log.Tracef("api request metadata: %+v", md)