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

Method RequireWorkspaceAccess

internal/server/middleware_auth.go:528–686  ·  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

526// "owner" role. Legacy API tokens (workspace-scoped, no user) are allowed
527// if the token's workspace matches the requested workspace.
528func (s *Server) RequireWorkspaceAccess(next http.Handler) http.Handler {
529 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
530 slugOrID := chi.URLParam(r, "slug")
531 if slugOrID == "" {
532 next.ServeHTTP(w, r)
533 return
534 }
535
536 // Resolve workspace: supports both UUID and slug.
537 ws, err := s.resolveWorkspace(slugOrID, currentUser(r))
538 if err != nil {
539 writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve workspace")
540 return
541 }
542 if ws == nil {
543 writeError(w, http.StatusNotFound, "not_found", "Workspace not found")
544 return
545 }
546
547 // OAuth token allow-list gate (TASK-953). The consent UI
548 // (TASK-952) lets the user pick which workspaces a token
549 // can access; MCPBearerAuth stashes that list in context
550 // via WithTokenAllowedWorkspaces. Reject any request hitting
551 // a workspace outside the list, even if the user is a
552 // member of it — the user explicitly chose not to grant the
553 // app that access.
554 //
555 // nil → no token-level constraint (PAT auth, or pre-TASK-952
556 // OAuth tokens that predate the consent UI). Wildcard `["*"]`
557 // → grant access to any membership the user has. Else: the
558 // resolved workspace's slug MUST appear in the list.
559 //
560 // Compares against the canonical slug (ws.Slug) because the
561 // consent UI persists slugs and the URL slugOrID may be a
562 // UUID which resolveWorkspace just translated. Slug-vs-slug
563 // is the right comparison.
564 if !tokenAllowedWorkspaceMatches(r.Context(), ws.Slug) {
565 // TASK-961: count workspace-allow-list denials so the
566 // MCP dashboard can flag tokens hitting workspaces outside
567 // their consent scope. Gated on MCP-origin requests only
568 // (presence of MCP token identity in context) so non-MCP
569 // /api/v1 traffic — which can't even reach this gate
570 // today, but might in a future PAT-with-allow-list world
571 // — doesn't pollute the MCP-specific counter.
572 s.recordMCPAuthzDenial(r, "workspace_not_in_allowlist")
573 writeError(w, http.StatusForbidden, "permission_denied",
574 "Token is not authorized for this workspace")
575 return
576 }
577
578 // Store resolved workspace ID in context for downstream handlers
579 ctx := context.WithValue(r.Context(), ctxResolvedWorkspaceID, ws.ID)
580
581 // Fresh install: no users → everyone gets owner access
582 count, _ := s.store.UserCount()
583 if count == 0 {
584 ctx = context.WithValue(ctx, ctxWorkspaceRole, "owner")
585 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