mirror of https://github.com/coder/coder.git
177 lines
5.6 KiB
Go
177 lines
5.6 KiB
Go
package rbac
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/rego"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/coderd/rbac/regosql"
|
|
"github.com/coder/coder/coderd/tracing"
|
|
)
|
|
|
|
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.
|
|
input map[string]interface{}
|
|
// 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)
|
|
|
|
func (pa *PartialAuthorizer) CompileToSQL(cfg regosql.ConvertConfig) (string, error) {
|
|
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
|
|
}
|
|
|
|
// No queries means always false
|
|
if len(pa.preparedQueries) == 0 {
|
|
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), pa.input, 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 {
|
|
continue EachQueryLoop
|
|
}
|
|
|
|
// 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.input, nil)
|
|
}
|
|
|
|
func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, scope Role, groups []string, action Action, objectType string) (*PartialAuthorizer, error) {
|
|
ctx, span := tracing.StartSpan(ctx)
|
|
defer span.End()
|
|
|
|
input := map[string]interface{}{
|
|
"subject": authSubject{
|
|
ID: subjectID,
|
|
Roles: roles,
|
|
Scope: scope,
|
|
Groups: groups,
|
|
},
|
|
"object": map[string]string{
|
|
"type": objectType,
|
|
},
|
|
"action": action,
|
|
}
|
|
|
|
// Run the rego policy with a few unknown fields. This should simplify our
|
|
// policy to a set of queries.
|
|
partialQueries, err := rego.New(
|
|
rego.Query("data.authz.allow = true"),
|
|
rego.Module("policy.rego", policy),
|
|
rego.Unknowns([]string{
|
|
"input.object.owner",
|
|
"input.object.org_owner",
|
|
"input.object.acl_user_list",
|
|
"input.object.acl_group_list",
|
|
}),
|
|
rego.Input(input),
|
|
).Partial(ctx)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("prepare: %w", err)
|
|
}
|
|
|
|
pAuth := &PartialAuthorizer{
|
|
partialQueries: partialQueries,
|
|
preparedQueries: []rego.PreparedEvalQuery{},
|
|
input: input,
|
|
}
|
|
|
|
// 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
|
|
}
|