From b4b69f89f689ea5e845ecd7e10ee809596034108 Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:48:57 +0800 Subject: [PATCH] fix(server): allow members to create and manage their own skills (#1017) Remove admin/owner-only restriction from skill creation and import routes. Add canManageSkill helper that lets skill creators manage their own skills, matching the existing canManageAgent pattern for agents. --- server/cmd/server/router.go | 4 ++-- server/internal/handler/skill.go | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/server/cmd/server/router.go b/server/cmd/server/router.go index b0ac83ba04..7f4af346b6 100644 --- a/server/cmd/server/router.go +++ b/server/cmd/server/router.go @@ -278,8 +278,8 @@ 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.With(middleware.RequireWorkspaceRole(queries, "owner", "admin")).Post("/", h.CreateSkill) - r.With(middleware.RequireWorkspaceRole(queries, "owner", "admin")).Post("/import", h.ImportSkill) + r.Post("/", h.CreateSkill) + r.Post("/import", h.ImportSkill) r.Route("/{id}", func(r chi.Router) { r.Get("/", h.GetSkill) r.Put("/", h.UpdateSkill) diff --git a/server/internal/handler/skill.go b/server/internal/handler/skill.go index c1a7404daf..b799b91c4e 100644 --- a/server/internal/handler/skill.go +++ b/server/internal/handler/skill.go @@ -268,13 +268,30 @@ func (h *Handler) CreateSkill(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, resp) } +// canManageSkill checks whether the current user can update or delete a skill. +// The skill creator or workspace owner/admin can manage any skill. +func (h *Handler) canManageSkill(w http.ResponseWriter, r *http.Request, skill db.Skill) bool { + wsID := uuidToString(skill.WorkspaceID) + member, ok := h.requireWorkspaceRole(w, r, wsID, "skill not found", "owner", "admin", "member") + if !ok { + return false + } + isAdmin := roleAllowed(member.Role, "owner", "admin") + isSkillCreator := skill.CreatedBy.Valid && uuidToString(skill.CreatedBy) == requestUserID(r) + if !isAdmin && !isSkillCreator { + writeError(w, http.StatusForbidden, "only the skill creator can manage this skill") + return false + } + return true +} + func (h *Handler) UpdateSkill(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") skill, ok := h.loadSkillForUser(w, r, id) if !ok { return } - if _, ok := h.requireWorkspaceRole(w, r, uuidToString(skill.WorkspaceID), "skill not found", "owner", "admin"); !ok { + if !h.canManageSkill(w, r, skill) { return } @@ -376,7 +393,7 @@ func (h *Handler) DeleteSkill(w http.ResponseWriter, r *http.Request) { if !ok { return } - if _, ok := h.requireWorkspaceRole(w, r, uuidToString(skill.WorkspaceID), "skill not found", "owner", "admin"); !ok { + if !h.canManageSkill(w, r, skill) { return } @@ -913,7 +930,7 @@ func (h *Handler) UpsertSkillFile(w http.ResponseWriter, r *http.Request) { if !ok { return } - if _, ok := h.requireWorkspaceRole(w, r, uuidToString(skill.WorkspaceID), "skill not found", "owner", "admin"); !ok { + if !h.canManageSkill(w, r, skill) { return } @@ -947,7 +964,7 @@ func (h *Handler) DeleteSkillFile(w http.ResponseWriter, r *http.Request) { if !ok { return } - if _, ok := h.requireWorkspaceRole(w, r, uuidToString(skill.WorkspaceID), "skill not found", "owner", "admin"); !ok { + if !h.canManageSkill(w, r, skill) { return }