271 lines
8.0 KiB
Go
271 lines
8.0 KiB
Go
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")
|
|
}
|