Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5a425af

Browse files
committed
feat: Add detailed scope authorization metrics
This change introduces new Prometheus metrics to provide detailed insightsinto authorization decisions, particularly for API key scopes. Thesemetrics help administrators understand why a request was allowed ordenied by breaking down the outcome.The new metrics are:- `coderd_authz_scope_enforcement_total`: Classifies each authorization request by its outcome (e.g., scope_allow, scope_deny, allow_list_deny) and resource type.- `coderd_authz_scope_enforcement_duration_seconds`: Measures the latency of scope enforcement decisions.- `coderd_authz_scope_allowlist_miss_total`: Tracks requests denied specifically due to a resource not being in a scope's allow-list.To implement this efficiently, a new `scope_metrics` rule was added tothe Rego policy. This allows the authorizer to gather detailed outcomeinformation in a single evaluation, avoiding redundant computations.The documentation for Prometheus has been updated to include details andexample queries for the new metrics.
1 parentf244193 commit5a425af

File tree

5 files changed

+397
-27
lines changed

5 files changed

+397
-27
lines changed

‎coderd/rbac/authz.go‎

Lines changed: 214 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ type AuthCall struct {
3333
ObjectObject
3434
}
3535

36+
typescopeDecisionstruct {
37+
allowbool
38+
scopeAllowbool
39+
scopeAllowListbool
40+
roleAllowbool
41+
aclAllowbool
42+
metricsErrerror
43+
}
44+
3645
// hashAuthorizeCall guarantees a unique hash for a given auth call.
3746
// If two hashes are equal, then the result of a given authorize() call
3847
// will be the same.
@@ -255,11 +264,15 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, a
255264

256265
// RegoAuthorizer will use a prepared rego query for performing authorize()
257266
typeRegoAuthorizerstruct {
258-
query rego.PreparedEvalQuery
259-
partialQuery rego.PreparedPartialQuery
267+
query rego.PreparedEvalQuery
268+
partialQuery rego.PreparedPartialQuery
269+
scopeMetricsQuery rego.PreparedEvalQuery
260270

261-
authorizeHist*prometheus.HistogramVec
262-
prepareHist prometheus.Histogram
271+
authorizeHist*prometheus.HistogramVec
272+
prepareHist prometheus.Histogram
273+
scopeDecisionCounter*prometheus.CounterVec
274+
scopeDecisionDuration*prometheus.HistogramVec
275+
scopeAllowListMisses*prometheus.CounterVec
263276

264277
// strict checking also verifies the inputs to the authorizer. Making sure
265278
// the action make sense for the input object.
@@ -272,10 +285,11 @@ var (
272285
// Load the policy from policy.rego in this directory.
273286
//
274287
//go:embed policy.rego
275-
regoPolicystring
276-
queryOnce sync.Once
277-
query rego.PreparedEvalQuery
278-
partialQuery rego.PreparedPartialQuery
288+
regoPolicystring
289+
queryOnce sync.Once
290+
query rego.PreparedEvalQuery
291+
partialQuery rego.PreparedPartialQuery
292+
scopeMetricsQuery rego.PreparedEvalQuery
279293
)
280294

281295
// NewCachingAuthorizer returns a new RegoAuthorizer that supports context based
@@ -317,6 +331,14 @@ func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer {
317331
iferr!=nil {
318332
panic(xerrors.Errorf("compile partial rego: %w",err))
319333
}
334+
335+
scopeMetricsQuery,err=rego.New(
336+
rego.Query("data.authz.scope_metrics"),
337+
rego.Module("policy.rego",regoPolicy),
338+
).PrepareForEval(context.Background())
339+
iferr!=nil {
340+
panic(xerrors.Errorf("compile scope metrics rego: %w",err))
341+
}
320342
})
321343

322344
// Register metrics to prometheus.
@@ -357,12 +379,38 @@ func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer {
357379
Buckets:buckets,
358380
})
359381

