|
|
@@ -4,9 +4,14 @@ import (
|
|
|
ctx "context"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
+ "fmt"
|
|
|
+ "net/http"
|
|
|
"os"
|
|
|
"path"
|
|
|
"sort"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
|
|
|
"connectrpc.com/connect"
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
@@ -16,11 +21,6 @@ import (
|
|
|
"github.com/google/uuid"
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
- "fmt"
|
|
|
- "net/http"
|
|
|
- "sync"
|
|
|
- "time"
|
|
|
-
|
|
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
|
|
auth "github.com/OliveTin/OliveTin/internal/auth"
|
|
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
|
|
@@ -156,15 +156,17 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.
|
|
|
}
|
|
|
|
|
|
authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
|
|
|
- if err := validateJustificationRequired(pair.Action, req.Msg.Justification, authenticatedUser); err != nil {
|
|
|
+ args := startActionArgumentsFromProto(req.Msg.Arguments)
|
|
|
+ justification := resolveStartJustification(pair.Action, pair, req.Msg.Justification, args)
|
|
|
+ if err := validateJustificationRequired(pair.Action, justification, authenticatedUser); err != nil {
|
|
|
return nil, connectInvalidJustification(err)
|
|
|
}
|
|
|
|
|
|
execReq := executor.ExecutionRequest{
|
|
|
Binding: pair,
|
|
|
TrackingID: req.Msg.UniqueTrackingId,
|
|
|
- Arguments: startActionArgumentsFromProto(req.Msg.Arguments),
|
|
|
- Justification: req.Msg.Justification,
|
|
|
+ Arguments: args,
|
|
|
+ Justification: justification,
|
|
|
AuthenticatedUser: authenticatedUser,
|
|
|
Cfg: api.cfg,
|
|
|
}
|
|
|
@@ -280,6 +282,21 @@ func (api *oliveTinAPI) findBindingByIDOrNotFound(bindingId string) (*executor.A
|
|
|
return api.findBindingOrNotFound(bindingId)
|
|
|
}
|
|
|
|
|
|
+func (api *oliveTinAPI) startActionAndWaitLogEntry(binding *executor.ActionBinding, args map[string]string, justification string, user *authpublic.AuthenticatedUser) (*apiv1.LogEntry, error) {
|
|
|
+ internalLogEntry, ok := api.startActionAndWaitRun(binding, args, justification, user)
|
|
|
+ if !ok {
|
|
|
+ return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
|
|
|
+ }
|
|
|
+ return api.logEntryForAllowedViewer(internalLogEntry, user)
|
|
|
+}
|
|
|
+
|
|
|
+func (api *oliveTinAPI) logEntryForAllowedViewer(internalLogEntry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) (*apiv1.LogEntry, error) {
|
|
|
+ if err := api.requireLogEntryAllowed(internalLogEntry, user); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return api.internalLogEntryToPb(internalLogEntry, user), nil
|
|
|
+}
|
|
|
+
|
|
|
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
|
|
|
binding, err := api.findBindingOrNotFound(req.Msg.ActionId)
|
|
|
if err != nil {
|
|
|
@@ -287,16 +304,18 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request
|
|
|
}
|
|
|
|
|
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
|
|
- if err := validateJustificationRequired(binding.Action, req.Msg.Justification, user); err != nil {
|
|
|
+ args := startActionArgumentsFromProto(req.Msg.Arguments)
|
|
|
+ justification := resolveStartJustification(binding.Action, binding, req.Msg.Justification, args)
|
|
|
+ if err := validateJustificationRequired(binding.Action, justification, user); err != nil {
|
|
|
return nil, connectInvalidJustification(err)
|
|
|
}
|
|
|
|
|
|
- internalLogEntry, ok := api.startActionAndWaitRun(binding, startActionArgumentsFromProto(req.Msg.Arguments), req.Msg.Justification, user)
|
|
|
- if !ok {
|
|
|
- return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
|
|
|
+ logEntry, err := api.startActionAndWaitLogEntry(binding, args, justification, user)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
}
|
|
|
return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
|
|
|
- LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
|
|
|
+ LogEntry: logEntry,
|
|
|
}), nil
|
|
|
}
|
|
|
|
|
|
@@ -323,16 +342,7 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a
|
|
|
}), nil
|
|
|
}
|
|
|
|
|
|
-func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
|
|
|
- binding := api.executor.FindBindingByID(req.Msg.ActionId)
|
|
|
- if binding == nil || binding.Action == nil {
|
|
|
- return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
|
|
|
- }
|
|
|
-
|
|
|
- args := make(map[string]string)
|
|
|
-
|
|
|
- user := auth.UserFromApiCall(ctx, req, api.cfg)
|
|
|
-
|
|
|
+func (api *oliveTinAPI) runBindingAndWait(binding *executor.ActionBinding, args map[string]string, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, bool) {
|
|
|
execReq := executor.ExecutionRequest{
|
|
|
Binding: binding,
|
|
|
TrackingID: uuid.NewString(),
|
|
|
@@ -344,14 +354,31 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re
|
|
|
wg, _ := api.executor.ExecRequest(&execReq)
|
|
|
wg.Wait()
|
|
|
|
|
|
- internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
|
|
+ return api.executor.GetLog(execReq.TrackingID)
|
|
|
+}
|
|
|
+
|
|
|
+func (api *oliveTinAPI) startActionByGetAndWaitLogEntry(binding *executor.ActionBinding, user *authpublic.AuthenticatedUser) (*apiv1.LogEntry, error) {
|
|
|
+ internalLogEntry, ok := api.runBindingAndWait(binding, map[string]string{}, user)
|
|
|
+ if !ok {
|
|
|
+ return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
|
|
|
+ }
|
|
|
+ return api.logEntryForAllowedViewer(internalLogEntry, user)
|
|
|
+}
|
|
|
+
|
|
|
+func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
|
|
|
+ binding := api.executor.FindBindingByID(req.Msg.ActionId)
|
|
|
+ if binding == nil || binding.Action == nil {
|
|
|
+ return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
|
|
|
+ }
|
|
|
|
|
|
- if ok {
|
|
|
- return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
|
|
|
- LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
|
|
|
- }), nil
|
|
|
+ user := auth.UserFromApiCall(ctx, req, api.cfg)
|
|
|
+ logEntry, err := api.startActionByGetAndWaitLogEntry(binding, user)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
}
|
|
|
- return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
|
|
|
+ return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
|
|
|
+ LogEntry: logEntry,
|
|
|
+ }), nil
|
|
|
}
|
|
|
|
|
|
func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string {
|
|
|
@@ -680,6 +707,13 @@ func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *au
|
|
|
return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
|
|
|
}
|
|
|
|
|
|
+func (api *oliveTinAPI) requireLogEntryAllowed(entry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) error {
|
|
|
+ if api.isLogEntryAllowed(entry, user) {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view this execution"))
|
|
|
+}
|
|
|
+
|
|
|
// mayViewExecutionEvent returns whether the user is allowed to receive this execution event (for EventStream ACL).
|
|
|
func (api *oliveTinAPI) mayViewExecutionEvent(entry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
|
|
|
if user == nil {
|
|
|
@@ -1373,21 +1407,7 @@ func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.
|
|
|
}
|
|
|
|
|
|
entityMap := entities.GetEntities()
|
|
|
- entityNames := make([]string, 0, len(entityMap))
|
|
|
- for name := range entityMap {
|
|
|
- entityNames = append(entityNames, name)
|
|
|
- }
|
|
|
- sort.Strings(entityNames)
|
|
|
-
|
|
|
- entityDefinitions := make([]*apiv1.EntityDefinition, 0, len(entityNames))
|
|
|
- for _, name := range entityNames {
|
|
|
- def := &apiv1.EntityDefinition{
|
|
|
- Title: name,
|
|
|
- UsedOnDashboards: findDashboardsForEntity(name, api.cfg.Dashboards),
|
|
|
- Instances: buildSortedEntityInstances(name, entityMap[name]),
|
|
|
- }
|
|
|
- entityDefinitions = append(entityDefinitions, def)
|
|
|
- }
|
|
|
+ entityDefinitions := api.buildEntityDefinitionsResponse(req.Msg, entityMap)
|
|
|
|
|
|
res := &apiv1.GetEntitiesResponse{
|
|
|
EntityDefinitions: entityDefinitions,
|
|
|
@@ -1396,7 +1416,7 @@ func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.
|
|
|
return connect.NewResponse(res), nil
|
|
|
}
|
|
|
|
|
|
-func buildSortedEntityInstances(entityType string, entityInstances map[string]*entities.Entity) []*apiv1.Entity {
|
|
|
+func buildSortedEntityInstances(entityType string, entityInstances map[string]*entities.Entity, properties []config.EntityProperty) []*apiv1.Entity {
|
|
|
instanceKeys := make([]string, 0, len(entityInstances))
|
|
|
for key := range entityInstances {
|
|
|
instanceKeys = append(instanceKeys, key)
|
|
|
@@ -1410,6 +1430,7 @@ func buildSortedEntityInstances(entityType string, entityInstances map[string]*e
|
|
|
Title: e.Title,
|
|
|
UniqueKey: e.UniqueKey,
|
|
|
Type: entityType,
|
|
|
+ Fields: entityListFields(e.Data, properties),
|
|
|
})
|
|
|
}
|
|
|
return instances
|
|
|
@@ -1511,17 +1532,100 @@ func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.Ge
|
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity with unique key %s not found in type %s", req.Msg.UniqueKey, req.Msg.Type))
|
|
|
}
|
|
|
|
|
|
- res := buildEntityResponse(entity, req.Msg.Type, api.cfg.Dashboards)
|
|
|
+ res := buildEntityResponse(entity, req.Msg.Type, api.cfg)
|
|
|
+ res.RelatedActions = api.relatedActionsForEntity(user, req.Msg.Type, entity)
|
|
|
return connect.NewResponse(res), nil
|
|
|
}
|
|
|
|
|
|
-func buildEntityResponse(entity *entities.Entity, entityType string, dashboards []*config.DashboardComponent) *apiv1.Entity {
|
|
|
+func entityTypeIcon(cfg *config.Config, entityType string) string {
|
|
|
+ entityFile := entityFileForType(cfg, entityType)
|
|
|
+ if entityFile == nil {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ return entityFile.Icon
|
|
|
+}
|
|
|
+
|
|
|
+func entityFileForType(cfg *config.Config, entityType string) *config.EntityFile {
|
|
|
+ for _, entityFile := range cfg.Entities {
|
|
|
+ if entityFile != nil && entityFile.Name == entityType {
|
|
|
+ return entityFile
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func entityPropertiesFromFile(entityFile *config.EntityFile) []config.EntityProperty {
|
|
|
+ if entityFile == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return entityFile.Properties
|
|
|
+}
|
|
|
+
|
|
|
+func entityDefinitionProperties(properties []config.EntityProperty) []*apiv1.EntityProperty {
|
|
|
+ if len(properties) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ result := make([]*apiv1.EntityProperty, 0, len(properties))
|
|
|
+ for _, property := range properties {
|
|
|
+ result = append(result, &apiv1.EntityProperty{
|
|
|
+ Name: property.Name,
|
|
|
+ Title: property.Title,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+func entityListFields(data any, properties []config.EntityProperty) map[string]string {
|
|
|
+ if len(properties) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ fields := make(map[string]string, len(properties))
|
|
|
+ for _, property := range properties {
|
|
|
+ fields[property.Name] = entityPropertyValue(data, property.Name)
|
|
|
+ }
|
|
|
+
|
|
|
+ return fields
|
|
|
+}
|
|
|
+
|
|
|
+func entityPropertyValue(data any, propertyName string) string {
|
|
|
+ dataMap, ok := data.(map[string]any)
|
|
|
+ if !ok {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ if value, found := dataMap[propertyName]; found {
|
|
|
+ return fmt.Sprintf("%v", value)
|
|
|
+ }
|
|
|
+
|
|
|
+ return entityPropertyValueCaseInsensitive(dataMap, propertyName)
|
|
|
+}
|
|
|
+
|
|
|
+func entityPropertyValueCaseInsensitive(dataMap map[string]any, propertyName string) string {
|
|
|
+ propertyNameLower := strings.ToLower(propertyName)
|
|
|
+ for key, value := range dataMap {
|
|
|
+ if strings.ToLower(key) == propertyNameLower {
|
|
|
+ return fmt.Sprintf("%v", value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ""
|
|
|
+}
|
|
|
+
|
|
|
+func buildEntityResponse(entity *entities.Entity, entityType string, cfg *config.Config) *apiv1.Entity {
|
|
|
+ properties := entityPropertiesFromFile(entityFileForType(cfg, entityType))
|
|
|
res := &apiv1.Entity{
|
|
|
Title: entity.Title,
|
|
|
UniqueKey: entity.UniqueKey,
|
|
|
Type: entityType,
|
|
|
- Directories: findDirectoriesInEntityFieldsets(entityType, dashboards),
|
|
|
- Fields: serializeEntityFields(entity.Data),
|
|
|
+ Directories: findDirectoriesInEntityFieldsets(entityType, cfg.Dashboards),
|
|
|
+ Fields: entityFieldsForResponse(entity.Data, properties),
|
|
|
+ Icon: entityTypeIcon(cfg, entityType),
|
|
|
}
|
|
|
return res
|
|
|
}
|