| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- 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"
- config "github.com/OliveTin/OliveTin/internal/config"
- "github.com/OliveTin/OliveTin/internal/entities"
- "github.com/OliveTin/OliveTin/internal/executor"
- )
- func buildEntityArgumentGuessConfig() *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: "servers", File: "servers.yaml", Acls: []string{"ops"}},
- }
- cfg.Actions = []*config.Action{
- {
- ID: "reboot-server",
- Title: "Reboot server",
- Shell: "echo reboot '{{ target }}'",
- Arguments: []config.ActionArgument{
- {
- Name: "target",
- Title: "Server",
- Entity: "servers",
- Choices: []config.ActionArgumentChoice{
- {Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
- },
- },
- },
- },
- }
- cfg.Sanitize()
- return cfg
- }
- func seedEntityArgumentGuessEntities(t *testing.T) {
- t.Helper()
- entities.ClearEntitiesOfType("servers")
- t.Cleanup(func() {
- entities.ClearEntitiesOfType("servers")
- })
- entities.AddEntity("servers", "0", map[string]any{"name": "web01"})
- entities.AddEntity("servers", "1", map[string]any{"name": "db01"})
- }
- func TestStartActionRejectsGuessedRestrictedEntityArgument(t *testing.T) {
- seedEntityArgumentGuessEntities(t)
- cfg := buildEntityArgumentGuessConfig()
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
- defer ts.Close()
- _, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
- BindingId: "reboot-server",
- Arguments: []*apiv1.StartActionArgument{
- {Name: "target", Value: "web01"},
- },
- }))
- require.Error(t, err)
- assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
- }
- func TestStartActionRejectsUnknownEntityArgumentValue(t *testing.T) {
- seedEntityArgumentGuessEntities(t)
- cfg := buildEntityArgumentGuessConfig()
- // Make servers unrestricted so guests can view the type but not invent values.
- cfg.Entities[0].Acls = nil
- cfg.Sanitize()
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
- defer ts.Close()
- _, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
- BindingId: "reboot-server",
- Arguments: []*apiv1.StartActionArgument{
- {Name: "target", Value: "not-a-real-server"},
- },
- }))
- require.Error(t, err)
- assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
- }
- func TestStartActionAllowsListedEntityArgumentValue(t *testing.T) {
- seedEntityArgumentGuessEntities(t)
- cfg := buildEntityArgumentGuessConfig()
- cfg.Entities[0].Acls = nil
- cfg.Sanitize()
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
- defer ts.Close()
- resp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
- BindingId: "reboot-server",
- Arguments: []*apiv1.StartActionArgument{
- {Name: "target", Value: "web01"},
- },
- }))
- require.NoError(t, err)
- assert.NotEmpty(t, resp.Msg.ExecutionTrackingId)
- }
- func TestChecklistEntityValuesAllowedRejectsBlankOnlyInput(t *testing.T) {
- allowed := map[string]struct{}{"web01": {}, "db01": {}}
- arg := &config.ActionArgument{Type: "checklist"}
- assert.False(t, checklistEntityValuesAllowed(arg, ",,,", allowed))
- assert.False(t, checklistEntityValuesAllowed(arg, " , ", allowed))
- assert.False(t, checklistEntityValuesAllowed(arg, "", allowed),
- "all-blank checklist parts are rejected here; empty string is accepted by the caller separately")
- assert.True(t, checklistEntityValuesAllowed(arg, "web01", allowed))
- assert.True(t, checklistEntityValuesAllowed(arg, `["web01","db01"]`, allowed))
- assert.False(t, checklistEntityValuesAllowed(arg, `["web01","unknown"]`, allowed))
- }
- func TestChecklistEntityValuesAllowedAcceptsJSONArrayWithEntityTitles(t *testing.T) {
- entities.ClearEntitiesOfType("servers")
- t.Cleanup(func() {
- entities.ClearEntitiesOfType("servers")
- })
- entities.AddEntity("servers", "0", map[string]any{"name": "web01", "label": "Web Server One"})
- entities.AddEntity("servers", "1", map[string]any{"name": "db01", "label": "Database One"})
- arg := &config.ActionArgument{
- Type: "checklist",
- Entity: "servers",
- Choices: []config.ActionArgumentChoice{
- {Title: "{{ servers.label }}", Value: "{{ servers.name }}"},
- },
- }
- allowed := entityArgumentAllowedValues(arg)
- assert.True(t, checklistEntityValuesAllowed(arg, `["Web Server One","Database One"]`, allowed))
- assert.False(t, checklistEntityValuesAllowed(arg, `["Web Server One","unknown"]`, allowed))
- }
- func TestEntityArgumentValueAllowedAcceptsEntityChoiceTitle(t *testing.T) {
- entities.ClearEntitiesOfType("servers")
- t.Cleanup(func() {
- entities.ClearEntitiesOfType("servers")
- })
- entities.AddEntity("servers", "0", map[string]any{"name": "web01", "label": "Web Server One"})
- arg := &config.ActionArgument{
- Entity: "servers",
- Choices: []config.ActionArgumentChoice{
- {Title: "{{ servers.label }}", Value: "{{ servers.name }}"},
- },
- }
- assert.True(t, entityArgumentValueAllowed(arg, "Web Server One"))
- assert.True(t, entityArgumentValueAllowed(arg, "web01"))
- assert.False(t, entityArgumentValueAllowed(arg, "unknown"))
- }
- func TestStartActionRejectsMalformedMultiChoiceEntityArgument(t *testing.T) {
- seedEntityArgumentGuessEntities(t)
- cfg := buildEntityArgumentGuessConfig()
- // After sanitize, force an invalid entity+multi-choice shape that would
- // previously skip ACL and fall through to static UI choices.
- cfg.Actions[0].Arguments[0].Choices = []config.ActionArgumentChoice{
- {Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
- {Title: "web01", Value: "web01"},
- }
- ex := executor.DefaultExecutor(cfg)
- ex.RebuildActionMap()
- ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
- defer ts.Close()
- _, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
- BindingId: "reboot-server",
- Arguments: []*apiv1.StartActionArgument{
- {Name: "target", Value: "web01"},
- },
- }))
- require.Error(t, err)
- assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
- }
|