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

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.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);
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);
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);

View File

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

View File

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

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