This removes the Cartesian product in the DB for GrantsForUser in favor of returning the actual grant scope information and dealing with it in application code. (cherry picked from commit 913cd536823d73994acc4f4d833208c1cc05f91b)
361 lines
9.3 KiB
Go
361 lines
9.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package perms
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/types/action"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
"github.com/hashicorp/boundary/internal/types/scope"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_OutputFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type input struct {
|
|
name string
|
|
fields []string
|
|
startMap *OutputFields
|
|
resMap *OutputFields
|
|
resStar bool
|
|
}
|
|
tests := []input{
|
|
{
|
|
name: "nil map, add nil",
|
|
resMap: &OutputFields{},
|
|
},
|
|
{
|
|
name: "nil map, add empty fields",
|
|
fields: []string{},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{},
|
|
},
|
|
},
|
|
{
|
|
name: "nil map, add fields",
|
|
fields: []string{"id", "version"},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"id": true,
|
|
"version": true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing map, add nil",
|
|
startMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"id": true,
|
|
"version": true,
|
|
},
|
|
},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"id": true,
|
|
"version": true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing with star, add nil",
|
|
startMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"*": true,
|
|
},
|
|
},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"*": true,
|
|
},
|
|
},
|
|
resStar: true,
|
|
},
|
|
{
|
|
name: "existing with star, add new",
|
|
fields: []string{"id", "version"},
|
|
startMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"*": true,
|
|
},
|
|
},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"*": true,
|
|
},
|
|
},
|
|
resStar: true,
|
|
},
|
|
{
|
|
name: "existing without star, add new",
|
|
fields: []string{"id", "version"},
|
|
startMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"name": true,
|
|
},
|
|
},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"id": true,
|
|
"version": true,
|
|
"name": true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing without star, add star",
|
|
fields: []string{"id", "*"},
|
|
startMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"name": true,
|
|
},
|
|
},
|
|
resMap: &OutputFields{
|
|
fields: map[string]bool{
|
|
"*": true,
|
|
},
|
|
},
|
|
resStar: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
out := test.startMap.AddFields(test.fields)
|
|
assert.True(out.Has("*") == test.resStar)
|
|
assert.Equal(test.resMap, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ACLOutputFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
type input struct {
|
|
name string
|
|
grants []string
|
|
resource Resource
|
|
action action.Type
|
|
fields []string
|
|
authorized bool
|
|
}
|
|
tests := []input{
|
|
{
|
|
name: "default",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
action: action.Read,
|
|
grants: []string{"ids=u_bar;actions=read,update"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "single value",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{"ids=u_bar;actions=read,update;output_fields=id"},
|
|
action: action.Read,
|
|
fields: []string{"id"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "compound no overlap",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read,update;output_fields=id",
|
|
"ids=*;type=host-catalog;actions=read,update;output_fields=version",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"id"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "compound",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read,update;output_fields=id",
|
|
"ids=*;type=role;output_fields=version",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"id", "version"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "wildcard with type",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read,update;output_fields=read",
|
|
"ids=*;type=role;output_fields=*",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"*"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "wildcard with wildcard type",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read,update;output_fields=read",
|
|
"ids=*;type=*;output_fields=*",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"*"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "subaction exact",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read:self,update;output_fields=version",
|
|
},
|
|
action: action.ReadSelf,
|
|
fields: []string{"version"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
// If the action is a subaction, parent output fields will apply, in
|
|
// addition to subaction. This matches authorization.
|
|
name: "subaction parent action",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read,update;output_fields=version",
|
|
"ids=u_bar;actions=read:self;output_fields=id",
|
|
},
|
|
action: action.ReadSelf,
|
|
fields: []string{"id", "version"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
// The inverse isn't true. Similarly to authorization, if you have
|
|
// specific output fields on a self action, they don't apply to
|
|
// non-self actions. This is useful to allow more visibility to self
|
|
// actions and less in the general case.
|
|
name: "subaction child action",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;actions=read:self,update;output_fields=version",
|
|
"ids=u_bar;actions=read;output_fields=id",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"id"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "initial grant unauthorized with star",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;output_fields=*",
|
|
"ids=u_bar;actions=delete;output_fields=id",
|
|
},
|
|
action: action.Delete,
|
|
fields: []string{"*"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "unauthorized id only",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"ids=u_bar;output_fields=name",
|
|
},
|
|
action: action.Delete,
|
|
fields: []string{"name"},
|
|
},
|
|
{
|
|
name: "unauthorized type only",
|
|
resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Type: resource.Role},
|
|
grants: []string{
|
|
"type=role;output_fields=name",
|
|
},
|
|
action: action.List,
|
|
fields: []string{"name"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var grants []Grant
|
|
for _, g := range test.grants {
|
|
grant, err := Parse(ctx, GrantTuple{RoleScopeId: "o_myorg", GrantScopeId: "o_myorg", Grant: g})
|
|
require.NoError(t, err)
|
|
grants = append(grants, grant)
|
|
}
|
|
acl := NewACL(grants...)
|
|
results := acl.Allowed(test.resource, test.action, globals.AnonymousUserId, WithSkipAnonymousUserRestrictions(true))
|
|
fields, _ := results.OutputFields.Fields()
|
|
assert.ElementsMatch(t, fields, test.fields)
|
|
assert.True(t, test.authorized == results.Authorized)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ACLSelfOrDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type input struct {
|
|
name string
|
|
input *OutputFields
|
|
output *OutputFields
|
|
userId string
|
|
}
|
|
tests := []input{
|
|
{
|
|
name: "nil, no user ID, set but empty",
|
|
output: &OutputFields{
|
|
fields: map[string]bool{},
|
|
},
|
|
},
|
|
{
|
|
name: "nil, non anon id",
|
|
output: &OutputFields{
|
|
fields: map[string]bool{
|
|
"*": true,
|
|
},
|
|
},
|
|
userId: "u_abc123",
|
|
},
|
|
{
|
|
name: "nil, anon id",
|
|
output: &OutputFields{
|
|
fields: map[string]bool{
|
|
globals.IdField: true,
|
|
globals.ScopeField: true,
|
|
globals.ScopeIdField: true,
|
|
globals.PluginField: true,
|
|
globals.PluginIdField: true,
|
|
globals.NameField: true,
|
|
globals.DescriptionField: true,
|
|
globals.TypeField: true,
|
|
globals.IsPrimaryField: true,
|
|
globals.PrimaryAuthMethodIdField: true,
|
|
globals.AuthorizedActionsField: true,
|
|
globals.AuthorizedCollectionActionsField: true,
|
|
},
|
|
},
|
|
userId: globals.AnonymousUserId,
|
|
},
|
|
{
|
|
name: "not nil",
|
|
input: &OutputFields{
|
|
fields: map[string]bool{
|
|
"foo": true,
|
|
},
|
|
},
|
|
output: &OutputFields{
|
|
fields: map[string]bool{
|
|
"foo": true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assert.Equal(t, test.output, test.input.SelfOrDefaults(test.userId))
|
|
})
|
|
}
|
|
}
|