360-
return&RegoAuthorizer{
361-
query:query,
362-
partialQuery:partialQuery,
382+
scopeDecisionCounter:=factory.NewCounterVec(prometheus.CounterOpts{
383+
Namespace:"coderd",
384+
Subsystem:"authz",
385+
Name:"scope_enforcement_total",
386+
Help:"Scope evaluation outcomes keyed by decision, scope, and resource.",
387+
}, []string{"decision","scope","resource","outcome"})
363388

364-
authorizeHist:authorizeHistogram,
365-
prepareHist:prepareHistogram,
389+
scopeDecisionDuration:=factory.NewHistogramVec(prometheus.HistogramOpts{
390+
Namespace:"coderd",
391+
Subsystem:"authz",
392+
Name:"scope_enforcement_duration_seconds",
393+
Help:"Duration of scope enforcement decisions in seconds.",
394+
Buckets:buckets,
395+
}, []string{"decision","scope","resource","outcome"})
396+
397+
scopeAllowListMisses:=factory.NewCounterVec(prometheus.CounterOpts{
398+
Namespace:"coderd",
399+
Subsystem:"authz",
400+
Name:"scope_allowlist_miss_total",
401+
Help:"Requests denied because a scope allow-list did not include the resource.",
402+
}, []string{"scope","resource"})
403+
404+
return&RegoAuthorizer{
405+
query:query,
406+
partialQuery:partialQuery,
407+
scopeMetricsQuery:scopeMetricsQuery,
408+
409+
authorizeHist:authorizeHistogram,
410+
prepareHist:prepareHistogram,
411+
scopeDecisionCounter:scopeDecisionCounter,
412+
scopeDecisionDuration:scopeDecisionDuration,
413+
scopeAllowListMisses:scopeAllowListMisses,
366414
}
367415
}
368416

@@ -396,11 +444,12 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action p
396444
)
397445
deferspan.End()
398446

399-
err:=a.authorize(ctx,subject,action,object)
447+
decision,err:=a.authorize(ctx,subject,action,object)
400448
authorized:=err==nil
401449
span.SetAttributes(attribute.Bool("authorized",authorized))
402450

