James Read 11 месяцев назад
Родитель
Сommit
a8ac719af7

+ 1 - 0
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 {

+ 11 - 2
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" +

+ 9 - 0
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)
 

+ 2 - 0
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

+ 2 - 1
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 {

+ 49 - 21
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)
+	if !ret.Found {
+		log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
+		return ret, nil
+	}
 
-		err := api.executor.Kill(execReqLogEntry)
+	log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId)
 
-		if err != nil {
-			log.Warnf("Killing execution request err: %v", err)
-			ret.AlreadyCompleted = true
-			ret.Killed = false
-		} else {
-			ret.Killed = true
-		}
+	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 {
-		log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
+		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)
 		}

+ 1 - 1
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)