From eeb3d63be6c0447200452cb2ce62906d2f752e58 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 29 Mar 2024 10:14:27 -0500 Subject: [PATCH] chore: merge authorization contexts (#12816) * chore: merge authorization contexts Instead of 2 auth contexts from apikey and dbauthz, merge them to just use dbauthz. It is annoying to have two. * fixup authorization reference --- coderd/authorize.go | 34 ++++++++--------- coderd/coderd.go | 2 +- coderd/database/dbauthz/dbauthz.go | 12 ++++-- coderd/httpmw/apikey.go | 51 +++++++++---------------- coderd/httpmw/authorize_test.go | 4 +- coderd/httpmw/provisionerdaemon.go | 14 ------- coderd/httpmw/ratelimit.go | 2 +- coderd/httpmw/workspaceproxy.go | 12 ------ coderd/identityprovider/middleware.go | 2 +- coderd/rbac/authz.go | 6 +++ coderd/roles.go | 4 +- coderd/users.go | 2 +- coderd/workspaceapps/db.go | 14 +++---- enterprise/coderd/coderd.go | 4 +- enterprise/coderd/provisionerdaemons.go | 2 +- site/site.go | 2 +- 16 files changed, 68 insertions(+), 99 deletions(-) diff --git a/coderd/authorize.go b/coderd/authorize.go index e8d4274ab8..19a633bf46 100644 --- a/coderd/authorize.go +++ b/coderd/authorize.go @@ -19,15 +19,15 @@ import ( // This is faster than calling Authorize() on each object. func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action rbac.Action, objects []O) ([]O, error) { roles := httpmw.UserAuthorization(r) - objects, err := rbac.Filter(r.Context(), h.Authorizer, roles.Actor, action, objects) + objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects) if err != nil { // Log the error as Filter should not be erroring. h.Logger.Error(r.Context(), "authorization filter failed", slog.Error(err), - slog.F("user_id", roles.Actor.ID), - slog.F("username", roles.ActorName), - slog.F("roles", roles.Actor.SafeRoleNames()), - slog.F("scope", roles.Actor.SafeScopeName()), + slog.F("user_id", roles.ID), + slog.F("username", roles), + slog.F("roles", roles.SafeRoleNames()), + slog.F("scope", roles.SafeScopeName()), slog.F("route", r.URL.Path), slog.F("action", action), ) @@ -65,7 +65,7 @@ func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objec // } func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool { roles := httpmw.UserAuthorization(r) - err := h.Authorizer.Authorize(r.Context(), roles.Actor, action, object.RBACObject()) + err := h.Authorizer.Authorize(r.Context(), roles, action, object.RBACObject()) if err != nil { // Log the errors for debugging internalError := new(rbac.UnauthorizedError) @@ -76,10 +76,10 @@ func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object r // Log information for debugging. This will be very helpful // in the early days logger.Warn(r.Context(), "requester is not authorized to access the object", - slog.F("roles", roles.Actor.SafeRoleNames()), - slog.F("actor_id", roles.Actor.ID), - slog.F("actor_name", roles.ActorName), - slog.F("scope", roles.Actor.SafeScopeName()), + slog.F("roles", roles.SafeRoleNames()), + slog.F("actor_id", roles.ID), + slog.F("actor_name", roles), + slog.F("scope", roles.SafeScopeName()), slog.F("route", r.URL.Path), slog.F("action", action), slog.F("object", object), @@ -97,7 +97,7 @@ func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object r // Note the authorization is only for the given action and object type. func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action rbac.Action, objectType string) (rbac.PreparedAuthorized, error) { roles := httpmw.UserAuthorization(r) - prepared, err := h.Authorizer.Prepare(r.Context(), roles.Actor, action, objectType) + prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType) if err != nil { return nil, xerrors.Errorf("prepare filter: %w", err) } @@ -128,10 +128,10 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) { api.Logger.Debug(ctx, "check-auth", slog.F("my_id", httpmw.APIKey(r).UserID), - slog.F("got_id", auth.Actor.ID), - slog.F("name", auth.ActorName), - slog.F("roles", auth.Actor.SafeRoleNames()), - slog.F("scope", auth.Actor.SafeScopeName()), + slog.F("got_id", auth.ID), + slog.F("name", auth), + slog.F("roles", auth.SafeRoleNames()), + slog.F("scope", auth.SafeScopeName()), ) response := make(codersdk.AuthorizationResponse) @@ -171,7 +171,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) { Type: v.Object.ResourceType.String(), } if obj.Owner == "me" { - obj.Owner = auth.Actor.ID + obj.Owner = auth.ID } // If a resource ID is specified, fetch that specific resource. @@ -219,7 +219,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) { obj = dbObj.RBACObject() } - err := api.Authorizer.Authorize(ctx, auth.Actor, rbac.Action(v.Action), obj) + err := api.Authorizer.Authorize(ctx, auth, rbac.Action(v.Action), obj) response[k] = err == nil } diff --git a/coderd/coderd.go b/coderd/coderd.go index 77d52d05b5..0cc0962316 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -133,7 +133,7 @@ type Options struct { // after a successful authentication. // This is somewhat janky, but seemingly the only reasonable way to add a header // for all authenticated users under a condition, only in Enterprise. - PostAuthAdditionalHeadersFunc func(auth httpmw.Authorization, header http.Header) + PostAuthAdditionalHeadersFunc func(auth rbac.Subject, header http.Header) // TLSCertificates is used to mesh DERP servers securely. TLSCertificates []tls.Certificate diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 93dd6617c4..97a695cb37 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -155,7 +155,8 @@ func ActorFromContext(ctx context.Context) (rbac.Subject, bool) { var ( subjectProvisionerd = rbac.Subject{ - ID: uuid.Nil.String(), + FriendlyName: "Provisioner Daemon", + ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Name: "provisionerd", @@ -182,7 +183,8 @@ var ( }.WithCachedASTValue() subjectAutostart = rbac.Subject{ - ID: uuid.Nil.String(), + FriendlyName: "Autostart", + ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Name: "autostart", @@ -203,7 +205,8 @@ var ( // See unhanger package. subjectHangDetector = rbac.Subject{ - ID: uuid.Nil.String(), + FriendlyName: "Hang Detector", + ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Name: "hangdetector", @@ -221,7 +224,8 @@ var ( }.WithCachedASTValue() subjectSystemRestricted = rbac.Subject{ - ID: uuid.Nil.String(), + FriendlyName: "System", + ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ { Name: "system", diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 733e722e04..0bd064bf8e 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -44,27 +44,15 @@ func APIKey(r *http.Request) database.APIKey { return key } -// User roles are the 'subject' field of Authorize() -type userAuthKey struct{} - -type Authorization struct { - Actor rbac.Subject - // ActorName is required for logging and human friendly related identification. - // It is usually the "username" of the user, but it can be the name of the - // external workspace proxy or other service type actor. - ActorName string -} - // UserAuthorizationOptional may return the roles and scope used for // authorization. Depends on the ExtractAPIKey handler. -func UserAuthorizationOptional(r *http.Request) (Authorization, bool) { - auth, ok := r.Context().Value(userAuthKey{}).(Authorization) - return auth, ok +func UserAuthorizationOptional(r *http.Request) (rbac.Subject, bool) { + return dbauthz.ActorFromContext(r.Context()) } // UserAuthorization returns the roles and scope used for authorization. Depends // on the ExtractAPIKey handler. -func UserAuthorization(r *http.Request) Authorization { +func UserAuthorization(r *http.Request) rbac.Subject { auth, ok := UserAuthorizationOptional(r) if !ok { panic("developer error: ExtractAPIKey middleware not provided") @@ -119,7 +107,7 @@ type ExtractAPIKeyConfig struct { // // This is originally implemented to send entitlement warning headers after // a user is authenticated to prevent additional CLI invocations. - PostAuthAdditionalHeadersFunc func(a Authorization, header http.Header) + PostAuthAdditionalHeadersFunc func(a rbac.Subject, header http.Header) } // ExtractAPIKeyMW calls ExtractAPIKey with the given config on each request, @@ -142,9 +130,8 @@ func ExtractAPIKeyMW(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { // Actor is the user's authorization context. ctx := r.Context() ctx = context.WithValue(ctx, apiKeyContextKey{}, key) - ctx = context.WithValue(ctx, userAuthKey{}, authz) - // Set the auth context for the authzquerier as well. - ctx = dbauthz.As(ctx, authz.Actor) + // Set the auth context for the user. + ctx = dbauthz.As(ctx, authz) next.ServeHTTP(rw, r.WithContext(ctx)) }) @@ -209,12 +196,12 @@ func APIKeyFromRequest(ctx context.Context, db database.Store, sessionTokenFunc // and authz object may be returned. False is returned if a response was written // to the request and the caller should give up. // nolint:revive -func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyConfig) (*database.APIKey, *Authorization, bool) { +func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyConfig) (*database.APIKey, *rbac.Subject, bool) { ctx := r.Context() // Write wraps writing a response to redirect if the handler // specified it should. This redirect is used for user-facing pages // like workspace applications. - write := func(code int, response codersdk.Response) (*database.APIKey, *Authorization, bool) { + write := func(code int, response codersdk.Response) (*database.APIKey, *rbac.Subject, bool) { if cfg.RedirectToLogin { RedirectToLogin(rw, r, nil, response.Message) return nil, nil, false @@ -229,7 +216,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon // // It should be used when the API key is not provided or is invalid, // but not when there are other errors. - optionalWrite := func(code int, response codersdk.Response) (*database.APIKey, *Authorization, bool) { + optionalWrite := func(code int, response codersdk.Response) (*database.APIKey, *rbac.Subject, bool) { if cfg.Optional { return nil, nil, true } @@ -451,21 +438,19 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon } // Actor is the user's authorization context. - authz := Authorization{ - ActorName: roles.Username, - Actor: rbac.Subject{ - ID: key.UserID.String(), - Roles: rbac.RoleNames(roles.Roles), - Groups: roles.Groups, - Scope: rbac.ScopeName(key.Scope), - }.WithCachedASTValue(), - } + actor := rbac.Subject{ + FriendlyName: roles.Username, + ID: key.UserID.String(), + Roles: rbac.RoleNames(roles.Roles), + Groups: roles.Groups, + Scope: rbac.ScopeName(key.Scope), + }.WithCachedASTValue() if cfg.PostAuthAdditionalHeadersFunc != nil { - cfg.PostAuthAdditionalHeadersFunc(authz, rw.Header()) + cfg.PostAuthAdditionalHeadersFunc(actor, rw.Header()) } - return key, &authz, true + return key, &actor, true } // APITokenFromRequest returns the api token from the request. diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go index 42be7f4a9a..c67be2ca2b 100644 --- a/coderd/httpmw/authorize_test.go +++ b/coderd/httpmw/authorize_test.go @@ -127,8 +127,8 @@ func TestExtractUserRoles(t *testing.T) { ) rtr.Get("/", func(_ http.ResponseWriter, r *http.Request) { roles := httpmw.UserAuthorization(r) - require.Equal(t, user.ID.String(), roles.Actor.ID) - require.ElementsMatch(t, expRoles, roles.Actor.Roles.Names()) + require.Equal(t, user.ID.String(), roles.ID) + require.ElementsMatch(t, expRoles, roles.Roles.Names()) }) req := httptest.NewRequest("GET", "/", nil) diff --git a/coderd/httpmw/provisionerdaemon.go b/coderd/httpmw/provisionerdaemon.go index 84e335751f..d0fbfe0e6b 100644 --- a/coderd/httpmw/provisionerdaemon.go +++ b/coderd/httpmw/provisionerdaemon.go @@ -5,8 +5,6 @@ import ( "crypto/subtle" "net/http" - "golang.org/x/xerrors" - "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" @@ -68,18 +66,6 @@ func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, ps ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true) // nolint:gocritic // Authenticating as a provisioner daemon. ctx = dbauthz.AsProvisionerd(ctx) - subj, ok := dbauthz.ActorFromContext(ctx) - if !ok { - // This should never happen - httpapi.InternalServerError(w, xerrors.New("developer error: ExtractProvisionerDaemonAuth missing rbac actor")) - } - - // Use the same subject for the userAuthKey - ctx = context.WithValue(ctx, userAuthKey{}, Authorization{ - Actor: subj, - ActorName: "provisioner_daemon", - }) - next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/coderd/httpmw/ratelimit.go b/coderd/httpmw/ratelimit.go index bd1d1d6423..932373b5ba 100644 --- a/coderd/httpmw/ratelimit.go +++ b/coderd/httpmw/ratelimit.go @@ -47,7 +47,7 @@ func RateLimit(count int, window time.Duration) func(http.Handler) http.Handler // We avoid using rbac.Authorizer since rego is CPU-intensive // and undermines the DoS-prevention goal of the rate limiter. - for _, role := range auth.Actor.SafeRoleNames() { + for _, role := range auth.SafeRoleNames() { if role == rbac.RoleOwner() { // HACK: use a random key each time to // de facto disable rate limiting. The diff --git a/coderd/httpmw/workspaceproxy.go b/coderd/httpmw/workspaceproxy.go index d3a93962aa..8ee5318785 100644 --- a/coderd/httpmw/workspaceproxy.go +++ b/coderd/httpmw/workspaceproxy.go @@ -141,18 +141,6 @@ func ExtractWorkspaceProxy(opts ExtractWorkspaceProxyConfig) func(http.Handler) // they can still only access the routes that the middleware is // mounted to. ctx = dbauthz.AsSystemRestricted(ctx) - subj, ok := dbauthz.ActorFromContext(ctx) - if !ok { - // This should never happen - httpapi.InternalServerError(w, xerrors.New("developer error: ExtractWorkspaceProxy missing rbac actor")) - return - } - // Use the same subject for the userAuthKey - ctx = context.WithValue(ctx, userAuthKey{}, Authorization{ - Actor: subj, - ActorName: "proxy_" + proxy.Name, - }) - next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/coderd/identityprovider/middleware.go b/coderd/identityprovider/middleware.go index 663e9f608e..1704ab2270 100644 --- a/coderd/identityprovider/middleware.go +++ b/coderd/identityprovider/middleware.go @@ -142,7 +142,7 @@ func authorizeMW(accessURL *url.URL) func(next http.Handler) http.Handler { AppName: app.Name, CancelURI: cancel.String(), RedirectURI: r.URL.String(), - Username: ua.ActorName, + Username: ua.FriendlyName, }) }) } diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 51a28882c4..08ff61da13 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -74,6 +74,12 @@ func hashAuthorizeCall(actor Subject, action Action, object Object) [32]byte { // Subject is a struct that contains all the elements of a subject in an rbac // authorize. type Subject struct { + // FriendlyName is entirely optional and is used for logging and debugging + // It is not used in any functional way. + // It is usually the "username" of the user, but it can be the name of the + // external workspace proxy or other service type actor. + FriendlyName string + ID string Roles ExpandableRoles Groups []string diff --git a/coderd/roles.go b/coderd/roles.go index d985d7dca1..7d49eadba7 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -28,7 +28,7 @@ func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { } roles := rbac.SiteRoles() - httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Actor.Roles, roles)) + httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles)) } // assignableSiteRoles returns all org wide roles that can be assigned. @@ -52,7 +52,7 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { } roles := rbac.OrganizationRoles(organization.ID) - httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Actor.Roles, roles)) + httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles)) } func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role) []codersdk.AssignableRoles { diff --git a/coderd/users.go b/coderd/users.go index a0dc128fc9..47cb6e1cd3 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -518,7 +518,7 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) { aReq.Old = user defer commitAudit() - if auth.Actor.ID == user.ID.String() { + if auth.ID == user.ID.String() { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "You cannot delete yourself!", }) diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index c514b1d3f9..32eaec1cf0 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -224,7 +224,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r * // are warnings that aid in debugging. These messages do not prevent authorization, // but may indicate that the request is not configured correctly. // If an error is returned, the request should be aborted with a 500 error. -func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *httpmw.Authorization, dbReq *databaseRequest) (bool, []string, error) { +func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *rbac.Subject, dbReq *databaseRequest) (bool, []string, error) { var warnings []string accessMethod := dbReq.AccessMethod if accessMethod == "" { @@ -267,12 +267,12 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *httpmw.Au // workspaces owned by different users. if isPathApp && sharingLevel == database.AppSharingLevelOwner && - dbReq.Workspace.OwnerID.String() != roles.Actor.ID && + dbReq.Workspace.OwnerID.String() != roles.ID && !p.DeploymentValues.Dangerous.AllowPathAppSiteOwnerAccess.Value() { // This is not ideal to check for the 'owner' role, but we are only checking // to determine whether to show a warning for debugging reasons. This does // not do any authz checks, so it is ok. - if roles != nil && slices.Contains(roles.Actor.Roles.Names(), rbac.RoleOwner()) { + if roles != nil && slices.Contains(roles.Roles.Names(), rbac.RoleOwner()) { warnings = append(warnings, "path-based apps with \"owner\" share level are only accessible by the workspace owner (see --dangerous-allow-path-app-site-owner-access)") } return false, warnings, nil @@ -286,11 +286,11 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *httpmw.Au // rbacResourceOwned is for the level "authenticated". We still need to // make sure the API key has permissions to connect to the actor's own // workspace. Scopes would prevent this. - rbacResourceOwned rbac.Object = rbac.ResourceWorkspaceApplicationConnect.WithOwner(roles.Actor.ID) + rbacResourceOwned rbac.Object = rbac.ResourceWorkspaceApplicationConnect.WithOwner(roles.ID) ) if dbReq.AccessMethod == AccessMethodTerminal { rbacResource = dbReq.Workspace.ExecutionRBAC() - rbacResourceOwned = rbac.ResourceWorkspaceExecution.WithOwner(roles.Actor.ID) + rbacResourceOwned = rbac.ResourceWorkspaceExecution.WithOwner(roles.ID) } // Do a standard RBAC check. This accounts for share level "owner" and any @@ -299,7 +299,7 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *httpmw.Au // Regardless of share level or whether it's enabled or not, the owner of // the workspace can always access applications (as long as their API key's // scope allows it). - err := p.Authorizer.Authorize(ctx, roles.Actor, rbacAction, rbacResource) + err := p.Authorizer.Authorize(ctx, *roles, rbacAction, rbacResource) if err == nil { return true, []string{}, nil } @@ -312,7 +312,7 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *httpmw.Au case database.AppSharingLevelAuthenticated: // Check with the owned resource to ensure the API key has permissions // to connect to the actor's own workspace. This enforces scopes. - err := p.Authorizer.Authorize(ctx, roles.Actor, rbacAction, rbacResourceOwned) + err := p.Authorizer.Authorize(ctx, *roles, rbacAction, rbacResourceOwned) if err == nil { return true, []string{}, nil } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 47759b1942..c3b8cc0199 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -476,8 +476,8 @@ type API struct { // // This header is used by the CLI to display warnings to the user without having // to make additional requests! -func (api *API) writeEntitlementWarningsHeader(a httpmw.Authorization, header http.Header) { - roles, err := a.Actor.Roles.Expand() +func (api *API) writeEntitlementWarningsHeader(a rbac.Subject, header http.Header) { + roles, err := a.Roles.Expand() if err != nil { return } diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index fdf03c2030..0161e7a265 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -107,7 +107,7 @@ func (p *provisionerDaemonAuth) authorize(r *http.Request, tags map[string]strin return tags, true } ua := httpmw.UserAuthorization(r) - if err := p.authorizer.Authorize(ctx, ua.Actor, rbac.ActionCreate, rbac.ResourceProvisionerDaemon); err == nil { + if err := p.authorizer.Authorize(ctx, ua, rbac.ActionCreate, rbac.ResourceProvisionerDaemon); err == nil { // User is allowed to create provisioner daemons return tags, true } diff --git a/site/site.go b/site/site.go index 7875fa3140..1f7a1120f1 100644 --- a/site/site.go +++ b/site/site.go @@ -351,7 +351,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht return execTmpl(tmpl, state) } - ctx := dbauthz.As(r.Context(), actor.Actor) + ctx := dbauthz.As(r.Context(), *actor) var eg errgroup.Group var user database.User