403451
dur:=time.Since(start)
452+
a.observeScopeMetrics(decision,subject.Scope,object,dur)
404453
if!authorized {
405454
a.authorizeHist.WithLabelValues("false").Observe(dur.Seconds())
406455
returnerr
@@ -414,38 +463,182 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action p
414463
// It is a different function so the exported one can add tracing + metrics.
415464
// That code tends to clutter up the actual logic, so it's separated out.
416465
// nolint:revive
417-
func (aRegoAuthorizer)authorize(ctx context.Context,subjectSubject,action policy.Action,objectObject)error {
466+
func (aRegoAuthorizer)authorize(ctx context.Context,subjectSubject,action policy.Action,objectObject) (scopeDecision,error) {
467+
decision:=scopeDecision{}
418468
ifsubject.Roles==nil {
419-
returnxerrors.Errorf("subject must have roles")
469+
returndecision,xerrors.Errorf("subject must have roles")
420470
}
421471
ifsubject.Scope==nil {
422-
returnxerrors.Errorf("subject must have a scope")
472+
returndecision,xerrors.Errorf("subject must have a scope")
423473
}
424474

425475
// The caller should use either 1 or the other (or none).
426476
// Using "AnyOrgOwner" and an OrgID is a contradiction.
427477
// An empty uuid or a nil uuid means "no org owner".
428478
ifobject.AnyOrgOwner&&!(object.OrgID==""||object.OrgID=="00000000-0000-0000-0000-000000000000") {
429-
returnxerrors.Errorf("object cannot have 'any_org' and an 'org_id' specified, values are mutually exclusive")
479+
returndecision,xerrors.Errorf("object cannot have 'any_org' and an 'org_id' specified, values are mutually exclusive")
430480
}
431481

432482
astV,err:=regoInputValue(subject,action,object)
433483
iferr!=nil {
434-
returnxerrors.Errorf("convert input to value: %w",err)
484+
returndecision,xerrors.Errorf("convert input to value: %w",err)
485+
}
486+
487+
metricsResults,metricsErr:=a.scopeMetricsQuery.Eval(ctx,rego.EvalParsedInput(astV))
488+
ifmetricsErr!=nil {
489+
decision.metricsErr=correctCancelError(metricsErr)
435490
}
436491

437492
results,err:=a.query.Eval(ctx,rego.EvalParsedInput(astV))
438493
iferr!=nil {
439494
err=correctCancelError(err)
440-
returnxerrors.Errorf("evaluate rego: %w",err)
495+
returndecision,xerrors.Errorf("evaluate rego: %w",err)
496+
}
497+
498+
decision.allow=results.Allowed()
499+
ifdecision.metricsErr==nil {
500+
iferr:=applyScopeMetrics(&decision,metricsResults);err!=nil {
501+
decision.metricsErr=err
502+
}
441503
}
442504

443505
if!results.Allowed() {
444-
returnForbiddenWithInternal(xerrors.Errorf("policy disallows request"),subject,action,object,results)
506+
returndecision,ForbiddenWithInternal(xerrors.Errorf("policy disallows request"),subject,action,object,results)
507+
}
508+
returndecision,nil
509+
}
510+
511+
funcapplyScopeMetrics(decision*scopeDecision,results rego.ResultSet)error {
512+
iflen(results)==0 {
513+
returnxerrors.Errorf("scope metrics returned no results")
514+
}
515+
iflen(results[0].Expressions)==0 {
516+
returnxerrors.Errorf("scope metrics returned no expressions")
517+
}
518+
metricsMap,ok:=results[0].Expressions[0].Value.(map[string]interface{})
519+
if!ok {
520+
returnxerrors.Errorf("scope metrics expression unexpected type %T",results[0].Expressions[0].Value)
521+
}
522+
523+
boolVal:=func(keystring) (bool,error) {
524+
v,ok:=metricsMap[key]
525+
if!ok {
526+
returnfalse,xerrors.Errorf("scope metrics missing key %q",key)
527+
}
528+
val,ok:=v.(bool)
529+
if!ok {
530+
returnfalse,xerrors.Errorf("scope metrics key %q has unexpected type %T",key,v)
531+
}
532+
returnval,nil
533+
}
534+
535+
varerrerror
536+
ifdecision.allow,err=boolVal("allow");err!=nil {
537+
returnerr
538+
}
539+
ifdecision.scopeAllow,err=boolVal("scope_allow");err!=nil {
540+
returnerr
541+
}
542+
ifdecision.scopeAllowList,err=boolVal("scope_allow_list");err!=nil {
543+
returnerr
544+
}
545+
ifdecision.roleAllow,err=boolVal("role_allow");err!=nil {
546+
returnerr
547+
}
548+
ifdecision.aclAllow,err=boolVal("acl_allow");err!=nil {
549+
returnerr
445550
}
446551
returnnil
447552
}
448553

554+
varscopeLabelReplacer=strings.NewReplacer(
555+
" ","_",
556+
":","_",
557+
"*","all",
558+
"-","_",
559+
".","_",
560+
"[","_",
561+
"]","_",
562+
"+","_",
563+
"=","_",
564+
"/","_",
565+
"|","_",
566+
)
567+
568+
funcsanitizeScopeLabel(scopeExpandableScope)string {
569+
ifscope==nil {
570+
return"none"
571+
}
572+
identifier:=scope.Name()
573+
raw:=identifier.Name
574+
ifraw=="" {
575+
raw=identifier.String()
576+
}
577+
ifraw=="" {
578+
return"unknown"
579+
}
580+
ifstrings.HasPrefix(raw,"scopes[")&&strings.HasSuffix(raw,"]") {
581+
inner:=strings.TrimSuffix(strings.TrimPrefix(raw,"scopes["),"]")
582+
ifinner=="" {
583+
return"multi(0)"
584+
}
585+
count:=strings.Count(inner,"+")+1
586+
returnfmt.Sprintf("multi(%d)",count)
587+
}
588+
sanitized:=strings.ToLower(scopeLabelReplacer.Replace(strings.TrimSpace(raw)))
589+
ifsanitized=="" {
590+
sanitized="unknown"
591+
}
592+
iflen(sanitized)>63 {
593+
sanitized=sanitized[:63]
594+
}
595+
returnsanitized
596+
}
597+
598+
funcclassifyScopeDecision(decisionscopeDecision)string {
599+
ifdecision.metricsErr!=nil {
600+
return"unknown"
601+
}
602+
ifdecision.scopeAllow {
603+
ifdecision.allow {
604+
return"scope_allow"
605+
}
606+
if!decision.roleAllow&&!decision.aclAllow {
607+
return"role_deny"
608+
}
609+
return"other"
610+
}
611+
if!decision.scopeAllowList {
612+
return"allow_list_deny"
613+
}
614+
return"scope_deny"
615+
}
616+
617+
func (aRegoAuthorizer)observeScopeMetrics(decisionscopeDecision,scopeExpandableScope,objectObject,dur time.Duration) {
618+
ifa.scopeDecisionCounter==nil||a.scopeDecisionDuration==nil||a.scopeAllowListMisses==nil {
619+
return
620+
}
621+
ifdecision.metricsErr!=nil {
622+
return
623+
}
624+
scopeLabel:=sanitizeScopeLabel(scope)
625+
resource:=object.Type
626+
ifresource=="" {
627+
resource="unknown"
628+
}
629+
outcome:="deny"
630+
ifdecision.allow {
631+
outcome="allow"
632+
}
633+
decisionLabel:=classifyScopeDecision(decision)
634+
635+
a.scopeDecisionCounter.WithLabelValues(decisionLabel,scopeLabel,resource,outcome).Inc()
636+
a.scopeDecisionDuration.WithLabelValues(decisionLabel,scopeLabel,resource,outcome).Observe(dur.Seconds())
637+
if!decision.scopeAllowList {
638+
a.scopeAllowListMisses.WithLabelValues(scopeLabel,resource).Inc()
639+
}
640+
}
641+
449642
// Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
450643
// This will vastly speed up performance if batch authorization on the same type of objects is needed.
451644
func (aRegoAuthorizer)Prepare(ctx context.Context,subjectSubject,action policy.Action,objectTypestring) (PreparedAuthorized,error) {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp