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

-16
This file was deleted.

coderd/rbac/authz.go

+203
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)