This adds several relationships into the resource package that are then made use of to improve the grant validation that runs during Parse. By understanding mappings between ID and resource type the validation checks can far better align with actual allowed grant formats so we can find several common misunderstandings before they simply result in an authorization failure. In the future some of these functions could be used in the logic in the actual ACL checks but it's not currently necessary and I don't want to lump that change in with this. Note that the tests have not meaningfully changed, but they _have_ had to be updated because the stricter validation is actually catching issues.
357 lines
8.7 KiB
Go
357 lines
8.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package perms
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/types/action"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
"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()
|
|
|
|
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", Id: "u_bar", Type: resource.Role},
|
|
action: action.Read,
|
|
grants: []string{"id=u_bar;actions=read,update"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "single value",
|
|
resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{"id=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", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;actions=read,update;output_fields=id",
|
|
"id=*;type=host-catalog;actions=read,update;output_fields=version",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"id"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "compound",
|
|
resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;actions=read,update;output_fields=id",
|
|
"id=*;type=role;output_fields=version",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"id", "version"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "wildcard with type",
|
|
resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;actions=read,update;output_fields=read",
|
|
"id=*;type=role;output_fields=*",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"*"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "wildcard with wildcard type",
|
|
resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;actions=read,update;output_fields=read",
|
|
"id=*;type=*;output_fields=*",
|
|
},
|
|
action: action.Read,
|
|
fields: []string{"*"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "subaction exact",
|
|
resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=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", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;actions=read,update;output_fields=version",
|
|
"id=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", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;actions=read:self,update;output_fields=version",
|
|
"id=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", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;output_fields=*",
|
|
"id=u_bar;actions=delete;output_fields=id",
|
|
},
|
|
action: action.Delete,
|
|
fields: []string{"*"},
|
|
authorized: true,
|
|
},
|
|
{
|
|
name: "unauthorized id only",
|
|
resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role},
|
|
grants: []string{
|
|
"id=u_bar;output_fields=name",
|
|
},
|
|
action: action.Delete,
|
|
fields: []string{"name"},
|
|
},
|
|
{
|
|
name: "unauthorized type only",
|
|
resource: Resource{ScopeId: "o_myorg", 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("o_myorg", 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))
|
|
})
|
|
}
|
|
}
|