Restrict anonymous user capabilities (#2156)
This commit is contained in:
1 parent
3521c21cd5
commit
d6362f9e5e
11 files changed
+266
-63
No files matched your search
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user