mirror of
https://github.com/OliveTin/OliveTin
synced 2026-08-26 04:26:28 +00:00
215 lines
6.3 KiB
Go
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)
|
|
}
|