Files

215 lines
6.3 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 buildEntityAclTestConfig() *config.Config {
cfg := config.DefaultConfig()
cfg.DefaultPermissions.View = false
cfg.DefaultPermissions.Exec = false
cfg.AccessControlLists = []*config.AccessControlList{
{
Name: "ops",
MatchUsernames: []string{"admin"},
Permissions: config.PermissionsList{View: true, Exec: true},
},
{
Name: "everyone",
MatchUsernames: []string{"guest", "admin"},
Permissions: config.PermissionsList{View: true, Exec: true},
AddToEveryAction: true,
},
}
cfg.Entities = []*config.EntityFile{
{Name: "printers", File: "printers.yaml"},
{Name: "servers", File: "servers.yaml", Acls: []string{"ops"}},
}
cfg.Actions = []*config.Action{
{
ID: "restart-server",
Title: "Restart {{ servers.name }}",
Shell: "echo restart",
Entity: "servers",
},
{
ID: "ping-printer",
Title: "Ping printer",
Shell: "echo ping",
},
}
cfg.Dashboards = []*config.DashboardComponent{
{
Title: "Infra",
Contents: []*config.DashboardComponent{
{
Title: "{{ servers.name }}",
Type: "fieldset",
Entity: "servers",
Contents: []*config.DashboardComponent{
{Title: "Restart {{ servers.name }}"},
},
},
},
},
}
cfg.Sanitize()
return cfg
}
func seedEntityAclTestEntities(t *testing.T) {
t.Helper()
entities.ClearEntitiesOfType("printers")
entities.ClearEntitiesOfType("servers")
t.Cleanup(func() {
entities.ClearEntitiesOfType("printers")
entities.ClearEntitiesOfType("servers")
})
entities.AddEntity("printers", "p1", map[string]any{"name": "lobby"})
entities.AddEntity("servers", "0", map[string]any{"name": "web01"})
}
func TestGetEntitiesOmitsRestrictedEntityTypes(t *testing.T) {
seedEntityAclTestEntities(t)
cfg := buildEntityAclTestConfig()
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
api := newServer(ex)
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
guest.BuildUserAcls(cfg)
defs := api.buildEntityDefinitionsResponse(guest, &apiv1.GetEntitiesRequest{}, entities.GetEntities())
titles := make([]string, 0, len(defs))
for _, def := range defs {
titles = append(titles, def.Title)
}
assert.Contains(t, titles, "printers")
assert.NotContains(t, titles, "servers")
}
func TestGetEntityNotFoundForRestrictedType(t *testing.T) {
seedEntityAclTestEntities(t)
cfg := buildEntityAclTestConfig()
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
defer ts.Close()
// Guest Init/API without login uses guest user with default ACLs from UserFromApiCall.
_, err := client.GetEntity(context.Background(), connect.NewRequest(&apiv1.GetEntityRequest{
Type: "servers",
UniqueKey: "0",
}))
require.Error(t, err)
assert.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
resp, err := client.GetEntity(context.Background(), connect.NewRequest(&apiv1.GetEntityRequest{
Type: "printers",
UniqueKey: "p1",
}))
require.NoError(t, err)
assert.Equal(t, "lobby", resp.Msg.Title)
}
func TestSearchHintsOmitRestrictedEntitiesAndEntityBoundActions(t *testing.T) {
seedEntityAclTestEntities(t)
cfg := buildEntityAclTestConfig()
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
api := newServer(ex)
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)
guestEntityKeys := make([]string, 0)
for _, hint := range guestHints.Entities {
guestEntityKeys = append(guestEntityKeys, hint.Type+":"+hint.UniqueKey)
}
assert.Contains(t, guestEntityKeys, "printers:p1")
assert.NotContains(t, guestEntityKeys, "servers:0")
guestActionIDs := actionHintBindingIDs(guestHints.Actions)
require.NotEmpty(t, guestActionIDs)
assert.Contains(t, guestActionIDs, "ping-printer")
assert.NotContains(t, guestActionIDs, "restart")
adminHints := api.buildSearchHints(admin)
require.NotNil(t, adminHints)
adminEntityKeys := make([]string, 0)
for _, hint := range adminHints.Entities {
adminEntityKeys = append(adminEntityKeys, hint.Type+":"+hint.UniqueKey)
}
assert.Contains(t, adminEntityKeys, "servers:0")
assert.NotEmpty(t, adminHints.Actions)
}
func TestEntityFieldsetOmitsRestrictedEntityType(t *testing.T) {
seedEntityAclTestEntities(t)
cfg := buildEntityAclTestConfig()
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
api := newServer(ex)
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
guest.BuildUserAcls(cfg)
admin := &authpublic.AuthenticatedUser{Username: "admin"}
admin.BuildUserAcls(cfg)
guestRR := api.createDashboardRenderRequest(guest, "", "")
guestDB := renderDashboard(guestRR, "Infra")
require.NotNil(t, guestDB)
assert.Empty(t, guestDB.Contents, "restricted entity fieldsets must not leak instances")
adminRR := api.createDashboardRenderRequest(admin, "", "")
adminDB := renderDashboard(adminRR, "Infra")
require.NotNil(t, adminDB)
require.NotEmpty(t, adminDB.Contents)
}
func TestBuildChoicesEntityRespectsEntityACL(t *testing.T) {
seedEntityAclTestEntities(t)
cfg := buildEntityAclTestConfig()
ex := executor.DefaultExecutor(cfg)
ex.RebuildActionMap()
guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"}
guest.BuildUserAcls(cfg)
admin := &authpublic.AuthenticatedUser{Username: "admin"}
admin.BuildUserAcls(cfg)
arg := config.ActionArgument{
Entity: "servers",
Choices: []config.ActionArgumentChoice{
{Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
},
}
guestRR := &DashboardRenderRequest{AuthenticatedUser: guest, cfg: cfg, ex: ex}
assert.Empty(t, buildChoices(arg, guestRR))
adminRR := &DashboardRenderRequest{AuthenticatedUser: admin, cfg: cfg, ex: ex}
choices := buildChoices(arg, adminRR)
require.Len(t, choices, 1)
assert.Equal(t, "web01", choices[0].Value)
}