feat(api): unify permission checks for hide/unhide/archive operations

Add unified permission model for skill lifecycle operations:
- Skill owners can hide/unhide/archive their own skills
- Namespace admins/owners can manage skills in their namespace
- Platform admins retain full access via admin endpoints

Changes:
- SkillGovernanceService: add permission checks to hideSkill/unhideSkill
- SkillLifecycleAppService: add hideSkill/unhideSkill methods
- GovernanceWorkflowAppService: add facade methods for hide/unhide
- SkillLifecycleController: add POST /{namespace}/{slug}/hide and /unhide endpoints

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
chenbaowang 2026-04-24 15:19:17 +08:00
parent c14b844c10
commit 1ba1c9018a
4 changed files with 113 additions and 17 deletions

View file

@ -141,11 +141,11 @@ public class SkillLifecycleController extends BaseApiController {
@PostMapping("/{namespace}/{slug}/confirm-publish")
public ApiResponse<SkillLifecycleMutationResponse> confirmPublish(@PathVariable String namespace,
@PathVariable String slug,
@Valid @RequestBody ConfirmPublishRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
HttpServletRequest httpRequest) {
@PathVariable String slug,
@Valid @RequestBody ConfirmPublishRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.confirmPublish(
namespace,
@ -155,4 +155,36 @@ public class SkillLifecycleController extends BaseApiController {
userNsRoles,
AuditRequestContext.from(httpRequest)));
}
@PostMapping("/{namespace}/{slug}/hide")
public ApiResponse<SkillLifecycleMutationResponse> hideSkill(@PathVariable String namespace,
@PathVariable String slug,
@RequestBody(required = false) AdminSkillActionRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.hideSkill(
namespace,
slug,
request,
userId,
userNsRoles,
AuditRequestContext.from(httpRequest)));
}
@PostMapping("/{namespace}/{slug}/unhide")
public ApiResponse<SkillLifecycleMutationResponse> unhideSkill(@PathVariable String namespace,
@PathVariable String slug,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.unhideSkill(
namespace,
slug,
userId,
userNsRoles,
AuditRequestContext.from(httpRequest)));
}
}

View file

@ -273,11 +273,11 @@ public class GovernanceWorkflowAppService {
}
public SkillLifecycleMutationResponse confirmPublish(String namespace,
String slug,
String version,
String userId,
Map<Long, NamespaceRole> userNsRoles,
AuditRequestContext auditContext) {
String slug,
String version,
String userId,
Map<Long, NamespaceRole> userNsRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.confirmPublish(
namespace,
slug,
@ -286,4 +286,21 @@ public class GovernanceWorkflowAppService {
userNsRoles,
auditContext);
}
public SkillLifecycleMutationResponse hideSkill(String namespace,
String slug,
AdminSkillActionRequest request,
String userId,
Map<Long, NamespaceRole> userNsRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.hideSkill(namespace, slug, request, userId, userNsRoles, auditContext);
}
public SkillLifecycleMutationResponse unhideSkill(String namespace,
String slug,
String userId,
Map<Long, NamespaceRole> userNsRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.unhideSkill(namespace, slug, userId, userNsRoles, auditContext);
}
}

View file

@ -213,11 +213,11 @@ public class SkillLifecycleAppService {
@Transactional
public SkillLifecycleMutationResponse confirmPublish(String namespace,
String slug,
String version,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
AuditRequestContext auditContext) {
String slug,
String version,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
SkillVersion skillVersion = findVersion(skill.getId(), version);
skillReviewSubmitService.confirmPublish(
@ -244,6 +244,42 @@ public class SkillLifecycleAppService {
);
}
@Transactional
public SkillLifecycleMutationResponse hideSkill(String namespace,
String slug,
AdminSkillActionRequest request,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
Skill hidden = skillGovernanceService.hideSkill(
skill.getId(),
userId,
normalizeRoles(userNamespaceRoles),
auditContext.clientIp(),
auditContext.userAgent(),
request != null ? request.reason() : null
);
return new SkillLifecycleMutationResponse(hidden.getId(), null, "HIDE", hidden.getStatus().name());
}
@Transactional
public SkillLifecycleMutationResponse unhideSkill(String namespace,
String slug,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
Skill unhidden = skillGovernanceService.unhideSkill(
skill.getId(),
userId,
normalizeRoles(userNamespaceRoles),
auditContext.clientIp(),
auditContext.userAgent()
);
return new SkillLifecycleMutationResponse(unhidden.getId(), null, "UNHIDE", unhidden.getStatus().name());
}
private Skill findSkill(String namespaceSlug, String skillSlug, String currentUserId) {
String cleanNamespace = namespaceSlug.startsWith("@") ? namespaceSlug.substring(1) : namespaceSlug;
Namespace namespace = namespaceRepository.findBySlug(cleanNamespace)

View file

@ -69,9 +69,15 @@ public class SkillGovernanceService {
}
@Transactional
public Skill hideSkill(Long skillId, String actorUserId, String clientIp, String userAgent, String reason) {
public Skill hideSkill(Long skillId,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
String clientIp,
String userAgent,
String reason) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
skill.setHidden(true);
skill.setHiddenAt(currentInstant());
skill.setHiddenBy(actorUserId);
@ -120,9 +126,14 @@ public class SkillGovernanceService {
}
@Transactional
public Skill unhideSkill(Long skillId, String actorUserId, String clientIp, String userAgent) {
public Skill unhideSkill(Long skillId,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
String clientIp,
String userAgent) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
skill.setHidden(false);
skill.setHiddenAt(null);
skill.setHiddenBy(null);