mirror of
https://github.com/OliveTin/OliveTin
synced 2026-08-26 04:26:28 +00:00
176 lines
4.8 KiB
Go
176 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"connectrpc.com/connect"
|
|
|
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
|
config "github.com/OliveTin/OliveTin/internal/config"
|
|
"github.com/OliveTin/OliveTin/internal/entities"
|
|
"github.com/OliveTin/OliveTin/internal/executor"
|
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
|
)
|
|
|
|
// errUnlessStartEntityAccessAllowed enforces entity-type view ACL on the binding
|
|
// and rejects entity-backed argument values the user may not use.
|
|
func (api *oliveTinAPI) errUnlessStartEntityAccessAllowed(user *authpublic.AuthenticatedUser, binding *executor.ActionBinding, args map[string]string) error {
|
|
if err := api.errUnlessBindingEntityTypeAllowed(user, binding); err != nil {
|
|
return err
|
|
}
|
|
|
|
if binding == nil {
|
|
return nil
|
|
}
|
|
|
|
return api.errUnlessEntityArgumentsAllowed(user, binding.Action, args)
|
|
}
|
|
|
|
// errUnlessEntityArgumentsAllowed rejects starts that use entity-backed arguments
|
|
// the user may not view, or guessed values that are not in the allowed choice set.
|
|
func (api *oliveTinAPI) errUnlessEntityArgumentsAllowed(user *authpublic.AuthenticatedUser, action *config.Action, args map[string]string) error {
|
|
if action == nil {
|
|
return nil
|
|
}
|
|
|
|
for argumentIndex := range action.Arguments {
|
|
arg := &action.Arguments[argumentIndex]
|
|
if arg.Entity == "" {
|
|
continue
|
|
}
|
|
|
|
if err := api.errUnlessEntityArgumentAllowed(user, arg, args[arg.Name]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isEntityBackedArgument(arg *config.ActionArgument) bool {
|
|
return arg != nil && arg.Entity != "" && len(arg.Choices) == 1
|
|
}
|
|
|
|
func isMalformedEntityArgument(arg *config.ActionArgument) bool {
|
|
return arg != nil && arg.Entity != "" && len(arg.Choices) != 1
|
|
}
|
|
|
|
func (api *oliveTinAPI) errUnlessEntityArgumentAllowed(user *authpublic.AuthenticatedUser, arg *config.ActionArgument, value string) error {
|
|
if err := errUnlessEntityArgumentShapeAllowed(arg); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !isEntityBackedArgument(arg) {
|
|
return nil
|
|
}
|
|
|
|
if !api.userCanViewEntityType(user, arg.Entity) {
|
|
return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
|
}
|
|
|
|
if err := errUnlessEntityArgumentValueAllowed(arg, value); err != nil {
|
|
return connect.NewError(connect.CodeInvalidArgument, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func errUnlessEntityArgumentShapeAllowed(arg *config.ActionArgument) error {
|
|
if !isMalformedEntityArgument(arg) {
|
|
return nil
|
|
}
|
|
|
|
return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("argument %q with entity must define exactly one choice template", arg.Name))
|
|
}
|
|
|
|
func errUnlessEntityArgumentValueAllowed(arg *config.ActionArgument, value string) error {
|
|
if isMalformedEntityArgument(arg) {
|
|
return fmt.Errorf("argument %q with entity must define exactly one choice template", arg.Name)
|
|
}
|
|
|
|
if !isEntityBackedArgument(arg) {
|
|
return nil
|
|
}
|
|
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
|
|
if !entityArgumentValueAllowed(arg, value) {
|
|
return fmt.Errorf("argument %q is not a permitted entity value", arg.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func entityArgumentValueAllowed(arg *config.ActionArgument, value string) bool {
|
|
allowed := entityArgumentAllowedValues(arg)
|
|
if strings.EqualFold(arg.Type, "checklist") {
|
|
return checklistEntityValuesAllowed(arg, value, allowed)
|
|
}
|
|
|
|
normalized := normalizeEntityArgumentValue(arg, value)
|
|
_, ok := allowed[normalized]
|
|
return ok
|
|
}
|
|
|
|
func entityArgumentAllowedValues(arg *config.ActionArgument) map[string]struct{} {
|
|
allowed := make(map[string]struct{})
|
|
if arg == nil || len(arg.Choices) != 1 {
|
|
return allowed
|
|
}
|
|
|
|
for _, ent := range entities.GetEntityInstancesOrdered(arg.Entity) {
|
|
resolved := tpl.ParseTemplateOfActionBeforeExec(arg.Choices[0].Value, ent)
|
|
if resolved == "" {
|
|
continue
|
|
}
|
|
allowed[resolved] = struct{}{}
|
|
}
|
|
|
|
return allowed
|
|
}
|
|
|
|
func normalizeEntityArgumentValue(arg *config.ActionArgument, value string) string {
|
|
if arg == nil || arg.Entity == "" || len(arg.Choices) != 1 {
|
|
return value
|
|
}
|
|
|
|
if resolved, ok := entityChoiceValueForTitle(arg, value); ok {
|
|
return resolved
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func entityChoiceValueForTitle(arg *config.ActionArgument, title string) (string, bool) {
|
|
for _, ent := range entities.GetEntityInstancesOrdered(arg.Entity) {
|
|
expandedTitle := tpl.ParseTemplateOfActionBeforeExec(arg.Choices[0].Title, ent)
|
|
if title != expandedTitle {
|
|
continue
|
|
}
|
|
|
|
return tpl.ParseTemplateOfActionBeforeExec(arg.Choices[0].Value, ent), true
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func checklistEntityValuesAllowed(arg *config.ActionArgument, value string, allowed map[string]struct{}) bool {
|
|
segments, err := config.ParseChecklistValue(value)
|
|
if err != nil || len(segments) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, segment := range segments {
|
|
normalized := normalizeEntityArgumentValue(arg, strings.TrimSpace(segment))
|
|
if _, ok := allowed[normalized]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|