mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-03 12:10:31 +00:00
9f6c1d8f47
The OAuth token consent allow-list (TokenAllowedWorkspaces) was enforced only
by RequireWorkspaceAccess, which fires solely for /{slug} path-param routes.
Every MCP-reachable read that is workspace-global or takes the workspace as a
query/body param bypassed the gate, so a token consented to workspace A could
reach data in other co-membership workspaces. Investigation found five
bypasses; this closes all of them:
- pad_search (HIGH): fan-out (no workspace) searched ALL memberships; naming a
workspace returned its item titles + content. Now the fan-out is restricted
to the allow-list and a named non-consented workspace returns empty (no
existence leak).
- pad_workspace.list (the original BUG-2102): filtered by the allow-list.
- pad_workspace.deleted: filtered by the allow-list.
- pad_workspace.audit-log: platform-wide admin surface; denied for
consent-scoped tokens.
- pad_workspace.restore: gated by the allow-list (404 for out-of-consent slugs).
All gates are no-ops for nil/wildcard allow-lists, so PAT auth, web sessions,
and local stdio are unchanged.
The allow-set semantics move into internal/server as the canonical
TokenAllowedWorkspaceSet(ctx) (promoted from internal/mcp's buildAllowSet);
the two mcp call sites (error-hint lister, workspaces resource) and its unit
tests migrate with it, so server handlers and MCP filters share one
implementation instead of drifting per-surface (the pattern that caused this
bug: TASK-977 and TASK-2101 each point-fixed one surface).
Tests: per-handler regression tests carrying the WithTokenAllowedWorkspaces
context MCPBearerAuth produces; each asserts the consent layer (not membership)
drives exclusion, with nil/wildcard baselines guarding against over-blocking.
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
// handleAuditLog returns a filtered audit log. Admin-only.
|
|
// Supports filtering by action, user (user ID), workspace, days, and pagination.
|
|
func (s *Server) handleAuditLog(w http.ResponseWriter, r *http.Request) {
|
|
user := currentUser(r)
|
|
if user == nil || user.Role != "admin" {
|
|
writeError(w, http.StatusForbidden, "forbidden", "Admin access required")
|
|
return
|
|
}
|
|
|
|
// OAuth consent scoping (BUG-2102): the audit log is a platform-wide,
|
|
// cross-workspace admin surface with no per-workspace routing, so a
|
|
// consent-scoped OAuth token (specific allow-list) must not receive it —
|
|
// it would leak audit entries for every workspace and user, well outside
|
|
// the token's granted scope. nil/wildcard allow-list (PAT / web session /
|
|
// broad consent) is unaffected.
|
|
if TokenAllowedWorkspaceSet(r.Context()) != nil {
|
|
writeError(w, http.StatusForbidden, "forbidden",
|
|
"The platform audit log is not available to workspace-scoped tokens")
|
|
return
|
|
}
|
|
|
|
// Accept both "user" and "actor" query params for filtering by user ID.
|
|
actorFilter := r.URL.Query().Get("user")
|
|
if actorFilter == "" {
|
|
actorFilter = r.URL.Query().Get("actor")
|
|
}
|
|
|
|
params := models.AuditLogParams{
|
|
Action: r.URL.Query().Get("action"),
|
|
Actor: actorFilter,
|
|
WorkspaceID: r.URL.Query().Get("workspace"),
|
|
Days: 30, // default
|
|
}
|
|
|
|
if d := r.URL.Query().Get("days"); d != "" {
|
|
if days, err := strconv.Atoi(d); err == nil && days > 0 {
|
|
params.Days = days
|
|
}
|
|
}
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if limit, err := strconv.Atoi(l); err == nil && limit > 0 {
|
|
params.Limit = limit
|
|
}
|
|
}
|
|
if o := r.URL.Query().Get("offset"); o != "" {
|
|
if offset, err := strconv.Atoi(o); err == nil && offset >= 0 {
|
|
params.Offset = offset
|
|
}
|
|
}
|
|
|
|
activities, err := s.store.ListAuditLog(params)
|
|
if err != nil {
|
|
writeInternalError(w, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, activities)
|
|
}
|