Restrict anonymous user capabilities (#2156)

This commit is contained in:
Jeff Mitchell authored and GitHub committed 2022-06-09 11:30:01 -04:00
1 parent 3521c21cd5
commit d6362f9e5e
11 files changed
+266 -63

No files matched your search

+4 -4
View File
@@ -569,7 +569,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (
}
retAcl = perms.NewACL(parsedGrants...)
aclResults = retAcl.Allowed(*v.res, v.act)
aclResults = retAcl.Allowed(*v.res, v.act, userId)
// We don't set authenticated above because setting this but not authorized
// is used for further permissions checks, such as during recursive listing.
// So we want to make sure any code relying on that has the full set of
@@ -619,7 +619,7 @@ func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActi
ret := make(action.ActionSet, 0, len(availableActions))
for _, act := range availableActions {
if r.v.acl.Allowed(*res, act).Authorized {
if r.v.acl.Allowed(*res, act, r.UserId).Authorized {
ret = append(ret, act)
}
}
@@ -637,7 +637,7 @@ func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) p
return nil
}
return r.v.acl.Allowed(res, act).OutputFields
return r.v.acl.Allowed(res, act, r.UserId).OutputFields
}
// GetTokenFromRequest pulls the token from either the Authorization header or
@@ -675,7 +675,7 @@ func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Reque
if receivedTokenType == AuthTokenTypeUnknown || fullToken == "" {
// We didn't find auth info or a client screwed up and put in a blank
// header instead of nothing at all, so return blank which will indicate
// the anonymouse user
// the anonymous user
return "", "", uint32(AuthTokenTypeUnknown)
}
+45 -6
View File
@@ -41,7 +41,7 @@ type ACLResults struct {
}
// Resource defines something within boundary that requires authorization
// capabilities. Resources must have a ScopeId.
// capabilities. Resources must have a ScopeId.
type Resource struct {
// ScopeId is the scope that contains the Resource.
ScopeId string `json:"scope_id,omitempty"`
@@ -71,7 +71,9 @@ func NewACL(grants ...Grant) ACL {
}
// Allowed determines if the grants for an ACL allow an action for a resource.
func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) {
func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option) (results ACLResults) {
opts := getOpts(opt...)
// First, get the grants within the specified scope
grants := a.scopeMap[r.ScopeId]
results.scopeMap = a.scopeMap
@@ -116,6 +118,30 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) {
// up the output fields patterns.
var found bool
switch {
// Case 1: We only allow specific actions on specific types for the
// anonymous user. ID being supplied or not doesn't matter in this case,
// it must be an explicit type and action(s); adding this as an explicit
// case here prevents duplicating logic in two of the other more
// general-purpose cases below (3 and 4). See notes there about ID being
// present or not.
case !opts.withSkipAnonymousUserRestrictions &&
(userId == AnonymousUserId || userId == ""):
switch {
// Allow discovery of scopes, so that auth methods within can be
// discovered
case grant.typ == r.Type &&
grant.typ == resource.Scope &&
(aType == action.List || aType == action.NoOp):
found = true
// Allow discovery of and authenticating to auth methods
case grant.typ == r.Type &&
grant.typ == resource.AuthMethod &&
(aType == action.List || aType == action.NoOp || aType == action.Authenticate):
found = true
}
// Case 2:
// id=<resource.id>;actions=<action> where ID cannot be a wildcard; or
// id=<resource.id>;output_fields=<fields> where fields cannot be a
// wildcard.
@@ -128,10 +154,21 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) {
found = true
// type=<resource.type>;actions=<action> when action is list or create.
// Must be a top level collection, otherwise must be one of the two
// formats specified below. Or,
// type=resource.type;output_fields=<fields> and no action.
// Case 3: type=<resource.type>;actions=<action> when action is list or
// create. Must be a top level collection, otherwise must be one of the
// two formats specified in cases 4 or 5. Or,
// type=resource.type;output_fields=<fields> and no action. This is more
// of a semantic difference compared to 4 more than a security
// difference; this type is for clarity as it ties more closely to the
// concept of create and list as actions on a collection, operating on a
// collection directly. The format in case 4 will still work for
// create/list on collections but that's more of a shortcut to allow
// things like id=*;type=*;actions=* for admin flows so that you don't
// need to separate out explicit collection actions into separate typed
// grants for each collection within a role. This does mean there are
// "two ways of doing things" but it's a reasonable UX tradeoff given
// that "all IDs" can reasonably be construed to include "and the one
// I'm making" and "all of them for listing".
case grant.id == "" &&
r.Id == "" &&
grant.typ == r.Type &&
@@ -142,6 +179,7 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) {
found = true
// Case 4:
// id=*;type=<resource.type>;actions=<action> where type cannot be
// unknown but can be a wildcard to allow any resource at all; or
// id=*;type=<resource.type>;output_fields=<fields> with no action.
@@ -152,6 +190,7 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) {
found = true
// Case 5:
// id=<pin>;type=<resource.type>;actions=<action> where type can be a
// wildcard and this this is operating on a non-top-level type. Same for
// output fields only.
+89 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/boundary/internal/intglobals"
"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"
)
@@ -329,8 +330,12 @@ func Test_ACLAllowed(t *testing.T) {
}
acl := NewACL(grants...)
for _, aa := range test.actionsAuthorized {
result := acl.Allowed(test.resource, aa.action)
assert.True(t, result.Authorized == aa.authorized, "action: %s, acl authorized: %t, test action authorized: %t", aa.action, acl.Allowed(test.resource, aa.action).Authorized, aa.authorized)
userId := test.userId
if userId == "" {
userId = "u_1234567890"
}
result := acl.Allowed(test.resource, aa.action, userId)
assert.True(t, result.Authorized == aa.authorized, "action: %s, acl authorized: %t, test action authorized: %t", aa.action, result.Authorized, aa.authorized)
assert.ElementsMatch(t, result.OutputFields.Fields(), aa.outputFields)
}
})
@@ -349,3 +354,85 @@ func TestJsonMarshal(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, `{"scope_id":"scope","id":"id","type":"controller"}`, string(out))
}
// Test_AnonRestrictions loops through every resource and action and ensures
// that it always fails for the anonymous user, regardless of what is granted,
// except for the specific things allowed.
func Test_AnonRestrictions(t *testing.T) {
t.Parallel()
type input struct {
name string
grant string
templatedType bool
shouldHaveSuccess bool
}
tests := []input{
{
name: "id-specific",
grant: "id=foobar;actions=%s",
},
{
name: "wildcard-id",
grant: "id=*;type=%s;actions=%s",
templatedType: true,
shouldHaveSuccess: true,
},
{
name: "wildcard-id-and-type",
grant: "id=*;type=*;actions=%s",
},
{
name: "no-id",
grant: "type=%s;actions=%s",
templatedType: true,
shouldHaveSuccess: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
for i := resource.Type(1); i <= resource.CredentialLibrary; i++ {
if i == resource.Controller || i == resource.Worker {
continue
}
for j := action.Type(1); j <= action.RemoveHostSources; j++ {
res := Resource{
ScopeId: scope.Global.String(),
Id: "foobar",
Type: resource.Type(i),
}
grant := test.grant
if test.templatedType {
grant = fmt.Sprintf(grant, resource.Type(i).String(), action.Type(j).String())
} else {
grant = fmt.Sprintf(grant, action.Type(j).String())
}
parsedGrant, err := Parse(scope.Global.String(), grant, WithSkipFinalValidation(true))
require.NoError(err)
acl := NewACL(parsedGrant)
results := acl.Allowed(res, action.Type(j), AnonymousUserId)
switch test.shouldHaveSuccess {
case true:
// Ensure it's one of the specific cases and fail otherwise
switch {
case i == resource.Scope && (j == action.List || j == action.NoOp):
assert.True(results.Authorized, fmt.Sprintf("i: %v, j: %v", i, j))
case i == resource.AuthMethod && (j == action.List || j == action.NoOp || j == action.Authenticate):
assert.True(results.Authorized, fmt.Sprintf("i: %v, j: %v", i, j))
default:
assert.False(results.Authorized, fmt.Sprintf("i: %v, j: %v", i, j))
}
default:
// Should always fail
assert.False(results.Authorized)
}
}
}
})
}
}
+1 -1
View File
@@ -408,7 +408,7 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) {
}
var allowed bool
for k := range grant.actions {
results := acl.Allowed(r, k)
results := acl.Allowed(r, k, AnonymousUserId, WithSkipAnonymousUserRestrictions(true))
if results.Authorized {
allowed = true
}
+16 -4
View File
@@ -4,7 +4,9 @@ package perms
func getOpts(opt ...Option) options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
if o != nil {
o(&opts)
}
}
return opts
}
@@ -14,9 +16,10 @@ type Option func(*options)
// options = how options are represented
type options struct {
withUserId string
withAccountId string
withSkipFinalValidation bool
withUserId string
withAccountId string
withSkipFinalValidation bool
withSkipAnonymousUserRestrictions bool
}
func getDefaultOptions() options {
@@ -45,3 +48,12 @@ func WithSkipFinalValidation(skipFinalValidation bool) Option {
o.withSkipFinalValidation = skipFinalValidation
}
}
// WithSkipAnonymousUserRestrictions allows skipping the restrictions on
// anonymous users, useful when e.g. validating parsed grants where we may not
// have a user ID yet.
func WithSkipAnonymousUserRestrictions(with bool) Option {
return func(o *options) {
o.withSkipAnonymousUserRestrictions = with
}
}
+43
View File
@@ -0,0 +1,43 @@
package perms
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("nil", func(t *testing.T) {
opts := getOpts(nil)
assert.NotNil(t, opts)
})
t.Run("with-user-id", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts()
assert.Empty(opts.withUserId)
opts = getOpts(WithUserId("foo"))
assert.Equal("foo", opts.withUserId)
})
t.Run("with-account-id", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts()
assert.Empty(opts.withAccountId)
opts = getOpts(WithAccountId("foo"))
assert.Equal("foo", opts.withAccountId)
})
t.Run("with-skip-final-validation", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts()
assert.False(opts.withSkipFinalValidation)
opts = getOpts(WithSkipFinalValidation(true))
assert.True(opts.withSkipFinalValidation)
})
t.Run("with-skip-anonymous-user-restrictions", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts()
assert.False(opts.withSkipAnonymousUserRestrictions)
opts = getOpts(WithSkipAnonymousUserRestrictions(true))
assert.True(opts.withSkipAnonymousUserRestrictions)
})
}
+1 -1
View File
@@ -221,7 +221,7 @@ func Test_ACLOutputFields(t *testing.T) {
grants = append(grants, grant)
}
acl := NewACL(grants...)
results := acl.Allowed(test.resource, test.action)
results := acl.Allowed(test.resource, test.action, AnonymousUserId, WithSkipAnonymousUserRestrictions(true))
assert.ElementsMatch(t, results.OutputFields.Fields(), test.fields)
assert.True(t, test.authorized == results.Authorized)
})
+4
View File
@@ -53,6 +53,10 @@ const (
AddHostSources Type = 42
SetHostSources Type = 43
RemoveHostSources Type = 44
// When adding new actions, be sure to update:
//
// * The Test_AnonRestrictions test
)
var Map = map[string]Type{
+1
View File
@@ -30,6 +30,7 @@ const (
// * The Grant.validateType function and test
// * The perms.topLevelTypes function
// * The scopes service collection actions for appropriate scopes
// * The Test_AnonRestrictions test
)
func (r Type) MarshalJSON() ([]byte, error) {