4
0

api_entity_argument_acl_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package api
  2. import (
  3. "context"
  4. "testing"
  5. "connectrpc.com/connect"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
  9. config "github.com/OliveTin/OliveTin/internal/config"
  10. "github.com/OliveTin/OliveTin/internal/entities"
  11. "github.com/OliveTin/OliveTin/internal/executor"
  12. )
  13. func buildEntityArgumentGuessConfig() *config.Config {
  14. cfg := config.DefaultConfig()
  15. cfg.DefaultPermissions.View = false
  16. cfg.DefaultPermissions.Exec = false
  17. cfg.AccessControlLists = []*config.AccessControlList{
  18. {
  19. Name: "ops",
  20. MatchUsernames: []string{"admin"},
  21. Permissions: config.PermissionsList{View: true, Exec: true},
  22. },
  23. {
  24. Name: "everyone",
  25. MatchUsernames: []string{"guest", "admin"},
  26. Permissions: config.PermissionsList{View: true, Exec: true},
  27. AddToEveryAction: true,
  28. },
  29. }
  30. cfg.Entities = []*config.EntityFile{
  31. {Name: "servers", File: "servers.yaml", Acls: []string{"ops"}},
  32. }
  33. cfg.Actions = []*config.Action{
  34. {
  35. ID: "reboot-server",
  36. Title: "Reboot server",
  37. Shell: "echo reboot '{{ target }}'",
  38. Arguments: []config.ActionArgument{
  39. {
  40. Name: "target",
  41. Title: "Server",
  42. Entity: "servers",
  43. Choices: []config.ActionArgumentChoice{
  44. {Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
  45. },
  46. },
  47. },
  48. },
  49. }
  50. cfg.Sanitize()
  51. return cfg
  52. }
  53. func seedEntityArgumentGuessEntities(t *testing.T) {
  54. t.Helper()
  55. entities.ClearEntitiesOfType("servers")
  56. t.Cleanup(func() {
  57. entities.ClearEntitiesOfType("servers")
  58. })
  59. entities.AddEntity("servers", "0", map[string]any{"name": "web01"})
  60. entities.AddEntity("servers", "1", map[string]any{"name": "db01"})
  61. }
  62. func TestStartActionRejectsGuessedRestrictedEntityArgument(t *testing.T) {
  63. seedEntityArgumentGuessEntities(t)
  64. cfg := buildEntityArgumentGuessConfig()
  65. ex := executor.DefaultExecutor(cfg)
  66. ex.RebuildActionMap()
  67. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  68. defer ts.Close()
  69. _, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
  70. BindingId: "reboot-server",
  71. Arguments: []*apiv1.StartActionArgument{
  72. {Name: "target", Value: "web01"},
  73. },
  74. }))
  75. require.Error(t, err)
  76. assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
  77. }
  78. func TestStartActionRejectsUnknownEntityArgumentValue(t *testing.T) {
  79. seedEntityArgumentGuessEntities(t)
  80. cfg := buildEntityArgumentGuessConfig()
  81. // Make servers unrestricted so guests can view the type but not invent values.
  82. cfg.Entities[0].Acls = nil
  83. cfg.Sanitize()
  84. ex := executor.DefaultExecutor(cfg)
  85. ex.RebuildActionMap()
  86. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  87. defer ts.Close()
  88. _, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
  89. BindingId: "reboot-server",
  90. Arguments: []*apiv1.StartActionArgument{
  91. {Name: "target", Value: "not-a-real-server"},
  92. },
  93. }))
  94. require.Error(t, err)
  95. assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
  96. }
  97. func TestStartActionAllowsListedEntityArgumentValue(t *testing.T) {
  98. seedEntityArgumentGuessEntities(t)
  99. cfg := buildEntityArgumentGuessConfig()
  100. cfg.Entities[0].Acls = nil
  101. cfg.Sanitize()
  102. ex := executor.DefaultExecutor(cfg)
  103. ex.RebuildActionMap()
  104. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  105. defer ts.Close()
  106. resp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
  107. BindingId: "reboot-server",
  108. Arguments: []*apiv1.StartActionArgument{
  109. {Name: "target", Value: "web01"},
  110. },
  111. }))
  112. require.NoError(t, err)
  113. assert.NotEmpty(t, resp.Msg.ExecutionTrackingId)
  114. }
  115. func TestChecklistEntityValuesAllowedRejectsBlankOnlyInput(t *testing.T) {
  116. allowed := map[string]struct{}{"web01": {}, "db01": {}}
  117. assert.False(t, checklistEntityValuesAllowed(",,,", allowed))
  118. assert.False(t, checklistEntityValuesAllowed(" , ", allowed))
  119. assert.False(t, checklistEntityValuesAllowed("", allowed),
  120. "all-blank checklist parts are rejected here; empty string is accepted by the caller separately")
  121. assert.True(t, checklistEntityValuesAllowed("web01", allowed))
  122. assert.True(t, checklistEntityValuesAllowed("web01, db01", allowed))
  123. assert.False(t, checklistEntityValuesAllowed("web01, unknown", allowed))
  124. }
  125. func TestStartActionRejectsMalformedMultiChoiceEntityArgument(t *testing.T) {
  126. seedEntityArgumentGuessEntities(t)
  127. cfg := buildEntityArgumentGuessConfig()
  128. // After sanitize, force an invalid entity+multi-choice shape that would
  129. // previously skip ACL and fall through to static UI choices.
  130. cfg.Actions[0].Arguments[0].Choices = []config.ActionArgumentChoice{
  131. {Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
  132. {Title: "web01", Value: "web01"},
  133. }
  134. ex := executor.DefaultExecutor(cfg)
  135. ex.RebuildActionMap()
  136. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  137. defer ts.Close()
  138. _, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
  139. BindingId: "reboot-server",
  140. Arguments: []*apiv1.StartActionArgument{
  141. {Name: "target", Value: "web01"},
  142. },
  143. }))
  144. require.Error(t, err)
  145. assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
  146. }