api_entity_acl_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 buildEntityAclTestConfig() *config.Config {
  15. cfg := config.DefaultConfig()
  16. cfg.DefaultPermissions.View = false
  17. cfg.DefaultPermissions.Exec = false
  18. cfg.AccessControlLists = []*config.AccessControlList{
  19. {
  20. Name: "ops",
  21. MatchUsernames: []string{"admin"},
  22. Permissions: config.PermissionsList{View: true, Exec: true},
  23. },
  24. {
  25. Name: "everyone",
  26. MatchUsernames: []string{"guest", "admin"},
  27. Permissions: config.PermissionsList{View: true, Exec: true},
  28. AddToEveryAction: true,
  29. },
  30. }
  31. cfg.Entities = []*config.EntityFile{
  32. {Name: "printers", File: "printers.yaml"},
  33. {Name: "servers", File: "servers.yaml", Acls: []string{"ops"}},
  34. }
  35. cfg.Actions = []*config.Action{
  36. {
  37. ID: "restart-server",
  38. Title: "Restart {{ servers.name }}",
  39. Shell: "echo restart",
  40. Entity: "servers",
  41. },
  42. {
  43. ID: "ping-printer",
  44. Title: "Ping printer",
  45. Shell: "echo ping",
  46. },
  47. }
  48. cfg.Dashboards = []*config.DashboardComponent{
  49. {
  50. Title: "Infra",
  51. Contents: []*config.DashboardComponent{
  52. {
  53. Title: "{{ servers.name }}",
  54. Type: "fieldset",
  55. Entity: "servers",
  56. Contents: []*config.DashboardComponent{
  57. {Title: "Restart {{ servers.name }}"},
  58. },
  59. },
  60. },
  61. },
  62. }
  63. cfg.Sanitize()
  64. return cfg
  65. }
  66. func seedEntityAclTestEntities(t *testing.T) {
  67. t.Helper()
  68. entities.ClearEntitiesOfType("printers")
  69. entities.ClearEntitiesOfType("servers")
  70. t.Cleanup(func() {
  71. entities.ClearEntitiesOfType("printers")
  72. entities.ClearEntitiesOfType("servers")
  73. })
  74. entities.AddEntity("printers", "p1", map[string]any{"name": "lobby"})
  75. entities.AddEntity("servers", "0", map[string]any{"name": "web01"})
  76. }
  77. func TestGetEntitiesOmitsRestrictedEntityTypes(t *testing.T) {
  78. seedEntityAclTestEntities(t)
  79. cfg := buildEntityAclTestConfig()
  80. ex := executor.DefaultExecutor(cfg)
  81. ex.RebuildActionMap()
  82. api := newServer(ex)
  83. guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  84. guest.BuildUserAcls(cfg)
  85. defs := api.buildEntityDefinitionsResponse(guest, &apiv1.GetEntitiesRequest{}, entities.GetEntities())
  86. titles := make([]string, 0, len(defs))
  87. for _, def := range defs {
  88. titles = append(titles, def.Title)
  89. }
  90. assert.Contains(t, titles, "printers")
  91. assert.NotContains(t, titles, "servers")
  92. }
  93. func TestGetEntityNotFoundForRestrictedType(t *testing.T) {
  94. seedEntityAclTestEntities(t)
  95. cfg := buildEntityAclTestConfig()
  96. ex := executor.DefaultExecutor(cfg)
  97. ex.RebuildActionMap()
  98. ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
  99. defer ts.Close()
  100. // Guest Init/API without login uses guest user with default ACLs from UserFromApiCall.
  101. _, err := client.GetEntity(context.Background(), connect.NewRequest(&apiv1.GetEntityRequest{
  102. Type: "servers",
  103. UniqueKey: "0",
  104. }))
  105. require.Error(t, err)
  106. assert.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
  107. resp, err := client.GetEntity(context.Background(), connect.NewRequest(&apiv1.GetEntityRequest{
  108. Type: "printers",
  109. UniqueKey: "p1",
  110. }))
  111. require.NoError(t, err)
  112. assert.Equal(t, "lobby", resp.Msg.Title)
  113. }
  114. func TestSearchHintsOmitRestrictedEntitiesAndEntityBoundActions(t *testing.T) {
  115. seedEntityAclTestEntities(t)
  116. cfg := buildEntityAclTestConfig()
  117. ex := executor.DefaultExecutor(cfg)
  118. ex.RebuildActionMap()
  119. api := newServer(ex)
  120. guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  121. guest.BuildUserAcls(cfg)
  122. admin := &authpublic.AuthenticatedUser{Username: "admin"}
  123. admin.BuildUserAcls(cfg)
  124. guestHints := api.buildSearchHints(guest)
  125. require.NotNil(t, guestHints)
  126. guestEntityKeys := make([]string, 0)
  127. for _, hint := range guestHints.Entities {
  128. guestEntityKeys = append(guestEntityKeys, hint.Type+":"+hint.UniqueKey)
  129. }
  130. assert.Contains(t, guestEntityKeys, "printers:p1")
  131. assert.NotContains(t, guestEntityKeys, "servers:0")
  132. guestActionIDs := actionHintBindingIDs(guestHints.Actions)
  133. require.NotEmpty(t, guestActionIDs)
  134. assert.Contains(t, guestActionIDs, "ping-printer")
  135. assert.NotContains(t, guestActionIDs, "restart")
  136. adminHints := api.buildSearchHints(admin)
  137. require.NotNil(t, adminHints)
  138. adminEntityKeys := make([]string, 0)
  139. for _, hint := range adminHints.Entities {
  140. adminEntityKeys = append(adminEntityKeys, hint.Type+":"+hint.UniqueKey)
  141. }
  142. assert.Contains(t, adminEntityKeys, "servers:0")
  143. assert.NotEmpty(t, adminHints.Actions)
  144. }
  145. func TestEntityFieldsetOmitsRestrictedEntityType(t *testing.T) {
  146. seedEntityAclTestEntities(t)
  147. cfg := buildEntityAclTestConfig()
  148. ex := executor.DefaultExecutor(cfg)
  149. ex.RebuildActionMap()
  150. api := newServer(ex)
  151. guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  152. guest.BuildUserAcls(cfg)
  153. admin := &authpublic.AuthenticatedUser{Username: "admin"}
  154. admin.BuildUserAcls(cfg)
  155. guestRR := api.createDashboardRenderRequest(guest, "", "")
  156. guestDB := renderDashboard(guestRR, "Infra")
  157. require.NotNil(t, guestDB)
  158. assert.Empty(t, guestDB.Contents, "restricted entity fieldsets must not leak instances")
  159. adminRR := api.createDashboardRenderRequest(admin, "", "")
  160. adminDB := renderDashboard(adminRR, "Infra")
  161. require.NotNil(t, adminDB)
  162. require.NotEmpty(t, adminDB.Contents)
  163. }
  164. func TestBuildChoicesEntityRespectsEntityACL(t *testing.T) {
  165. seedEntityAclTestEntities(t)
  166. cfg := buildEntityAclTestConfig()
  167. ex := executor.DefaultExecutor(cfg)
  168. ex.RebuildActionMap()
  169. guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
  170. guest.BuildUserAcls(cfg)
  171. admin := &authpublic.AuthenticatedUser{Username: "admin"}
  172. admin.BuildUserAcls(cfg)
  173. arg := config.ActionArgument{
  174. Entity: "servers",
  175. Choices: []config.ActionArgumentChoice{
  176. {Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
  177. },
  178. }
  179. guestRR := &DashboardRenderRequest{AuthenticatedUser: guest, cfg: cfg, ex: ex}
  180. assert.Empty(t, buildChoices(arg, guestRR))
  181. adminRR := &DashboardRenderRequest{AuthenticatedUser: admin, cfg: cfg, ex: ex}
  182. choices := buildChoices(arg, adminRR)
  183. require.Len(t, choices, 1)
  184. assert.Equal(t, "web01", choices[0].Value)
  185. }