Add OAuth2 API scope registration and enforcement

Register v1 API scopes in coredata, advertise them in OIDC discovery
and protected-resource metadata, show them on the consent screen, and
enforce scope-to-action mapping in the IAM Authorizer before policy
evaluation.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-06-15 17:33:15 +02:00
parent 25151fa089
commit 3ebb221a9b
56 changed files with 1918 additions and 290 deletions

View File

@@ -27,6 +27,7 @@ import (
"go.gearno.de/kit/pg"
"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/policy"
)
@@ -82,19 +83,21 @@ type AuthorizeMultiParams struct {
// Authorizer evaluates authorization requests against registered policies.
type Authorizer struct {
pg *pg.Client
evaluator *policy.Evaluator
policySet *PolicySet
logger *log.Logger
pg *pg.Client
evaluator *policy.Evaluator
policySet *PolicySet
oauth2ScopeSet *ScopeSet
logger *log.Logger
}
// NewAuthorizer creates a new Authorizer instance.
func NewAuthorizer(pgClient *pg.Client, logger *log.Logger) *Authorizer {
return &Authorizer{
pg: pgClient,
evaluator: policy.NewEvaluator(),
policySet: NewPolicySet(),
logger: logger,
pg: pgClient,
evaluator: policy.NewEvaluator(),
policySet: NewPolicySet(),
oauth2ScopeSet: NewScopeSet(),
logger: logger,
}
}
@@ -103,6 +106,37 @@ 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,
action Action,
) error {
accessToken, ok := oauth2.AccessTokenFromContext(ctx)
if !ok {
return nil
}
if a.oauth2ScopeSet == nil || !a.oauth2ScopeSet.Allows(accessToken.Scopes, action) {
return NewInsufficientOAuth2ScopeError(principal, action)
}
return nil
}
// Authorize checks if the principal is allowed to perform the action on the resource.
func (a *Authorizer) Authorize(ctx context.Context, params AuthorizeParams) (*coredata.Scope, error) {
return a.AuthorizeBatch(
@@ -459,6 +493,22 @@ func (a *Authorizer) evaluateMultiInTx(
continue
}
if err := a.checkOAuth2Scope(ctx, params.Principal, item.Action); err != nil {
decisions[i] = err
a.logDecision(
ctx,
DecisionRecord{
Effect: effectError,
Action: item.Action,
ResourceID: item.Resource,
Principal: params.Principal,
Reason: err.Error(),
},
)
continue
}
req := policy.AuthorizationRequest{
Principal: params.Principal,
Resource: item.Resource,