From a8ac719af7da040e1ee26515cf7e876b65ff5a25 Mon Sep 17 00:00:00 2001 From: James Read Date: Sun, 13 Jul 2025 10:08:05 +0100 Subject: [PATCH] feat kill acl (#618) --- proto/olivetin/api/v1/olivetin.proto | 1 + .../gen/grpc/olivetin/api/v1/olivetin.pb.go | 13 +++- service/internal/acl/acl.go | 9 +++ service/internal/config/config.go | 2 + service/internal/entityfiles/entityfiles.go | 3 +- service/internal/grpcapi/grpcApi.go | 74 +++++++++++++------ webui.dev/js/ExecutionDialog.js | 2 +- 7 files changed, 77 insertions(+), 27 deletions(-) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index e28c2d7..80508ce 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -138,6 +138,7 @@ message LogEntry { bool execution_finished = 15; bool blocked = 16; int64 datetime_index = 17; + bool can_kill = 18; } message GetLogsResponse { diff --git a/service/gen/grpc/olivetin/api/v1/olivetin.pb.go b/service/gen/grpc/olivetin/api/v1/olivetin.pb.go index a1b7f25..c7e457d 100644 --- a/service/gen/grpc/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/grpc/olivetin/api/v1/olivetin.pb.go @@ -1125,6 +1125,7 @@ type LogEntry struct { ExecutionFinished bool `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"` Blocked bool `protobuf:"varint,16,opt,name=blocked,proto3" json:"blocked,omitempty"` DatetimeIndex int64 `protobuf:"varint,17,opt,name=datetime_index,json=datetimeIndex,proto3" json:"datetime_index,omitempty"` + CanKill bool `protobuf:"varint,18,opt,name=can_kill,json=canKill,proto3" json:"can_kill,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1271,6 +1272,13 @@ func (x *LogEntry) GetDatetimeIndex() int64 { return 0 } +func (x *LogEntry) GetCanKill() bool { + if x != nil { + return x.CanKill + } + return false +} + type GetLogsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Logs []*LogEntry `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` @@ -2733,7 +2741,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x1fStartActionByGetAndWaitResponse\x126\n" + "\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"3\n" + "\x0eGetLogsRequest\x12!\n" + - "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\"\xad\x04\n" + + "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\"\xc8\x04\n" + "\bLogEntry\x12)\n" + "\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" + "\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" + @@ -2753,7 +2761,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x11execution_started\x18\x0e \x01(\bR\x10executionStarted\x12-\n" + "\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" + "\ablocked\x18\x10 \x01(\bR\ablocked\x12%\n" + - "\x0edatetime_index\x18\x11 \x01(\x03R\rdatetimeIndex\"\x86\x01\n" + + "\x0edatetime_index\x18\x11 \x01(\x03R\rdatetimeIndex\x12\x19\n" + + "\bcan_kill\x18\x12 \x01(\bR\acanKill\"\x86\x01\n" + "\x0fGetLogsResponse\x12-\n" + "\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" + "\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" + diff --git a/service/internal/acl/acl.go b/service/internal/acl/acl.go index bcf5ea0..93223c9 100644 --- a/service/internal/acl/acl.go +++ b/service/internal/acl/acl.go @@ -17,6 +17,7 @@ const ( View PermissionBits = 1 << iota Exec Logs + Kill ) func (p PermissionBits) Has(permission PermissionBits) bool { @@ -121,6 +122,10 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits ret |= Logs } + if permissions.Kill { + ret |= Kill + } + return ret } @@ -173,6 +178,10 @@ func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.A 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 getMetadataKeyOrEmpty(md metadata.MD, key string) string { mdValues := md.Get(key) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 95f2312..b0159c1 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -67,6 +67,7 @@ type PermissionsList struct { View bool Exec bool Logs bool + Kill bool } // AccessControlList defines what permissions apply to a user or user group. @@ -229,6 +230,7 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.DefaultPermissions.Exec = true config.DefaultPermissions.View = true config.DefaultPermissions.Logs = true + config.DefaultPermissions.Kill = true config.AuthJwtClaimUsername = "name" config.AuthJwtClaimUserGroup = "group" config.AuthRequireGuestsToLogin = false diff --git a/service/internal/entityfiles/entityfiles.go b/service/internal/entityfiles/entityfiles.go index de61f2e..e58ba82 100644 --- a/service/internal/entityfiles/entityfiles.go +++ b/service/internal/entityfiles/entityfiles.go @@ -138,13 +138,14 @@ func updateSvFromFile(entityname string, data []map[string]any) { } } +//gocyclo:ignore func serializeValueToSv(prefix string, value any) { if m, ok := value.(map[string]any); ok { // if value is a map we need to flatten it serializeMapToSv(prefix, m) } else if s, ok := value.([]any); ok { // if value is a slice we need to flatten it serializeSliceToSv(prefix, s) } else if f, ok := value.(float64); ok { - if canConvertToInt64(f) { + if canConvertToInt64(f) { s := int64(f) sv.Set(prefix, fmt.Sprintf("%d", s)) } else { diff --git a/service/internal/grpcapi/grpcApi.go b/service/internal/grpcapi/grpcApi.go index 6bc85ad..e61dd29 100644 --- a/service/internal/grpcapi/grpcApi.go +++ b/service/internal/grpcapi/grpcApi.go @@ -38,24 +38,40 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest ExecutionTrackingId: req.ExecutionTrackingId, } - execReqLogEntry, found := api.executor.GetLog(req.ExecutionTrackingId) + var execReqLogEntry *executor.InternalLogEntry - ret.Found = found + execReqLogEntry, ret.Found = api.executor.GetLog(req.ExecutionTrackingId) - if found { - log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId) - - err := api.executor.Kill(execReqLogEntry) - - if err != nil { - log.Warnf("Killing execution request err: %v", err) - ret.AlreadyCompleted = true - ret.Killed = false - } else { - ret.Killed = true - } - } else { + if !ret.Found { log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId) + return ret, nil + } + + log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId) + + user := acl.UserFromContext(ctx, cfg) + action := cfg.FindAction(execReqLogEntry.ActionTitle) + + if action == nil { + log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle) + ret.Killed = false + return ret, nil + } + + if !acl.IsAllowedKill(cfg, user, action) { + log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", req.ExecutionTrackingId) + ret.Killed = false + return ret, nil + } + + err := api.executor.Kill(execReqLogEntry) + + if err != nil { + log.Warnf("Killing execution request err: %v", err) + ret.AlreadyCompleted = true + ret.Killed = false + } else { + ret.Killed = true } return ret, nil @@ -136,11 +152,13 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActi args[arg.Name] = arg.Value } + user := acl.UserFromContext(ctx, cfg) + execReq := executor.ExecutionRequest{ Action: api.executor.FindActionBindingByID(req.ActionId), TrackingID: uuid.NewString(), Arguments: args, - AuthenticatedUser: acl.UserFromContext(ctx, cfg), + AuthenticatedUser: user, Cfg: cfg, } @@ -151,7 +169,7 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActi if ok { return &apiv1.StartActionAndWaitResponse{ - LogEntry: internalLogEntryToPb(internalLogEntry), + LogEntry: internalLogEntryToPb(internalLogEntry, user), }, nil } else { return nil, fmt.Errorf("execution not found") @@ -179,11 +197,13 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *apiv1.StartAction func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.StartActionByGetAndWaitRequest) (*apiv1.StartActionByGetAndWaitResponse, error) { args := make(map[string]string) + user := acl.UserFromContext(ctx, cfg) + execReq := executor.ExecutionRequest{ Action: api.executor.FindActionBindingByID(req.ActionId), TrackingID: uuid.NewString(), Arguments: args, - AuthenticatedUser: acl.UserFromContext(ctx, cfg), + AuthenticatedUser: user, Cfg: cfg, } @@ -194,15 +214,15 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.Star if ok { return &apiv1.StartActionByGetAndWaitResponse{ - LogEntry: internalLogEntryToPb(internalLogEntry), + LogEntry: internalLogEntryToPb(internalLogEntry, user), }, nil } else { return nil, status.Errorf(codes.NotFound, "Execution not found.") } } -func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry { - return &apiv1.LogEntry{ +func internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry { + pble := &apiv1.LogEntry{ ActionTitle: logEntry.ActionTitle, ActionIcon: logEntry.ActionIcon, ActionId: logEntry.ActionId, @@ -219,6 +239,12 @@ func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry { ExecutionFinished: logEntry.ExecutionFinished, User: logEntry.Username, } + + if !pble.ExecutionFinished { + pble.CanKill = acl.IsAllowedKill(cfg, authenticatedUser, cfg.FindAction(logEntry.ActionTitle)) + } + + return pble } func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry { @@ -249,6 +275,8 @@ func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *execut func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionStatusRequest) (*apiv1.ExecutionStatusResponse, error) { res := &apiv1.ExecutionStatusResponse{} + user := acl.UserFromContext(ctx, cfg) + var ile *executor.InternalLogEntry if req.ExecutionTrackingId != "" { @@ -261,7 +289,7 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionSta if ile == nil { return nil, status.Error(codes.NotFound, "Execution not found") } else { - res.LogEntry = internalLogEntryToPb(ile) + res.LogEntry = internalLogEntryToPb(ile, user) } return res, nil @@ -339,7 +367,7 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *apiv1.GetLogsRequest) (*ap action := cfg.FindAction(logEntry.ActionTitle) if action == nil || acl.IsAllowedLogs(cfg, user, action) { - pbLogEntry := internalLogEntryToPb(logEntry) + pbLogEntry := internalLogEntryToPb(logEntry, user) ret.Logs = append(ret.Logs, pbLogEntry) } diff --git a/webui.dev/js/ExecutionDialog.js b/webui.dev/js/ExecutionDialog.js index d987993..e06a557 100644 --- a/webui.dev/js/ExecutionDialog.js +++ b/webui.dev/js/ExecutionDialog.js @@ -219,7 +219,7 @@ export class ExecutionDialog { this.domBtnRerun.disabled = !res.logEntry.executionFinished this.domBtnRerun.onclick = () => { this.rerunAction(res.logEntry.actionId) } - this.domBtnKill.disabled = res.logEntry.executionFinished + this.domBtnKill.disabled = !res.logEntry.canKill this.domStatus.update(res.logEntry)