api_config_issues_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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/configissues"
  11. )
  12. func TestGetDiagnosticsReturnsConfigIssues(t *testing.T) {
  13. cfg := config.DefaultConfig()
  14. cfg.Actions = []*config.Action{
  15. {
  16. Title: "Deploy",
  17. ID: "deploy",
  18. Entity: "host",
  19. Arguments: []config.ActionArgument{
  20. {
  21. Name: "ansible_host",
  22. Type: "ascii_identifier",
  23. Default: `{{ host.name | default "All" }}`,
  24. },
  25. },
  26. ExecOnCron: []string{"0 1 * * *"},
  27. },
  28. }
  29. testServer, client := getNewTestServerAndClient(cfg)
  30. defer testServer.Close()
  31. res, err := client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{}))
  32. require.NoError(t, err)
  33. require.NotNil(t, res.Msg)
  34. require.NotEmpty(t, res.Msg.ConfigIssues)
  35. codes := map[string]bool{}
  36. for _, issue := range res.Msg.ConfigIssues {
  37. codes[issue.Code] = true
  38. }
  39. assert.True(t, codes[configissues.CodeTemplateParse])
  40. assert.True(t, codes[configissues.CodeCronEntityBinding])
  41. initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
  42. require.NoError(t, err)
  43. assert.Equal(t, int32(len(res.Msg.ConfigIssues)), initRes.Msg.ConfigIssueCount)
  44. }
  45. func TestInitHidesConfigIssueCountWithoutDiagnostics(t *testing.T) {
  46. cfg := config.DefaultConfig()
  47. cfg.DefaultPolicy.ShowDiagnostics = false
  48. cfg.Actions = []*config.Action{
  49. {
  50. Title: "Deploy",
  51. ID: "deploy",
  52. Entity: "host",
  53. Arguments: []config.ActionArgument{
  54. {
  55. Name: "ansible_host",
  56. Type: "ascii_identifier",
  57. Default: `{{ host.name | default "All" }}`,
  58. },
  59. },
  60. },
  61. }
  62. testServer, client := getNewTestServerAndClient(cfg)
  63. defer testServer.Close()
  64. initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
  65. require.NoError(t, err)
  66. assert.Equal(t, int32(0), initRes.Msg.ConfigIssueCount)
  67. _, err = client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{}))
  68. require.Error(t, err)
  69. assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
  70. }
  71. func attachBrokenTemplateArgToSecretAction(cfg *config.Config) {
  72. for i := range cfg.Actions {
  73. if cfg.Actions[i].ID != "secret_action" {
  74. continue
  75. }
  76. cfg.Actions[i].Arguments = []config.ActionArgument{
  77. {
  78. Name: "target",
  79. Type: "ascii_identifier",
  80. Default: `{{ host.name | default "All" }}`,
  81. },
  82. }
  83. return
  84. }
  85. }
  86. func assertNoActionScopedConfigIssues(t *testing.T, issues []*apiv1.ConfigIssue) {
  87. t.Helper()
  88. for _, issue := range issues {
  89. assert.Empty(t, issue.ActionId,
  90. "user with view:false must not see action-scoped config issues")
  91. }
  92. }
  93. func configIssuesContainActionID(issues []*apiv1.ConfigIssue, actionID string) bool {
  94. for _, issue := range issues {
  95. if issue.ActionId == actionID {
  96. return true
  97. }
  98. }
  99. return false
  100. }
  101. func TestConfigIssuesHideActionsWithoutViewPermission(t *testing.T) {
  102. cfg, _, _ := buildViewPermissionTestConfig(t)
  103. cfg.AuthHttpHeaderUsername = "X-Ot-User"
  104. attachBrokenTemplateArgToSecretAction(cfg)
  105. testServer, client := getNewTestServerAndClient(cfg)
  106. defer testServer.Close()
  107. configissues.Report(configissues.Issue{
  108. Severity: configissues.SeverityWarning,
  109. Code: configissues.CodeEnvUnset,
  110. Message: "TEST_GLOBAL_ENV is not set",
  111. Source: "TEST_GLOBAL_ENV",
  112. })
  113. lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
  114. lowReq.Header().Set("X-Ot-User", "low")
  115. lowRes, err := client.GetDiagnostics(context.Background(), lowReq)
  116. require.NoError(t, err)
  117. assertNoActionScopedConfigIssues(t, lowRes.Msg.ConfigIssues)
  118. require.NotEmpty(t, lowRes.Msg.ConfigIssues, "low user should still see global config issues")
  119. lowInit := connect.NewRequest(&apiv1.InitRequest{})
  120. lowInit.Header().Set("X-Ot-User", "low")
  121. lowInitRes, err := client.Init(context.Background(), lowInit)
  122. require.NoError(t, err)
  123. assert.Equal(t, int32(len(lowRes.Msg.ConfigIssues)), lowInitRes.Msg.ConfigIssueCount)
  124. adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
  125. adminReq.Header().Set("X-Ot-User", "admin")
  126. adminRes, err := client.GetDiagnostics(context.Background(), adminReq)
  127. require.NoError(t, err)
  128. assert.True(t, configIssuesContainActionID(adminRes.Msg.ConfigIssues, "secret_action"),
  129. "admin must still see config issues for secret_action")
  130. assert.Greater(t, len(adminRes.Msg.ConfigIssues), len(lowRes.Msg.ConfigIssues))
  131. }
  132. func TestConfigIssuesHideRuntimeWatcherFailuresWithoutViewPermission(t *testing.T) {
  133. cfg, _, _ := buildViewPermissionTestConfig(t)
  134. cfg.AuthHttpHeaderUsername = "X-Ot-User"
  135. testServer, client := getNewTestServerAndClient(cfg)
  136. defer testServer.Close()
  137. configissues.Report(configissues.Issue{
  138. Severity: configissues.SeverityError,
  139. Code: configissues.CodeWatcherPath,
  140. Message: `Could not create watcher for "/secret/path": permission denied`,
  141. ActionID: "secret_action",
  142. ActionTitle: "Secret Action",
  143. Source: "/secret/path",
  144. ConfigFile: "/etc/OliveTin/secret.yaml",
  145. })
  146. lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
  147. lowReq.Header().Set("X-Ot-User", "low")
  148. lowRes, err := client.GetDiagnostics(context.Background(), lowReq)
  149. require.NoError(t, err)
  150. for _, issue := range lowRes.Msg.ConfigIssues {
  151. assert.NotEqual(t, configissues.CodeWatcherPath, issue.Code,
  152. "user with view:false must not see action-scoped watcher_path issues")
  153. assert.NotContains(t, issue.Source, "/secret/path")
  154. }
  155. adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
  156. adminReq.Header().Set("X-Ot-User", "admin")
  157. adminRes, err := client.GetDiagnostics(context.Background(), adminReq)
  158. require.NoError(t, err)
  159. foundWatcher := false
  160. for _, issue := range adminRes.Msg.ConfigIssues {
  161. if issue.Code == configissues.CodeWatcherPath && issue.ActionId == "secret_action" {
  162. foundWatcher = true
  163. break
  164. }
  165. }
  166. assert.True(t, foundWatcher, "admin must see action-scoped watcher_path issues")
  167. }