mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: Implement standard rbac.Subject to be reused everywhere (#5881)
* chore: Implement standard rbac.Subject to be reused everywhere An rbac subject is created in multiple spots because of the way we expand roles, scopes, etc. This difference in use creates a list of arguments which is unwieldy. Use of the expander interface lets us conform to a single subject in every case
This commit is contained in:
@ -17,24 +17,34 @@ import (
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
)
|
||||
|
||||
// ExpandableRoles is any type that can be expanded into a []Role. This is implemented
|
||||
// as an interface so we can have RoleNames for user defined roles, and implement
|
||||
// custom ExpandableRoles for system type users (eg autostart/autostop system role).
|
||||
// We want a clear divide between the two types of roles so users have no codepath
|
||||
// to interact or assign system roles.
|
||||
//
|
||||
// Note: We may also want to do the same thing with scopes to allow custom scope
|
||||
// support unavailable to the user. Eg: Scope to a single resource.
|
||||
type ExpandableRoles interface {
|
||||
Expand() ([]Role, error)
|
||||
// Names is for logging and tracing purposes, we want to know the human
|
||||
// names of the expanded roles.
|
||||
Names() []string
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ByRoleName(ctx context.Context, subjectID string, roleNames ExpandableRoles, scope ScopeName, groups []string, action Action, object Object) error
|
||||
PrepareByRoleName(ctx context.Context, subjectID string, roleNames ExpandableRoles, scope ScopeName, groups []string, action Action, objectType string) (PreparedAuthorized, error)
|
||||
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 {
|
||||
@ -48,7 +58,7 @@ type PreparedAuthorized interface {
|
||||
//
|
||||
// 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, subjID string, subjRoles ExpandableRoles, scope ScopeName, groups []string, action Action, objects []O) ([]O, error) {
|
||||
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
|
||||
@ -60,9 +70,9 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
|
||||
// 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(subjRoles.Names(), len(groups), scope, action, objectType,
|
||||
rbacTraceAttributes(subject, action, objectType,
|
||||
// For filtering, we are only measuring the total time for the entire
|
||||
// set of objects. This and the 'PrepareByRoleName' span time
|
||||
// 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)),
|
||||
@ -71,8 +81,8 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
|
||||
defer span.End()
|
||||
|
||||
// Running benchmarks on this function, it is **always** faster to call
|
||||
// auth.ByRoleName on <10 objects. This is because the overhead of
|
||||
// 'PrepareByRoleName'. Once we cross 10 objects, then it starts to become
|
||||
// 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 {
|
||||
@ -80,7 +90,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, groups, action, o.RBACObject())
|
||||
err := auth.Authorize(ctx, subject, action, o.RBACObject())
|
||||
if err == nil {
|
||||
filtered = append(filtered, o)
|
||||
}
|
||||
@ -88,7 +98,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, scope, groups, action, objectType)
|
||||
prepared, err := auth.Prepare(ctx, subject, action, objectType)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("prepare: %w", err)
|
||||
}
|
||||
@ -191,14 +201,15 @@ type authSubject struct {
|
||||
Scope Scope `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 ExpandableRoles, scope ScopeName, groups []string, action Action, object Object) error {
|
||||
// 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(roleNames.Names(), len(groups), scope, action, object.Type,
|
||||
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)),
|
||||
@ -207,18 +218,9 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
roles, err := roleNames.Expand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := a.authorize(ctx, subject, action, object)
|
||||
span.SetAttributes(attribute.Bool("authorized", err == nil))
|
||||
|
||||
scopeRole, err := ExpandScope(scope)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = a.Authorize(ctx, subjectID, roles, scopeRole, groups, action, object)
|
||||
span.AddEvent("authorized", trace.WithAttributes(attribute.Bool("authorized", err == nil)))
|
||||
dur := time.Since(start)
|
||||
if err != nil {
|
||||
a.authorizeHist.WithLabelValues("false").Observe(dur.Seconds())
|
||||
@ -229,15 +231,34 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 Scope, groups []string, action Action, object Object) error {
|
||||
// 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")
|
||||
}
|
||||
|
||||
subjRoles, err := subject.Roles.Expand()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("expand roles: %w", err)
|
||||
}
|
||||
|
||||
subjScope, err := subject.Scope.Expand()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("expand scope: %w", err)
|
||||
}
|
||||
|
||||
input := map[string]interface{}{
|
||||
"subject": authSubject{
|
||||
ID: subjectID,
|
||||
Roles: roles,
|
||||
Groups: groups,
|
||||
Scope: scope,
|
||||
ID: subject.ID,
|
||||
Roles: subjRoles,
|
||||
Groups: subject.Groups,
|
||||
Scope: subjScope,
|
||||
},
|
||||
"object": object,
|
||||
"action": action,
|
||||
@ -254,27 +275,19 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames ExpandableRoles, scope ScopeName, groups []string, action Action, objectType string) (PreparedAuthorized, error) {
|
||||
// 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(roleNames.Names(), len(groups), scope, action, objectType),
|
||||
rbacTraceAttributes(subject, action, objectType),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
roles, err := roleNames.Expand()
|
||||
prepared, err := newPartialAuthorizer(ctx, subject, action, objectType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeRole, err := ExpandScope(scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prepared, err := a.Prepare(ctx, subjectID, roles, scopeRole, groups, action, objectType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, xerrors.Errorf("new partial authorizer: %w", err)
|
||||
}
|
||||
|
||||
// Add attributes of the Prepare results. This will help understand the
|
||||
@ -287,14 +300,3 @@ func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string,
|
||||
a.prepareHist.Observe(time.Since(start).Seconds())
|
||||
return prepared, 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 Scope, groups []string, action Action, objectType string) (*PartialAuthorizer, error) {
|
||||
auth, err := newPartialAuthorizer(ctx, subjectID, roles, scope, groups, action, objectType)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("new partial authorizer: %w", err)
|
||||
}
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user