mirror of https://github.com/halo-dev/halo
feat: any user who accesses the system has the anonymous role (#2443)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: - 未认证用户在系统中被定义为匿名用户(username=anonymousUser),它在授权系统中具有 principle=anonymousUser 且 role=anonymous,key=[secure randomly generated key] - 未认证用户和已认证用户都将被赋予一个匿名用户角色 参考: - [Spring security#ServerHttpSecurity.AnonymousSpec](pull/2456/head70460ca009/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java (L4387)
) - [Spring security reactive AnonymousAuthenticationWebFilter](70460ca009/web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java (L46)
) #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note None ```
parent
bfbc4ec70a
commit
cda6402780
|
@ -34,6 +34,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.core.extension.service.UserService;
|
import run.halo.app.core.extension.service.UserService;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.properties.JwtProperties;
|
import run.halo.app.infra.properties.JwtProperties;
|
||||||
import run.halo.app.security.DefaultUserDetailService;
|
import run.halo.app.security.DefaultUserDetailService;
|
||||||
|
@ -68,6 +69,10 @@ public class WebServerSecurityConfig {
|
||||||
.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout"))
|
.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout"))
|
||||||
.authorizeExchange(exchanges ->
|
.authorizeExchange(exchanges ->
|
||||||
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleService)))
|
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleService)))
|
||||||
|
.anonymous(anonymousSpec -> {
|
||||||
|
anonymousSpec.authorities(AnonymousUserConst.Role);
|
||||||
|
anonymousSpec.principal(AnonymousUserConst.PRINCIPAL);
|
||||||
|
})
|
||||||
.httpBasic(withDefaults())
|
.httpBasic(withDefaults())
|
||||||
.formLogin(withDefaults())
|
.formLogin(withDefaults())
|
||||||
.logout(withDefaults())
|
.logout(withDefaults())
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package run.halo.app.infra;
|
||||||
|
|
||||||
|
public interface AnonymousUserConst {
|
||||||
|
String PRINCIPAL = "anonymousUser";
|
||||||
|
|
||||||
|
String Role = "anonymous";
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
||||||
import run.halo.app.core.extension.service.RoleBindingService;
|
import run.halo.app.core.extension.service.RoleBindingService;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +57,10 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
Set<String> roleNamesImmutable =
|
Set<String> roleNamesImmutable =
|
||||||
roleBindingService.listBoundRoleNames(user.getAuthorities());
|
roleBindingService.listBoundRoleNames(user.getAuthorities());
|
||||||
Set<String> roleNames = new HashSet<>(roleNamesImmutable);
|
Set<String> roleNames = new HashSet<>(roleNamesImmutable);
|
||||||
|
if (!AnonymousUserConst.PRINCIPAL.equals(user.getUsername())) {
|
||||||
roleNames.add(AUTHENTICATED_ROLE);
|
roleNames.add(AUTHENTICATED_ROLE);
|
||||||
|
roleNames.add(AnonymousUserConst.Role);
|
||||||
|
}
|
||||||
|
|
||||||
List<Role.PolicyRule> rules = Collections.emptyList();
|
List<Role.PolicyRule> rules = Collections.emptyList();
|
||||||
for (String roleName : roleNames) {
|
for (String roleName : roleNames) {
|
||||||
|
@ -84,7 +88,10 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
public Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo) {
|
public Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo) {
|
||||||
var roleNamesImmutable = roleBindingService.listBoundRoleNames(user.getAuthorities());
|
var roleNamesImmutable = roleBindingService.listBoundRoleNames(user.getAuthorities());
|
||||||
var roleNames = new HashSet<>(roleNamesImmutable);
|
var roleNames = new HashSet<>(roleNamesImmutable);
|
||||||
|
if (!AnonymousUserConst.PRINCIPAL.equals(user.getUsername())) {
|
||||||
roleNames.add(AUTHENTICATED_ROLE);
|
roleNames.add(AUTHENTICATED_ROLE);
|
||||||
|
roleNames.add(AnonymousUserConst.Role);
|
||||||
|
}
|
||||||
|
|
||||||
var record = new AttributesRecord(user, requestInfo);
|
var record = new AttributesRecord(user, requestInfo);
|
||||||
var visitor = new AuthorizingVisitor(record);
|
var visitor = new AuthorizingVisitor(record);
|
||||||
|
|
|
@ -3,8 +3,6 @@ package run.halo.app.security.authorization;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
|
||||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
|
||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
@ -20,8 +18,6 @@ import run.halo.app.core.extension.service.RoleService;
|
||||||
public class RequestInfoAuthorizationManager
|
public class RequestInfoAuthorizationManager
|
||||||
implements ReactiveAuthorizationManager<AuthorizationContext> {
|
implements ReactiveAuthorizationManager<AuthorizationContext> {
|
||||||
|
|
||||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
|
||||||
|
|
||||||
private final AuthorizationRuleResolver ruleResolver;
|
private final AuthorizationRuleResolver ruleResolver;
|
||||||
|
|
||||||
public RequestInfoAuthorizationManager(RoleService roleService) {
|
public RequestInfoAuthorizationManager(RoleService roleService) {
|
||||||
|
@ -48,12 +44,7 @@ public class RequestInfoAuthorizationManager
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isGranted(Authentication authentication) {
|
private boolean isGranted(Authentication authentication) {
|
||||||
return authentication != null && isNotAnonymous(authentication)
|
return authentication != null && authentication.isAuthenticated();
|
||||||
&& authentication.isAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNotAnonymous(Authentication authentication) {
|
|
||||||
return !this.trustResolver.isAnonymous(authentication);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserDetails createUserDetails(Authentication authentication) {
|
private UserDetails createUserDetails(Authentication authentication) {
|
||||||
|
|
|
@ -2,9 +2,12 @@ package run.halo.app.security.authentication.jwt;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
@ -18,6 +21,7 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
import run.halo.app.security.LoginUtils;
|
import run.halo.app.security.LoginUtils;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ -33,6 +37,12 @@ class JwtAuthenticationTest {
|
||||||
@MockBean
|
@MockBean
|
||||||
RoleService roleService;
|
RoleService roleService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
lenient().when(roleService.getMonoRole(eq(AnonymousUserConst.Role)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void accessProtectedApiWithoutToken() {
|
void accessProtectedApiWithoutToken() {
|
||||||
webClient.get().uri("/api/v1/test/hello").exchange().expectStatus().isUnauthorized();
|
webClient.get().uri("/api/v1/test/hello").exchange().expectStatus().isUnauthorized();
|
||||||
|
|
|
@ -10,6 +10,7 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -24,6 +25,7 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
@ -32,7 +34,9 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.Role.PolicyRule;
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
import run.halo.app.security.LoginUtils;
|
import run.halo.app.security.LoginUtils;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ -69,6 +73,8 @@ class AuthorizationTest {
|
||||||
when(userDetailsService.findByUsername(eq("user"))).thenReturn(Mono.just(
|
when(userDetailsService.findByUsername(eq("user"))).thenReturn(Mono.just(
|
||||||
User.withDefaultPasswordEncoder().username("user").password("password")
|
User.withDefaultPasswordEncoder().username("user").password("password")
|
||||||
.roles("post.read").build()));
|
.roles("post.read").build()));
|
||||||
|
when(roleService.getMonoRole(eq(AnonymousUserConst.Role)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
|
||||||
var role = new Role();
|
var role = new Role();
|
||||||
role.setRules(List.of(
|
role.setRules(List.of(
|
||||||
|
@ -94,6 +100,68 @@ class AuthorizationTest {
|
||||||
verify(roleService, times(2)).getMonoRole("post.read");
|
verify(roleService, times(2)).getMonoRole("post.read");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anonymousUserAccessProtectedApi() {
|
||||||
|
when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
when(roleService.getMonoRole(AnonymousUserConst.Role))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
|
||||||
|
.isUnauthorized();
|
||||||
|
|
||||||
|
verify(roleService, times(1)).getMonoRole(AnonymousUserConst.Role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anonymousUserAccessAuthenticationFreeApi() {
|
||||||
|
when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
Role role = new Role();
|
||||||
|
role.setMetadata(new Metadata());
|
||||||
|
role.getMetadata().setName(AnonymousUserConst.Role);
|
||||||
|
role.setRules(new ArrayList<>());
|
||||||
|
PolicyRule policyRule = new PolicyRule.Builder()
|
||||||
|
.apiGroups("fake.halo.run")
|
||||||
|
.verbs("list")
|
||||||
|
.resources("posts")
|
||||||
|
.build();
|
||||||
|
role.getRules().add(policyRule);
|
||||||
|
when(roleService.getMonoRole(AnonymousUserConst.Role))
|
||||||
|
.thenReturn(Mono.just(role));
|
||||||
|
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
|
||||||
|
.isOk()
|
||||||
|
.expectBody(String.class).isEqualTo("returned posts");
|
||||||
|
|
||||||
|
verify(roleService, times(1)).getMonoRole(AnonymousUserConst.Role);
|
||||||
|
|
||||||
|
webClient.get().uri("/apis/fake.halo.run/v1/posts/hello-halo").exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isUnauthorized();
|
||||||
|
verify(roleService, times(2)).getMonoRole(AnonymousUserConst.Role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "user", roles = "post.read")
|
||||||
|
void authenticatedUserAccessAuthenticationFreeApi() {
|
||||||
|
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.empty());
|
||||||
|
when(roleService.getMonoRole("post.read")).thenReturn(Mono.empty());
|
||||||
|
Role role = new Role();
|
||||||
|
role.setMetadata(new Metadata());
|
||||||
|
role.getMetadata().setName(AnonymousUserConst.Role);
|
||||||
|
role.setRules(new ArrayList<>());
|
||||||
|
PolicyRule policyRule = new PolicyRule.Builder()
|
||||||
|
.apiGroups("fake.halo.run")
|
||||||
|
.verbs("list")
|
||||||
|
.resources("posts")
|
||||||
|
.build();
|
||||||
|
role.getRules().add(policyRule);
|
||||||
|
when(roleService.getMonoRole(AnonymousUserConst.Role))
|
||||||
|
.thenReturn(Mono.just(role));
|
||||||
|
webClient.get().uri("/apis/fake.halo.run/v1/posts").exchange().expectStatus()
|
||||||
|
.isOk()
|
||||||
|
.expectBody(String.class).isEqualTo("returned posts");
|
||||||
|
}
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
static class TestConfig {
|
static class TestConfig {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue