| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- package api
- import (
- "context"
- "testing"
- "connectrpc.com/connect"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
- authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
- config "github.com/OliveTin/OliveTin/internal/config"
- "github.com/OliveTin/OliveTin/internal/entities"
- "github.com/OliveTin/OliveTin/internal/executor"
- )
- func setupHostEntityTestData(t *testing.T) {
- t.Helper()
- entities.ClearEntitiesOfType("host")
- entities.AddEntity("host", "0", map[string]any{"name": "stuffbox", "hostname": "192.168.66.8"})
- entities.AddEntity("host", "1", map[string]any{"name": "lurker", "hostname": "192.168.66.1"})
- t.Cleanup(func() {
- entities.ClearEntitiesOfType("host")
- })
- }
- func buildRelatedActionsTestConfig(t *testing.T) (*config.Config, *authpublic.AuthenticatedUser, *authpublic.AuthenticatedUser) {
- t.Helper()
- cfg := config.DefaultConfig()
- cfg.DefaultPermissions.View = false
- cfg.DefaultPermissions.Exec = false
- cfg.Actions = append(cfg.Actions,
- &config.Action{
- Title: "Secret Entity Action",
- Shell: "echo secret",
- Entity: "host",
- },
- &config.Action{
- Title: "Hidden Host Action",
- Shell: "echo hidden",
- Entity: "host",
- Hidden: true,
- },
- &config.Action{
- ID: "run_playbook",
- Title: "Run Automation Playbook",
- Shell: "host '{{ ansible_host }}'",
- Arguments: []config.ActionArgument{
- {
- Name: "ansible_host",
- Title: "Host",
- Entity: "host",
- Choices: []config.ActionArgumentChoice{
- {Title: "{{ host.name }} ({{ host.hostname }})", Value: "{{ host.hostname }}"},
- },
- },
- },
- },
- &config.Action{
- Title: "Public Host Action",
- Shell: "echo public",
- Entity: "host",
- },
- )
- cfg.AccessControlLists = append(cfg.AccessControlLists,
- &config.AccessControlList{
- Name: "restricted",
- MatchUsernames: []string{"low"},
- AddToEveryAction: true,
- Permissions: config.PermissionsList{View: false, Exec: false, Logs: false, Kill: false},
- },
- &config.AccessControlList{
- Name: "full",
- MatchUsernames: []string{"admin"},
- AddToEveryAction: true,
- Permissions: config.PermissionsList{View: true, Exec: true, Logs: true, Kill: true},
- },
- )
- cfg.Entities = []*config.EntityFile{
- {File: "hosts.yaml", Name: "host", Icon: "ssh"},
- }
- cfg.Sanitize()
- lowUser := &authpublic.AuthenticatedUser{Username: "low", Acls: []string{"restricted"}}
- adminUser := &authpublic.AuthenticatedUser{Username: "admin", Acls: []string{"full"}}
- return cfg, lowUser, adminUser
- }
- func getEntityRelatedActionTitles(t *testing.T, api *oliveTinAPI, user *authpublic.AuthenticatedUser, entityType, entityKey string) []string {
- t.Helper()
- entity, ok := entities.GetEntityInstances(entityType)[entityKey]
- require.True(t, ok, "entity %s/%s must exist", entityType, entityKey)
- related := api.relatedActionsForEntity(user, entityType, entity)
- titles := make([]string, 0, len(related))
- for _, item := range related {
- if item.Action != nil {
- titles = append(titles, item.Action.Title)
- }
- }
- return titles
- }
- func getEntityRelatedBindingIDs(t *testing.T, api *oliveTinAPI, user *authpublic.AuthenticatedUser, entityType, entityKey string) []string {
- t.Helper()
- entity, ok := entities.GetEntityInstances(entityType)[entityKey]
- require.True(t, ok, "entity %s/%s must exist", entityType, entityKey)
- related := api.relatedActionsForEntity(user, entityType, entity)
- ids := make([]string, 0, len(related))
- for _, item := range related {
- if item.Action != nil {
- ids = append(ids, item.Action.BindingId)
- }
- }
- return ids
- }
- func TestGetEntityRelatedActionsDeniesRestrictedView(t *testing.T) {
- setupHostEntityTestData(t)
- cfg, lowUser, _ := buildRelatedActionsTestConfig(t)
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- api := newServer(ex)
- ids := getEntityRelatedBindingIDs(t, api, lowUser, "host", "0")
- assert.Empty(t, ids)
- titles := getEntityRelatedActionTitles(t, api, lowUser, "host", "0")
- assert.Empty(t, titles)
- }
- func TestGetEntityRelatedActionsAllowsAdminView(t *testing.T) {
- setupHostEntityTestData(t)
- cfg, _, adminUser := buildRelatedActionsTestConfig(t)
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- api := newServer(ex)
- titles := getEntityRelatedActionTitles(t, api, adminUser, "host", "0")
- assert.Contains(t, titles, "Run Automation Playbook")
- assert.Contains(t, titles, "Public Host Action")
- assert.Contains(t, titles, "Secret Entity Action")
- }
- func TestGetEntityRelatedActionsExcludesHiddenActions(t *testing.T) {
- setupHostEntityTestData(t)
- cfg, _, adminUser := buildRelatedActionsTestConfig(t)
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- api := newServer(ex)
- titles := getEntityRelatedActionTitles(t, api, adminUser, "host", "0")
- assert.NotContains(t, titles, "Hidden Host Action")
- }
- func TestGetEntityRelatedActionsEntityBoundMatchesInstanceOnly(t *testing.T) {
- setupHostEntityTestData(t)
- cfg := config.DefaultConfig()
- cfg.Actions = append(cfg.Actions, &config.Action{
- Title: "{{ host.name }} Wake",
- Shell: "echo wake",
- Entity: "host",
- })
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- api := newServer(ex)
- user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
- titlesHost0 := getEntityRelatedActionTitles(t, api, user, "host", "0")
- assert.Len(t, titlesHost0, 1)
- assert.Equal(t, "stuffbox Wake", titlesHost0[0])
- titlesHost1 := getEntityRelatedActionTitles(t, api, user, "host", "1")
- assert.Len(t, titlesHost1, 1)
- assert.Equal(t, "lurker Wake", titlesHost1[0])
- }
- func TestGetEntityRelatedActionsPrefillsArgumentEntityValues(t *testing.T) {
- setupHostEntityTestData(t)
- cfg := config.DefaultConfig()
- cfg.Actions = append(cfg.Actions, &config.Action{
- ID: "run_playbook",
- Title: "Run Automation Playbook",
- Shell: "host '{{ ansible_host }}'",
- Arguments: []config.ActionArgument{
- {
- Name: "ansible_host",
- Title: "Host",
- Entity: "host",
- Choices: []config.ActionArgumentChoice{
- {Title: "{{ host.name }} ({{ host.hostname }})", Value: "{{ host.hostname }}"},
- },
- },
- },
- })
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- api := newServer(ex)
- user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
- entity := entities.GetEntityInstances("host")["0"]
- related := api.relatedActionsForEntity(user, "host", entity)
- require.Len(t, related, 1)
- require.NotNil(t, related[0].Action)
- assert.Equal(t, "run_playbook", related[0].Action.BindingId)
- assert.Equal(t, "192.168.66.8", related[0].PrefilledArguments["ansible_host"])
- }
- func TestGetEntityDeniesGuestsWhenLoginRequired(t *testing.T) {
- setupHostEntityTestData(t)
- cfg := config.DefaultConfig()
- cfg.AuthRequireGuestsToLogin = true
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
- defer ts.Close()
- _, err := client.GetEntity(context.Background(), connect.NewRequest(&apiv1.GetEntityRequest{
- Type: "host",
- UniqueKey: "0",
- }))
- require.Error(t, err)
- assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
- }
- func TestGetEntityReturnsRelatedActionsForAdmin(t *testing.T) {
- setupHostEntityTestData(t)
- cfg, _, adminUser := buildRelatedActionsTestConfig(t)
- cfg.AuthHttpHeaderUsername = "X-Ot-User"
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
- defer ts.Close()
- req := connect.NewRequest(&apiv1.GetEntityRequest{
- Type: "host",
- UniqueKey: "0",
- })
- req.Header().Set("X-Ot-User", adminUser.Username)
- resp, err := client.GetEntity(context.Background(), req)
- require.NoError(t, err)
- require.NotNil(t, resp.Msg)
- assert.Equal(t, "🔐", resp.Msg.Icon)
- foundPlaybook := false
- for _, related := range resp.Msg.RelatedActions {
- if related.Action != nil && related.Action.BindingId == "run_playbook" {
- foundPlaybook = true
- assert.Equal(t, "192.168.66.8", related.PrefilledArguments["ansible_host"])
- }
- }
- assert.True(t, foundPlaybook, "admin should see argument-entity related action in GetEntity response")
- }
|