fix: support reactive wildcard permissions

This commit is contained in:
pnoker
2026-06-11 20:38:52 +08:00
parent f75003b030
commit 1811d4f8ff
9 changed files with 118 additions and 16 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ DC3_LOG_MAX_FILE=20
DC3_BIND_HOST=127.0.0.1
APM_AGENT_ENABLE=false
# Auth / HMAC shared secret for X-Auth-User header signing between gateway and backend services
# Auth token signing key and HMAC shared secret for X-Auth-User header signing between gateway and backend services
DC3_SECURITY_KEY=dc3.security.key.2026.io.github.pnoker
AUTH_HMAC_SECRET=io.github.pnoker.dc3
# Base dependency stack / local database defaults
@@ -18,6 +18,7 @@
package io.github.pnoker.common.auth.security;
import io.github.pnoker.common.auth.service.RoleResourceBindService;
import io.github.pnoker.common.security.PermissionMethods;
import io.github.pnoker.common.security.PermissionProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -75,7 +76,7 @@ public class AuthPermissionProvider implements PermissionProvider {
String cacheKey = tenantId + ":" + userId;
CacheEntry entry = cache.get(cacheKey);
if (entry != null && entry.isValid()) {
return Mono.just(entry.resourceCodes.contains(resourceCode));
return Mono.just(entry.hasPermission(resourceCode));
}
return Mono.fromCallable(() -> {
var resources = roleResourceBindService.listResourceByUserId(userId, tenantId);
@@ -84,7 +85,7 @@ public class AuthPermissionProvider implements PermissionProvider {
.filter(code -> code != null && !code.isBlank())
.collect(Collectors.toSet());
cache.put(cacheKey, new CacheEntry(codes, CACHE_TTL_MS));
return codes.contains(resourceCode);
return codes.contains(PermissionMethods.WILDCARD) || codes.contains(resourceCode);
}).subscribeOn(Schedulers.boundedElastic());
}
@@ -100,5 +101,9 @@ public class AuthPermissionProvider implements PermissionProvider {
boolean isValid() {
return System.currentTimeMillis() < expiresAt;
}
boolean hasPermission(String resourceCode) {
return resourceCodes.contains(PermissionMethods.WILDCARD) || resourceCodes.contains(resourceCode);
}
}
}
@@ -23,6 +23,7 @@ import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.exception.AccessDeniedException;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.security.GatewayAuthenticationToken;
import io.github.pnoker.common.security.PermissionMethods;
import io.github.pnoker.common.security.PermissionProvider;
import io.github.pnoker.common.utils.UserHeaderUtil;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@@ -161,7 +162,8 @@ public interface BaseController {
Set<String> authorities = token.getAuthorities().stream()
.map(a -> a.getAuthority())
.collect(Collectors.toSet());
boolean granted = required.stream().anyMatch(authorities::contains);
boolean granted = authorities.contains(PermissionMethods.WILDCARD)
|| required.stream().anyMatch(authorities::contains);
if (granted) {
return Mono.<Void>empty();
}
@@ -42,7 +42,7 @@ public class FacadePermissionProvider implements PermissionProvider {
return Mono.just(false);
}
return listPermissionCodes(tenantId, userId)
.map(codes -> codes.contains(resourceCode));
.map(codes -> codes.contains(PermissionMethods.WILDCARD) || codes.contains(resourceCode));
}
@Override
@@ -21,9 +21,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -68,12 +70,16 @@ public class PermissionMethods {
* @param scope operation scope (e.g. "get", "list", "add", "update", "delete")
* @return true if granted
*/
public boolean can(String domain, String scope) {
public Mono<Boolean> can(String domain, String scope) {
String resourceCode = serviceName + ":" + domain + ":" + scope;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
return false;
}
return ReactiveSecurityContextHolder.getContext()
.map(context -> context.getAuthentication())
.filter(Objects::nonNull)
.map(auth -> hasAuthority(auth, resourceCode))
.defaultIfEmpty(false);
}
private boolean hasAuthority(Authentication auth, String resourceCode) {
Set<String> authorities = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
@@ -92,11 +98,15 @@ public class PermissionMethods {
* @param specs varargs of {@code domain:scope} strings
* @return true if at least one is granted
*/
public boolean any(String... specs) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
return false;
}
public Mono<Boolean> any(String... specs) {
return ReactiveSecurityContextHolder.getContext()
.map(context -> context.getAuthentication())
.filter(Objects::nonNull)
.map(auth -> hasAnyAuthority(auth, specs))
.defaultIfEmpty(false);
}
private boolean hasAnyAuthority(Authentication auth, String... specs) {
Set<String> authorities = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
@@ -0,0 +1,81 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.security;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import reactor.test.StepVerifier;
class PermissionMethodsTest {
private final PermissionMethods permissionMethods = new PermissionMethods("dc3-center-auth");
@Test
void canUsesWildcardFromReactiveSecurityContext() {
TestingAuthenticationToken auth = authenticated(PermissionMethods.WILDCARD);
StepVerifier.create(permissionMethods.can("menu", "list")
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)))
.expectNext(true)
.verifyComplete();
}
@Test
void canMatchesServiceScopedPermission() {
TestingAuthenticationToken auth = authenticated("dc3-center-auth:menu:list");
StepVerifier.create(permissionMethods.can("menu", "list")
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)))
.expectNext(true)
.verifyComplete();
}
@Test
void canRejectsMissingPermission() {
TestingAuthenticationToken auth = authenticated("dc3-center-auth:menu:get");
StepVerifier.create(permissionMethods.can("menu", "list")
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)))
.expectNext(false)
.verifyComplete();
}
@Test
void canRejectsAnonymousContext() {
StepVerifier.create(permissionMethods.can("menu", "list"))
.expectNext(false)
.verifyComplete();
}
@Test
void anyMatchesOneServiceScopedPermission() {
TestingAuthenticationToken auth = authenticated("dc3-center-auth:menu:list");
StepVerifier.create(permissionMethods.any("menu:get", "menu:list")
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)))
.expectNext(true)
.verifyComplete();
}
private TestingAuthenticationToken authenticated(String authority) {
TestingAuthenticationToken auth = new TestingAuthenticationToken("dc3", "n/a", authority);
auth.setAuthenticated(true);
return auth;
}
}
+1
View File
@@ -24,6 +24,7 @@ x-logging: &default-logging
x-app-runtime-env: &app-runtime-env
NODE_ENV: test
APM_AGENT_ENABLE: ${APM_AGENT_ENABLE:-false}
DC3_SECURITY_KEY: ${DC3_SECURITY_KEY:-dc3.security.key.2026.io.github.pnoker}
AUTH_HMAC_SECRET: ${AUTH_HMAC_SECRET:-io.github.pnoker.dc3}
POSTGRES_HOST: dc3-postgres
POSTGRES_PORT: "5432"
+1
View File
@@ -24,6 +24,7 @@ x-logging: &default-logging
x-app-runtime-env: &app-runtime-env
NODE_ENV: test
APM_AGENT_ENABLE: ${APM_AGENT_ENABLE:-false}
DC3_SECURITY_KEY: ${DC3_SECURITY_KEY:-dc3.security.key.2026.io.github.pnoker}
AUTH_HMAC_SECRET: ${AUTH_HMAC_SECRET:-io.github.pnoker.dc3}
POSTGRES_HOST: dc3-postgres
POSTGRES_PORT: "5432"
+1
View File
@@ -192,6 +192,7 @@ DC3_FACADE_MODE=grpc
| 变量 | 范围 | 说明 |
|--------------------|---------|------------------------------------------------------------------|
| `DC3_SECURITY_KEY` | Runtime | Auth Center 生成和校验登录 Token 的共享签名密钥;生产环境应设置强随机值 |
| `AUTH_HMAC_SECRET` | Runtime | Gateway 与后端服务之间用于签名 `X-Auth-User` 的共享 HMAC-SHA256 密钥;生产环境应设置强随机值 |
### 可选依赖和可观测栈