mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat: add template RBAC/groups (#4235)
This commit is contained in:
@ -3,7 +3,6 @@ package rbac
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
@ -15,8 +14,8 @@ import (
|
||||
)
|
||||
|
||||
type Authorizer interface {
|
||||
ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, object Object) error
|
||||
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error)
|
||||
ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, object Object) error
|
||||
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, objectType string) (PreparedAuthorized, error)
|
||||
}
|
||||
|
||||
type PreparedAuthorized interface {
|
||||
@ -27,7 +26,7 @@ type PreparedAuthorized interface {
|
||||
// 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.
|
||||
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, scope Scope, action Action, objects []O) ([]O, error) {
|
||||
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, scope Scope, groups []string, action Action, objects []O) ([]O, error) {
|
||||
ctx, span := tracing.StartSpan(ctx, trace.WithAttributes(
|
||||
attribute.String("subject_id", subjID),
|
||||
attribute.StringSlice("subject_roles", subjRoles),
|
||||
@ -52,7 +51,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
|
||||
if rbacObj.Type != objectType {
|
||||
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, rbacObj)
|
||||
}
|
||||
err := auth.ByRoleName(ctx, subjID, subjRoles, scope, action, o.RBACObject())
|
||||
err := auth.ByRoleName(ctx, subjID, subjRoles, scope, groups, action, o.RBACObject())
|
||||
if err == nil {
|
||||
filtered = append(filtered, o)
|
||||
}
|
||||
@ -60,7 +59,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, scope, action, objectType)
|
||||
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, scope, groups, action, objectType)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("prepare: %w", err)
|
||||
}
|
||||
@ -95,21 +94,11 @@ var (
|
||||
query rego.PreparedEvalQuery
|
||||
)
|
||||
|
||||
const (
|
||||
rolesOkCheck = "role_ok"
|
||||
scopeOkCheck = "scope_ok"
|
||||
)
|
||||
|
||||
func NewAuthorizer() *RegoAuthorizer {
|
||||
queryOnce.Do(func() {
|
||||
var err error
|
||||
query, err = rego.New(
|
||||
// Bind the results to 2 variables for easy checking later.
|
||||
rego.Query(
|
||||
fmt.Sprintf("%s := data.authz.role_allow "+
|
||||
"%s := data.authz.scope_allow",
|
||||
rolesOkCheck, scopeOkCheck),
|
||||
),
|
||||
rego.Query("data.authz.allow"),
|
||||
rego.Module("policy.rego", policy),
|
||||
).PrepareForEval(context.Background())
|
||||
if err != nil {
|
||||
@ -120,15 +109,16 @@ func NewAuthorizer() *RegoAuthorizer {
|
||||
}
|
||||
|
||||
type authSubject struct {
|
||||
ID string `json:"id"`
|
||||
Roles []Role `json:"roles"`
|
||||
Scope Role `json:"scope"`
|
||||
ID string `json:"id"`
|
||||
Roles []Role `json:"roles"`
|
||||
Groups []string `json:"groups"`
|
||||
Scope Role `json:"scope"`
|
||||
}
|
||||
|
||||
// ByRoleName will expand all roleNames into roles before calling Authorize().
|
||||
// This is the function intended to be used outside this package.
|
||||
// The role is fetched from the builtin map located in memory.
|
||||
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, object Object) error {
|
||||
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, object Object) error {
|
||||
roles, err := RolesByNames(roleNames)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -139,7 +129,7 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
|
||||
return err
|
||||
}
|
||||
|
||||
err = a.Authorize(ctx, subjectID, roles, scopeRole, action, object)
|
||||
err = a.Authorize(ctx, subjectID, roles, scopeRole, groups, action, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -149,12 +139,16 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
|
||||
|
||||
// Authorize allows passing in custom Roles.
|
||||
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
|
||||
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, scope Role, action Action, object Object) error {
|
||||
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, scope Role, groups []string, action Action, object Object) error {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"subject": authSubject{
|
||||
ID: subjectID,
|
||||
Roles: roles,
|
||||
Scope: scope,
|
||||
ID: subjectID,
|
||||
Roles: roles,
|
||||
Groups: groups,
|
||||
Scope: scope,
|
||||
},
|
||||
"object": object,
|
||||
"action": action,
|
||||
@ -165,37 +159,19 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
|
||||
return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w", err), input, results)
|
||||
}
|
||||
|
||||
// We expect only the 2 bindings for scopes and roles checks.
|
||||
if len(results) == 1 && len(results[0].Bindings) == 2 {
|
||||
roleCheck, ok := results[0].Bindings[rolesOkCheck].(bool)
|
||||
if !ok || !roleCheck {
|
||||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
|
||||
}
|
||||
|
||||
scopeCheck, ok := results[0].Bindings[scopeOkCheck].(bool)
|
||||
if !ok || !scopeCheck {
|
||||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
|
||||
}
|
||||
|
||||
// This is purely defensive programming. The two above checks already
|
||||
// check for 'true' expressions. This is just a sanity check to make
|
||||
// sure we don't add non-boolean expressions to our query.
|
||||
// This is super cheap to do, and just adds in some extra safety for
|
||||
// programmer error.
|
||||
for _, exp := range results[0].Expressions {
|
||||
if b, ok := exp.Value.(bool); !ok || !b {
|
||||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if !results.Allowed() {
|
||||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
|
||||
}
|
||||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, 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 (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, scope Role, action Action, objectType string) (*PartialAuthorizer, error) {
|
||||
auth, err := newPartialAuthorizer(ctx, subjectID, roles, scope, action, objectType)
|
||||
func (RegoAuthorizer) Prepare(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()
|
||||
|
||||
auth, err := newPartialAuthorizer(ctx, subjectID, roles, scope, groups, action, objectType)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("new partial authorizer: %w", err)
|
||||
}
|
||||
@ -203,7 +179,10 @@ func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Rol
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error) {
|
||||
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, objectType string) (PreparedAuthorized, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
roles, err := RolesByNames(roleNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -214,5 +193,5 @@ func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.Prepare(ctx, subjectID, roles, scopeRole, action, objectType)
|
||||
return a.Prepare(ctx, subjectID, roles, scopeRole, groups, action, objectType)
|
||||
}
|
||||
|
Reference in New Issue
Block a user