@@ -29,6 +29,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/iam/policy"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
)
|
||||
|
||||
// AuthorizationAttributer is implemented by entities that can provide
|
||||
@@ -86,17 +87,17 @@ type Authorizer struct {
|
||||
pg *pg.Client
|
||||
evaluator *policy.Evaluator
|
||||
policySet *PolicySet
|
||||
oauth2ScopeSet *ScopeSet
|
||||
oauth2ScopeSet *scopeset.ScopeSet
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewAuthorizer creates a new Authorizer instance.
|
||||
func NewAuthorizer(pgClient *pg.Client, logger *log.Logger) *Authorizer {
|
||||
func NewAuthorizer(pgClient *pg.Client, logger *log.Logger, scopeSet *scopeset.ScopeSet) *Authorizer {
|
||||
return &Authorizer{
|
||||
pg: pgClient,
|
||||
evaluator: policy.NewEvaluator(),
|
||||
policySet: NewPolicySet(),
|
||||
oauth2ScopeSet: NewScopeSet(),
|
||||
oauth2ScopeSet: scopeSet,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -106,20 +107,6 @@ func (a *Authorizer) RegisterPolicySet(ps *PolicySet) {
|
||||
a.policySet.Merge(ps)
|
||||
}
|
||||
|
||||
// RegisterScopes merges OAuth2 scope-to-action mappings into the authorizer.
|
||||
func (a *Authorizer) RegisterScopes(ss *ScopeSet) {
|
||||
a.oauth2ScopeSet.Merge(ss)
|
||||
}
|
||||
|
||||
// APIScopes returns OAuth2 API scopes advertised in discovery metadata.
|
||||
func (a *Authorizer) APIScopes() []coredata.OAuth2Scope {
|
||||
if a.oauth2ScopeSet == nil {
|
||||
return []coredata.OAuth2Scope{}
|
||||
}
|
||||
|
||||
return a.oauth2ScopeSet.APIScopes()
|
||||
}
|
||||
|
||||
func (a *Authorizer) checkOAuth2Scope(
|
||||
ctx context.Context,
|
||||
principal gid.GID,
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/policy"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
@@ -216,7 +217,7 @@ func TestAuthorizer_AuthorizeBatch(t *testing.T) {
|
||||
t.Run("empty input", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authorizer := iam.NewAuthorizer(nil, log.NewLogger(log.WithOutput(io.Discard)))
|
||||
authorizer := iam.NewAuthorizer(nil, log.NewLogger(log.WithOutput(io.Discard)), scopeset.New())
|
||||
|
||||
_, err := authorizer.AuthorizeBatch(
|
||||
context.Background(),
|
||||
@@ -644,7 +645,7 @@ func TestAuthorizer_AuthorizeMulti(t *testing.T) {
|
||||
t.Run("rejects empty items", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authorizer := iam.NewAuthorizer(nil, log.NewLogger(log.WithOutput(io.Discard)))
|
||||
authorizer := iam.NewAuthorizer(nil, log.NewLogger(log.WithOutput(io.Discard)), scopeset.New())
|
||||
|
||||
scope, decisions, err := authorizer.AuthorizeMulti(
|
||||
context.Background(),
|
||||
@@ -663,7 +664,7 @@ func TestAuthorizer_AuthorizeMulti(t *testing.T) {
|
||||
t.Run("rejects unsupported principal type", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authorizer := iam.NewAuthorizer(nil, log.NewLogger(log.WithOutput(io.Discard)))
|
||||
authorizer := iam.NewAuthorizer(nil, log.NewLogger(log.WithOutput(io.Discard)), scopeset.New())
|
||||
|
||||
scope, decisions, err := authorizer.AuthorizeMulti(
|
||||
context.Background(),
|
||||
@@ -782,7 +783,7 @@ func newTestAuthorizer(client *pg.Client, action string, allowResourceID *gid.GI
|
||||
}
|
||||
|
||||
func newTestAuthorizerWithStatements(client *pg.Client, statements ...policy.Statement) *iam.Authorizer {
|
||||
authorizer := iam.NewAuthorizer(client, log.NewLogger(log.WithOutput(io.Discard)))
|
||||
authorizer := iam.NewAuthorizer(client, log.NewLogger(log.WithOutput(io.Discard)), scopeset.New())
|
||||
authorizer.RegisterPolicySet(
|
||||
iam.NewPolicySet().AddRolePolicy(
|
||||
string(coredata.MembershipRoleOwner),
|
||||
@@ -794,7 +795,7 @@ func newTestAuthorizerWithStatements(client *pg.Client, statements ...policy.Sta
|
||||
}
|
||||
|
||||
func newTestAuthorizerWithIdentityScopedStatements(client *pg.Client, statements ...policy.Statement) *iam.Authorizer {
|
||||
authorizer := iam.NewAuthorizer(client, log.NewLogger(log.WithOutput(io.Discard)))
|
||||
authorizer := iam.NewAuthorizer(client, log.NewLogger(log.WithOutput(io.Discard)), scopeset.New())
|
||||
authorizer.RegisterPolicySet(
|
||||
iam.NewPolicySet().AddIdentityScopedPolicy(
|
||||
policy.NewPolicy("batch-authorize-identity-test", "Batch Authorize Identity Test", statements...),
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/policy"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
)
|
||||
|
||||
func TestAuthorizer_DecisionLogging(t *testing.T) {
|
||||
@@ -151,7 +152,7 @@ func newTestAuthorizerWithLogger(
|
||||
|
||||
statements = append(statements, extraStatements...)
|
||||
|
||||
authorizer := iam.NewAuthorizer(client, log.NewLogger(log.WithOutput(logOutput)))
|
||||
authorizer := iam.NewAuthorizer(client, log.NewLogger(log.WithOutput(logOutput)), scopeset.New())
|
||||
authorizer.RegisterPolicySet(
|
||||
iam.NewPolicySet().AddRolePolicy(
|
||||
string(coredata.MembershipRoleOwner),
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
)
|
||||
|
||||
func TestAuthorizer_checkOAuth2Scope(t *testing.T) {
|
||||
@@ -65,15 +66,14 @@ func TestAuthorizer_checkOAuth2Scope(t *testing.T) {
|
||||
t.Run("allows when registered scopes authorize the action", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := NewAuthorizer(nil, nil)
|
||||
a.RegisterScopes(
|
||||
CreateScopeSet(
|
||||
map[coredata.OAuth2Scope][]Action{
|
||||
scopeV1OrgRead: {action},
|
||||
},
|
||||
),
|
||||
scopeSet := scopeset.New().Register(
|
||||
map[coredata.OAuth2Scope][]string{
|
||||
scopeV1OrgRead: {action},
|
||||
},
|
||||
)
|
||||
|
||||
a := NewAuthorizer(nil, nil, scopeSet)
|
||||
|
||||
ctx := oauth2.ContextWithAccessToken(
|
||||
context.Background(),
|
||||
&coredata.OAuth2AccessToken{Scopes: coredata.OAuth2Scopes{scopeV1OrgRead}},
|
||||
|
||||
@@ -334,14 +334,6 @@ func (f *cimdFetcher) storeCache(clientIDURL string, doc *ClientMetadataDocument
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) ResolveClient(
|
||||
ctx context.Context,
|
||||
clientIDRaw string,
|
||||
redirectURI string,
|
||||
) (*coredata.OAuth2Client, error) {
|
||||
return s.resolveClient(ctx, nil, clientIDRaw, redirectURI)
|
||||
}
|
||||
|
||||
func (s *Service) resolveClient(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
@@ -415,7 +407,7 @@ func (s *Service) upsertCIMDClient(
|
||||
ScopeEmail,
|
||||
ScopeOfflineAccess,
|
||||
},
|
||||
s.apiScopes,
|
||||
s.scopeSet.APIScopes(),
|
||||
)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/net"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
"go.probo.inc/probo/pkg/uri"
|
||||
)
|
||||
|
||||
@@ -62,7 +63,7 @@ type (
|
||||
gc *GarbageCollector
|
||||
cimd *cimdFetcher
|
||||
cimdAllowedClientIDs []string
|
||||
apiScopes []coredata.OAuth2Scope
|
||||
scopeSet *scopeset.ScopeSet
|
||||
accessTokenDuration time.Duration
|
||||
refreshTokenDuration time.Duration
|
||||
authorizationCodeDuration time.Duration
|
||||
@@ -159,9 +160,9 @@ func WithDeviceCodeDuration(d time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAPIScopes(scopes []coredata.OAuth2Scope) Option {
|
||||
func WithScopeSet(scopeSet *scopeset.ScopeSet) Option {
|
||||
return func(s *Service) {
|
||||
s.apiScopes = scopes
|
||||
s.scopeSet = scopeSet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1438,6 +1439,8 @@ func (s *Service) Authorize(
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("X: %+v\n", client)
|
||||
|
||||
if !client.IsRedirectURIAllowed(req.RedirectURI) {
|
||||
return ErrInvalidRedirectURI
|
||||
}
|
||||
@@ -1736,7 +1739,7 @@ func (s *Service) AuthenticateClient(
|
||||
clientIDRaw string,
|
||||
clientSecret string,
|
||||
) (*coredata.OAuth2Client, error) {
|
||||
client, err := s.ResolveClient(ctx, clientIDRaw, "")
|
||||
client, err := s.resolveClient(ctx, nil, clientIDRaw, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -22,30 +22,21 @@ import (
|
||||
"go.probo.inc/probo/pkg/agentrun"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
)
|
||||
|
||||
func allRegisteredOAuth2ScopeSets() *iam.ScopeSet {
|
||||
return iam.NewScopeSet().
|
||||
Merge(iam.IAMOAuth2ScopeSet()).
|
||||
Merge(probo.OAuth2ScopeSet()).
|
||||
Merge(accessreview.OAuth2ScopeSet()).
|
||||
Merge(agentrun.OAuth2ScopeSet())
|
||||
}
|
||||
|
||||
func registerAllOAuth2ScopeSets(authorizer *iam.Authorizer) {
|
||||
authorizer.RegisterScopes(iam.IAMOAuth2ScopeSet())
|
||||
authorizer.RegisterScopes(probo.OAuth2ScopeSet())
|
||||
authorizer.RegisterScopes(accessreview.OAuth2ScopeSet())
|
||||
authorizer.RegisterScopes(agentrun.OAuth2ScopeSet())
|
||||
func allRegisteredOAuth2ScopeSets() *scopeset.ScopeSet {
|
||||
return scopeset.New().
|
||||
Register(iam.IAMOAuth2ScopeMappings).
|
||||
Register(probo.OAuth2ScopeMappings).
|
||||
Register(accessreview.OAuth2ScopeMappings).
|
||||
Register(agentrun.OAuth2ScopeMappings)
|
||||
}
|
||||
|
||||
func TestRegisteredOAuth2ScopeSets_OrganizationRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authorizer := iam.NewAuthorizer(nil, nil)
|
||||
registerAllOAuth2ScopeSets(authorizer)
|
||||
|
||||
scopeSet := allRegisteredOAuth2ScopeSets()
|
||||
tokenScopes := coredata.OAuth2Scopes{probo.ScopeV1OrgRead}
|
||||
|
||||
@@ -57,9 +48,6 @@ func TestRegisteredOAuth2ScopeSets_OrganizationRead(t *testing.T) {
|
||||
func TestRegisteredOAuth2ScopeSets_UnmappedActionDenies(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authorizer := iam.NewAuthorizer(nil, nil)
|
||||
registerAllOAuth2ScopeSets(authorizer)
|
||||
|
||||
scopeSet := allRegisteredOAuth2ScopeSets()
|
||||
tokenScopes := coredata.OAuth2Scopes{
|
||||
probo.ScopeV1OrgRead,
|
||||
|
||||
@@ -21,69 +21,64 @@ const (
|
||||
ScopeV1IAM coredata.OAuth2Scope = "v1:iam"
|
||||
)
|
||||
|
||||
// IAMOAuth2ScopeSet returns OAuth2 scope mappings for IAM actions.
|
||||
func IAMOAuth2ScopeSet() *ScopeSet {
|
||||
return CreateScopeSet(
|
||||
map[coredata.OAuth2Scope][]Action{
|
||||
ScopeV1IAMRead: {
|
||||
ActionOrganizationGet,
|
||||
ActionOrganizationList,
|
||||
ActionIdentityGet,
|
||||
ActionSessionList,
|
||||
ActionSessionGet,
|
||||
ActionInvitationList,
|
||||
ActionInvitationGet,
|
||||
ActionMembershipGet,
|
||||
ActionMembershipList,
|
||||
ActionMembershipProfileGet,
|
||||
ActionMembershipProfileList,
|
||||
ActionPersonalAPIKeyGet,
|
||||
ActionPersonalAPIKeyList,
|
||||
ActionSAMLConfigurationGet,
|
||||
ActionSAMLConfigurationList,
|
||||
ActionSCIMConfigurationGet,
|
||||
ActionSCIMEventList,
|
||||
ActionSCIMEventGet,
|
||||
ActionSCIMBridgeGet,
|
||||
ActionOAuth2ConsentGet,
|
||||
ActionAuditLogEntryGet,
|
||||
ActionAuditLogEntryList,
|
||||
ActionOAuth2AccessTokenGet,
|
||||
ActionOAuth2AccessTokenList,
|
||||
},
|
||||
ScopeV1IAM: {
|
||||
ActionOrganizationCreate,
|
||||
ActionOrganizationUpdate,
|
||||
ActionOrganizationDelete,
|
||||
ActionIdentityUpdate,
|
||||
ActionIdentityDelete,
|
||||
ActionSessionRevoke,
|
||||
ActionSessionRevokeAll,
|
||||
ActionInvitationCreate,
|
||||
ActionInvitationAccept,
|
||||
ActionInvitationDelete,
|
||||
ActionMembershipUpdate,
|
||||
ActionMembershipDelete,
|
||||
ActionMembershipRoleSetOwner,
|
||||
ActionMembershipProfileCreate,
|
||||
ActionMembershipProfileUpdate,
|
||||
ActionMembershipProfileDelete,
|
||||
ActionMembershipProfileActivate,
|
||||
ActionMembershipProfileDeactivate,
|
||||
ActionPersonalAPIKeyCreate,
|
||||
ActionPersonalAPIKeyUpdate,
|
||||
ActionPersonalAPIKeyDelete,
|
||||
ActionSAMLConfigurationCreate,
|
||||
ActionSAMLConfigurationUpdate,
|
||||
ActionSAMLConfigurationDelete,
|
||||
ActionSCIMConfigurationCreate,
|
||||
ActionSCIMConfigurationUpdate,
|
||||
ActionSCIMConfigurationDelete,
|
||||
ActionSCIMBridgeCreate,
|
||||
ActionSCIMBridgeUpdate,
|
||||
ActionSCIMBridgeDelete,
|
||||
ActionOAuth2ConsentApprove,
|
||||
},
|
||||
},
|
||||
)
|
||||
var IAMOAuth2ScopeMappings = map[coredata.OAuth2Scope][]string{
|
||||
ScopeV1IAMRead: {
|
||||
ActionOrganizationGet,
|
||||
ActionOrganizationList,
|
||||
ActionIdentityGet,
|
||||
ActionSessionList,
|
||||
ActionSessionGet,
|
||||
ActionInvitationList,
|
||||
ActionInvitationGet,
|
||||
ActionMembershipGet,
|
||||
ActionMembershipList,
|
||||
ActionMembershipProfileGet,
|
||||
ActionMembershipProfileList,
|
||||
ActionPersonalAPIKeyGet,
|
||||
ActionPersonalAPIKeyList,
|
||||
ActionSAMLConfigurationGet,
|
||||
ActionSAMLConfigurationList,
|
||||
ActionSCIMConfigurationGet,
|
||||
ActionSCIMEventList,
|
||||
ActionSCIMEventGet,
|
||||
ActionSCIMBridgeGet,
|
||||
ActionOAuth2ConsentGet,
|
||||
ActionAuditLogEntryGet,
|
||||
ActionAuditLogEntryList,
|
||||
ActionOAuth2AccessTokenGet,
|
||||
ActionOAuth2AccessTokenList,
|
||||
},
|
||||
ScopeV1IAM: {
|
||||
ActionOrganizationCreate,
|
||||
ActionOrganizationUpdate,
|
||||
ActionOrganizationDelete,
|
||||
ActionIdentityUpdate,
|
||||
ActionIdentityDelete,
|
||||
ActionSessionRevoke,
|
||||
ActionSessionRevokeAll,
|
||||
ActionInvitationCreate,
|
||||
ActionInvitationAccept,
|
||||
ActionInvitationDelete,
|
||||
ActionMembershipUpdate,
|
||||
ActionMembershipDelete,
|
||||
ActionMembershipRoleSetOwner,
|
||||
ActionMembershipProfileCreate,
|
||||
ActionMembershipProfileUpdate,
|
||||
ActionMembershipProfileDelete,
|
||||
ActionMembershipProfileActivate,
|
||||
ActionMembershipProfileDeactivate,
|
||||
ActionPersonalAPIKeyCreate,
|
||||
ActionPersonalAPIKeyUpdate,
|
||||
ActionPersonalAPIKeyDelete,
|
||||
ActionSAMLConfigurationCreate,
|
||||
ActionSAMLConfigurationUpdate,
|
||||
ActionSAMLConfigurationDelete,
|
||||
ActionSCIMConfigurationCreate,
|
||||
ActionSCIMConfigurationUpdate,
|
||||
ActionSCIMConfigurationDelete,
|
||||
ActionSCIMBridgeCreate,
|
||||
ActionSCIMBridgeUpdate,
|
||||
ActionSCIMBridgeDelete,
|
||||
ActionOAuth2ConsentApprove,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,65 +20,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
)
|
||||
|
||||
func TestScopeSet_Allows(t *testing.T) {
|
||||
func TestAuthorizer_UsesOAuth2ScopeSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const scopeV1OrgRead = coredata.OAuth2Scope("v1:org:read")
|
||||
|
||||
scopeSet := CreateScopeSet(
|
||||
map[coredata.OAuth2Scope][]Action{
|
||||
scopeSet := scopeset.New().Register(
|
||||
map[coredata.OAuth2Scope][]string{
|
||||
scopeV1OrgRead: {"core:organization:get"},
|
||||
},
|
||||
)
|
||||
|
||||
tokenScopes := coredata.OAuth2Scopes{scopeV1OrgRead}
|
||||
|
||||
assert.True(t, scopeSet.Allows(tokenScopes, "core:organization:get"))
|
||||
assert.False(t, scopeSet.Allows(tokenScopes, "core:organization:update"))
|
||||
}
|
||||
|
||||
func TestScopeSet_Merge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const scopeV1OrgRead = coredata.OAuth2Scope("v1:org:read")
|
||||
|
||||
scopeSet := NewScopeSet().
|
||||
Merge(
|
||||
CreateScopeSet(
|
||||
map[coredata.OAuth2Scope][]Action{
|
||||
scopeV1OrgRead: {"core:organization:get"},
|
||||
},
|
||||
),
|
||||
).
|
||||
Merge(
|
||||
CreateScopeSet(
|
||||
map[coredata.OAuth2Scope][]Action{
|
||||
scopeV1OrgRead: {"core:organization-context:get"},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
tokenScopes := coredata.OAuth2Scopes{scopeV1OrgRead}
|
||||
|
||||
assert.True(t, scopeSet.Allows(tokenScopes, "core:organization:get"))
|
||||
assert.True(t, scopeSet.Allows(tokenScopes, "core:organization-context:get"))
|
||||
}
|
||||
|
||||
func TestAuthorizer_RegisterScopes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const scopeV1OrgRead = coredata.OAuth2Scope("v1:org:read")
|
||||
|
||||
authorizer := NewAuthorizer(nil, nil)
|
||||
authorizer.RegisterScopes(
|
||||
CreateScopeSet(
|
||||
map[coredata.OAuth2Scope][]Action{
|
||||
scopeV1OrgRead: {"core:organization:get"},
|
||||
},
|
||||
),
|
||||
)
|
||||
authorizer := NewAuthorizer(nil, nil, scopeSet)
|
||||
|
||||
require.NotNil(t, authorizer.oauth2ScopeSet)
|
||||
|
||||
|
||||
@@ -12,34 +12,32 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package iam
|
||||
package scopeset
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
// ScopeSet holds OAuth2 scope to IAM action mappings. Services create their
|
||||
// own ScopeSet and register it on the Authorizer at composition time.
|
||||
type ScopeSet struct {
|
||||
scopeActions map[coredata.OAuth2Scope][]Action
|
||||
actionScopes map[Action][]coredata.OAuth2Scope
|
||||
mu sync.RWMutex
|
||||
scopeActions map[coredata.OAuth2Scope][]string
|
||||
actionScopes map[string][]coredata.OAuth2Scope
|
||||
}
|
||||
|
||||
// NewScopeSet creates an empty ScopeSet.
|
||||
func NewScopeSet() *ScopeSet {
|
||||
func New() *ScopeSet {
|
||||
return &ScopeSet{
|
||||
scopeActions: make(map[coredata.OAuth2Scope][]Action),
|
||||
scopeActions: make(map[coredata.OAuth2Scope][]string),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateScopeSet creates a ScopeSet from scope-to-action mappings. Entries with
|
||||
// no actions are skipped.
|
||||
func CreateScopeSet(mappings map[coredata.OAuth2Scope][]Action) *ScopeSet {
|
||||
s := NewScopeSet()
|
||||
func (s *ScopeSet) Register(mappings map[coredata.OAuth2Scope][]string) *ScopeSet {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for scope, actions := range mappings {
|
||||
if len(actions) == 0 {
|
||||
@@ -54,24 +52,17 @@ func CreateScopeSet(mappings map[coredata.OAuth2Scope][]Action) *ScopeSet {
|
||||
return s
|
||||
}
|
||||
|
||||
// Merge combines another ScopeSet into this one.
|
||||
func (s *ScopeSet) Merge(other *ScopeSet) *ScopeSet {
|
||||
for scope, actions := range other.scopeActions {
|
||||
s.scopeActions[scope] = append(s.scopeActions[scope], actions...)
|
||||
}
|
||||
|
||||
s.rebuildActionScopes()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// APIScopes returns every registered OAuth2 API scope in this set.
|
||||
func (s *ScopeSet) APIScopes() []coredata.OAuth2Scope {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return sortedScopes(slices.Collect(maps.Keys(s.scopeActions)))
|
||||
}
|
||||
|
||||
// Allows reports whether tokenScopes authorize action.
|
||||
func (s *ScopeSet) Allows(tokenScopes coredata.OAuth2Scopes, action Action) bool {
|
||||
func (s *ScopeSet) Allows(tokenScopes coredata.OAuth2Scopes, action string) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
grantingScopes, ok := s.actionScopes[action]
|
||||
if !ok {
|
||||
return false
|
||||
@@ -81,7 +72,7 @@ func (s *ScopeSet) Allows(tokenScopes coredata.OAuth2Scopes, action Action) bool
|
||||
}
|
||||
|
||||
func (s *ScopeSet) rebuildActionScopes() {
|
||||
actionScopes := make(map[Action][]coredata.OAuth2Scope, len(s.scopeActions)*4)
|
||||
actionScopes := make(map[string][]coredata.OAuth2Scope, len(s.scopeActions)*4)
|
||||
|
||||
for scope, actions := range s.scopeActions {
|
||||
for _, action := range actions {
|
||||
@@ -94,9 +85,12 @@ func (s *ScopeSet) rebuildActionScopes() {
|
||||
|
||||
func sortedScopes(scopes []coredata.OAuth2Scope) []coredata.OAuth2Scope {
|
||||
sorted := slices.Clone(scopes)
|
||||
slices.SortFunc(sorted, func(a, b coredata.OAuth2Scope) int {
|
||||
return cmp.Compare(string(a), string(b))
|
||||
})
|
||||
slices.SortFunc(
|
||||
sorted,
|
||||
func(a, b coredata.OAuth2Scope) int {
|
||||
return cmp.Compare(string(a), string(b))
|
||||
},
|
||||
)
|
||||
|
||||
return sorted
|
||||
}
|
||||
63
pkg/iam/scopeset/scopeset_test.go
Normal file
63
pkg/iam/scopeset/scopeset_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package scopeset_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
)
|
||||
|
||||
func TestScopeSet_Allows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const scopeV1OrgRead = coredata.OAuth2Scope("v1:org:read")
|
||||
|
||||
scopeSet := scopeset.New().Register(
|
||||
map[coredata.OAuth2Scope][]string{
|
||||
scopeV1OrgRead: {"core:organization:get"},
|
||||
},
|
||||
)
|
||||
|
||||
tokenScopes := coredata.OAuth2Scopes{scopeV1OrgRead}
|
||||
|
||||
assert.True(t, scopeSet.Allows(tokenScopes, "core:organization:get"))
|
||||
assert.False(t, scopeSet.Allows(tokenScopes, "core:organization:update"))
|
||||
}
|
||||
|
||||
func TestScopeSet_Register(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const scopeV1OrgRead = coredata.OAuth2Scope("v1:org:read")
|
||||
|
||||
scopeSet := scopeset.New().
|
||||
Register(
|
||||
map[coredata.OAuth2Scope][]string{
|
||||
scopeV1OrgRead: {"core:organization:get"},
|
||||
},
|
||||
).
|
||||
Register(
|
||||
map[coredata.OAuth2Scope][]string{
|
||||
scopeV1OrgRead: {"core:organization-context:get"},
|
||||
},
|
||||
)
|
||||
|
||||
tokenScopes := coredata.OAuth2Scopes{scopeV1OrgRead}
|
||||
|
||||
assert.True(t, scopeSet.Allows(tokenScopes, "core:organization:get"))
|
||||
assert.True(t, scopeSet.Allows(tokenScopes, "core:organization-context:get"))
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/iam/oidc"
|
||||
"go.probo.inc/probo/pkg/iam/saml"
|
||||
"go.probo.inc/probo/pkg/iam/scim"
|
||||
"go.probo.inc/probo/pkg/iam/scopeset"
|
||||
"go.probo.inc/probo/pkg/uri"
|
||||
)
|
||||
|
||||
@@ -69,6 +70,7 @@ type (
|
||||
APIKeyService *APIKeyService
|
||||
OAuth2ServerService *oauth2.Service
|
||||
Authorizer *Authorizer
|
||||
OAuth2ScopeSet *scopeset.ScopeSet
|
||||
|
||||
samlDomainVerifier *SAMLDomainVerifier
|
||||
}
|
||||
@@ -157,12 +159,15 @@ func NewService(
|
||||
svc.AuthService = NewAuthService(svc)
|
||||
svc.APIKeyService = NewAPIKeyService(svc)
|
||||
|
||||
svc.OAuth2ScopeSet = scopeset.New()
|
||||
svc.OAuth2ScopeSet.Register(IAMOAuth2ScopeMappings)
|
||||
|
||||
svc.Authorizer = NewAuthorizer(
|
||||
pgClient,
|
||||
cfg.Logger.Named("authorizer"),
|
||||
svc.OAuth2ScopeSet,
|
||||
)
|
||||
svc.Authorizer.RegisterPolicySet(IAMPolicySet())
|
||||
svc.Authorizer.RegisterScopes(IAMOAuth2ScopeSet())
|
||||
|
||||
samlService, err := saml.NewService(svc.pg, svc.baseURL, svc.certificate, svc.privateKey, cfg.Logger)
|
||||
if err != nil {
|
||||
@@ -201,7 +206,7 @@ func NewService(
|
||||
uri.URI(cfg.BaseURL.String()),
|
||||
cfg.Logger.Named("oauth2"),
|
||||
append(
|
||||
[]oauth2.Option{oauth2.WithAPIScopes(svc.Authorizer.APIScopes())},
|
||||
[]oauth2.Option{oauth2.WithScopeSet(svc.OAuth2ScopeSet)},
|
||||
cfg.OAuth2ServerOptions...,
|
||||
)...,
|
||||
)
|
||||
@@ -219,12 +224,12 @@ func NewService(
|
||||
|
||||
// OAuth2ServerMetadata returns the OIDC discovery document.
|
||||
func (s *Service) OAuth2ServerMetadata(endpoints oauth2.Endpoints) *oauth2.ServerMetadata {
|
||||
return oauth2.NewMetadata(uri.URI(s.baseURL), endpoints, s.Authorizer.APIScopes())
|
||||
return oauth2.NewMetadata(uri.URI(s.baseURL), endpoints, s.OAuth2ScopeSet.APIScopes())
|
||||
}
|
||||
|
||||
// OAuth2ProtectedResourceMetadata returns the RFC 9728 protected resource metadata document.
|
||||
func (s *Service) OAuth2ProtectedResourceMetadata(resource uri.URI) *oauth2.ProtectedResourceMetadata {
|
||||
return oauth2.NewProtectedResourceMetadata(resource, resource, s.Authorizer.APIScopes())
|
||||
return oauth2.NewProtectedResourceMetadata(resource, resource, s.OAuth2ScopeSet.APIScopes())
|
||||
}
|
||||
|
||||
func (s *Service) IsSignUpEnabled() bool {
|
||||
|
||||
Reference in New Issue
Block a user