MCPcopy Create free account
hub / github.com/PerpetualSoftware/pad / RequireWorkspaceAccess

Method RequireWorkspaceAccess

internal/server/middleware_auth.go:590–748  ·  view source on GitHub ↗

RequireWorkspaceAccess checks that the current user is a member of the workspace identified by the {slug} URL parameter. The user's workspace role is stored in the request context for downstream permission checks. When no users exist (fresh install), access is granted with an implicit "owner" role.

(next http.Handler)

Source from the content-addressed store, hash-verified

588// "owner" role. Legacy API tokens (workspace-scoped, no user) are allowed
589// if the token's workspace matches the requested workspace.
590func (s *Server) RequireWorkspaceAccess(next http.Handler) http.Handler {
591 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
592 slugOrID := chi.URLParam(r, "slug")
593 if slugOrID == "" {
594 next.ServeHTTP(w, r)
595 return
596 }
597
598 // Resolve workspace: supports both UUID and slug.
599 ws, err := s.resolveWorkspace(slugOrID, currentUser(r))
600 if err != nil {
601 writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve workspace")
602 return
603 }
604 if ws == nil {
605 writeError(w, http.StatusNotFound, "not_found", "Workspace not found")
606 return
607 }
608
609 // OAuth token allow-list gate (TASK-953). The consent UI
610 // (TASK-952) lets the user pick which workspaces a token
611 // can access; MCPBearerAuth stashes that list in context
612 // via WithTokenAllowedWorkspaces. Reject any request hitting
613 // a workspace outside the list, even if the user is a
614 // member of it — the user explicitly chose not to grant the
615 // app that access.
616 //
617 // nil → no token-level constraint (PAT auth, or pre-TASK-952
618 // OAuth tokens that predate the consent UI). Wildcard `["*"]`
619 // → grant access to any membership the user has. Else: the
620 // resolved workspace's slug MUST appear in the list.
621 //
622 // Compares against the canonical slug (ws.Slug) because the
623 // consent UI persists slugs and the URL slugOrID may be a
624 // UUID which resolveWorkspace just translated. Slug-vs-slug
625 // is the right comparison.
626 if !tokenAllowedWorkspaceMatches(r.Context(), ws.Slug) {
627 // TASK-961: count workspace-allow-list denials so the
628 // MCP dashboard can flag tokens hitting workspaces outside
629 // their consent scope. Gated on MCP-origin requests only
630 // (presence of MCP token identity in context) so non-MCP
631 // /api/v1 traffic — which can't even reach this gate
632 // today, but might in a future PAT-with-allow-list world
633 // — doesn't pollute the MCP-specific counter.
634 s.recordMCPAuthzDenial(r, "workspace_not_in_allowlist")
635 writeError(w, http.StatusForbidden, "permission_denied",
636 "Token is not authorized for this workspace")
637 return
638 }
639
640 // Store resolved workspace ID in context for downstream handlers
641 ctx := context.WithValue(r.Context(), ctxResolvedWorkspaceID, ws.ID)
642
643 // Fresh install: no users → everyone gets owner access
644 count, _ := s.store.UserCount()
645 if count == 0 {
646 ctx = context.WithValue(ctx, ctxWorkspaceRole, "owner")
647 next.ServeHTTP(w, r.WithContext(ctx))

Callers

nothing calls this directly

Calls 12

resolveWorkspaceMethod · 0.95
recordMCPAuthzDenialMethod · 0.95
currentUserFunction · 0.85
writeErrorFunction · 0.85
tokenWorkspaceIDFunction · 0.85
isBearerAuthFunction · 0.85
UserCountMethod · 0.80
GetWorkspaceMemberMethod · 0.80
ServeHTTPMethod · 0.45
ErrorMethod · 0.45

Tested by

no test coverage detected