feat(knowledge): add application and principal space grants
This commit is contained in:
@@ -31,6 +31,11 @@
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package org.dromara.aihr.knowledge.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class AihrKnowledgeAppDto {
|
||||
|
||||
private AihrKnowledgeAppDto() {
|
||||
}
|
||||
|
||||
public record AuthenticatedApp(
|
||||
Long id,
|
||||
String tenantId,
|
||||
String appCode,
|
||||
String appName,
|
||||
String authType,
|
||||
Integer rateLimitPerMinute,
|
||||
LocalDateTime expiresTime
|
||||
) {
|
||||
}
|
||||
|
||||
public record AppResponse(
|
||||
Long id,
|
||||
String appCode,
|
||||
String appName,
|
||||
String authType,
|
||||
String internalClientKey,
|
||||
String status,
|
||||
Integer rateLimitPerMinute,
|
||||
LocalDateTime expiresTime,
|
||||
List<String> spaceCodes
|
||||
) {
|
||||
}
|
||||
|
||||
public record TokenIssuedResponse(String appCode, String plainToken, LocalDateTime expiresTime) {
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package org.dromara.aihr.knowledge.domain;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public record AihrKnowledgePrincipal(
|
||||
String tenantId,
|
||||
Long userId,
|
||||
String userType,
|
||||
String extPartyId,
|
||||
Set<String> roles,
|
||||
Set<String> projectCodes,
|
||||
String clientKey
|
||||
) {
|
||||
public AihrKnowledgePrincipal {
|
||||
roles = roles == null ? Set.of() : Set.copyOf(roles);
|
||||
projectCodes = projectCodes == null ? Set.of() : Set.copyOf(projectCodes);
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package org.dromara.aihr.knowledge.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeAppDto.AuthenticatedApp;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AihrKnowledgeAccessService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public Set<Long> resolveInternalSpaceIds(AihrKnowledgePrincipal principal, AuthenticatedApp app,
|
||||
List<String> requestedCodes, String permission) {
|
||||
requireSameTenant(principal.tenantId(), app.tenantId());
|
||||
Set<Long> appSpaces = appSpaces(app);
|
||||
Set<Long> principalSpaces = principalSpaces(principal, permission);
|
||||
Set<Long> requestedSpaces = requestedSpaceIds(principal.tenantId(), requestedCodes);
|
||||
return intersect(appSpaces, principalSpaces, requestedSpaces);
|
||||
}
|
||||
|
||||
public Set<Long> resolveExternalSpaceIds(AuthenticatedApp app, List<String> requestedCodes) {
|
||||
Set<Long> appSpaces = appSpaces(app);
|
||||
Set<Long> requestedSpaces = requestedSpaceIds(app.tenantId(), requestedCodes);
|
||||
return narrow(appSpaces, requestedSpaces);
|
||||
}
|
||||
|
||||
public static Set<Long> intersect(Set<Long> appSpaces, Set<Long> principalSpaces, Set<Long> requestedSpaces) {
|
||||
Set<Long> effective = new LinkedHashSet<>(appSpaces);
|
||||
effective.retainAll(principalSpaces);
|
||||
return narrow(effective, requestedSpaces);
|
||||
}
|
||||
|
||||
private static Set<Long> narrow(Set<Long> effective, Set<Long> requested) {
|
||||
if (effective.isEmpty()) {
|
||||
throw forbidden("当前应用和账号没有共同可访问的知识空间");
|
||||
}
|
||||
if (requested == null || requested.isEmpty()) {
|
||||
return Set.copyOf(effective);
|
||||
}
|
||||
if (!effective.containsAll(requested)) {
|
||||
throw forbidden("请求包含未授权的知识空间");
|
||||
}
|
||||
return Set.copyOf(requested);
|
||||
}
|
||||
|
||||
private Set<Long> appSpaces(AuthenticatedApp app) {
|
||||
return new LinkedHashSet<>(jdbcTemplate.queryForList("""
|
||||
select s.knowledge_id
|
||||
from aihr_knowledge_app_space s
|
||||
join aihr_knowledge_info k on k.id = s.knowledge_id and k.tenant_id = s.tenant_id
|
||||
where s.tenant_id = ? and s.app_id = ? and k.status = 'ACTIVE'
|
||||
""", Long.class, app.tenantId(), app.id()));
|
||||
}
|
||||
|
||||
private Set<Long> principalSpaces(AihrKnowledgePrincipal principal, String permission) {
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(principal.tenantId());
|
||||
args.add("MANAGE".equalsIgnoreCase(permission) ? "MANAGE" : "READ");
|
||||
args.add(principal.userId() == null ? "" : String.valueOf(principal.userId()));
|
||||
String roleClause = "";
|
||||
if (!principal.roles().isEmpty()) {
|
||||
roleClause = " or (g.principal_type = 'ROLE' and g.principal_value in (" + placeholders(principal.roles()) + "))";
|
||||
args.addAll(principal.roles());
|
||||
}
|
||||
String permissionClause = "MANAGE".equalsIgnoreCase(permission)
|
||||
? "g.permission = ?"
|
||||
: "g.permission in (?, 'MANAGE')";
|
||||
String sql = """
|
||||
select distinct g.knowledge_id
|
||||
from aihr_knowledge_space_grant g
|
||||
join aihr_knowledge_info k on k.id = g.knowledge_id and k.tenant_id = g.tenant_id
|
||||
where g.tenant_id = ? and %s and g.status = 'ACTIVE' and k.status = 'ACTIVE'
|
||||
and ((g.principal_type = 'USER' and g.principal_value = ?)%s)
|
||||
""".formatted(permissionClause, roleClause);
|
||||
return new LinkedHashSet<>(jdbcTemplate.queryForList(sql, Long.class, args.toArray()));
|
||||
}
|
||||
|
||||
private Set<Long> requestedSpaceIds(String tenantId, List<String> requestedCodes) {
|
||||
Set<String> codes = cleanCodes(requestedCodes);
|
||||
if (codes.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(tenantId);
|
||||
args.addAll(codes);
|
||||
List<Long> ids = jdbcTemplate.queryForList("""
|
||||
select id from aihr_knowledge_info
|
||||
where tenant_id = ? and status = 'ACTIVE' and code in (%s)
|
||||
""".formatted(placeholders(codes)), Long.class, args.toArray());
|
||||
if (ids.size() != codes.size()) {
|
||||
throw forbidden("请求包含不存在、停用或其他租户的知识空间");
|
||||
}
|
||||
return new LinkedHashSet<>(ids);
|
||||
}
|
||||
|
||||
private static Set<String> cleanCodes(List<String> values) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
if (values != null) {
|
||||
values.stream().filter(AihrKnowledgeAccessService::hasText).map(String::trim).forEach(result::add);
|
||||
}
|
||||
if (result.size() > 20) {
|
||||
throw new ServiceException("单次最多选择 20 个知识空间", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String placeholders(Collection<?> values) {
|
||||
return String.join(",", java.util.Collections.nCopies(values.size(), "?"));
|
||||
}
|
||||
|
||||
private static void requireSameTenant(String principalTenant, String appTenant) {
|
||||
if (!hasText(principalTenant) || !principalTenant.equals(appTenant)) {
|
||||
throw forbidden("调用应用不属于当前租户");
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceException forbidden(String message) {
|
||||
return new ServiceException(message, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private static boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package org.dromara.aihr.knowledge.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeAppDto.AuthenticatedApp;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeAppDto.TokenIssuedResponse;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.redisson.api.RateType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AihrKnowledgeAppService {
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
private static final Pattern APP_CODE = Pattern.compile("[a-z0-9_]{3,64}");
|
||||
private static final int TOKEN_RANDOM_CHARS = 43;
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AuthenticatedApp requireSessionApp(String tenantId, String clientKey) {
|
||||
if (!hasText(tenantId) || !hasText(clientKey)) {
|
||||
throw forbidden("当前登录端未配置知识问答应用");
|
||||
}
|
||||
List<AppRow> rows = jdbcTemplate.query("""
|
||||
select id, tenant_id, app_code, app_name, auth_type, token_hash, status,
|
||||
rate_limit_per_minute, expires_time
|
||||
from aihr_knowledge_app
|
||||
where tenant_id = ? and auth_type = 'SESSION' and internal_client_key = ?
|
||||
limit 1
|
||||
""", (rs, rowNum) -> new AppRow(
|
||||
rs.getLong("id"), rs.getString("tenant_id"), rs.getString("app_code"),
|
||||
rs.getString("app_name"), rs.getString("auth_type"), rs.getString("token_hash"),
|
||||
rs.getString("status"), rs.getInt("rate_limit_per_minute"),
|
||||
rs.getTimestamp("expires_time") == null ? null : rs.getTimestamp("expires_time").toLocalDateTime()
|
||||
), tenantId, clientKey.trim());
|
||||
if (rows.isEmpty() || !active(rows.get(0))) {
|
||||
throw forbidden("当前登录端未启用知识问答应用");
|
||||
}
|
||||
return rows.get(0).authenticated();
|
||||
}
|
||||
|
||||
public AuthenticatedApp authenticateBearer(String authorization) {
|
||||
String token = bearerToken(authorization);
|
||||
String code;
|
||||
try {
|
||||
code = appCode(token);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw unauthorized();
|
||||
}
|
||||
List<AppRow> rows = jdbcTemplate.query("""
|
||||
select id, tenant_id, app_code, app_name, auth_type, token_hash, status,
|
||||
rate_limit_per_minute, expires_time
|
||||
from aihr_knowledge_app
|
||||
where app_code = ? and auth_type = 'API_TOKEN'
|
||||
limit 1
|
||||
""", (rs, rowNum) -> new AppRow(
|
||||
rs.getLong("id"), rs.getString("tenant_id"), rs.getString("app_code"),
|
||||
rs.getString("app_name"), rs.getString("auth_type"), rs.getString("token_hash"),
|
||||
rs.getString("status"), rs.getInt("rate_limit_per_minute"),
|
||||
rs.getTimestamp("expires_time") == null ? null : rs.getTimestamp("expires_time").toLocalDateTime()
|
||||
), code);
|
||||
if (rows.isEmpty() || !active(rows.get(0)) || !hashMatches(token, rows.get(0).tokenHash())) {
|
||||
throw unauthorized();
|
||||
}
|
||||
AppRow row = rows.get(0);
|
||||
enforceRateLimit(row.authenticated());
|
||||
jdbcTemplate.update("update aihr_knowledge_app set last_used_time = now() where id = ? and tenant_id = ?",
|
||||
row.id(), row.tenantId());
|
||||
return row.authenticated();
|
||||
}
|
||||
|
||||
public TokenIssuedResponse rotateToken(String tenantId, Long appId, LocalDateTime expiresTime) {
|
||||
List<String> codes = jdbcTemplate.queryForList("""
|
||||
select app_code from aihr_knowledge_app
|
||||
where tenant_id = ? and id = ? and auth_type = 'API_TOKEN'
|
||||
""", String.class, tenantId, appId);
|
||||
if (codes.isEmpty()) {
|
||||
throw new ServiceException("外部调用应用不存在", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
String token = generateToken(codes.get(0));
|
||||
jdbcTemplate.update("""
|
||||
update aihr_knowledge_app
|
||||
set token_hash = ?, expires_time = ?, update_time = now()
|
||||
where tenant_id = ? and id = ? and auth_type = 'API_TOKEN'
|
||||
""", sha256(token), expiresTime, tenantId, appId);
|
||||
return new TokenIssuedResponse(codes.get(0), token, expiresTime);
|
||||
}
|
||||
|
||||
public void enforceRateLimit(AuthenticatedApp app) {
|
||||
int rate = Math.max(1, app.rateLimitPerMinute() == null ? 60 : app.rateLimitPerMinute());
|
||||
try {
|
||||
if (RedisUtils.rateLimiter("knowledge:app:" + app.id(), RateType.OVERALL, rate, 60) < 0) {
|
||||
throw new ServiceException("调用过于频繁,请稍后重试", 429);
|
||||
}
|
||||
} catch (ServiceException ex) {
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new ServiceException("知识问答限流服务暂不可用", 503);
|
||||
}
|
||||
}
|
||||
|
||||
public static String generateToken(String appCode) {
|
||||
String code = normalizeCode(appCode);
|
||||
byte[] random = new byte[32];
|
||||
SECURE_RANDOM.nextBytes(random);
|
||||
return "ak_" + code + "_" + Base64.getUrlEncoder().withoutPadding().encodeToString(random);
|
||||
}
|
||||
|
||||
public static String appCode(String token) {
|
||||
if (token == null || !token.startsWith("ak_")) {
|
||||
throw new IllegalArgumentException("invalid app token");
|
||||
}
|
||||
int split = token.length() - TOKEN_RANDOM_CHARS - 1;
|
||||
if (split <= 3 || split == token.length() - 1) {
|
||||
throw new IllegalArgumentException("invalid app token");
|
||||
}
|
||||
if (token.charAt(split) != '_') {
|
||||
throw new IllegalArgumentException("invalid app token");
|
||||
}
|
||||
return normalizeCode(token.substring(3, split));
|
||||
}
|
||||
|
||||
public static String sha256(String value) {
|
||||
try {
|
||||
byte[] bytes = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
return java.util.HexFormat.of().formatHex(bytes);
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hashMatches(String token, String expectedHash) {
|
||||
if (!hasText(token) || !hasText(expectedHash)) {
|
||||
return false;
|
||||
}
|
||||
return MessageDigest.isEqual(
|
||||
sha256(token).getBytes(StandardCharsets.US_ASCII),
|
||||
expectedHash.getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
private static String bearerToken(String authorization) {
|
||||
if (authorization == null || !authorization.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
throw unauthorized();
|
||||
}
|
||||
String token = authorization.substring(7).trim();
|
||||
if (token.isBlank()) {
|
||||
throw unauthorized();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private static String normalizeCode(String value) {
|
||||
String code = value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
|
||||
if (!APP_CODE.matcher(code).matches()) {
|
||||
throw new IllegalArgumentException("appCode 仅支持 3-64 位小写字母、数字和下划线");
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private static boolean active(AppRow row) {
|
||||
return "ACTIVE".equals(row.status())
|
||||
&& (row.expiresTime() == null || row.expiresTime().isAfter(LocalDateTime.now()));
|
||||
}
|
||||
|
||||
private static ServiceException unauthorized() {
|
||||
return new ServiceException("调用应用认证失败", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
private static ServiceException forbidden(String message) {
|
||||
return new ServiceException(message, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private static boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
|
||||
private record AppRow(
|
||||
Long id,
|
||||
String tenantId,
|
||||
String appCode,
|
||||
String appName,
|
||||
String authType,
|
||||
String tokenHash,
|
||||
String status,
|
||||
Integer rateLimit,
|
||||
LocalDateTime expiresTime
|
||||
) {
|
||||
private AuthenticatedApp authenticated() {
|
||||
return new AuthenticatedApp(id, tenantId, appCode, appName, authType, rateLimit, expiresTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package org.dromara.aihr.knowledge.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgPersonRow;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.dromara.aihr.service.AihrOrgSyncService;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AihrKnowledgePrincipalResolver {
|
||||
|
||||
private final AihrOrgSyncService orgSyncService;
|
||||
|
||||
public AihrKnowledgePrincipal current() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || loginUser.getTenantId() == null || loginUser.getUserId() == null) {
|
||||
throw new ServiceException("登录身份缺失,请重新登录", 401);
|
||||
}
|
||||
return from(loginUser);
|
||||
}
|
||||
|
||||
AihrKnowledgePrincipal from(LoginUser loginUser) {
|
||||
Set<String> roles = new LinkedHashSet<>();
|
||||
Set<String> projectCodes = new LinkedHashSet<>();
|
||||
String extPartyId = "";
|
||||
if (UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
roles.add("employee");
|
||||
extPartyId = trim(loginUser.getUsername());
|
||||
List<OrgPersonRow> rows = orgRows(extPartyId);
|
||||
if (rows.stream().anyMatch(AihrKnowledgePrincipalResolver::isSupervisor)) {
|
||||
roles.add("supervisor");
|
||||
}
|
||||
rows.stream().map(OrgPersonRow::projectCode).filter(AihrKnowledgePrincipalResolver::hasText)
|
||||
.forEach(projectCodes::add);
|
||||
} else if (loginUser.getRolePermission() != null) {
|
||||
roles.addAll(loginUser.getRolePermission());
|
||||
}
|
||||
return new AihrKnowledgePrincipal(
|
||||
loginUser.getTenantId(), loginUser.getUserId(), loginUser.getUserType(), extPartyId,
|
||||
roles, projectCodes, trim(loginUser.getClientKey()));
|
||||
}
|
||||
|
||||
private List<OrgPersonRow> orgRows(String extPartyId) {
|
||||
if (extPartyId.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return orgSyncService.snapshot(extPartyId, null, null, "active", 1, 500, 500).rows();
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("knowledge principal org lookup failed, keeping employee-only scope");
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSupervisor(OrgPersonRow row) {
|
||||
return "主管".equals(row.positionLevel()) || "项目经理".equals(row.positionLevel());
|
||||
}
|
||||
|
||||
private static boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.dromara.aihr.knowledge;
|
||||
|
||||
import org.dromara.aihr.knowledge.service.AihrKnowledgeAccessService;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrKnowledgeAccessServiceTest {
|
||||
|
||||
@Test
|
||||
void appAndPrincipalGrantsAreIntersected() {
|
||||
assertEquals(Set.of(2L), AihrKnowledgeAccessService.intersect(
|
||||
Set.of(1L, 2L), Set.of(2L, 3L), Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestedSpacesCanOnlyNarrowEffectiveScope() {
|
||||
assertEquals(Set.of(2L), AihrKnowledgeAccessService.intersect(
|
||||
Set.of(1L, 2L), Set.of(2L, 3L), Set.of(2L)));
|
||||
assertThrows(ServiceException.class, () -> AihrKnowledgeAccessService.intersect(
|
||||
Set.of(1L, 2L), Set.of(2L, 3L), Set.of(2L, 3L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyEffectiveScopeIsRejected() {
|
||||
assertThrows(ServiceException.class, () -> AihrKnowledgeAccessService.intersect(
|
||||
Set.of(1L), Set.of(2L), Set.of()));
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package org.dromara.aihr.knowledge;
|
||||
|
||||
import org.dromara.aihr.knowledge.service.AihrKnowledgeAppService;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrKnowledgeAppServiceTest {
|
||||
|
||||
@Test
|
||||
void generatedTokenContainsAppCodeAndPersistsOnlySha256() {
|
||||
String token = AihrKnowledgeAppService.generateToken("mt_card_miniapp");
|
||||
String hash = AihrKnowledgeAppService.sha256(token);
|
||||
|
||||
assertTrue(token.startsWith("ak_mt_card_miniapp_"));
|
||||
assertEquals("mt_card_miniapp", AihrKnowledgeAppService.appCode(token));
|
||||
assertEquals(64, hash.length());
|
||||
assertFalse(hash.contains("mt_card_miniapp"));
|
||||
assertTrue(AihrKnowledgeAppService.hashMatches(token, hash));
|
||||
assertFalse(AihrKnowledgeAppService.hashMatches(token + "x", hash));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tokenUsesAtLeastThirtyTwoRandomBytes() {
|
||||
String token = AihrKnowledgeAppService.generateToken("demo");
|
||||
String randomPart = token.substring("ak_demo_".length());
|
||||
assertTrue(java.util.Base64.getUrlDecoder().decode(randomPart).length >= 32);
|
||||
assertFalse(java.util.Arrays.equals(
|
||||
token.getBytes(StandardCharsets.UTF_8),
|
||||
AihrKnowledgeAppService.generateToken("demo").getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package org.dromara.aihr.knowledge.service;
|
||||
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgPersonRow;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgSnapshotResponse;
|
||||
import org.dromara.aihr.service.AihrOrgSyncService;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrKnowledgePrincipalResolverTest {
|
||||
|
||||
@Test
|
||||
void appSupervisorRoleAndProjectsComeFromOrgSnapshot() {
|
||||
AihrOrgSyncService orgService = mock(AihrOrgSyncService.class);
|
||||
when(orgService.snapshot(eq("party-1"), any(), any(), eq("active"), eq(1), eq(500), eq(500)))
|
||||
.thenReturn(new OrgSnapshotResponse(1, 1, 0, 1, 0, 1, 500, List.of(), List.of(), List.of(
|
||||
new OrgPersonRow(1L, "P01", "", "", "party-1", "", "项目经理", "项目经理", "active", "2026-07-16")
|
||||
)));
|
||||
LoginUser login = login(UserType.APP_USER.getUserType(), "party-1", Set.of());
|
||||
|
||||
var principal = new AihrKnowledgePrincipalResolver(orgService).from(login);
|
||||
|
||||
assertEquals(Set.of("employee", "supervisor"), principal.roles());
|
||||
assertEquals(Set.of("P01"), principal.projectCodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sysUserRolesComeFromLoginUser() {
|
||||
LoginUser login = login(UserType.SYS_USER.getUserType(), "admin", Set.of("hr_operator"));
|
||||
var principal = new AihrKnowledgePrincipalResolver(mock(AihrOrgSyncService.class)).from(login);
|
||||
assertTrue(principal.roles().contains("hr_operator"));
|
||||
assertTrue(principal.projectCodes().isEmpty());
|
||||
}
|
||||
|
||||
private static LoginUser login(String userType, String username, Set<String> roles) {
|
||||
LoginUser login = new LoginUser();
|
||||
login.setTenantId("000000");
|
||||
login.setUserId(1L);
|
||||
login.setUserType(userType);
|
||||
login.setUsername(username);
|
||||
login.setRolePermission(roles);
|
||||
login.setClientKey("app");
|
||||
return login;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user