Files
OliveTin/service/internal/api/api_init_search_hints_test.go

274 lines
8.4 KiB
Go

package api
import (
"context"
"fmt"
"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 TestInitIncludesEntitySearchHints(t *testing.T) {
entities.ClearEntitiesOfType("server")
entities.ClearEntitiesOfType("database")
t.Cleanup(func() {
entities.ClearEntitiesOfType("server")
entities.ClearEntitiesOfType("database")
})
entities.AddEntity("server", "0", map[string]any{
"name": "web01",
"secret": "must-not-appear-in-search-hints",
})
entities.AddEntity("database", "db-1", map[string]any{
"title": "postgres",
})
cfg := config.DefaultConfig()
cfg.Features.HeaderSearch = true
cfg.Sanitize()
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
testServer, client := getNewTestServerAndClientWithExecutor(cfg, testExecutor)
defer testServer.Close()
resp, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
require.NoError(t, err)
require.NotNil(t, resp.Msg.Features)
assert.True(t, resp.Msg.Features.HeaderSearch)
require.NotNil(t, resp.Msg.SearchHints)
byKey := map[string]*apiv1.EntitySearchHint{}
for _, hint := range resp.Msg.SearchHints.Entities {
byKey[hint.Type+":"+hint.UniqueKey] = hint
}
host, ok := byKey["server:0"]
require.True(t, ok, "expected server:0 search hint")
assert.Equal(t, "web01", host.Title)
assert.Equal(t, "server", host.Type)
assert.Equal(t, "0", host.UniqueKey)
db, ok := byKey["database:db-1"]
require.True(t, ok, "expected database:db-1 search hint")
assert.Equal(t, "postgres", db.Title)
}
func TestInitOmitsSearchHintsWhenLoginRequired(t *testing.T) {
entities.ClearEntitiesOfType("server")
t.Cleanup(func() {
entities.ClearEntitiesOfType("server")
})
entities.AddEntity("server", "0", map[string]any{"name": "web01"})
cfg := config.DefaultConfig()
cfg.AuthRequireGuestsToLogin = true
cfg.Features.HeaderSearch = true
cfg.Sanitize()
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
testServer, client := getNewTestServerAndClientWithExecutor(cfg, testExecutor)
defer testServer.Close()
resp, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
require.NoError(t, err)
require.True(t, resp.Msg.LoginRequired)
assert.Nil(t, resp.Msg.SearchHints)
}
func TestInitOmitsSearchHintsWhenHeaderSearchDisabled(t *testing.T) {
entities.ClearEntitiesOfType("server")
t.Cleanup(func() {
entities.ClearEntitiesOfType("server")
})
entities.AddEntity("server", "0", map[string]any{"name": "web01"})
cfg := config.DefaultConfig()
cfg.Sanitize()
require.False(t, cfg.Features.HeaderSearch)
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
testServer, client := getNewTestServerAndClientWithExecutor(cfg, testExecutor)
defer testServer.Close()
resp, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
require.NoError(t, err)
require.NotNil(t, resp.Msg.Features)
assert.False(t, resp.Msg.Features.HeaderSearch)
assert.Nil(t, resp.Msg.SearchHints)
}
func TestBuildSearchHintsRespectsActionACL(t *testing.T) {
cfg := config.DefaultConfig()
cfg.DefaultPermissions.View = false
cfg.DefaultPermissions.Exec = false
cfg.Actions = []*config.Action{
{ID: "public_action", Title: "Public Action", Shell: "echo public"},
{ID: "secret_action", Title: "Secret Action", Shell: "echo secret", Acls: []string{"admins"}},
}
cfg.AccessControlLists = []*config.AccessControlList{
{
Name: "everyone",
MatchUsernames: []string{"guest", "admin"},
AddToEveryAction: false,
Permissions: config.PermissionsList{View: true, Exec: true},
},
{
Name: "admins",
MatchUsernames: []string{"admin"},
Permissions: config.PermissionsList{View: true, Exec: true},
},
}
cfg.Actions[0].Acls = []string{"everyone"}
cfg.Sanitize()
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
api := newServer(testExecutor)
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
guest.BuildUserAcls(cfg)
admin := &authpublic.AuthenticatedUser{Username: "admin"}
admin.BuildUserAcls(cfg)
guestHints := api.buildSearchHints(guest)
require.NotNil(t, guestHints)
guestActionIDs := actionHintBindingIDs(guestHints.Actions)
assert.Contains(t, guestActionIDs, "public_action")
assert.NotContains(t, guestActionIDs, "secret_action")
adminHints := api.buildSearchHints(admin)
require.NotNil(t, adminHints)
adminActionIDs := actionHintBindingIDs(adminHints.Actions)
assert.Contains(t, adminActionIDs, "public_action")
assert.Contains(t, adminActionIDs, "secret_action")
}
func TestBuildSearchHintsOmitsHiddenActions(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{ID: "visible", Title: "Visible", Shell: "echo visible"},
{ID: "hidden", Title: "Hidden", Shell: "echo hidden", Hidden: true},
}
cfg.Sanitize()
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
api := newServer(testExecutor)
user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
user.BuildUserAcls(cfg)
hints := api.buildSearchHints(user)
require.NotNil(t, hints)
ids := actionHintBindingIDs(hints.Actions)
assert.Contains(t, ids, "visible")
assert.NotContains(t, ids, "hidden")
}
func TestBuildSearchHintsCapsActionsAndEntitiesPerType(t *testing.T) {
entities.ClearEntitiesOfType("cap_host")
entities.ClearEntitiesOfType("cap_container")
t.Cleanup(func() {
entities.ClearEntitiesOfType("cap_host")
entities.ClearEntitiesOfType("cap_container")
})
for entityIndex := 0; entityIndex < maxSearchHintEntitiesPerType+5; entityIndex++ {
entities.AddEntity("cap_host", fmt.Sprintf("%03d", entityIndex), map[string]any{"name": fmt.Sprintf("host-%03d", entityIndex)})
entities.AddEntity("cap_container", fmt.Sprintf("%03d", entityIndex), map[string]any{"name": fmt.Sprintf("ctr-%03d", entityIndex)})
}
cfg := config.DefaultConfig()
cfg.Entities = []*config.EntityFile{
{Name: "cap_host", File: "cap_host.yaml"},
{Name: "cap_container", File: "cap_container.yaml"},
}
cfg.Actions = make([]*config.Action, 0, maxSearchHintActions+5)
for actionIndex := 0; actionIndex < maxSearchHintActions+5; actionIndex++ {
cfg.Actions = append(cfg.Actions, &config.Action{
ID: fmt.Sprintf("action-%03d", actionIndex),
Title: fmt.Sprintf("Action %03d", actionIndex),
Shell: "echo",
})
}
cfg.Sanitize()
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
api := newServer(testExecutor)
user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
user.BuildUserAcls(cfg)
hints := api.buildSearchHints(user)
require.NotNil(t, hints)
assert.Len(t, hints.Actions, maxSearchHintActions)
assert.Equal(t, maxSearchHintEntitiesPerType, countEntityHintsByType(hints.Entities, "cap_host"))
assert.Equal(t, maxSearchHintEntitiesPerType, countEntityHintsByType(hints.Entities, "cap_container"))
}
func countEntityHintsByType(hints []*apiv1.EntitySearchHint, entityType string) int {
count := 0
for _, hint := range hints {
if hint.Type == entityType {
count++
}
}
return count
}
func TestBuildSearchHintsPrefersNonEntityActions(t *testing.T) {
entities.ClearEntitiesOfType("host")
t.Cleanup(func() {
entities.ClearEntitiesOfType("host")
})
for i := 0; i < 10; i++ {
entities.AddEntity("host", fmt.Sprintf("%d", i), map[string]any{"name": fmt.Sprintf("host-%d", i)})
}
cfg := config.DefaultConfig()
cfg.Actions = []*config.Action{
{ID: "plain", Title: "Plain Action", Shell: "echo plain"},
{Title: "Entity Action {{ name }}", Shell: "echo entity", Entity: "host"},
}
cfg.Sanitize()
testExecutor := executor.DefaultExecutor(cfg)
testExecutor.RebuildActionMap()
api := newServer(testExecutor)
user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
user.BuildUserAcls(cfg)
hints := api.buildSearchHints(user)
require.NotNil(t, hints)
require.NotEmpty(t, hints.Actions)
assert.Equal(t, "plain", hints.Actions[0].BindingId)
}
func actionHintBindingIDs(hints []*apiv1.ActionSearchHint) []string {
ids := make([]string, 0, len(hints))
for _, hint := range hints {
ids = append(ids, hint.BindingId)
}
return ids
}