api_entity_argument_acl.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package api
  2. import (
  3. "fmt"
  4. "strings"
  5. "connectrpc.com/connect"
  6. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  7. config "github.com/OliveTin/OliveTin/internal/config"
  8. "github.com/OliveTin/OliveTin/internal/entities"
  9. "github.com/OliveTin/OliveTin/internal/executor"
  10. "github.com/OliveTin/OliveTin/internal/tpl"
  11. )
  12. // errUnlessStartEntityAccessAllowed enforces entity-type view ACL on the binding
  13. // and rejects entity-backed argument values the user may not use.
  14. func (api *oliveTinAPI) errUnlessStartEntityAccessAllowed(user *authpublic.AuthenticatedUser, binding *executor.ActionBinding, args map[string]string) error {
  15. if err := api.errUnlessBindingEntityTypeAllowed(user, binding); err != nil {
  16. return err
  17. }
  18. if binding == nil {
  19. return nil
  20. }
  21. return api.errUnlessEntityArgumentsAllowed(user, binding.Action, args)
  22. }
  23. // errUnlessEntityArgumentsAllowed rejects starts that use entity-backed arguments
  24. // the user may not view, or guessed values that are not in the allowed choice set.
  25. func (api *oliveTinAPI) errUnlessEntityArgumentsAllowed(user *authpublic.AuthenticatedUser, action *config.Action, args map[string]string) error {
  26. if action == nil {
  27. return nil
  28. }
  29. for argumentIndex := range action.Arguments {
  30. arg := &action.Arguments[argumentIndex]
  31. if arg.Entity == "" {
  32. continue
  33. }
  34. if err := api.errUnlessEntityArgumentAllowed(user, arg, args[arg.Name]); err != nil {
  35. return err
  36. }
  37. }
  38. return nil
  39. }
  40. func isEntityBackedArgument(arg *config.ActionArgument) bool {
  41. return arg != nil && arg.Entity != "" && len(arg.Choices) == 1
  42. }
  43. func isMalformedEntityArgument(arg *config.ActionArgument) bool {
  44. return arg != nil && arg.Entity != "" && len(arg.Choices) != 1
  45. }
  46. func (api *oliveTinAPI) errUnlessEntityArgumentAllowed(user *authpublic.AuthenticatedUser, arg *config.ActionArgument, value string) error {
  47. if err := errUnlessEntityArgumentShapeAllowed(arg); err != nil {
  48. return err
  49. }
  50. if !isEntityBackedArgument(arg) {
  51. return nil
  52. }
  53. if !api.userCanViewEntityType(user, arg.Entity) {
  54. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
  55. }
  56. if err := errUnlessEntityArgumentValueAllowed(arg, value); err != nil {
  57. return connect.NewError(connect.CodeInvalidArgument, err)
  58. }
  59. return nil
  60. }
  61. func errUnlessEntityArgumentShapeAllowed(arg *config.ActionArgument) error {
  62. if !isMalformedEntityArgument(arg) {
  63. return nil
  64. }
  65. return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("argument %q with entity must define exactly one choice template", arg.Name))
  66. }
  67. func errUnlessEntityArgumentValueAllowed(arg *config.ActionArgument, value string) error {
  68. if isMalformedEntityArgument(arg) {
  69. return fmt.Errorf("argument %q with entity must define exactly one choice template", arg.Name)
  70. }
  71. if !isEntityBackedArgument(arg) {
  72. return nil
  73. }
  74. value = strings.TrimSpace(value)
  75. if value == "" {
  76. return nil
  77. }
  78. if !entityArgumentValueAllowed(arg, value) {
  79. return fmt.Errorf("argument %q is not a permitted entity value", arg.Name)
  80. }
  81. return nil
  82. }
  83. func entityArgumentValueAllowed(arg *config.ActionArgument, value string) bool {
  84. allowed := entityArgumentAllowedValues(arg)
  85. if strings.EqualFold(arg.Type, "checklist") {
  86. return checklistEntityValuesAllowed(arg, value, allowed)
  87. }
  88. normalized := normalizeEntityArgumentValue(arg, value)
  89. _, ok := allowed[normalized]
  90. return ok
  91. }
  92. func entityArgumentAllowedValues(arg *config.ActionArgument) map[string]struct{} {
  93. allowed := make(map[string]struct{})
  94. if arg == nil || len(arg.Choices) != 1 {
  95. return allowed
  96. }
  97. for _, ent := range entities.GetEntityInstancesOrdered(arg.Entity) {
  98. resolved := tpl.ParseTemplateOfActionBeforeExec(arg.Choices[0].Value, ent)
  99. if resolved == "" {
  100. continue
  101. }
  102. allowed[resolved] = struct{}{}
  103. }
  104. return allowed
  105. }
  106. func normalizeEntityArgumentValue(arg *config.ActionArgument, value string) string {
  107. if arg == nil || arg.Entity == "" || len(arg.Choices) != 1 {
  108. return value
  109. }
  110. if resolved, ok := entityChoiceValueForTitle(arg, value); ok {
  111. return resolved
  112. }
  113. return value
  114. }
  115. func entityChoiceValueForTitle(arg *config.ActionArgument, title string) (string, bool) {
  116. for _, ent := range entities.GetEntityInstancesOrdered(arg.Entity) {
  117. expandedTitle := tpl.ParseTemplateOfActionBeforeExec(arg.Choices[0].Title, ent)
  118. if title != expandedTitle {
  119. continue
  120. }
  121. return tpl.ParseTemplateOfActionBeforeExec(arg.Choices[0].Value, ent), true
  122. }
  123. return "", false
  124. }
  125. func checklistEntityValuesAllowed(arg *config.ActionArgument, value string, allowed map[string]struct{}) bool {
  126. segments, err := config.ParseChecklistValue(value)
  127. if err != nil || len(segments) == 0 {
  128. return false
  129. }
  130. for _, segment := range segments {
  131. normalized := normalizeEntityArgumentValue(arg, strings.TrimSpace(segment))
  132. if _, ok := allowed[normalized]; !ok {
  133. return false
  134. }
  135. }
  136. return true
  137. }