mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: add x-authz-checks debug header when running in dev mode (#16873)
This commit is contained in:
@ -314,6 +314,9 @@ func New(options *Options) *API {
|
||||
|
||||
if options.Authorizer == nil {
|
||||
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
|
||||
if buildinfo.IsDev() {
|
||||
options.Authorizer = rbac.Recorder(options.Authorizer)
|
||||
}
|
||||
}
|
||||
|
||||
if options.AccessControlStore == nil {
|
||||
@ -456,8 +459,14 @@ func New(options *Options) *API {
|
||||
options.NotificationsEnqueuer = notifications.NewNoopEnqueuer()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
r := chi.NewRouter()
|
||||
// We add this middleware early, to make sure that authorization checks made
|
||||
// by other middleware get recorded.
|
||||
if buildinfo.IsDev() {
|
||||
r.Use(httpmw.RecordAuthzChecks)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// nolint:gocritic // Load deployment ID. This never changes
|
||||
depID, err := options.Database.GetDeploymentID(dbauthz.AsSystemRestricted(ctx))
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/coder/websocket/wsjson"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
@ -198,6 +199,20 @@ func Write(ctx context.Context, rw http.ResponseWriter, status int, response int
|
||||
_, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
if rec, ok := rbac.GetAuthzCheckRecorder(ctx); ok {
|
||||
// If you're here because you saw this header in a response, and you're
|
||||
// trying to investigate the code, here are a couple of notable things
|
||||
// for you to know:
|
||||
// - If any of the checks are `false`, they might not represent the whole
|
||||
// picture. There could be additional checks that weren't performed,
|
||||
// because processing stopped after the failure.
|
||||
// - The checks are recorded by the `authzRecorder` type, which is
|
||||
// configured on server startup for development and testing builds.
|
||||
// - If this header is missing from a response, make sure the response is
|
||||
// being written by calling `httpapi.Write`!
|
||||
rw.Header().Set("x-authz-checks", rec.String())
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
rw.WriteHeader(status)
|
||||
|
||||
@ -213,6 +228,10 @@ func WriteIndent(ctx context.Context, rw http.ResponseWriter, status int, respon
|
||||
_, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
if rec, ok := rbac.GetAuthzCheckRecorder(ctx); ok {
|
||||
rw.Header().Set("x-authz-checks", rec.String())
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
rw.WriteHeader(status)
|
||||
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
)
|
||||
|
||||
// AsAuthzSystem is a chained handler that temporarily sets the dbauthz context
|
||||
@ -35,3 +36,15 @@ func AsAuthzSystem(mws ...func(http.Handler) http.Handler) func(http.Handler) ht
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RecordAuthzChecks enables recording all of the authorization checks that
|
||||
// occurred in the processing of a request. This is mostly helpful for debugging
|
||||
// and understanding what permissions are required for a given action.
|
||||
//
|
||||
// Requires using a Recorder Authorizer.
|
||||
func RecordAuthzChecks(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(rbac.WithAuthzCheckRecorder(r.Context()))
|
||||
next.ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -362,11 +363,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 +742,112 @@ 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{}
|
||||
|
||||
type AuthzCheckRecorder struct {
|
||||
// lock guards checks
|
||||
lock sync.Mutex
|
||||
// checks is a list preformatted authz check IDs and their result
|
||||
checks []recordedCheck
|
||||
}
|
||||
|
||||
type recordedCheck struct {
|
||||
name string
|
||||
// true => authorized, false => not authorized
|
||||
result bool
|
||||
}
|
||||
|
||||
func WithAuthzCheckRecorder(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authzCheckRecorderKey{}, &AuthzCheckRecorder{})
|
||||
}
|
||||
|
||||
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.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
r.checks = append(r.checks, recordedCheck{name: b.String(), result: authorized})
|
||||
}
|
||||
|
||||
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 {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
if len(r.checks) == 0 {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
checks := make([]string, 0, len(r.checks))
|
||||
for _, check := range r.checks {
|
||||
checks = append(checks, fmt.Sprintf("%v=%v", check.name, check.result))
|
||||
}
|
||||
return strings.Join(checks, "; ")
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@ -273,8 +272,7 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
||||
organizationIDsByUserID[organizationIDsByMemberIDsRow.UserID] = organizationIDsByMemberIDsRow.OrganizationIDs
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, codersdk.GetUsersResponse{
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.GetUsersResponse{
|
||||
Users: convertUsers(users, organizationIDsByUserID),
|
||||
Count: int(userCount),
|
||||
})
|
||||
|
@ -1,6 +1,8 @@
|
||||
package syncmap
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Map is a type safe sync.Map
|
||||
type Map[K, V any] struct {
|
||||
|
Reference in New Issue
Block a user