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) }