feat(api): enhance permission checks for skill lifecycle operations

Add platformRoles parameter to skill lifecycle methods:
- hideSkill, archiveSkill, unhideSkill, unarchiveSkill, deleteVersion
- Support SUPER_ADMIN and SKILL_ADMIN role bypass
- Add hideSkillAsAdmin and unhideSkillAsAdmin for admin controllers
- Fix userId null checks in rating and star controllers

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 18:34:15 +08:00
parent b25bbf50f3
commit 0bef6fd0c7
8 changed files with 97 additions and 18 deletions

View file

@ -38,10 +38,9 @@ public class AdminSkillController extends BaseApiController {
@RequestBody(required = false) AdminSkillActionRequest request,
@AuthenticationPrincipal PlatformPrincipal principal,
HttpServletRequest httpRequest) {
var skill = skillGovernanceService.hideSkill(
var skill = skillGovernanceService.hideSkillAsAdmin(
skillId,
principal.userId(),
java.util.Map.of(),
httpRequest.getRemoteAddr(),
httpRequest.getHeader("User-Agent"),
request != null ? request.reason() : null
@ -54,10 +53,9 @@ public class AdminSkillController extends BaseApiController {
public ApiResponse<AdminSkillMutationResponse> unhideSkill(@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal,
HttpServletRequest httpRequest) {
var skill = skillGovernanceService.unhideSkill(
var skill = skillGovernanceService.unhideSkillAsAdmin(
skillId,
principal.userId(),
java.util.Map.of(),
httpRequest.getRemoteAddr(),
httpRequest.getHeader("User-Agent")
);

View file

@ -14,6 +14,7 @@ import com.iflytek.skillhub.service.GovernanceWorkflowAppService;
import jakarta.validation.Valid;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Set;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -44,6 +45,7 @@ public class SkillLifecycleController extends BaseApiController {
@RequestBody(required = false) AdminSkillActionRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.archiveSkill(
@ -52,6 +54,7 @@ public class SkillLifecycleController extends BaseApiController {
request,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}
@ -60,6 +63,7 @@ public class SkillLifecycleController extends BaseApiController {
@PathVariable String slug,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.unarchiveSkill(
@ -67,6 +71,7 @@ public class SkillLifecycleController extends BaseApiController {
slug,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}
@ -76,6 +81,7 @@ public class SkillLifecycleController extends BaseApiController {
@PathVariable String version,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.deleted",
governanceWorkflowAppService.deleteVersion(
@ -84,6 +90,7 @@ public class SkillLifecycleController extends BaseApiController {
version,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}
@ -162,6 +169,7 @@ public class SkillLifecycleController extends BaseApiController {
@RequestBody(required = false) AdminSkillActionRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.hideSkill(
@ -170,6 +178,7 @@ public class SkillLifecycleController extends BaseApiController {
request,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}
@ -178,6 +187,7 @@ public class SkillLifecycleController extends BaseApiController {
@PathVariable String slug,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.unhideSkill(
@ -185,6 +195,7 @@ public class SkillLifecycleController extends BaseApiController {
slug,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}
}

View file

@ -1,8 +1,11 @@
package com.iflytek.skillhub.controller.portal;
import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.dto.SkillRatingRequest;
import com.iflytek.skillhub.dto.SkillRatingStatusResponse;
import com.iflytek.skillhub.domain.social.SkillRatingService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@ -25,6 +28,9 @@ public class SkillRatingController extends BaseApiController {
@PathVariable Long skillId,
@Valid @RequestBody SkillRatingRequest request,
@RequestAttribute("userId") String userId) {
if (userId == null) {
throw new DomainForbiddenException("error.auth.required");
}
skillRatingService.rate(skillId, userId, request.score());
return ok("response.success.updated", null);
}

View file

@ -1,6 +1,7 @@
package com.iflytek.skillhub.controller.portal;
import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.domain.social.SkillStarService;
@ -22,6 +23,9 @@ public class SkillStarController extends BaseApiController {
public ApiResponse<Void> starSkill(
@PathVariable Long skillId,
@RequestAttribute("userId") String userId) {
if (userId == null) {
throw new DomainForbiddenException("error.auth.required");
}
skillStarService.star(skillId, userId);
return ok("response.success.updated", null);
}
@ -30,6 +34,9 @@ public class SkillStarController extends BaseApiController {
public ApiResponse<Void> unstarSkill(
@PathVariable Long skillId,
@RequestAttribute("userId") String userId) {
if (userId == null) {
throw new DomainForbiddenException("error.auth.required");
}
skillStarService.unstar(skillId, userId);
return ok("response.success.updated", null);
}

View file

@ -13,6 +13,7 @@ import com.iflytek.skillhub.dto.SkillLifecycleMutationResponse;
import com.iflytek.skillhub.dto.SkillVersionRereleaseRequest;
import java.io.InputStream;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Service;
/**
@ -182,16 +183,18 @@ public class GovernanceWorkflowAppService {
AdminSkillActionRequest request,
String userId,
Map<Long, NamespaceRole> userNsRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.archiveSkill(namespace, slug, request, userId, userNsRoles, auditContext);
return skillLifecycleAppService.archiveSkill(namespace, slug, request, userId, userNsRoles, platformRoles, auditContext);
}
public SkillLifecycleMutationResponse unarchiveSkill(String namespace,
String slug,
String userId,
Map<Long, NamespaceRole> userNsRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.unarchiveSkill(namespace, slug, userId, userNsRoles, auditContext);
return skillLifecycleAppService.unarchiveSkill(namespace, slug, userId, userNsRoles, platformRoles, auditContext);
}
public SkillLifecycleMutationResponse deleteVersion(String namespace,
@ -199,8 +202,9 @@ public class GovernanceWorkflowAppService {
String version,
String userId,
Map<Long, NamespaceRole> userNsRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.deleteVersion(namespace, slug, version, userId, userNsRoles, auditContext);
return skillLifecycleAppService.deleteVersion(namespace, slug, version, userId, userNsRoles, platformRoles, auditContext);
}
public SkillLifecycleMutationResponse withdrawReviewVersion(String namespace,
@ -292,15 +296,17 @@ public class GovernanceWorkflowAppService {
AdminSkillActionRequest request,
String userId,
Map<Long, NamespaceRole> userNsRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.hideSkill(namespace, slug, request, userId, userNsRoles, auditContext);
return skillLifecycleAppService.hideSkill(namespace, slug, request, userId, userNsRoles, platformRoles, auditContext);
}
public SkillLifecycleMutationResponse unhideSkill(String namespace,
String slug,
String userId,
Map<Long, NamespaceRole> userNsRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.unhideSkill(namespace, slug, userId, userNsRoles, auditContext);
return skillLifecycleAppService.unhideSkill(namespace, slug, userId, userNsRoles, platformRoles, auditContext);
}
}

View file

@ -17,6 +17,7 @@ import com.iflytek.skillhub.dto.AdminSkillActionRequest;
import com.iflytek.skillhub.dto.SkillLifecycleMutationResponse;
import com.iflytek.skillhub.dto.SkillVersionRereleaseRequest;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -60,12 +61,14 @@ public class SkillLifecycleAppService {
AdminSkillActionRequest request,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
Skill archived = skillGovernanceService.archiveSkill(
skill.getId(),
userId,
normalizeRoles(userNamespaceRoles),
platformRoles,
auditContext.clientIp(),
auditContext.userAgent(),
request != null ? request.reason() : null
@ -78,12 +81,14 @@ public class SkillLifecycleAppService {
String slug,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
Skill restored = skillGovernanceService.unarchiveSkill(
skill.getId(),
userId,
normalizeRoles(userNamespaceRoles),
platformRoles,
auditContext.clientIp(),
auditContext.userAgent()
);
@ -96,6 +101,7 @@ public class SkillLifecycleAppService {
String version,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
SkillVersion skillVersion = findVersion(skill.getId(), version);
@ -104,6 +110,7 @@ public class SkillLifecycleAppService {
skillVersion,
userId,
normalizeRoles(userNamespaceRoles),
platformRoles,
auditContext.clientIp(),
auditContext.userAgent(),
namespace
@ -250,12 +257,14 @@ public class SkillLifecycleAppService {
AdminSkillActionRequest request,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
Skill hidden = skillGovernanceService.hideSkill(
skill.getId(),
userId,
normalizeRoles(userNamespaceRoles),
platformRoles,
auditContext.clientIp(),
auditContext.userAgent(),
request != null ? request.reason() : null
@ -268,12 +277,14 @@ public class SkillLifecycleAppService {
String slug,
String userId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
AuditRequestContext auditContext) {
Skill skill = findSkill(namespace, slug, userId);
Skill unhidden = skillGovernanceService.unhideSkill(
skill.getId(),
userId,
normalizeRoles(userNamespaceRoles),
platformRoles,
auditContext.clientIp(),
auditContext.userAgent()
);

View file

@ -102,7 +102,7 @@ public class SkillReportService {
String userAgent) {
SkillReport report = requirePendingReport(reportId);
if (disposition == SkillReportDisposition.RESOLVE_AND_HIDE) {
skillGovernanceService.hideSkill(report.getSkillId(), actorUserId, java.util.Map.of(), clientIp, userAgent, comment);
skillGovernanceService.hideSkillAsAdmin(report.getSkillId(), actorUserId, clientIp, userAgent, comment);
} else if (disposition == SkillReportDisposition.RESOLVE_AND_ARCHIVE) {
skillGovernanceService.archiveSkillAsAdmin(report.getSkillId(), actorUserId, clientIp, userAgent, comment);
}

View file

@ -21,6 +21,7 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
@ -72,18 +73,34 @@ public class SkillGovernanceService {
public Skill hideSkill(Long skillId,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
String clientIp,
String userAgent,
String reason) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles, platformRoles);
return hideSkillInternal(skill, actorUserId, clientIp, userAgent, reason);
}
@Transactional
public Skill hideSkillAsAdmin(Long skillId,
String actorUserId,
String clientIp,
String userAgent,
String reason) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
return hideSkillInternal(skill, actorUserId, clientIp, userAgent, reason);
}
private Skill hideSkillInternal(Skill skill, String actorUserId, String clientIp, String userAgent, String reason) {
skill.setHidden(true);
skill.setHiddenAt(currentInstant());
skill.setHiddenBy(actorUserId);
skill.setUpdatedBy(actorUserId);
Skill saved = skillRepository.save(skill);
auditLogService.record(actorUserId, "HIDE_SKILL", "SKILL", skillId, null, clientIp, userAgent, jsonReason(reason));
auditLogService.record(actorUserId, "HIDE_SKILL", "SKILL", skill.getId(), null, clientIp, userAgent, jsonReason(reason));
return saved;
}
@ -91,12 +108,13 @@ public class SkillGovernanceService {
public Skill archiveSkill(Long skillId,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
String clientIp,
String userAgent,
String reason) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles, platformRoles);
return archiveSkillInternal(skill, actorUserId, clientIp, userAgent, reason);
}
@ -129,17 +147,32 @@ public class SkillGovernanceService {
public Skill unhideSkill(Long skillId,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
String clientIp,
String userAgent) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles, platformRoles);
return unhideSkillInternal(skill, actorUserId, clientIp, userAgent);
}
@Transactional
public Skill unhideSkillAsAdmin(Long skillId,
String actorUserId,
String clientIp,
String userAgent) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
return unhideSkillInternal(skill, actorUserId, clientIp, userAgent);
}
private Skill unhideSkillInternal(Skill skill, String actorUserId, String clientIp, String userAgent) {
skill.setHidden(false);
skill.setHiddenAt(null);
skill.setHiddenBy(null);
skill.setUpdatedBy(actorUserId);
Skill saved = skillRepository.save(skill);
auditLogService.record(actorUserId, "UNHIDE_SKILL", "SKILL", skillId, null, clientIp, userAgent, null);
auditLogService.record(actorUserId, "UNHIDE_SKILL", "SKILL", skill.getId(), null, clientIp, userAgent, null);
return saved;
}
@ -147,11 +180,12 @@ public class SkillGovernanceService {
public Skill unarchiveSkill(Long skillId,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
String clientIp,
String userAgent) {
Skill skill = skillRepository.findById(skillId)
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", skillId));
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles, platformRoles);
SkillStatus previousStatus = skill.getStatus();
skill.setStatus(SkillStatus.ACTIVE);
@ -167,10 +201,11 @@ public class SkillGovernanceService {
SkillVersion version,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles,
String clientIp,
String userAgent,
String namespaceSlug) {
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles);
assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles, platformRoles);
if (version.getStatus() != SkillVersionStatus.DRAFT
&& version.getStatus() != SkillVersionStatus.REJECTED
&& version.getStatus() != SkillVersionStatus.SCAN_FAILED
@ -297,7 +332,12 @@ public class SkillGovernanceService {
private void assertCanManageLifecycle(Skill skill,
String actorUserId,
Map<Long, NamespaceRole> userNamespaceRoles) {
Map<Long, NamespaceRole> userNamespaceRoles,
Set<String> platformRoles) {
if (platformRoles != null &&
(platformRoles.contains("SUPER_ADMIN") || platformRoles.contains("SKILL_ADMIN"))) {
return;
}
NamespaceRole namespaceRole = userNamespaceRoles != null ? userNamespaceRoles.get(skill.getNamespaceId()) : null;
boolean canManage = skill.getOwnerId().equals(actorUserId)
|| namespaceRole == NamespaceRole.ADMIN