refactor(server): consolidate workspace permission checks into middleware

Move workspace membership and role validation from individual handlers
into dedicated Chi middleware. The new middleware resolves workspace ID
(from query param, X-Workspace-ID header, or URL param), validates
membership via DB, and injects the member into request context.

Handlers now read workspace ID and member from context instead of
calling requireWorkspaceMember/requireWorkspaceRole directly. This
eliminates ~17 duplicated permission checks across handlers and makes
it harder to accidentally omit access control on new routes.
This commit is contained in:
Jiayuan 2026-03-30 03:40:20 +08:00
parent e1e4079da1
commit f4a6e7c475
8 changed files with 198 additions and 64 deletions

View file

@ -111,8 +111,8 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
// Issues
r.Route("/api/issues", func(r chi.Router) {
r.Get("/", h.ListIssues)
r.Post("/", h.CreateIssue)
r.With(middleware.RequireWorkspaceMember(queries)).Get("/", h.ListIssues)
r.With(middleware.RequireWorkspaceMember(queries)).Post("/", h.CreateIssue)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.GetIssue)
r.Put("/", h.UpdateIssue)
@ -134,8 +134,8 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
// Agents
r.Route("/api/agents", func(r chi.Router) {
r.Get("/", h.ListAgents)
r.Post("/", h.CreateAgent)
r.With(middleware.RequireWorkspaceMember(queries)).Get("/", h.ListAgents)
r.With(middleware.RequireWorkspaceRole(queries, "owner", "admin")).Post("/", h.CreateAgent)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.GetAgent)
r.Put("/", h.UpdateAgent)
@ -148,9 +148,9 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
// Skills
r.Route("/api/skills", func(r chi.Router) {
r.Get("/", h.ListSkills)
r.Post("/", h.CreateSkill)
r.Post("/import", h.ImportSkill)
r.With(middleware.RequireWorkspaceMember(queries)).Get("/", h.ListSkills)
r.With(middleware.RequireWorkspaceRole(queries, "owner", "admin")).Post("/", h.CreateSkill)
r.With(middleware.RequireWorkspaceRole(queries, "owner", "admin")).Post("/import", h.ImportSkill)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.GetSkill)
r.Put("/", h.UpdateSkill)
@ -162,7 +162,7 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
})
r.Route("/api/runtimes", func(r chi.Router) {
r.Get("/", h.ListAgentRuntimes)
r.With(middleware.RequireWorkspaceMember(queries)).Get("/", h.ListAgentRuntimes)
r.Get("/{runtimeId}/usage", h.GetRuntimeUsage)
r.Get("/{runtimeId}/activity", h.GetRuntimeTaskActivity)
r.Post("/{runtimeId}/ping", h.InitiatePing)
@ -195,17 +195,26 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
r.Get("/", h.ListWorkspaces)
r.Post("/", h.CreateWorkspace)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.GetWorkspace)
r.Put("/", h.UpdateWorkspace)
r.Patch("/", h.UpdateWorkspace)
r.Delete("/", h.DeleteWorkspace)
r.Get("/members", h.ListMembersWithUser)
r.Post("/members", h.CreateMember)
r.Post("/leave", h.LeaveWorkspace)
r.Route("/members/{memberId}", func(r chi.Router) {
r.Patch("/", h.UpdateMember)
r.Delete("/", h.DeleteMember)
// Member-level access
r.Group(func(r chi.Router) {
r.Use(middleware.RequireWorkspaceMemberFromURL(queries, "id"))
r.Get("/", h.GetWorkspace)
r.Get("/members", h.ListMembersWithUser)
r.Post("/leave", h.LeaveWorkspace)
})
// Admin-level access
r.Group(func(r chi.Router) {
r.Use(middleware.RequireWorkspaceRoleFromURL(queries, "id", "owner", "admin"))
r.Put("/", h.UpdateWorkspace)
r.Patch("/", h.UpdateWorkspace)
r.Post("/members", h.CreateMember)
r.Route("/members/{memberId}", func(r chi.Router) {
r.Patch("/", h.UpdateMember)
r.Delete("/", h.DeleteMember)
})
})
// Owner-only access
r.With(middleware.RequireWorkspaceRoleFromURL(queries, "id", "owner")).Delete("/", h.DeleteWorkspace)
})
})
})