mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
#7439 added global caching of RBAC results. Calls are cached based on hash(subject, object, action). We often use dbauthz.AsSystemRestricted to handle "internal" authz calls, and these are often repeated with similar arguments and are likely to get cached. So a transient error doing an authz check on a system function will be cached for up to a minute. I'm just starting off with excluding context.Canceled but there's likely a whole suite of different errors we want to also exclude from the global cache.
711 lines
22 KiB
Go
711 lines
22 KiB
Go
package rbac
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ammario/tlru"
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/rego"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/rbac/regosql"
|
|
"github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
|
|
"github.com/coder/coder/v2/coderd/tracing"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
)
|
|
|
|
// Action represents the allowed actions to be done on an object.
|
|
type Action string
|
|
|
|
const (
|
|
ActionCreate Action = "create"
|
|
ActionRead Action = "read"
|
|
ActionUpdate Action = "update"
|
|
ActionDelete Action = "delete"
|
|
)
|
|
|
|
// AllActions is a helper function to return all the possible actions types.
|
|
func AllActions() []Action {
|
|
return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
|
|
}
|
|
|
|
type AuthCall struct {
|
|
Actor Subject
|
|
Action Action
|
|
Object Object
|
|
}
|
|
|
|
// hashAuthorizeCall guarantees a unique hash for a given auth call.
|
|
// If two hashes are equal, then the result of a given authorize() call
|
|
// will be the same.
|
|
//
|
|
// Note that this ignores some fields such as the permissions within a given
|
|
// role, as this assumes all roles are static to a given role name.
|
|
func hashAuthorizeCall(actor Subject, action Action, object Object) [32]byte {
|
|
var hashOut [32]byte
|
|
hash := sha256.New()
|
|
|
|
// We use JSON for the forward security benefits if the rbac structs are
|
|
// modified without consideration for the caching layer.
|
|
enc := json.NewEncoder(hash)
|
|
_ = enc.Encode(actor)
|
|
_ = enc.Encode(action)
|
|
_ = enc.Encode(object)
|
|
|
|
// We might be able to avoid this extra copy?
|
|
// sha256.Sum256() returns a [32]byte. We need to return
|
|
// an array vs a slice so we can use it as a key in the cache.
|
|
image := hash.Sum(nil)
|
|
copy(hashOut[:], image)
|
|
return hashOut
|
|
}
|
|
|
|
// Subject is a struct that contains all the elements of a subject in an rbac
|
|
// authorize.
|
|
type Subject struct {
|
|
ID string
|
|
Roles ExpandableRoles
|
|
Groups []string
|
|
Scope ExpandableScope
|
|
|
|
// cachedASTValue is the cached ast value for this subject.
|
|
cachedASTValue ast.Value
|
|
}
|
|
|
|
// WithCachedASTValue can be called if the subject is static. This will compute
|
|
// the ast value once and cache it for future calls.
|
|
func (s Subject) WithCachedASTValue() Subject {
|
|
tmp := s
|
|
v, err := tmp.regoValue()
|
|
if err == nil {
|
|
tmp.cachedASTValue = v
|
|
}
|
|
return tmp
|
|
}
|
|
|
|
func (s Subject) Equal(b Subject) bool {
|
|
if s.ID != b.ID {
|
|
return false
|
|
}
|
|
|
|
if !slice.SameElements(s.Groups, b.Groups) {
|
|
return false
|
|
}
|
|
|
|
if !slice.SameElements(s.SafeRoleNames(), b.SafeRoleNames()) {
|
|
return false
|
|
}
|
|
|
|
if s.SafeScopeName() != b.SafeScopeName() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SafeScopeName prevent nil pointer dereference.
|
|
func (s Subject) SafeScopeName() string {
|
|
if s.Scope == nil {
|
|
return "no-scope"
|
|
}
|
|
return s.Scope.Name()
|
|
}
|
|
|
|
// SafeRoleNames prevent nil pointer dereference.
|
|
func (s Subject) SafeRoleNames() []string {
|
|
if s.Roles == nil {
|
|
return []string{}
|
|
}
|
|
return s.Roles.Names()
|
|
}
|
|
|
|
type Authorizer interface {
|
|
// Authorize will authorize the given subject to perform the given action
|
|
// on the given object. Authorize is pure and deterministic with respect to
|
|
// its arguments and the surrounding object.
|
|
Authorize(ctx context.Context, subject Subject, action Action, object Object) error
|
|
Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error)
|
|
}
|
|
|
|
type PreparedAuthorized interface {
|
|
Authorize(ctx context.Context, object Object) error
|
|
CompileToSQL(ctx context.Context, cfg regosql.ConvertConfig) (string, error)
|
|
}
|
|
|
|
// Filter takes in a list of objects, and will filter the list removing all
|
|
// the elements the subject does not have permission for. All objects must be
|
|
// of the same type.
|
|
//
|
|
// Ideally the 'CompileToSQL' is used instead for large sets. This cost scales
|
|
// linearly with the number of objects passed in.
|
|
func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, action Action, objects []O) ([]O, error) {
|
|
if len(objects) == 0 {
|
|
// Nothing to filter
|
|
return objects, nil
|
|
}
|
|
objectType := objects[0].RBACObject().Type
|
|
filtered := make([]O, 0)
|
|
|
|
// Start the span after the object type is detected. If we are filtering 0
|
|
// objects, then the span is not interesting. It would just add excessive
|
|
// 0 time spans that provide no insight.
|
|
ctx, span := tracing.StartSpan(ctx,
|
|
rbacTraceAttributes(subject, action, objectType,
|
|
// For filtering, we are only measuring the total time for the entire
|
|
// set of objects. This and the 'Prepare' span time
|
|
// is all that is required to measure the performance of this
|
|
// function on a per-object basis.
|
|
attribute.Int("num_objects", len(objects)),
|
|
),
|
|
)
|
|
defer span.End()
|
|
|
|
// Running benchmarks on this function, it is **always** faster to call
|
|
// auth.Authorize on <10 objects. This is because the overhead of
|
|
// 'Prepare'. Once we cross 10 objects, then it starts to become
|
|
// faster
|
|
if len(objects) < 10 {
|
|
for _, o := range objects {
|
|
rbacObj := o.RBACObject()
|
|
if rbacObj.Type != objectType {
|
|
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, rbacObj)
|
|
}
|
|
err := auth.Authorize(ctx, subject, action, o.RBACObject())
|
|
if err == nil {
|
|
filtered = append(filtered, o)
|
|
} else if !IsUnauthorizedError(err) {
|
|
// If the error is not the expected "Unauthorized" error, then
|
|
// it is something unexpected.
|
|
return nil, err
|
|
}
|
|
}
|
|
return filtered, nil
|
|
}
|
|
|
|
prepared, err := auth.Prepare(ctx, subject, action, objectType)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("prepare: %w", err)
|
|
}
|
|
|
|
for _, object := range objects {
|
|
rbacObj := object.RBACObject()
|
|
if rbacObj.Type != objectType {
|
|
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, object.RBACObject().Type)
|
|
}
|
|
err := prepared.Authorize(ctx, rbacObj)
|
|
if err == nil {
|
|
filtered = append(filtered, object)
|
|
} else if !IsUnauthorizedError(err) {
|
|
// If the error is not the expected "Unauthorized" error, then
|
|
// it is something unexpected.
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return filtered, nil
|
|
}
|
|
|
|
// RegoAuthorizer will use a prepared rego query for performing authorize()
|
|
type RegoAuthorizer struct {
|
|
query rego.PreparedEvalQuery
|
|
partialQuery rego.PreparedPartialQuery
|
|
|
|
authorizeHist *prometheus.HistogramVec
|
|
prepareHist prometheus.Histogram
|
|
}
|
|
|
|
var _ Authorizer = (*RegoAuthorizer)(nil)
|
|
|
|
var (
|
|
// Load the policy from policy.rego in this directory.
|
|
//
|
|
//go:embed policy.rego
|
|
policy string
|
|
queryOnce sync.Once
|
|
query rego.PreparedEvalQuery
|
|
partialQuery rego.PreparedPartialQuery
|
|
)
|
|
|
|
// NewCachingAuthorizer returns a new RegoAuthorizer that supports context based
|
|
// caching. To utilize the caching, the context passed to Authorize() must be
|
|
// created with 'WithCacheCtx(ctx)'.
|
|
func NewCachingAuthorizer(registry prometheus.Registerer) Authorizer {
|
|
return Cacher(NewAuthorizer(registry))
|
|
}
|
|
|
|
func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer {
|
|
queryOnce.Do(func() {
|
|
var err error
|
|
query, err = rego.New(
|
|
rego.Query("data.authz.allow"),
|
|
rego.Module("policy.rego", policy),
|
|
).PrepareForEval(context.Background())
|
|
if err != nil {
|
|
panic(xerrors.Errorf("compile rego: %w", err))
|
|
}
|
|
|
|
partialQuery, err = rego.New(
|
|
rego.Unknowns([]string{
|
|
"input.object.id",
|
|
"input.object.owner",
|
|
"input.object.org_owner",
|
|
"input.object.acl_user_list",
|
|
"input.object.acl_group_list",
|
|
}),
|
|
rego.Query("data.authz.allow = true"),
|
|
rego.Module("policy.rego", policy),
|
|
).PrepareForPartial(context.Background())
|
|
if err != nil {
|
|
panic(xerrors.Errorf("compile partial rego: %w", err))
|
|
}
|
|
})
|
|
|
|
// Register metrics to prometheus.
|
|
// These bucket values are based on the average time it takes to run authz
|
|
// being around 1ms. Anything under ~2ms is OK and does not need to be
|
|
// analyzed any further.
|
|
buckets := []float64{
|
|
0.0005, // 0.5ms
|
|
0.001, // 1ms
|
|
0.002, // 2ms
|
|
0.003,
|
|
0.005,
|
|
0.01, // 10ms
|
|
0.02,
|
|
0.035, // 35ms
|
|
0.05,
|
|
0.075,
|
|
0.1, // 100ms
|
|
0.25, // 250ms
|
|
0.75, // 750ms
|
|
1, // 1s
|
|
}
|
|
|
|
factory := promauto.With(registry)
|
|
authorizeHistogram := factory.NewHistogramVec(prometheus.HistogramOpts{
|
|
Namespace: "coderd",
|
|
Subsystem: "authz",
|
|
Name: "authorize_duration_seconds",
|
|
Help: "Duration of the 'Authorize' call in seconds. Only counts calls that succeed.",
|
|
Buckets: buckets,
|
|
}, []string{"allowed"})
|
|
|
|
prepareHistogram := factory.NewHistogram(prometheus.HistogramOpts{
|
|
Namespace: "coderd",
|
|
Subsystem: "authz",
|
|
Name: "prepare_authorize_duration_seconds",
|
|
Help: "Duration of the 'PrepareAuthorize' call in seconds.",
|
|
Buckets: buckets,
|
|
})
|
|
|
|
return &RegoAuthorizer{
|
|
query: query,
|
|
partialQuery: partialQuery,
|
|
|
|
authorizeHist: authorizeHistogram,
|
|
prepareHist: prepareHistogram,
|
|
}
|
|
}
|
|
|
|
type authSubject struct {
|
|
ID string `json:"id"`
|
|
Roles []Role `json:"roles"`
|
|
Groups []string `json:"groups"`
|
|
Scope Scope `json:"scope"`
|
|
}
|
|
|
|
// Authorize is the intended function to be used outside this package.
|
|
// It returns `nil` if the subject is authorized to perform the action on
|
|
// the object.
|
|
// If an error is returned, the authorization is denied.
|
|
func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
|
|
start := time.Now()
|
|
ctx, span := tracing.StartSpan(ctx,
|
|
trace.WithTimestamp(start), // Reuse the time.Now for metric and trace
|
|
rbacTraceAttributes(subject, action, object.Type,
|
|
// For authorizing a single object, this data is useful to know how
|
|
// complex our objects are getting.
|
|
attribute.Int("object_num_groups", len(object.ACLGroupList)),
|
|
attribute.Int("object_num_users", len(object.ACLUserList)),
|
|
),
|
|
)
|
|
defer span.End()
|
|
|
|
err := a.authorize(ctx, subject, action, object)
|
|
|
|
span.SetAttributes(attribute.Bool("authorized", err == nil))
|
|
|
|
dur := time.Since(start)
|
|
if err != nil {
|
|
a.authorizeHist.WithLabelValues("false").Observe(dur.Seconds())
|
|
return err
|
|
}
|
|
|
|
a.authorizeHist.WithLabelValues("true").Observe(dur.Seconds())
|
|
return nil
|
|
}
|
|
|
|
// authorize is the internal function that does the actual authorization.
|
|
// It is a different function so the exported one can add tracing + metrics.
|
|
// That code tends to clutter up the actual logic, so it's separated out.
|
|
// nolint:revive
|
|
func (a RegoAuthorizer) authorize(ctx context.Context, subject Subject, action Action, object Object) error {
|
|
if subject.Roles == nil {
|
|
return xerrors.Errorf("subject must have roles")
|
|
}
|
|
if subject.Scope == nil {
|
|
return xerrors.Errorf("subject must have a scope")
|
|
}
|
|
|
|
astV, err := regoInputValue(subject, action, object)
|
|
if err != nil {
|
|
return xerrors.Errorf("convert input to value: %w", err)
|
|
}
|
|
|
|
results, err := a.query.Eval(ctx, rego.EvalParsedInput(astV))
|
|
if err != nil {
|
|
err = correctCancelError(err)
|
|
return xerrors.Errorf("evaluate rego: %w", err)
|
|
}
|
|
|
|
if !results.Allowed() {
|
|
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), subject, action, object, results)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
|
|
// This will vastly speed up performance if batch authorization on the same type of objects is needed.
|
|
func (a RegoAuthorizer) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
|
|
start := time.Now()
|
|
ctx, span := tracing.StartSpan(ctx,
|
|
trace.WithTimestamp(start),
|
|
rbacTraceAttributes(subject, action, objectType),
|
|
)
|
|
defer span.End()
|
|
|
|
prepared, err := a.newPartialAuthorizer(ctx, subject, action, objectType)
|
|
if err != nil {
|
|
err = correctCancelError(err)
|
|
return nil, xerrors.Errorf("new partial authorizer: %w", err)
|
|
}
|
|
|
|
// Add attributes of the Prepare results. This will help understand the
|
|
// complexity of the roles and how it affects the time taken.
|
|
span.SetAttributes(
|
|
attribute.Int("num_queries", len(prepared.preparedQueries)),
|
|
attribute.Bool("always_true", prepared.alwaysTrue),
|
|
)
|
|
|
|
a.prepareHist.Observe(time.Since(start).Seconds())
|
|
return prepared, nil
|
|
}
|
|
|
|
// PartialAuthorizer is a prepared authorizer with the subject, action, and
|
|
// resource type fields already filled in. This speeds up authorization
|
|
// when authorizing the same type of object numerous times.
|
|
// See rbac.Filter for example usage.
|
|
type PartialAuthorizer struct {
|
|
// partialQueries is mainly used for unit testing to assert our rego policy
|
|
// can always be compressed into a set of queries.
|
|
partialQueries *rego.PartialQueries
|
|
|
|
// input is used purely for debugging and logging.
|
|
subjectInput Subject
|
|
subjectAction Action
|
|
subjectResourceType Object
|
|
|
|
// preparedQueries are the compiled set of queries after partial evaluation.
|
|
// Cache these prepared queries to avoid re-compiling the queries.
|
|
// If alwaysTrue is true, then ignore these.
|
|
preparedQueries []rego.PreparedEvalQuery
|
|
// alwaysTrue is if the subject can always perform the action on the
|
|
// resource type, regardless of the unknown fields.
|
|
alwaysTrue bool
|
|
}
|
|
|
|
var _ PreparedAuthorized = (*PartialAuthorizer)(nil)
|
|
|
|
// CompileToSQL converts the remaining rego queries into SQL WHERE clauses.
|
|
func (pa *PartialAuthorizer) CompileToSQL(ctx context.Context, cfg regosql.ConvertConfig) (string, error) {
|
|
_, span := tracing.StartSpan(ctx, trace.WithAttributes(
|
|
// Query count is a rough indicator of the complexity of the query
|
|
// that needs to be converted into SQL.
|
|
attribute.Int("query_count", len(pa.preparedQueries)),
|
|
attribute.Bool("always_true", pa.alwaysTrue),
|
|
))
|
|
defer span.End()
|
|
|
|
filter, err := Compile(cfg, pa)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("compile: %w", err)
|
|
}
|
|
return filter.SQLString(), nil
|
|
}
|
|
|
|
func (pa *PartialAuthorizer) Authorize(ctx context.Context, object Object) error {
|
|
if pa.alwaysTrue {
|
|
return nil
|
|
}
|
|
|
|
// If we have no queries, then no queries can return 'true'.
|
|
// So the result is always 'false'.
|
|
if len(pa.preparedQueries) == 0 {
|
|
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"),
|
|
pa.subjectInput, pa.subjectAction, pa.subjectResourceType, nil)
|
|
}
|
|
|
|
parsed, err := ast.InterfaceToValue(map[string]interface{}{
|
|
"object": object,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("parse object: %w", err)
|
|
}
|
|
|
|
// How to interpret the results of the partial queries.
|
|
// We have a list of queries that are along the lines of:
|
|
// `input.object.org_owner = ""; "me" = input.object.owner`
|
|
// `input.object.org_owner in {"feda2e52-8bf1-42ce-ad75-6c5595cb297a"} `
|
|
// All these queries are joined by an 'OR'. So we need to run through each
|
|
// query, and evaluate it.
|
|
//
|
|
// In each query, we have a list of the expressions, which should be
|
|
// all boolean expressions. In the above 1st example, there are 2.
|
|
// These expressions within a single query are `AND` together by rego.
|
|
EachQueryLoop:
|
|
for _, q := range pa.preparedQueries {
|
|
// We need to eval each query with the newly known fields.
|
|
results, err := q.Eval(ctx, rego.EvalParsedInput(parsed))
|
|
if err != nil {
|
|
err = correctCancelError(err)
|
|
return xerrors.Errorf("eval error: %w", err)
|
|
}
|
|
|
|
// If there are no results, then the query is false. This is because rego
|
|
// treats false queries as "undefined". So if any expression is false, the
|
|
// result is an empty list.
|
|
if len(results) == 0 {
|
|
continue EachQueryLoop
|
|
}
|
|
|
|
// If there is more than 1 result, that means there is more than 1 rule.
|
|
// This should not happen, because our query should always be an expression.
|
|
// If this every occurs, it is likely the original query was not an expression.
|
|
if len(results) > 1 {
|
|
continue EachQueryLoop
|
|
}
|
|
|
|
// Our queries should be simple, and should not yield any bindings.
|
|
// A binding is something like 'x := 1'. This binding as an expression is
|
|
// 'true', but in our case is unhelpful. We are not analyzing this ast to
|
|
// map bindings. So just error out. Similar to above, our queries should
|
|
// always be boolean expressions.
|
|
if len(results[0].Bindings) > 0 {
|
|
continue EachQueryLoop
|
|
}
|
|
|
|
// We have a valid set of boolean expressions! All expressions are 'AND'd
|
|
// together. This is automatic by rego, so we should not actually need to
|
|
// inspect this any further. But just in case, we will verify each expression
|
|
// did resolve to 'true'. This is purely defensive programming.
|
|
for _, exp := range results[0].Expressions {
|
|
if v, ok := exp.Value.(bool); !ok || !v {
|
|
continue EachQueryLoop
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"),
|
|
pa.subjectInput, pa.subjectAction, pa.subjectResourceType, nil)
|
|
}
|
|
|
|
func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subject, action Action, objectType string) (*PartialAuthorizer, error) {
|
|
if subject.Roles == nil {
|
|
return nil, xerrors.Errorf("subject must have roles")
|
|
}
|
|
if subject.Scope == nil {
|
|
return nil, xerrors.Errorf("subject must have a scope")
|
|
}
|
|
|
|
input, err := regoPartialInputValue(subject, action, objectType)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("prepare input: %w", err)
|
|
}
|
|
|
|
partialQueries, err := a.partialQuery.Partial(ctx, rego.EvalParsedInput(input))
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("prepare: %w", err)
|
|
}
|
|
|
|
pAuth := &PartialAuthorizer{
|
|
partialQueries: partialQueries,
|
|
preparedQueries: []rego.PreparedEvalQuery{},
|
|
subjectInput: subject,
|
|
subjectResourceType: Object{
|
|
Type: objectType,
|
|
ID: "prepared-object",
|
|
},
|
|
subjectAction: action,
|
|
}
|
|
|
|
// Prepare each query to optimize the runtime when we iterate over the objects.
|
|
preparedQueries := make([]rego.PreparedEvalQuery, 0, len(partialQueries.Queries))
|
|
for _, q := range partialQueries.Queries {
|
|
if q.String() == "" {
|
|
// No more work needed. An empty query is the same as
|
|
// 'WHERE true'
|
|
// This is likely an admin. We don't even need to use rego going
|
|
// forward.
|
|
pAuth.alwaysTrue = true
|
|
preparedQueries = []rego.PreparedEvalQuery{}
|
|
break
|
|
}
|
|
results, err := rego.New(
|
|
rego.ParsedQuery(q),
|
|
).PrepareForEval(ctx)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("prepare query %s: %w", q.String(), err)
|
|
}
|
|
preparedQueries = append(preparedQueries, results)
|
|
}
|
|
pAuth.preparedQueries = preparedQueries
|
|
|
|
return pAuth, nil
|
|
}
|
|
|
|
// AuthorizeFilter is a compiled partial query that can be converted to SQL.
|
|
// This allows enforcing the policy on the database side in a WHERE clause.
|
|
type AuthorizeFilter interface {
|
|
SQLString() string
|
|
}
|
|
|
|
type authorizedSQLFilter struct {
|
|
sqlString string
|
|
auth *PartialAuthorizer
|
|
}
|
|
|
|
// ConfigWithACL is the basic configuration for converting rego to SQL when
|
|
// the object has group and user ACL fields.
|
|
func ConfigWithACL() regosql.ConvertConfig {
|
|
return regosql.ConvertConfig{
|
|
VariableConverter: regosql.DefaultVariableConverter(),
|
|
}
|
|
}
|
|
|
|
// ConfigWithoutACL is the basic configuration for converting rego to SQL when
|
|
// the object has no ACL fields.
|
|
func ConfigWithoutACL() regosql.ConvertConfig {
|
|
return regosql.ConvertConfig{
|
|
VariableConverter: regosql.NoACLConverter(),
|
|
}
|
|
}
|
|
|
|
func ConfigWorkspaces() regosql.ConvertConfig {
|
|
return regosql.ConvertConfig{
|
|
VariableConverter: regosql.WorkspaceConverter(),
|
|
}
|
|
}
|
|
|
|
func Compile(cfg regosql.ConvertConfig, pa *PartialAuthorizer) (AuthorizeFilter, error) {
|
|
root, err := regosql.ConvertRegoAst(cfg, pa.partialQueries)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("convert rego ast: %w", err)
|
|
}
|
|
|
|
// Generate the SQL
|
|
gen := sqltypes.NewSQLGenerator()
|
|
sqlString := root.SQLString(gen)
|
|
if len(gen.Errors()) > 0 {
|
|
var errStrings []string
|
|
for _, err := range gen.Errors() {
|
|
errStrings = append(errStrings, err.Error())
|
|
}
|
|
return nil, xerrors.Errorf("sql generation errors: %v", strings.Join(errStrings, ", "))
|
|
}
|
|
|
|
return &authorizedSQLFilter{
|
|
sqlString: sqlString,
|
|
auth: pa,
|
|
}, nil
|
|
}
|
|
|
|
func (a *authorizedSQLFilter) SQLString() string {
|
|
return a.sqlString
|
|
}
|
|
|
|
type authCache struct {
|
|
// cache is a cache of hashed Authorize inputs to the result of the Authorize
|
|
// call.
|
|
// determistic function.
|
|
cache *tlru.Cache[[32]byte, error]
|
|
|
|
authz Authorizer
|
|
}
|
|
|
|
// Cacher returns an Authorizer that can use a cache to short circuit duplicate
|
|
// calls to the Authorizer. This is useful when multiple calls are made to the
|
|
// Authorizer for the same subject, action, and object.
|
|
// This is a GLOBAL cache shared between all requests.
|
|
// If no cache is found on the context, the Authorizer is called as normal.
|
|
//
|
|
// Cacher is safe for multiple actors.
|
|
func Cacher(authz Authorizer) Authorizer {
|
|
return &authCache{
|
|
authz: authz,
|
|
// In practice, this cache should never come close to filling since the
|
|
// authorization calls are kept for a minute at most.
|
|
cache: tlru.New[[32]byte](tlru.ConstantCost[error], 64*1024),
|
|
}
|
|
}
|
|
|
|
func (c *authCache) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
|
|
authorizeCacheKey := hashAuthorizeCall(subject, action, object)
|
|
|
|
var err error
|
|
err, _, ok := c.cache.Get(authorizeCacheKey)
|
|
if !ok {
|
|
err = c.authz.Authorize(ctx, subject, action, object)
|
|
// If there is a transient error such as a context cancellation, do not
|
|
// cache it.
|
|
if !errors.Is(err, context.Canceled) {
|
|
// In case there is a caching bug, bound the TTL to 1 minute.
|
|
c.cache.Set(authorizeCacheKey, err, time.Minute)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Prepare returns the underlying PreparedAuthorized. The cache does not apply
|
|
// to prepared authorizations. These should be using a SQL filter, and
|
|
// therefore the cache is not needed.
|
|
func (c *authCache) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
|
|
return c.authz.Prepare(ctx, subject, action, objectType)
|
|
}
|
|
|
|
// rbacTraceAttributes are the attributes that are added to all spans created by
|
|
// the rbac package. These attributes should help to debug slow spans.
|
|
func rbacTraceAttributes(actor Subject, action Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {
|
|
return trace.WithAttributes(
|
|
append(extra,
|
|
attribute.StringSlice("subject_roles", actor.SafeRoleNames()),
|
|
attribute.Int("num_subject_roles", len(actor.SafeRoleNames())),
|
|
attribute.Int("num_groups", len(actor.Groups)),
|
|
attribute.String("scope", actor.SafeScopeName()),
|
|
attribute.String("action", string(action)),
|
|
attribute.String("object_type", objectType),
|
|
)...)
|
|
}
|