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.UserService;
|
||||
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.JwtProperties;
|
||||
import run.halo.app.security.DefaultUserDetailService;
|
||||
|
@ -68,6 +69,10 @@ public class WebServerSecurityConfig {
|
|||
.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout"))
|
||||
.authorizeExchange(exchanges ->
|
||||
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleService)))
|
||||
.anonymous(anonymousSpec -> {
|
||||
anonymousSpec.authorities(AnonymousUserConst.Role);
|
||||
anonymousSpec.principal(AnonymousUserConst.PRINCIPAL);
|
||||
})
|
||||
.httpBasic(withDefaults())
|
||||
.formLogin(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.RoleService;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
|
@ -56,7 +57,10 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
Set<String> roleNamesImmutable =
|
||||
roleBindingService.listBoundRoleNames(user.getAuthorities());
|
||||
Set<String> roleNames = new HashSet<>(roleNamesImmutable);
|
||||
roleNames.add(AUTHENTICATED_ROLE);
|
||||
if (!AnonymousUserConst.PRINCIPAL.equals(user.getUsername())) {
|
||||
roleNames.add(AUTHENTICATED_ROLE);
|
||||
roleNames.add(AnonymousUserConst.Role);
|
||||
}
|
||||
|
||||
List<Role.PolicyRule> rules = Collections.emptyList();
|
||||
for (String roleName : roleNames) {
|
||||
|
@ -84,7 +88,10 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
public Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo) {
|
||||
var roleNamesImmutable = roleBindingService.listBoundRoleNames(user.getAuthorities());
|
||||
var roleNames = new HashSet<>(roleNamesImmutable);
|
||||
roleNames.add(AUTHENTICATED_ROLE);
|
||||
if (!AnonymousUserConst.PRINCIPAL.equals(user.getUsername())) {
|
||||
roleNames.add(AUTHENTICATED_ROLE);
|
||||
roleNames.add(AnonymousUserConst.Role);
|
||||
}
|
||||
|
||||
var record = new AttributesRecord(user, requestInfo);
|
||||
var visitor = new AuthorizingVisitor(record);
|
||||
|
|
|
@ -3,8 +3,6 @@ package run.halo.app.security.authorization;
|
|||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
@ -20,8 +18,6 @@ import run.halo.app.core.extension.service.RoleService;
|
|||
public class RequestInfoAuthorizationManager
|
||||
implements ReactiveAuthorizationManager<AuthorizationContext> {
|
||||
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
private final AuthorizationRuleResolver ruleResolver;
|
||||
|
||||
public RequestInfoAuthorizationManager(RoleService roleService) {
|
||||
|
@ -48,12 +44,7 @@ public class RequestInfoAuthorizationManager
|
|||
}
|
||||
|
||||
private boolean isGranted(Authentication authentication) {
|
||||
return authentication != null && isNotAnonymous(authentication)
|
||||
&& authentication.isAuthenticated();
|
||||
}
|
||||
|
||||
private boolean isNotAnonymous(Authentication authentication) {
|
||||
return !this.trustResolver.isAnonymous(authentication);
|
||||
return authentication != null && authentication.isAuthenticated();
|
||||
}
|
||||
|
||||
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.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.service.RoleService;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.security.LoginUtils;
|
||||
|
||||
@SpringBootTest
|
||||
|
@ -33,6 +37,12 @@ class JwtAuthenticationTest {
|
|||
@MockBean
|
||||
RoleService roleService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(roleService.getMonoRole(eq(AnonymousUserConst.Role)))
|
||||
.thenReturn(Mono.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessProtectedApiWithoutToken() {
|
||||
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.RouterFunctions.route;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -24,6 +25,7 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
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.web.reactive.function.server.RouterFunction;
|
||||
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.PolicyRule;
|
||||
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.infra.AnonymousUserConst;
|
||||
import run.halo.app.security.LoginUtils;
|
||||
|
||||
@SpringBootTest
|
||||
|
@ -69,6 +73,8 @@ class AuthorizationTest {
|
|||
when(userDetailsService.findByUsername(eq("user"))).thenReturn(Mono.just(
|
||||
User.withDefaultPasswordEncoder().username("user").password("password")
|
||||
.roles("post.read").build()));
|
||||
when(roleService.getMonoRole(eq(AnonymousUserConst.Role)))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
var role = new Role();
|
||||
role.setRules(List.of(
|
||||
|
@ -94,6 +100,68 @@ class AuthorizationTest {
|
|||
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
|
||||
static class TestConfig {
|
||||
|
||||
|
|
Loading…
Reference in New Issue