api_entity_related_actions_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  10. config "github.com/OliveTin/OliveTin/internal/config"
  11. "github.com/OliveTin/OliveTin/internal/entities"
  12. "github.com/OliveTin/OliveTin/internal/executor"
  13. )
  14. func setupHostEntityTestData(t *testing.T) {
  15. t.Helper()
  16. entities.ClearEntitiesOfType("host")
  17. entities.AddEntity("host", "0", map[string]any{"name": "stuffbox", "hostname": "192.168.66.8"})
  18. entities.AddEntity("host", "1", map[string]any{"name": "lurker", "hostname": "192.168.66.1"})
  19. t.Cleanup(func() {
  20. entities.ClearEntitiesOfType("host")
  21. })
  22. }
  23. func buildRelatedActionsTestConfig(t *testing.T) (*config.Config, *authpublic.AuthenticatedUser, *authpublic.AuthenticatedUser) {
  24. t.Helper()
  25. cfg := config.DefaultConfig()
  26. cfg.DefaultPermissions.View = false
  27. cfg.DefaultPermissions.Exec = false
  28. cfg.Actions = append(cfg.Actions,
  29. &config.Action{
  30. Title: "Secret Entity Action",
  31. Shell: "echo secret",
  32. Entity: "host",
  33. },
  34. &config.Action{
  35. Title: "Hidden Host Action",
  36. Shell: "echo hidden",
  37. Entity: "host",
  38. Hidden: true,
  39. },
  40. &config.Action{
  41. ID: "run_playbook",
  42. Title: "Run Automation Playbook",
  43. Shell: "host '{{ ansible_host }}'",
  44. Arguments: []config.ActionArgument{
  45. {
  46. Name: "ansible_host",
  47. Title: "Host",
  48. Entity: "host",
  49. Choices: []config.ActionArgumentChoice{
  50. {Title: "{{ host.name }} ({{ host.hostname }})", Value: "{{ host.hostname }}"},
  51. },
  52. },
  53. },
  54. },
  55. &config.Action{
  56. Title: "Public Host Action",
  57. Shell: "echo public",
  58. Entity: "host",
  59. },
  60. )
  61. cfg.AccessControlLists = append(cfg.AccessControlLists,
  62. &config.AccessControlList{
  63. Name: "restricted",
  64. MatchUsernames: []string{"low"},
  65. AddToEveryAction: true,
  66. Permissions: config.PermissionsList{View: false, Exec: false, Logs: false, Kill: false},
  67. },
  68. &config.AccessControlList{
  69. Name: "full",
  70. MatchUsernames: []string{"admin"},
  71. AddToEveryAction: true,
  72. Permissions: config.PermissionsList{View: true, Exec: true, Logs: true, Kill: true},
  73. },
  74. )
  75. cfg.Entities = []*config.EntityFile{
  76. {File: "hosts.yaml", Name: "host", Icon: "ssh"},
  77. }
  78. cfg.Sanitize()
  79. lowUser := &authpublic.AuthenticatedUser{Username: "low", Acls: []string{"restricted"}}
  80. adminUser := &authpublic.AuthenticatedUser{Username: "admin", Acls: []string{"full"}}
  81. return cfg, lowUser, adminUser
  82. }
  83. func getEntityRelatedActionTitles(t *testing.T, api *oliveTinAPI, user *authpublic.AuthenticatedUser, entityKey string) []string {
  84. t.Helper()
  85. entityType := "host"
  86. entity, ok := entities.GetEntityInstances(entityType)[entityKey]
  87. require.True(t, ok, "entity %s/%s must exist", entityType, entityKey)
  88. related := api.relatedActionsForEntity(user, entityType, entity)
  89. titles := make([]string, 0, len(related))
  90. for _, item := range related {
  91. if item.Action != nil {
  92. titles = append(titles, item.Action.Title)
  93. }
  94. }
  95. return titles
  96. }
  97. func getEntityRelatedBindingIDs(t *testing.T, api *oliveTinAPI, user *authpublic.AuthenticatedUser, entityKey string) []string {
  98. t.Helper()
  99. entityType := "host"
  100. entity, ok := entities.GetEntityInstances(entityType)[entityKey]
  101. require.True(t, ok, "entity %s/%s must exist", entityType, entityKey)
  102. related := api.relatedActionsForEntity(user, entityType, entity)
  103. ids := make([]string, 0, len(related))
  104. for _, item := range related {
  105. if item.Action != nil {
  106. ids = append(ids, item.Action.BindingId)
  107. }
  108. }
  109. return ids
  110. }
  111. func TestGetEntityRelatedActionsDeniesRestrictedView(t *testing.T) {
  112. setupHostEntityTestData(t)
  113. cfg, lowUser, _ := buildRelatedActionsTestConfig(t)
  114. ex := executor.DefaultExecutor(cfg)
  115. ex.RebuildActionMap()
  116. api := newServer(ex)
  117. ids := getEntityRelatedBindingIDs(t, api, lowUser, "0")
  118. assert.Empty(t, ids)
  119. titles := getEntityRelatedActionTitles(t, api, lowUser, "0")
  120. assert.Empty(t, titles)
  121. }
  122. func TestGetEntityRelatedActionsAllowsAdminView(t *testing.T) {
  123. setupHostEntityTestData(t)
  124. cfg, _, adminUser := buildRelatedActionsTestConfig(t)
  125. ex := executor.DefaultExecutor(cfg)
  126. ex.RebuildActionMap()
  127. api := newServer(ex)
  128. titles := getEntityRelatedActionTitles(t, api, adminUser, "0")
  129. assert.Contains(t, titles, "Run Automation Playbook")
  130. assert.Contains(t, titles, "Public Host Action")
  131. assert.Contains(t, titles, "Secret Entity Action")
  132. }
  133. func TestGetEntityRelatedActionsExcludesHiddenActions(t *testing.T) {
  134. setupHostEntityTestData(t)
  135. cfg, _, adminUser := buildRelatedActionsTestConfig(t)
  136. ex := executor.DefaultExecutor(cfg)
  137. ex.RebuildActionMap()
  138. api := newServer(ex)
  139. titles := getEntityRelatedActionTitles(t, api, adminUser, "0")
  140. assert.NotContains(t, titles, "Hidden Host Action")
  141. }
  142. func TestGetEntityRelatedActionsEntityBoundMatchesInstanceOnly(t *testing.T) {
  143. setupHostEntityTestData(t)
  144. cfg := config.DefaultConfig()
  145. cfg.Actions = append(cfg.Actions, &config.Action{
  146. Title: "{{ host.name }} Wake",
  147. Shell: "echo wake",
  148. Entity: "host",
  149. })
  150. ex := executor.DefaultExecutor(cfg)
  151. ex.RebuildActionMap()
  152. api := newServer(ex)
  153. user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  154. titlesHost0 := getEntityRelatedActionTitles(t, api, user, "0")
  155. assert.Len(t, titlesHost0, 1)
  156. assert.Equal(t, "stuffbox Wake", titlesHost0[0])
  157. titlesHost1 := getEntityRelatedActionTitles(t, api, user, "1")
  158. assert.Len(t, titlesHost1, 1)
  159. assert.Equal(t, "lurker Wake", titlesHost1[0])
  160. }
  161. func TestGetEntityRelatedActionsPrefillsArgumentEntityValues(t *testing.T) {
  162. setupHostEntityTestData(t)
  163. cfg := config.DefaultConfig()
  164. cfg.Actions = append(cfg.Actions, &config.Action{
  165. ID: "run_playbook",
  166. Title: "Run Automation Playbook",
  167. Shell: "host '{{ ansible_host }}'",
  168. Arguments: []config.ActionArgument{
  169. {
  170. Name: "ansible_host",
  171. Title: "Host",
  172. Entity: "host",
  173. Choices: []config.ActionArgumentChoice{
  174. {Title: "{{ host.name }} ({{ host.hostname }})", Value: "{{ host.hostname }}"},
  175. },
  176. },
  177. },
  178. })
  179. ex := executor.DefaultExecutor(cfg)
  180. ex.RebuildActionMap()
  181. api := newServer(ex)
  182. user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  183. entity := entities.GetEntityInstances("host")["0"]
  184. related := api.relatedActionsForEntity(user, "host", entity)
  185. require.Len(t, related, 1)
  186. require.NotNil(t, related[0].Action)
  187. assert.Equal(t, "run_playbook", related[0].Action.BindingId)
  188. assert.Equal(t, "192.168.66.8", related[0].PrefilledArguments["ansible_host"])
  189. }
  190. func TestGetEntityDeniesGuestsWhenLoginRequired(t *testing.T) {
  191. setupHostEntityTestData(t)
  192. cfg := config.DefaultConfig()
  193. cfg.AuthRequireGuestsToLogin = true
  194. ex := executor.DefaultExecutor(cfg)
  195. ex.RebuildActionMap()
  196. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  197. defer ts.Close()
  198. _, err := client.GetEntity(context.Background(), connect.NewRequest(&apiv1.GetEntityRequest{
  199. Type: "host",
  200. UniqueKey: "0",
  201. }))
  202. require.Error(t, err)
  203. assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
  204. }
  205. func TestGetEntityReturnsRelatedActionsForAdmin(t *testing.T) {
  206. setupHostEntityTestData(t)
  207. cfg, _, adminUser := buildRelatedActionsTestConfig(t)
  208. cfg.AuthHttpHeaderUsername = "X-Ot-User"
  209. ex := executor.DefaultExecutor(cfg)
  210. ex.RebuildActionMap()
  211. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  212. defer ts.Close()
  213. req := connect.NewRequest(&apiv1.GetEntityRequest{
  214. Type: "host",
  215. UniqueKey: "0",
  216. })
  217. req.Header().Set("X-Ot-User", adminUser.Username)
  218. resp, err := client.GetEntity(context.Background(), req)
  219. require.NoError(t, err)
  220. require.NotNil(t, resp.Msg)
  221. assert.Equal(t, "🔐", resp.Msg.Icon)
  222. foundPlaybook := false
  223. for _, related := range resp.Msg.RelatedActions {
  224. if related.Action != nil && related.Action.BindingId == "run_playbook" {
  225. foundPlaybook = true
  226. assert.Equal(t, "192.168.66.8", related.PrefilledArguments["ansible_host"])
  227. }
  228. }
  229. assert.True(t, foundPlaybook, "admin should see argument-entity related action in GetEntity response")
  230. }