mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(workspace): return memberRole on list and add access endpoint
This commit is contained in:
parent
65dd4c1f49
commit
f4964bc6d8
@ -9,8 +9,10 @@ import vip.mate.auth.model.UserEntity;
|
||||
import vip.mate.auth.service.AuthService;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.workspace.core.model.WorkspaceAccessVO;
|
||||
import vip.mate.workspace.core.model.WorkspaceEntity;
|
||||
import vip.mate.workspace.core.model.WorkspaceMemberEntity;
|
||||
import vip.mate.workspace.core.model.WorkspaceWithRoleVO;
|
||||
import vip.mate.workspace.core.service.WorkspaceService;
|
||||
|
||||
import java.util.List;
|
||||
@ -32,11 +34,12 @@ public class WorkspaceController {
|
||||
|
||||
// ==================== 工作区 CRUD ====================
|
||||
|
||||
@Operation(summary = "获取当前用户的工作区列表")
|
||||
@Operation(summary = "获取当前用户的工作区列表(含 memberRole 与 effectiveRole)")
|
||||
@GetMapping
|
||||
public R<List<WorkspaceEntity>> list(Authentication auth) {
|
||||
Long userId = resolveUserId(auth);
|
||||
return R.ok(workspaceService.listByUserId(userId));
|
||||
public R<List<WorkspaceWithRoleVO>> list(Authentication auth) {
|
||||
UserEntity user = resolveUser(auth);
|
||||
boolean isGlobalAdmin = isGlobalAdmin(user);
|
||||
return R.ok(workspaceService.listWithRoleByUserId(user.getId(), isGlobalAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取工作区详情")
|
||||
@ -45,6 +48,14 @@ public class WorkspaceController {
|
||||
return R.ok(workspaceService.getById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取当前用户在指定工作区的访问能力(路由守卫消费)")
|
||||
@GetMapping("/{id}/access")
|
||||
public R<WorkspaceAccessVO> getAccess(@PathVariable Long id, Authentication auth) {
|
||||
UserEntity user = resolveUser(auth);
|
||||
boolean isGlobalAdmin = isGlobalAdmin(user);
|
||||
return R.ok(workspaceService.getAccess(id, user.getId(), isGlobalAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建工作区")
|
||||
@PostMapping
|
||||
public R<WorkspaceEntity> create(@RequestBody WorkspaceEntity entity, Authentication auth) {
|
||||
@ -150,11 +161,19 @@ public class WorkspaceController {
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private Long resolveUserId(Authentication auth) {
|
||||
return resolveUser(auth).getId();
|
||||
}
|
||||
|
||||
private UserEntity resolveUser(Authentication auth) {
|
||||
String username = auth.getName();
|
||||
UserEntity user = authService.findByUsername(username);
|
||||
if (user == null) {
|
||||
throw new MateClawException("用户不存在: " + username);
|
||||
}
|
||||
return user.getId();
|
||||
return user;
|
||||
}
|
||||
|
||||
private boolean isGlobalAdmin(UserEntity user) {
|
||||
return user != null && "admin".equalsIgnoreCase(user.getRole());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
package vip.mate.workspace.core.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Response for {@code GET /api/v1/workspaces/{id}/access}.
|
||||
* <p>
|
||||
* The frontend calls this after switching workspace or when a 403 suggests
|
||||
* its cached capability set is stale.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class WorkspaceAccessVO {
|
||||
|
||||
private Long workspaceId;
|
||||
private String memberRole;
|
||||
private Boolean isGlobalAdmin;
|
||||
private String effectiveRole;
|
||||
private Set<String> capabilities;
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package vip.mate.workspace.core.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Workspace list item enriched with the current user's membership info.
|
||||
* <p>
|
||||
* Returned by {@code GET /api/v1/workspaces} so the frontend can render the
|
||||
* workspace switcher and derive route visibility without an extra round-trip.
|
||||
* <ul>
|
||||
* <li>{@code memberRole}: real membership role; {@code null} for non-members</li>
|
||||
* <li>{@code roleLevel}: numeric (owner=4..viewer=1, 0 for non-member)</li>
|
||||
* <li>{@code isGlobalAdmin}: {@code mate_user.role='admin'} — bypasses workspace gates</li>
|
||||
* <li>{@code effectiveRole}: {@code owner} for global admin, otherwise = {@code memberRole}</li>
|
||||
* </ul>
|
||||
* The display name in the UI uses {@code memberRole} so a global admin is shown as
|
||||
* "global admin (non-member)" rather than impersonated as workspace owner. Access
|
||||
* decisions use {@code effectiveRole} or the {@code /access} capability set.
|
||||
*/
|
||||
@Data
|
||||
public class WorkspaceWithRoleVO {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String slug;
|
||||
private String description;
|
||||
private String basePath;
|
||||
private Long ownerId;
|
||||
private String settingsJson;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private String memberRole;
|
||||
private Integer roleLevel;
|
||||
private Boolean isGlobalAdmin;
|
||||
private String effectiveRole;
|
||||
|
||||
public static WorkspaceWithRoleVO from(WorkspaceEntity entity,
|
||||
String memberRole,
|
||||
boolean isGlobalAdmin) {
|
||||
WorkspaceWithRoleVO vo = new WorkspaceWithRoleVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setName(entity.getName());
|
||||
vo.setSlug(entity.getSlug());
|
||||
vo.setDescription(entity.getDescription());
|
||||
vo.setBasePath(entity.getBasePath());
|
||||
vo.setOwnerId(entity.getOwnerId());
|
||||
vo.setSettingsJson(entity.getSettingsJson());
|
||||
vo.setCreateTime(entity.getCreateTime());
|
||||
vo.setUpdateTime(entity.getUpdateTime());
|
||||
vo.setMemberRole(memberRole);
|
||||
vo.setIsGlobalAdmin(isGlobalAdmin);
|
||||
|
||||
String effective = isGlobalAdmin ? "owner" : memberRole;
|
||||
vo.setEffectiveRole(effective);
|
||||
vo.setRoleLevel(memberRole == null && !isGlobalAdmin
|
||||
? 0
|
||||
: vip.mate.workspace.core.security.RoleCapabilities.roleLevel(effective));
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package vip.mate.workspace.core.security;
|
||||
|
||||
/**
|
||||
* Module-level capability constants for workspace RBAC.
|
||||
* <p>
|
||||
* Capability is the abstraction that sits between roles (viewer/member/admin/owner)
|
||||
* and concrete REST methods. Frontend routes declare {@code meta.requiredCapability}
|
||||
* and the backend {@code /api/v1/workspaces/{id}/access} endpoint returns
|
||||
* the resolved capability set for the current user.
|
||||
* <p>
|
||||
* Granularity is intentionally coarse: one capability per UI module, not one per
|
||||
* HTTP action.
|
||||
*/
|
||||
public final class Capability {
|
||||
|
||||
private Capability() {}
|
||||
|
||||
/** Conversation runtime: send/stream/execute + own conversation read/write. */
|
||||
public static final String CHAT = "chat";
|
||||
|
||||
/** Wiki knowledge base / page / relation read-only. */
|
||||
public static final String VIEW_WIKI = "view:wiki";
|
||||
|
||||
/** Memory / Fact / Dream operator surfaces (separate from chat runtime). */
|
||||
public static final String VIEW_MEMORY = "view:memory";
|
||||
|
||||
/** Dashboard, token usage, cron run history. */
|
||||
public static final String VIEW_DASHBOARD = "view:dashboard";
|
||||
|
||||
/** Wiki write: KB CRUD, transformations, research, hot cache. */
|
||||
public static final String MANAGE_WIKI = "manage:wiki";
|
||||
|
||||
/** Agent CRUD, bindings (skill/tool/provider), cron jobs, templates. */
|
||||
public static final String MANAGE_AGENTS = "manage:agents";
|
||||
|
||||
/** Skill catalog management, install, templates, secrets. */
|
||||
public static final String MANAGE_SKILLS = "manage:skills";
|
||||
|
||||
/** Channel CRUD, health, preflight. */
|
||||
public static final String MANAGE_CHANNELS = "manage:channels";
|
||||
|
||||
/** LLM provider, model config, OAuth credentials, datasource. */
|
||||
public static final String MANAGE_MODELS = "manage:models";
|
||||
|
||||
/** Tool guard, file guard, audit, activity feed. */
|
||||
public static final String MANAGE_SECURITY = "manage:security";
|
||||
|
||||
/** System settings, feature flags, MCP, plugins, ACP, workflow, trigger, members. */
|
||||
public static final String MANAGE_SETTINGS = "manage:settings";
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package vip.mate.workspace.core.security;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Authoritative role → capability mapping.
|
||||
* <p>
|
||||
* The frontend treats the {@code /access} response as the single source of truth
|
||||
* and does NOT keep a local copy of this table. Updating role permissions only
|
||||
* requires editing this file.
|
||||
* <p>
|
||||
* Role hierarchy is additive: each level inherits everything from the level below.
|
||||
*/
|
||||
public final class RoleCapabilities {
|
||||
|
||||
public static final String ROLE_VIEWER = "viewer";
|
||||
public static final String ROLE_MEMBER = "member";
|
||||
public static final String ROLE_ADMIN = "admin";
|
||||
public static final String ROLE_OWNER = "owner";
|
||||
|
||||
private static final Map<String, Set<String>> ROLE_TO_CAPS;
|
||||
|
||||
static {
|
||||
Set<String> viewer = new LinkedHashSet<>();
|
||||
viewer.add(Capability.CHAT);
|
||||
viewer.add(Capability.VIEW_WIKI);
|
||||
|
||||
Set<String> member = new LinkedHashSet<>(viewer);
|
||||
member.add(Capability.VIEW_MEMORY);
|
||||
member.add(Capability.VIEW_DASHBOARD);
|
||||
member.add(Capability.MANAGE_WIKI);
|
||||
member.add(Capability.MANAGE_AGENTS);
|
||||
|
||||
Set<String> admin = new LinkedHashSet<>(member);
|
||||
admin.add(Capability.MANAGE_SKILLS);
|
||||
admin.add(Capability.MANAGE_CHANNELS);
|
||||
admin.add(Capability.MANAGE_MODELS);
|
||||
admin.add(Capability.MANAGE_SECURITY);
|
||||
admin.add(Capability.MANAGE_SETTINGS);
|
||||
|
||||
// Owner currently has the same capability set as admin. Owner-only actions
|
||||
// (workspace deletion, transferring owner role) are enforced at the
|
||||
// service layer via role-level comparison, not capabilities.
|
||||
Set<String> owner = new LinkedHashSet<>(admin);
|
||||
|
||||
ROLE_TO_CAPS = Map.of(
|
||||
ROLE_VIEWER, Collections.unmodifiableSet(viewer),
|
||||
ROLE_MEMBER, Collections.unmodifiableSet(member),
|
||||
ROLE_ADMIN, Collections.unmodifiableSet(admin),
|
||||
ROLE_OWNER, Collections.unmodifiableSet(owner)
|
||||
);
|
||||
}
|
||||
|
||||
private RoleCapabilities() {}
|
||||
|
||||
/**
|
||||
* Returns the capability set for a given role. Unknown roles get an empty set
|
||||
* (default deny).
|
||||
*/
|
||||
public static Set<String> forRole(String role) {
|
||||
if (role == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> caps = ROLE_TO_CAPS.get(role.toLowerCase());
|
||||
return caps != null ? caps : Set.of();
|
||||
}
|
||||
|
||||
public static int roleLevel(String role) {
|
||||
if (role == null) return 0;
|
||||
return switch (role.toLowerCase()) {
|
||||
case ROLE_OWNER -> 4;
|
||||
case ROLE_ADMIN -> 3;
|
||||
case ROLE_MEMBER -> 2;
|
||||
case ROLE_VIEWER -> 1;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -11,13 +11,20 @@ import vip.mate.exception.MateClawException;
|
||||
import vip.mate.i18n.I18nService;
|
||||
import vip.mate.workspace.conversation.model.ConversationEntity;
|
||||
import vip.mate.workspace.conversation.repository.ConversationMapper;
|
||||
import vip.mate.workspace.core.model.WorkspaceAccessVO;
|
||||
import vip.mate.workspace.core.model.WorkspaceEntity;
|
||||
import vip.mate.workspace.core.model.WorkspaceMemberEntity;
|
||||
import vip.mate.workspace.core.model.WorkspaceWithRoleVO;
|
||||
import vip.mate.workspace.core.repository.WorkspaceMapper;
|
||||
import vip.mate.workspace.core.repository.WorkspaceMemberMapper;
|
||||
import vip.mate.workspace.core.security.RoleCapabilities;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作区业务服务
|
||||
@ -66,6 +73,53 @@ public class WorkspaceService {
|
||||
return workspaceMapper.selectBatchIds(wsIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* List workspaces visible to a user, each annotated with the user's membership
|
||||
* role. Global admins see every workspace (memberRole reflects their real
|
||||
* membership, or null when they are not actually a member).
|
||||
*/
|
||||
public List<WorkspaceWithRoleVO> listWithRoleByUserId(Long userId, boolean isGlobalAdmin) {
|
||||
List<WorkspaceMemberEntity> memberships = memberMapper.selectList(
|
||||
new LambdaQueryWrapper<WorkspaceMemberEntity>()
|
||||
.eq(WorkspaceMemberEntity::getUserId, userId));
|
||||
Map<Long, String> roleByWorkspaceId = new HashMap<>();
|
||||
for (WorkspaceMemberEntity m : memberships) {
|
||||
roleByWorkspaceId.put(m.getWorkspaceId(), m.getRole());
|
||||
}
|
||||
|
||||
List<WorkspaceEntity> entities;
|
||||
if (isGlobalAdmin) {
|
||||
entities = listAll();
|
||||
} else if (memberships.isEmpty()) {
|
||||
WorkspaceEntity defaultWs = getBySlug(DEFAULT_SLUG);
|
||||
entities = defaultWs != null ? List.of(defaultWs) : List.of();
|
||||
} else {
|
||||
entities = workspaceMapper.selectBatchIds(roleByWorkspaceId.keySet());
|
||||
}
|
||||
|
||||
List<WorkspaceWithRoleVO> result = new ArrayList<>(entities.size());
|
||||
for (WorkspaceEntity ws : entities) {
|
||||
String role = roleByWorkspaceId.get(ws.getId());
|
||||
result.add(WorkspaceWithRoleVO.from(ws, role, isGlobalAdmin));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the user's access summary for a workspace. Used by
|
||||
* {@code GET /api/v1/workspaces/{id}/access} so the frontend can
|
||||
* refresh its capability set after a role change without reloading the page.
|
||||
*/
|
||||
public WorkspaceAccessVO getAccess(Long workspaceId, Long userId, boolean isGlobalAdmin) {
|
||||
WorkspaceMemberEntity member = getMembership(workspaceId, userId);
|
||||
String memberRole = member != null ? member.getRole() : null;
|
||||
String effective = isGlobalAdmin ? "owner" : memberRole;
|
||||
Set<String> capabilities = effective != null
|
||||
? RoleCapabilities.forRole(effective)
|
||||
: Set.of();
|
||||
return new WorkspaceAccessVO(workspaceId, memberRole, isGlobalAdmin, effective, capabilities);
|
||||
}
|
||||
|
||||
public WorkspaceEntity getById(Long id) {
|
||||
WorkspaceEntity entity = workspaceMapper.selectById(id);
|
||||
if (entity == null) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user