From 5a151859d7696a1426c627f5f502c45e09eb32aa Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 19 Jul 2021 14:22:57 +0100 Subject: [PATCH] Initial work on auth --- OliveTin.proto | 3 ++ internal/acl/acl.go | 84 +++++++++++++++++++++++++++++++++++ internal/config/config.go | 21 +++++++++ internal/executor/executor.go | 17 ++----- internal/grpcapi/grpcApi.go | 34 +++++++++++++- 5 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 internal/acl/acl.go diff --git a/OliveTin.proto b/OliveTin.proto index 4ea1fca..4c59617 100644 --- a/OliveTin.proto +++ b/OliveTin.proto @@ -8,6 +8,7 @@ message ActionButton { string id = 1; string title = 2; string icon = 3; + bool canExec = 4; } message GetButtonsResponse { @@ -34,6 +35,8 @@ message LogEntry { string stderr = 4; bool timedOut = 5; int32 exitCode = 6; + string user = 7; + string userClass = 8; } message GetLogsResponse { diff --git a/internal/acl/acl.go b/internal/acl/acl.go new file mode 100644 index 0000000..4b03dd8 --- /dev/null +++ b/internal/acl/acl.go @@ -0,0 +1,84 @@ +package acl + +import ( + config "github.com/jamesread/OliveTin/internal/config" + log "github.com/sirupsen/logrus" + "context" +) + +type User struct { + Username string; +} + +func IsAllowedExec(cfg *config.Config, user *User, action *config.ActionButton) bool { + canExec := cfg.DefaultPermissions.Exec + + log.WithFields(log.Fields{ + "User": user.Username, + "Action": action.Title, + "CanExec": canExec, + }).Debug("isAllowedExec Permission Default") + + for _, permissionEntry := range action.Permissions { + if isUserInGroup(user, permissionEntry.Usergroup) { + log.WithFields(log.Fields{ + "User": user.Username, + "Action": action.Title, + "CanExec": permissionEntry.Exec, + }).Debug("isAllowedExec Permission Entry") + + canExec = permissionEntry.Exec + } + } + + log.WithFields(log.Fields{ + "User": user.Username, + "Action": action.Title, + "CanExec": canExec, + }).Debug("isAllowedExec Final Result") + + return canExec; +} + +func IsAllowedView(cfg *config.Config, user *User, action *config.ActionButton) bool { + canView := cfg.DefaultPermissions.View + + log.WithFields(log.Fields{ + "User": user.Username, + "Action": action.Title, + "CanView": canView, + }).Debug("isAllowedView Permission Default") + + for idx, permissionEntry := range action.Permissions { + if isUserInGroup(user, permissionEntry.Usergroup) { + log.WithFields(log.Fields{ + "User": user.Username, + "Action": action.Title, + "CanView": permissionEntry.View, + "Index": idx, + }).Debug("isAllowedView Permission Entry") + + canView = permissionEntry.View + } + } + + log.WithFields(log.Fields{ + "User": user.Username, + "Action": action.Title, + "CanView": canView, + }).Debug("isAllowedView Final Result") + + return canView; +} + + + +func isUserInGroup(user *User, usergroup string) bool { + return true; +} + +func UserFromContext(ctx context.Context) *User { + return &User { + Username: "Guest", + } +} diff --git a/internal/config/config.go b/internal/config/config.go index b75ffb1..9503f17 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,6 +10,7 @@ type ActionButton struct { Shell string CSS map[string]string `mapstructure:"omitempty"` Timeout int + Permissions []PermissionsEntry } // Entity represents a "thing" that can have multiple actions associated with it. @@ -21,6 +22,22 @@ type Entity struct { CSS map[string]string } +type PermissionsEntry struct { + Usergroup string + View bool + Exec bool +} + +type DefaultPermissions struct { + View bool + Exec bool +} + +type UserGroup struct { + Name string + Members []string +} + // Config is the global config used through the whole app. type Config struct { UseSingleHTTPFrontend bool @@ -34,6 +51,8 @@ type Config struct { ActionButtons []ActionButton `mapstructure:"actions"` Entities []Entity `mapstructure:"omitempty"` CheckForUpdates bool + Usergroups []UserGroup + DefaultPermissions DefaultPermissions } // DefaultConfig gets a new Config structure with sensible default values. @@ -46,6 +65,8 @@ func DefaultConfig() *Config { config.ListenAddressWebUI = "localhost:1340" config.LogLevel = "INFO" config.CheckForUpdates = true + config.DefaultPermissions.Exec = true + config.DefaultPermissions.View = true return &config } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index de69960..bfb7911 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -4,6 +4,7 @@ import ( pb "github.com/jamesread/OliveTin/gen/grpc" config "github.com/jamesread/OliveTin/internal/config" log "github.com/sirupsen/logrus" + acl "github.com/jamesread/OliveTin/internal/acl" "context" "errors" @@ -26,21 +27,11 @@ type Executor struct { } // ExecAction executes an action. -func (e *Executor) ExecAction(cfg *config.Config, action string) *pb.StartActionResponse { +func (e *Executor) ExecAction(cfg *config.Config, user *acl.User, actualAction *config.ActionButton) *pb.StartActionResponse { log.WithFields(log.Fields{ - "actionName": action, + "actionName": actualAction.Title, }).Infof("StartAction") - actualAction, err := findAction(cfg, action) - - if err != nil { - log.Errorf("Error finding action %s, %s", err, action) - - return &pb.StartActionResponse{ - LogEntry: nil, - } - } - res := execAction(cfg, actualAction) e.Logs = append(e.Logs, *res); @@ -103,7 +94,7 @@ func sanitizeAction(action *config.ActionButton) { } } -func findAction(cfg *config.Config, actionTitle string) (*config.ActionButton, error) { +func FindAction(cfg *config.Config, actionTitle string) (*config.ActionButton, error) { for _, action := range cfg.ActionButtons { if action.Title == actionTitle { sanitizeAction(&action) diff --git a/internal/grpcapi/grpcApi.go b/internal/grpcapi/grpcApi.go index 2ead0ba..d85a8f8 100644 --- a/internal/grpcapi/grpcApi.go +++ b/internal/grpcapi/grpcApi.go @@ -11,6 +11,7 @@ import ( config "github.com/jamesread/OliveTin/internal/config" executor "github.com/jamesread/OliveTin/internal/executor" + acl "github.com/jamesread/OliveTin/internal/acl" ) var ( @@ -23,23 +24,52 @@ type oliveTinAPI struct { } func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) { - return ex.ExecAction(cfg, req.ActionName), nil + actualAction, err := executor.FindAction(cfg, req.ActionName) + + if err != nil { + log.Errorf("Error finding action %s, %s", err, req.ActionName) + + return &pb.StartActionResponse{ + LogEntry: nil, + }, nil + } + + user := acl.UserFromContext(ctx) + + if !acl.IsAllowedExec(cfg, user, actualAction) { + return &pb.StartActionResponse{ + }, nil + + } + + return ex.ExecAction(cfg, acl.UserFromContext(ctx), actualAction), nil } func (api *oliveTinAPI) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) { res := &pb.GetButtonsResponse{} + user := acl.UserFromContext(ctx) + for _, action := range cfg.ActionButtons { + if !acl.IsAllowedView(cfg, user, &action) { + continue + } + btn := pb.ActionButton{ Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))), Title: action.Title, Icon: lookupHTMLIcon(action.Icon), + CanExec: acl.IsAllowedExec(cfg, user, &action), } res.Actions = append(res.Actions, &btn) } - log.Infof("getButtons: %v", res) + if len(res.Actions) == 0 { + log.Warn("Zero actions found - check that you have some actions defined, with a view permission") + } + + log.Debugf("getButtons: %v", res) return res, nil }