mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-08-28 22:21:16 +08:00
fix: support reactive wildcard permissions
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
+7
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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();
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
-11
@@ -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());
|
||||
|
||||
+81
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 密钥;生产环境应设置强随机值 |
|
||||
|
||||
### 可选依赖和可观测栈
|
||||
|
||||
Reference in New Issue
Block a user