feat(auth): 为数据管理和RAG服务增加资源访问控制

- 在DatasetApplicationService中注入ResourceAccessService并添加所有权验证
- 在KnowledgeSetApplicationService中注入ResourceAccessService并添加所有权验证
- 修改DatasetRepository接口和实现类,增加按创建者过滤的方法
- 修改KnowledgeSetRepository接口和实现类,增加按创建者过滤的方法
- 在RAG索引器服务中添加知识库访问权限检查和作用域过滤
- 更新实体元对象处理器以使用请求用户上下文获取当前用户
- 在前端设置页面添加用户权限管理功能和角色权限控制
- 为Python标注服务增加用户上下文和数据集访问权限验证
This commit is contained in:
2026-02-06 14:58:46 +08:00
parent 056cee11cc
commit 6a4c4ae3d7
28 changed files with 1063 additions and 158 deletions

View File

@@ -0,0 +1,58 @@
package com.datamate.common.auth.application;
import com.datamate.common.auth.infrastructure.context.RequestUserContextHolder;
import com.datamate.common.infrastructure.exception.BusinessAssert;
import com.datamate.common.infrastructure.exception.SystemErrorCode;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Objects;
/**
* 资源访问控制服务(基于请求用户上下文)
*/
@Service
public class ResourceAccessService {
public static final String ADMIN_ROLE_CODE = "ROLE_ADMIN";
public boolean isAdmin() {
return RequestUserContextHolder.hasRole(ADMIN_ROLE_CODE);
}
public String getCurrentUserId() {
return RequestUserContextHolder.getCurrentUserId();
}
public String requireCurrentUserId() {
String currentUserId = getCurrentUserId();
BusinessAssert.isTrue(StringUtils.hasText(currentUserId), SystemErrorCode.INSUFFICIENT_PERMISSIONS);
return currentUserId;
}
/**
* 资源列表查询的 owner 过滤:
* - 管理员返回 null(不过滤)
* - 非管理员返回当前用户ID
*/
public String resolveOwnerFilterUserId() {
if (isAdmin()) {
return null;
}
return requireCurrentUserId();
}
/**
* 校验当前用户是否可访问 owner 资源
*/
public void assertOwnerAccess(String ownerUserId) {
if (isAdmin()) {
return;
}
String currentUserId = requireCurrentUserId();
BusinessAssert.isTrue(
StringUtils.hasText(ownerUserId) && Objects.equals(ownerUserId, currentUserId),
SystemErrorCode.INSUFFICIENT_PERMISSIONS
);
}
}

View File

@@ -0,0 +1,40 @@
package com.datamate.common.auth.infrastructure.context;
import lombok.Getter;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 请求级用户上下文
*/
@Getter
public class RequestUserContext {
private final String userId;
private final String username;
private final List<String> roles;
private RequestUserContext(String userId, String username, List<String> roles) {
this.userId = userId;
this.username = username;
this.roles = roles == null ? Collections.emptyList() : List.copyOf(roles);
}
public static RequestUserContext of(String userId, String username, List<String> roles) {
return new RequestUserContext(userId, username, roles);
}
public static RequestUserContext empty() {
return new RequestUserContext(null, null, Collections.emptyList());
}
public boolean hasRole(String roleCode) {
if (!StringUtils.hasText(roleCode)) {
return false;
}
return roles.stream().anyMatch(role -> StringUtils.hasText(role) && Objects.equals(role.trim(), roleCode));
}
}

View File

@@ -0,0 +1,49 @@
package com.datamate.common.auth.infrastructure.context;
import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
/**
* 请求级用户上下文持有器
*/
public final class RequestUserContextHolder {
private static final ThreadLocal<RequestUserContext> USER_CONTEXT_HOLDER =
new NamedThreadLocal<>("datamate-request-user-context");
private RequestUserContextHolder() {
}
public static void set(RequestUserContext context) {
USER_CONTEXT_HOLDER.set(context == null ? RequestUserContext.empty() : context);
}
public static RequestUserContext get() {
RequestUserContext context = USER_CONTEXT_HOLDER.get();
return context == null ? RequestUserContext.empty() : context;
}
public static String getCurrentUserId() {
return get().getUserId();
}
public static List<String> getCurrentRoles() {
List<String> roles = get().getRoles();
return roles == null ? Collections.emptyList() : roles;
}
public static boolean hasRole(String roleCode) {
if (!StringUtils.hasText(roleCode)) {
return false;
}
return getCurrentRoles().stream()
.anyMatch(role -> StringUtils.hasText(role) && roleCode.equalsIgnoreCase(role.trim()));
}
public static void clear() {
USER_CONTEXT_HOLDER.remove();
}
}

View File

@@ -0,0 +1,53 @@
package com.datamate.common.auth.infrastructure.context;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 从网关透传请求头中提取用户上下文
*/
@Component
public class RequestUserContextInterceptor implements HandlerInterceptor {
private static final String HEADER_USER_ID = "X-User-Id";
private static final String HEADER_USER_NAME = "X-User-Name";
private static final String HEADER_USER_ROLES = "X-User-Roles";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userId = normalizeValue(request.getHeader(HEADER_USER_ID));
String username = normalizeValue(request.getHeader(HEADER_USER_NAME));
List<String> roleCodes = parseRoleCodes(request.getHeader(HEADER_USER_ROLES));
RequestUserContextHolder.set(RequestUserContext.of(userId, username, roleCodes));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
RequestUserContextHolder.clear();
}
private String normalizeValue(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
private List<String> parseRoleCodes(String roleHeader) {
if (!StringUtils.hasText(roleHeader)) {
return Collections.emptyList();
}
return Arrays.stream(roleHeader.split(","))
.map(String::trim)
.filter(StringUtils::hasText)
.toList();
}
}

View File

@@ -0,0 +1,21 @@
package com.datamate.common.auth.infrastructure.context;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 请求用户上下文拦截器注册
*/
@Configuration
@RequiredArgsConstructor
public class RequestUserContextWebMvcConfigurer implements WebMvcConfigurer {
private final RequestUserContextInterceptor requestUserContextInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestUserContextInterceptor).addPathPatterns("/**");
}
}

View File

@@ -1,9 +1,11 @@
package com.datamate.common.infrastructure.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.datamate.common.auth.infrastructure.context.RequestUserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
@@ -44,17 +46,10 @@ public class EntityMetaObjectHandler implements MetaObjectHandler {
* 获取当前用户(需要根据你的安全框架实现)
*/
private String getCurrentUser() {
// todo 这里需要根据你的安全框架实现,例如Spring Security、Shiro等
// 示例:返回默认用户或从SecurityContext获取
try {
// 如果是Spring Security
// return SecurityContextHolder.getContext().getAuthentication().getName();
// 临时返回默认值,请根据实际情况修改
return "system";
} catch (Exception e) {
log.error("Error getting current user", e);
return "unknown";
String currentUserId = RequestUserContextHolder.getCurrentUserId();
if (StringUtils.hasText(currentUserId)) {
return currentUserId;
}
return "system";
}
}