-
Notifications
You must be signed in to change notification settings - Fork 875
chore: add x-authz-checks debug header when running in dev mode #16873
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
038ad91
132ed27
93826da
3d16842
d59ecbe
fe0e466
f268227
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
_ "embed" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
@@ -23,7 +24,9 @@ import ( | |
"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/ptr" | ||
"github.com/coder/coder/v2/coderd/util/slice" | ||
"github.com/coder/coder/v2/coderd/util/syncmap" | ||
) | ||
|
||
type AuthCall struct { | ||
|
@@ -362,11 +365,11 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action p | |
defer span.End() | ||
|
||
err := a.authorize(ctx, subject, action, object) | ||
|
||
span.SetAttributes(attribute.Bool("authorized", err == nil)) | ||
authorized := err == nil | ||
span.SetAttributes(attribute.Bool("authorized", authorized)) | ||
|
||
dur := time.Since(start) | ||
if err != nil { | ||
if !authorized { | ||
a.authorizeHist.WithLabelValues("false").Observe(dur.Seconds()) | ||
return err | ||
} | ||
|
@@ -741,3 +744,98 @@ func rbacTraceAttributes(actor Subject, action policy.Action, objectType string, | |
attribute.String("object_type", objectType), | ||
)...) | ||
} | ||
|
||
type authRecorder struct { | ||
authz Authorizer | ||
} | ||
|
||
// Recorder returns an Authorizer that records any authorization checks made | ||
// on the Context provided for the authorization check. | ||
// | ||
// Requires using the RecordAuthzChecks middleware. | ||
func Recorder(authz Authorizer) Authorizer { | ||
return &authRecorder{authz: authz} | ||
} | ||
|
||
func (c *authRecorder) Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error { | ||
err := c.authz.Authorize(ctx, subject, action, object) | ||
authorized := err == nil | ||
recordAuthzCheck(ctx, action, object, authorized) | ||
return err | ||
} | ||
|
||
func (c *authRecorder) Prepare(ctx context.Context, subject Subject, action policy.Action, objectType string) (PreparedAuthorized, error) { | ||
return c.authz.Prepare(ctx, subject, action, objectType) | ||
} | ||
|
||
type authzCheckRecorderKey struct{} | ||
|
||
func WithAuthzCheckRecorder(ctx context.Context) context.Context { | ||
return context.WithValue(ctx, authzCheckRecorderKey{}, ptr.Ref(AuthzCheckRecorder{ | ||
checks: syncmap.Map[string, bool]{}, | ||
})) | ||
} | ||
|
||
type AuthzCheckRecorder struct { | ||
// Checks is a map from preformatted authz check IDs to their authorization | ||
// status (true => authorized, false => not authorized) | ||
checks syncmap.Map[string, bool] | ||
} | ||
|
||
func recordAuthzCheck(ctx context.Context, action policy.Action, object Object, authorized bool) { | ||
r, ok := ctx.Value(authzCheckRecorderKey{}).(*AuthzCheckRecorder) | ||
if !ok { | ||
return | ||
} | ||
|
||
// We serialize the check using the following syntax | ||
var b strings.Builder | ||
if object.OrgID != "" { | ||
_, err := fmt.Fprintf(&b, "organization:%v::", object.OrgID) | ||
if err != nil { | ||
return | ||
} | ||
} | ||
if object.AnyOrgOwner { | ||
_, err := fmt.Fprint(&b, "organization:any::") | ||
if err != nil { | ||
return | ||
} | ||
} | ||
if object.Owner != "" { | ||
_, err := fmt.Fprintf(&b, "owner:%v::", object.Owner) | ||
if err != nil { | ||
return | ||
} | ||
} | ||
if object.ID != "" { | ||
_, err := fmt.Fprintf(&b, "id:%v::", object.ID) | ||
if err != nil { | ||
return | ||
} | ||
} | ||
_, err := fmt.Fprintf(&b, "%v.%v", object.RBACObject().Type, action) | ||
if err != nil { | ||
return | ||
} | ||
|
||
r.checks.Store(b.String(), authorized) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this probably shouldn't happen in the real world, but what if the first authz check returns true, and a subsequent authz check for the same subject, action, and object returns false? I think this would be a really good opportunity to catch something like this and make a lot of confused and angry noises. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe I should just use a slice so that duplicates get recorded There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, a slice would also show you how many times a particular authz check gets done. Maybe a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point actually! You can then reason about the order of the checks. 🧠 |
||
} | ||
|
||
func GetAuthzCheckRecorder(ctx context.Context) (*AuthzCheckRecorder, bool) { | ||
checks, ok := ctx.Value(authzCheckRecorderKey{}).(*AuthzCheckRecorder) | ||
if !ok { | ||
return nil, false | ||
} | ||
|
||
return checks, true | ||
} | ||
|
||
// String serializes all of the checks recorded, using the following syntax: | ||
func (r *AuthzCheckRecorder) String() string { | ||
checks := make([]string, 0) | ||
for check, result := range r.checks.Seq() { | ||
checks = append(checks, fmt.Sprintf("%v=%v", check, result)) | ||
} | ||
return strings.Join(checks, "; ") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will actually run on dogfood as well. Is this what we want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup! makes for a nice quick resource to check