Skip to content

Commit ce51435

Browse files
authored
chore: Merge some rbac files, move some functions around (coder#6916)
* chore: Merge some rbac files, move some functions around * Fix imports
1 parent 27e17ff commit ce51435

File tree

8 files changed

+203
-223
lines changed

8 files changed

+203
-223
lines changed

coderd/rbac/action.go

Lines changed: 0 additions & 16 deletions
This file was deleted.

coderd/rbac/authz.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"sync"
77
"time"
88

9+
"github.com/open-policy-agent/opa/ast"
910
"github.com/open-policy-agent/opa/rego"
1011
"github.com/prometheus/client_golang/prometheus"
1112
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -18,6 +19,21 @@ import (
1819
"github.com/coder/coder/coderd/util/slice"
1920
)
2021

22+
// Action represents the allowed actions to be done on an object.
23+
type Action string
24+
25+
const (
26+
ActionCreate Action = "create"
27+
ActionRead Action = "read"
28+
ActionUpdate Action = "update"
29+
ActionDelete Action = "delete"
30+
)
31+
32+
// AllActions is a helper function to return all the possible actions types.
33+
func AllActions() []Action {
34+
return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
35+
}
36+
2137
// Subject is a struct that contains all the elements of a subject in an rbac
2238
// authorize.
2339
type Subject struct {
@@ -329,3 +345,190 @@ func (a RegoAuthorizer) Prepare(ctx context.Context, subject Subject, action Act
329345
a.prepareHist.Observe(time.Since(start).Seconds())
330346
return prepared, nil
331347
}
348+
349+
// PartialAuthorizer is a prepared authorizer with the subject, action, and
350+
// resource type fields already filled in. This speeds up authorization
351+
// when authorizing the same type of object numerous times.
352+
// See rbac.Filter for example usage.
353+
type PartialAuthorizer struct {
354+
// partialQueries is mainly used for unit testing to assert our rego policy
355+
// can always be compressed into a set of queries.
356+
partialQueries *rego.PartialQueries
357+
358+
// input is used purely for debugging and logging.
359+
subjectInput Subject
360+
subjectAction Action
361+
subjectResourceType Object
362+
363+
// preparedQueries are the compiled set of queries after partial evaluation.
364+
// Cache these prepared queries to avoid re-compiling the queries.
365+
// If alwaysTrue is true, then ignore these.
366+
preparedQueries []rego.PreparedEvalQuery
367+
// alwaysTrue is if the subject can always perform the action on the
368+
// resource type, regardless of the unknown fields.
369+
alwaysTrue bool
370+
}
371+
372+
var _ PreparedAuthorized = (*PartialAuthorizer)(nil)
373+
374+
// CompileToSQL converts the remaining rego queries into SQL WHERE clauses.
375+
func (pa *PartialAuthorizer) CompileToSQL(ctx context.Context, cfg regosql.ConvertConfig) (string, error) {
376+
_, span := tracing.StartSpan(ctx, trace.WithAttributes(
377+
// Query count is a rough indicator of the complexity of the query
378+
// that needs to be converted into SQL.
379+
attribute.Int("query_count", len(pa.preparedQueries)),
380+
attribute.Bool("always_true", pa.alwaysTrue),
381+
))
382+
defer span.End()
383+
384+
filter, err := Compile(cfg, pa)
385+
if err != nil {
386+
return "", xerrors.Errorf("compile: %w", err)
387+
}
388+
return filter.SQLString(), nil
389+
}
390+
391+
func (pa *PartialAuthorizer) Authorize(ctx context.Context, object Object) error {
392+
if pa.alwaysTrue {
393+
return nil
394+
}
395+
396+
// If we have no queries, then no queries can return 'true'.
397+
// So the result is always 'false'.
398+
if len(pa.preparedQueries) == 0 {
399+
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"),
400+
pa.subjectInput, pa.subjectAction, pa.subjectResourceType, nil)
401+
}
402+
403+
parsed, err := ast.InterfaceToValue(map[string]interface{}{
404+
"object": object,
405+
})
406+
if err != nil {
407+
return xerrors.Errorf("parse object: %w", err)
408+
}
409+
410+
// How to interpret the results of the partial queries.
411+
// We have a list of queries that are along the lines of:
412+
// `input.object.org_owner = ""; "me" = input.object.owner`
413+
// `input.object.org_owner in {"feda2e52-8bf1-42ce-ad75-6c5595cb297a"} `
414+
// All these queries are joined by an 'OR'. So we need to run through each
415+
// query, and evaluate it.
416+
//
417+
// In each query, we have a list of the expressions, which should be
418+
// all boolean expressions. In the above 1st example, there are 2.
419+
// These expressions within a single query are `AND` together by rego.
420+
EachQueryLoop:
421+
for _, q := range pa.preparedQueries {
422+
// We need to eval each query with the newly known fields.
423+
results, err := q.Eval(ctx, rego.EvalParsedInput(parsed))
424+
if err != nil {
425+
continue EachQueryLoop
426+
}
427+
428+
// If there are no results, then the query is false. This is because rego
429+
// treats false queries as "undefined". So if any expression is false, the
430+
// result is an empty list.
431+
if len(results) == 0 {
432+
continue EachQueryLoop
433+
}
434+
435+
// If there is more than 1 result, that means there is more than 1 rule.
436+
// This should not happen, because our query should always be an expression.
437+
// If this every occurs, it is likely the original query was not an expression.
438+
if len(results) > 1 {
439+
continue EachQueryLoop
440+
}
441+
442+
// Our queries should be simple, and should not yield any bindings.
443+
// A binding is something like 'x := 1'. This binding as an expression is
444+
// 'true', but in our case is unhelpful. We are not analyzing this ast to
445+
// map bindings. So just error out. Similar to above, our queries should
446+
// always be boolean expressions.
447+
if len(results[0].Bindings) > 0 {
448+
continue EachQueryLoop
449+
}
450+
451+
// We have a valid set of boolean expressions! All expressions are 'AND'd
452+
// together. This is automatic by rego, so we should not actually need to
453+
// inspect this any further. But just in case, we will verify each expression
454+
// did resolve to 'true'. This is purely defensive programming.
455+
for _, exp := range results[0].Expressions {
456+
if v, ok := exp.Value.(bool); !ok || !v {
457+
continue EachQueryLoop
458+
}
459+
}
460+
461+
return nil
462+
}
463+
464+
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"),
465+
pa.subjectInput, pa.subjectAction, pa.subjectResourceType, nil)
466+
}
467+
468+
func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subject, action Action, objectType string) (*PartialAuthorizer, error) {
469+
if subject.Roles == nil {
470+
return nil, xerrors.Errorf("subject must have roles")
471+
}
472+
if subject.Scope == nil {
473+
return nil, xerrors.Errorf("subject must have a scope")
474+
}
475+
476+
input, err := regoPartialInputValue(subject, action, objectType)
477+
if err != nil {
478+
return nil, xerrors.Errorf("prepare input: %w", err)
479+
}
480+
481+
partialQueries, err := a.partialQuery.Partial(ctx, rego.EvalParsedInput(input))
482+
if err != nil {
483+
return nil, xerrors.Errorf("prepare: %w", err)
484+
}
485+
486+
pAuth := &PartialAuthorizer{
487+
partialQueries: partialQueries,
488+
preparedQueries: []rego.PreparedEvalQuery{},
489+
subjectInput: subject,
490+
subjectResourceType: Object{
491+
Type: objectType,
492+
ID: "prepared-object",
493+
},
494+
subjectAction: action,
495+
}
496+
497+
// Prepare each query to optimize the runtime when we iterate over the objects.
498+
preparedQueries := make([]rego.PreparedEvalQuery, 0, len(partialQueries.Queries))
499+
for _, q := range partialQueries.Queries {
500+
if q.String() == "" {
501+
// No more work needed. An empty query is the same as
502+
// 'WHERE true'
503+
// This is likely an admin. We don't even need to use rego going
504+
// forward.
505+
pAuth.alwaysTrue = true
506+
preparedQueries = []rego.PreparedEvalQuery{}
507+
break
508+
}
509+
results, err := rego.New(
510+
rego.ParsedQuery(q),
511+
).PrepareForEval(ctx)
512+
if err != nil {
513+
return nil, xerrors.Errorf("prepare query %s: %w", q.String(), err)
514+
}
515+
preparedQueries = append(preparedQueries, results)
516+
}
517+
pAuth.preparedQueries = preparedQueries
518+
519+
return pAuth, nil
520+
}
521+
522+
// rbacTraceAttributes are the attributes that are added to all spans created by
523+
// the rbac package. These attributes should help to debug slow spans.
524+
func rbacTraceAttributes(actor Subject, action Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {
525+
return trace.WithAttributes(
526+
append(extra,
527+
attribute.StringSlice("subject_roles", actor.SafeRoleNames()),
528+
attribute.Int("num_subject_roles", len(actor.SafeRoleNames())),
529+
attribute.Int("num_groups", len(actor.Groups)),
530+
attribute.String("scope", actor.SafeScopeName()),
531+
attribute.String("action", string(action)),
532+
attribute.String("object_type", objectType),
533+
)...)
534+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy