4
0

api_init_search_hints_test.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package api
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "connectrpc.com/connect"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
  10. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  11. config "github.com/OliveTin/OliveTin/internal/config"
  12. "github.com/OliveTin/OliveTin/internal/entities"
  13. "github.com/OliveTin/OliveTin/internal/executor"
  14. )
  15. func TestInitIncludesEntitySearchHints(t *testing.T) {
  16. entities.ClearEntitiesOfType("server")
  17. entities.ClearEntitiesOfType("database")
  18. t.Cleanup(func() {
  19. entities.ClearEntitiesOfType("server")
  20. entities.ClearEntitiesOfType("database")
  21. })
  22. entities.AddEntity("server", "0", map[string]any{
  23. "name": "web01",
  24. "secret": "must-not-appear-in-search-hints",
  25. })
  26. entities.AddEntity("database", "db-1", map[string]any{
  27. "title": "postgres",
  28. })
  29. cfg := config.DefaultConfig()
  30. cfg.Features.HeaderSearch = true
  31. cfg.Sanitize()
  32. testExecutor := executor.DefaultExecutor(cfg)
  33. testExecutor.RebuildActionMap()
  34. testServer, client := getNewTestServerAndClientWithExecutor(cfg, testExecutor)
  35. defer testServer.Close()
  36. resp, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
  37. require.NoError(t, err)
  38. require.NotNil(t, resp.Msg.Features)
  39. assert.True(t, resp.Msg.Features.HeaderSearch)
  40. require.NotNil(t, resp.Msg.SearchHints)
  41. byKey := map[string]*apiv1.EntitySearchHint{}
  42. for _, hint := range resp.Msg.SearchHints.Entities {
  43. byKey[hint.Type+":"+hint.UniqueKey] = hint
  44. }
  45. host, ok := byKey["server:0"]
  46. require.True(t, ok, "expected server:0 search hint")
  47. assert.Equal(t, "web01", host.Title)
  48. assert.Equal(t, "server", host.Type)
  49. assert.Equal(t, "0", host.UniqueKey)
  50. db, ok := byKey["database:db-1"]
  51. require.True(t, ok, "expected database:db-1 search hint")
  52. assert.Equal(t, "postgres", db.Title)
  53. }
  54. func TestInitOmitsSearchHintsWhenLoginRequired(t *testing.T) {
  55. entities.ClearEntitiesOfType("server")
  56. t.Cleanup(func() {
  57. entities.ClearEntitiesOfType("server")
  58. })
  59. entities.AddEntity("server", "0", map[string]any{"name": "web01"})
  60. cfg := config.DefaultConfig()
  61. cfg.AuthRequireGuestsToLogin = true
  62. cfg.Features.HeaderSearch = true
  63. cfg.Sanitize()
  64. testExecutor := executor.DefaultExecutor(cfg)
  65. testExecutor.RebuildActionMap()
  66. testServer, client := getNewTestServerAndClientWithExecutor(cfg, testExecutor)
  67. defer testServer.Close()
  68. resp, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
  69. require.NoError(t, err)
  70. require.True(t, resp.Msg.LoginRequired)
  71. assert.Nil(t, resp.Msg.SearchHints)
  72. }
  73. func TestInitOmitsSearchHintsWhenHeaderSearchDisabled(t *testing.T) {
  74. entities.ClearEntitiesOfType("server")
  75. t.Cleanup(func() {
  76. entities.ClearEntitiesOfType("server")
  77. })
  78. entities.AddEntity("server", "0", map[string]any{"name": "web01"})
  79. cfg := config.DefaultConfig()
  80. cfg.Sanitize()
  81. require.False(t, cfg.Features.HeaderSearch)
  82. testExecutor := executor.DefaultExecutor(cfg)
  83. testExecutor.RebuildActionMap()
  84. testServer, client := getNewTestServerAndClientWithExecutor(cfg, testExecutor)
  85. defer testServer.Close()
  86. resp, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
  87. require.NoError(t, err)
  88. require.NotNil(t, resp.Msg.Features)
  89. assert.False(t, resp.Msg.Features.HeaderSearch)
  90. assert.Nil(t, resp.Msg.SearchHints)
  91. }
  92. func TestBuildSearchHintsRespectsActionACL(t *testing.T) {
  93. cfg := config.DefaultConfig()
  94. cfg.DefaultPermissions.View = false
  95. cfg.DefaultPermissions.Exec = false
  96. cfg.Actions = []*config.Action{
  97. {ID: "public_action", Title: "Public Action", Shell: "echo public"},
  98. {ID: "secret_action", Title: "Secret Action", Shell: "echo secret", Acls: []string{"admins"}},
  99. }
  100. cfg.AccessControlLists = []*config.AccessControlList{
  101. {
  102. Name: "everyone",
  103. MatchUsernames: []string{"guest", "admin"},
  104. AddToEveryAction: false,
  105. Permissions: config.PermissionsList{View: true, Exec: true},
  106. },
  107. {
  108. Name: "admins",
  109. MatchUsernames: []string{"admin"},
  110. Permissions: config.PermissionsList{View: true, Exec: true},
  111. },
  112. }
  113. cfg.Actions[0].Acls = []string{"everyone"}
  114. cfg.Sanitize()
  115. testExecutor := executor.DefaultExecutor(cfg)
  116. testExecutor.RebuildActionMap()
  117. api := newServer(testExecutor)
  118. guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  119. guest.BuildUserAcls(cfg)
  120. admin := &authpublic.AuthenticatedUser{Username: "admin"}
  121. admin.BuildUserAcls(cfg)
  122. guestHints := api.buildSearchHints(guest)
  123. require.NotNil(t, guestHints)
  124. guestActionIDs := actionHintBindingIDs(guestHints.Actions)
  125. assert.Contains(t, guestActionIDs, "public_action")
  126. assert.NotContains(t, guestActionIDs, "secret_action")
  127. adminHints := api.buildSearchHints(admin)
  128. require.NotNil(t, adminHints)
  129. adminActionIDs := actionHintBindingIDs(adminHints.Actions)
  130. assert.Contains(t, adminActionIDs, "public_action")
  131. assert.Contains(t, adminActionIDs, "secret_action")
  132. }
  133. func TestBuildSearchHintsOmitsHiddenActions(t *testing.T) {
  134. cfg := config.DefaultConfig()
  135. cfg.Actions = []*config.Action{
  136. {ID: "visible", Title: "Visible", Shell: "echo visible"},
  137. {ID: "hidden", Title: "Hidden", Shell: "echo hidden", Hidden: true},
  138. }
  139. cfg.Sanitize()
  140. testExecutor := executor.DefaultExecutor(cfg)
  141. testExecutor.RebuildActionMap()
  142. api := newServer(testExecutor)
  143. user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  144. user.BuildUserAcls(cfg)
  145. hints := api.buildSearchHints(user)
  146. require.NotNil(t, hints)
  147. ids := actionHintBindingIDs(hints.Actions)
  148. assert.Contains(t, ids, "visible")
  149. assert.NotContains(t, ids, "hidden")
  150. }
  151. func TestBuildSearchHintsCapsActionsAndEntitiesPerType(t *testing.T) {
  152. entities.ClearEntitiesOfType("cap_host")
  153. entities.ClearEntitiesOfType("cap_container")
  154. t.Cleanup(func() {
  155. entities.ClearEntitiesOfType("cap_host")
  156. entities.ClearEntitiesOfType("cap_container")
  157. })
  158. for entityIndex := 0; entityIndex < maxSearchHintEntitiesPerType+5; entityIndex++ {
  159. entities.AddEntity("cap_host", fmt.Sprintf("%03d", entityIndex), map[string]any{"name": fmt.Sprintf("host-%03d", entityIndex)})
  160. entities.AddEntity("cap_container", fmt.Sprintf("%03d", entityIndex), map[string]any{"name": fmt.Sprintf("ctr-%03d", entityIndex)})
  161. }
  162. cfg := config.DefaultConfig()
  163. cfg.Entities = []*config.EntityFile{
  164. {Name: "cap_host", File: "cap_host.yaml"},
  165. {Name: "cap_container", File: "cap_container.yaml"},
  166. }
  167. cfg.Actions = make([]*config.Action, 0, maxSearchHintActions+5)
  168. for actionIndex := 0; actionIndex < maxSearchHintActions+5; actionIndex++ {
  169. cfg.Actions = append(cfg.Actions, &config.Action{
  170. ID: fmt.Sprintf("action-%03d", actionIndex),
  171. Title: fmt.Sprintf("Action %03d", actionIndex),
  172. Shell: "echo",
  173. })
  174. }
  175. cfg.Sanitize()
  176. testExecutor := executor.DefaultExecutor(cfg)
  177. testExecutor.RebuildActionMap()
  178. api := newServer(testExecutor)
  179. user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  180. user.BuildUserAcls(cfg)
  181. hints := api.buildSearchHints(user)
  182. require.NotNil(t, hints)
  183. assert.Len(t, hints.Actions, maxSearchHintActions)
  184. assert.Equal(t, maxSearchHintEntitiesPerType, countEntityHintsByType(hints.Entities, "cap_host"))
  185. assert.Equal(t, maxSearchHintEntitiesPerType, countEntityHintsByType(hints.Entities, "cap_container"))
  186. }
  187. func countEntityHintsByType(hints []*apiv1.EntitySearchHint, entityType string) int {
  188. count := 0
  189. for _, hint := range hints {
  190. if hint.Type == entityType {
  191. count++
  192. }
  193. }
  194. return count
  195. }
  196. func TestBuildSearchHintsPrefersNonEntityActions(t *testing.T) {
  197. entities.ClearEntitiesOfType("host")
  198. t.Cleanup(func() {
  199. entities.ClearEntitiesOfType("host")
  200. })
  201. for i := 0; i < 10; i++ {
  202. entities.AddEntity("host", fmt.Sprintf("%d", i), map[string]any{"name": fmt.Sprintf("host-%d", i)})
  203. }
  204. cfg := config.DefaultConfig()
  205. cfg.Actions = []*config.Action{
  206. {ID: "plain", Title: "Plain Action", Shell: "echo plain"},
  207. {Title: "Entity Action {{ name }}", Shell: "echo entity", Entity: "host"},
  208. }
  209. cfg.Sanitize()
  210. testExecutor := executor.DefaultExecutor(cfg)
  211. testExecutor.RebuildActionMap()
  212. api := newServer(testExecutor)
  213. user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  214. user.BuildUserAcls(cfg)
  215. hints := api.buildSearchHints(user)
  216. require.NotNil(t, hints)
  217. require.NotEmpty(t, hints.Actions)
  218. assert.Equal(t, "plain", hints.Actions[0].BindingId)
  219. }
  220. func actionHintBindingIDs(hints []*apiv1.ActionSearchHint) []string {
  221. ids := make([]string, 0, len(hints))
  222. for _, hint := range hints {
  223. ids = append(ids, hint.BindingId)
  224. }
  225. return ids
  226. }