feat kill acl (#618)

This commit is contained in:
James Read 2025-07-13 10:08:05 +01:00 committed by GitHub
parent b99b3f4345
commit a8ac719af7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 77 additions and 27 deletions

View File

@ -138,6 +138,7 @@ message LogEntry {
bool execution_finished = 15; bool execution_finished = 15;
bool blocked = 16; bool blocked = 16;
int64 datetime_index = 17; int64 datetime_index = 17;
bool can_kill = 18;
} }
message GetLogsResponse { message GetLogsResponse {

View File

@ -1125,6 +1125,7 @@ type LogEntry struct {
ExecutionFinished bool `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1271,6 +1272,13 @@ func (x *LogEntry) GetDatetimeIndex() int64 {
return 0 return 0
} }
func (x *LogEntry) GetCanKill() bool {
if x != nil {
return x.CanKill
}
return false
}
type GetLogsResponse struct { type GetLogsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Logs []*LogEntry `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` 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" + "\x1fStartActionByGetAndWaitResponse\x126\n" +
"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"3\n" + "\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"3\n" +
"\x0eGetLogsRequest\x12!\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" + "\bLogEntry\x12)\n" +
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" + "\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\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" + "\x11execution_started\x18\x0e \x01(\bR\x10executionStarted\x12-\n" +
"\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" + "\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" +
"\ablocked\x18\x10 \x01(\bR\ablocked\x12%\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" + "\x0fGetLogsResponse\x12-\n" +
"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\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" + "\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +

View File

@ -17,6 +17,7 @@ const (
View PermissionBits = 1 << iota View PermissionBits = 1 << iota
Exec Exec
Logs Logs
Kill
) )
func (p PermissionBits) Has(permission PermissionBits) bool { func (p PermissionBits) Has(permission PermissionBits) bool {
@ -121,6 +122,10 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
ret |= Logs ret |= Logs
} }
if permissions.Kill {
ret |= Kill
}
return ret 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) 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 { func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
mdValues := md.Get(key) mdValues := md.Get(key)

View File

@ -67,6 +67,7 @@ type PermissionsList struct {
View bool View bool
Exec bool Exec bool
Logs bool Logs bool
Kill bool
} }
// AccessControlList defines what permissions apply to a user or user group. // 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.Exec = true
config.DefaultPermissions.View = true config.DefaultPermissions.View = true
config.DefaultPermissions.Logs = true config.DefaultPermissions.Logs = true
config.DefaultPermissions.Kill = true
config.AuthJwtClaimUsername = "name" config.AuthJwtClaimUsername = "name"
config.AuthJwtClaimUserGroup = "group" config.AuthJwtClaimUserGroup = "group"
config.AuthRequireGuestsToLogin = false config.AuthRequireGuestsToLogin = false

View File

@ -138,6 +138,7 @@ func updateSvFromFile(entityname string, data []map[string]any) {
} }
} }
//gocyclo:ignore
func serializeValueToSv(prefix string, value any) { func serializeValueToSv(prefix string, value any) {
if m, ok := value.(map[string]any); ok { // if value is a map we need to flatten it if m, ok := value.(map[string]any); ok { // if value is a map we need to flatten it
serializeMapToSv(prefix, m) serializeMapToSv(prefix, m)

View File

@ -38,13 +38,32 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest
ExecutionTrackingId: req.ExecutionTrackingId, 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 !ret.Found {
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
return ret, nil
}
if found {
log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId) 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) err := api.executor.Kill(execReqLogEntry)
if err != nil { if err != nil {
@ -54,9 +73,6 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest
} else { } else {
ret.Killed = true ret.Killed = true
} }
} else {
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
}
return ret, nil return ret, nil
} }
@ -136,11 +152,13 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActi
args[arg.Name] = arg.Value args[arg.Name] = arg.Value
} }
user := acl.UserFromContext(ctx, cfg)
execReq := executor.ExecutionRequest{ execReq := executor.ExecutionRequest{
Action: api.executor.FindActionBindingByID(req.ActionId), Action: api.executor.FindActionBindingByID(req.ActionId),
TrackingID: uuid.NewString(), TrackingID: uuid.NewString(),
Arguments: args, Arguments: args,
AuthenticatedUser: acl.UserFromContext(ctx, cfg), AuthenticatedUser: user,
Cfg: cfg, Cfg: cfg,
} }
@ -151,7 +169,7 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActi
if ok { if ok {
return &apiv1.StartActionAndWaitResponse{ return &apiv1.StartActionAndWaitResponse{
LogEntry: internalLogEntryToPb(internalLogEntry), LogEntry: internalLogEntryToPb(internalLogEntry, user),
}, nil }, nil
} else { } else {
return nil, fmt.Errorf("execution not found") 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) { func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.StartActionByGetAndWaitRequest) (*apiv1.StartActionByGetAndWaitResponse, error) {
args := make(map[string]string) args := make(map[string]string)
user := acl.UserFromContext(ctx, cfg)
execReq := executor.ExecutionRequest{ execReq := executor.ExecutionRequest{
Action: api.executor.FindActionBindingByID(req.ActionId), Action: api.executor.FindActionBindingByID(req.ActionId),
TrackingID: uuid.NewString(), TrackingID: uuid.NewString(),
Arguments: args, Arguments: args,
AuthenticatedUser: acl.UserFromContext(ctx, cfg), AuthenticatedUser: user,
Cfg: cfg, Cfg: cfg,
} }
@ -194,15 +214,15 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.Star
if ok { if ok {
return &apiv1.StartActionByGetAndWaitResponse{ return &apiv1.StartActionByGetAndWaitResponse{
LogEntry: internalLogEntryToPb(internalLogEntry), LogEntry: internalLogEntryToPb(internalLogEntry, user),
}, nil }, nil
} else { } else {
return nil, status.Errorf(codes.NotFound, "Execution not found.") return nil, status.Errorf(codes.NotFound, "Execution not found.")
} }
} }
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry { func internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
return &apiv1.LogEntry{ pble := &apiv1.LogEntry{
ActionTitle: logEntry.ActionTitle, ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon, ActionIcon: logEntry.ActionIcon,
ActionId: logEntry.ActionId, ActionId: logEntry.ActionId,
@ -219,6 +239,12 @@ func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry {
ExecutionFinished: logEntry.ExecutionFinished, ExecutionFinished: logEntry.ExecutionFinished,
User: logEntry.Username, 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 { 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) { func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionStatusRequest) (*apiv1.ExecutionStatusResponse, error) {
res := &apiv1.ExecutionStatusResponse{} res := &apiv1.ExecutionStatusResponse{}
user := acl.UserFromContext(ctx, cfg)
var ile *executor.InternalLogEntry var ile *executor.InternalLogEntry
if req.ExecutionTrackingId != "" { if req.ExecutionTrackingId != "" {
@ -261,7 +289,7 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionSta
if ile == nil { if ile == nil {
return nil, status.Error(codes.NotFound, "Execution not found") return nil, status.Error(codes.NotFound, "Execution not found")
} else { } else {
res.LogEntry = internalLogEntryToPb(ile) res.LogEntry = internalLogEntryToPb(ile, user)
} }
return res, nil return res, nil
@ -339,7 +367,7 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *apiv1.GetLogsRequest) (*ap
action := cfg.FindAction(logEntry.ActionTitle) action := cfg.FindAction(logEntry.ActionTitle)
if action == nil || acl.IsAllowedLogs(cfg, user, action) { if action == nil || acl.IsAllowedLogs(cfg, user, action) {
pbLogEntry := internalLogEntryToPb(logEntry) pbLogEntry := internalLogEntryToPb(logEntry, user)
ret.Logs = append(ret.Logs, pbLogEntry) ret.Logs = append(ret.Logs, pbLogEntry)
} }

View File

@ -219,7 +219,7 @@ export class ExecutionDialog {
this.domBtnRerun.disabled = !res.logEntry.executionFinished this.domBtnRerun.disabled = !res.logEntry.executionFinished
this.domBtnRerun.onclick = () => { this.rerunAction(res.logEntry.actionId) } 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) this.domStatus.update(res.logEntry)