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](70460ca009/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
```
pull/2456/head
guqing 2022-09-22 14:58:12 +08:00 committed by GitHub
parent bfbc4ec70a
commit cda6402780
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 12 deletions

View File

@ -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())

View File

@ -0,0 +1,7 @@
package run.halo.app.infra;
public interface AnonymousUserConst {
String PRINCIPAL = "anonymousUser";
String Role = "anonymous";
}

View File

@ -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);
roleNames.add(AUTHENTICATED_ROLE); if (!AnonymousUserConst.PRINCIPAL.equals(user.getUsername())) {
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);
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 record = new AttributesRecord(user, requestInfo);
var visitor = new AuthorizingVisitor(record); var visitor = new AuthorizingVisitor(record);

View File

@ -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) {

View File

@ -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();

View File

@ -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 {