refactor: rule resolver and role binding lister (#2119)

pull/2131/head
guqing 2022-05-30 17:08:09 +08:00 committed by GitHub
parent 9a05942bc4
commit f4a943e45a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 42 additions and 200 deletions

View File

@ -53,10 +53,8 @@ import run.halo.app.identity.authentication.ProviderSettings;
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter; import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
import run.halo.app.identity.authentication.verifier.JwtAccessTokenNonBlockedValidator; import run.halo.app.identity.authentication.verifier.JwtAccessTokenNonBlockedValidator;
import run.halo.app.identity.authentication.verifier.TokenAuthenticationManagerResolver; import run.halo.app.identity.authentication.verifier.TokenAuthenticationManagerResolver;
import run.halo.app.identity.authorization.DefaultRoleBindingLister;
import run.halo.app.identity.authorization.DefaultRoleGetter; import run.halo.app.identity.authorization.DefaultRoleGetter;
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager; import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.RoleBindingLister;
import run.halo.app.identity.authorization.RoleGetter; import run.halo.app.identity.authorization.RoleGetter;
import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler; import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler;
import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint; import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint;
@ -131,9 +129,8 @@ public class WebSecurityConfig {
} }
RequestInfoAuthorizationManager requestInfoAuthorizationManager() { RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
RoleBindingLister roleBindingLister = new DefaultRoleBindingLister();
RoleGetter roleGetter = new DefaultRoleGetter(extensionClient); RoleGetter roleGetter = new DefaultRoleGetter(extensionClient);
return new RequestInfoAuthorizationManager(roleGetter, roleBindingLister); return new RequestInfoAuthorizationManager(roleGetter);
} }
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver() { AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver() {

View File

@ -1,12 +1,13 @@
package run.halo.app.identity.authorization; package run.halo.app.identity.authorization;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import run.halo.app.extension.Metadata; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
/** /**
* <p>Obtain the authorities from the authenticated authentication and construct it as a RoleBinding * <p>Obtain the authorities from the authenticated authentication and construct it as a RoleBinding
@ -15,67 +16,29 @@ import run.halo.app.extension.Metadata;
* so there is no need to query from the database.</p> * so there is no need to query from the database.</p>
* <p>For tokens in other formats, after authentication, fill the authorities with the token into * <p>For tokens in other formats, after authentication, fill the authorities with the token into
* the SecurityContextHolder.</p> * the SecurityContextHolder.</p>
* <pre>
* kind: RoleBinding
* metadata:
* name: some-name
* subjects:
* # You can specify more than one "subject"
* - kind: User
* name: some-username
* roleRef:
* kind: Role
* name: role-name
* </pre>
* *
* @author guqing * @author guqing
* @see AnonymousAuthenticationFilter
* @since 2.0.0 * @since 2.0.0
*/ */
@Slf4j
public class DefaultRoleBindingLister implements RoleBindingLister { public class DefaultRoleBindingLister implements RoleBindingLister {
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
private static final String ROLE_AUTHORITY_PREFIX = "ROLE_"; private static final String ROLE_AUTHORITY_PREFIX = "ROLE_";
@Override @Override
public List<RoleBinding> listRoleBindings() { public Set<String> listBoundRoleNames() {
Authentication authentication = SecurityContextHolder.getContext() Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication(); .getAuthentication();
if (authentication == null) { if (authentication == null) {
return Collections.emptyList(); log.debug("No authentication found in SecurityContext.");
return Collections.emptySet();
} }
String username = authentication.getName();
List<String> roleNames = roleNamesFromAuthentication();
List<RoleBinding> roleBindings = new ArrayList<>(roleNames.size());
for (String roleName : roleNames) {
RoleBinding roleBinding = new RoleBinding();
// metadata
Metadata metadata = new Metadata();
metadata.setName(username + "_" + roleName);
roleBinding.setMetadata(metadata);
// role ref
RoleRef roleRef = new RoleRef();
roleRef.setKind("Role");
roleRef.setName(roleName);
roleBinding.setRoleRef(roleRef);
// subject
Subject subject = new Subject();
subject.setKind("User");
subject.setName(username);
roleBinding.setSubjects(List.of(subject));
roleBindings.add(roleBinding);
}
return roleBindings;
}
private List<String> roleNamesFromAuthentication() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
return authentication.getAuthorities() return authentication.getAuthorities()
.stream() .stream()
.map(GrantedAuthority::getAuthority) .map(GrantedAuthority::getAuthority)
// Exclude anonymous user roles
.filter(authority -> !authority.equals("ROLE_ANONYMOUS"))
.map(scope -> { .map(scope -> {
if (scope.startsWith(SCOPE_AUTHORITY_PREFIX)) { if (scope.startsWith(SCOPE_AUTHORITY_PREFIX)) {
return scope.replaceFirst(SCOPE_AUTHORITY_PREFIX, ""); return scope.replaceFirst(SCOPE_AUTHORITY_PREFIX, "");
@ -85,6 +48,6 @@ public class DefaultRoleBindingLister implements RoleBindingLister {
} }
return scope; return scope;
}) })
.toList(); .collect(Collectors.toSet());
} }
} }

View File

@ -2,9 +2,10 @@ package run.halo.app.identity.authorization;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
/** /**
* @author guqing * @author guqing
@ -12,13 +13,13 @@ import org.springframework.security.core.userdetails.UserDetails;
*/ */
@Data @Data
public class DefaultRuleResolver implements AuthorizationRuleResolver { public class DefaultRuleResolver implements AuthorizationRuleResolver {
private static final String USER_KIND = "User";
RoleGetter roleGetter;
RoleBindingLister roleBindingLister;
public DefaultRuleResolver(RoleGetter roleGetter, RoleBindingLister roleBindingLister) { private RoleGetter roleGetter;
private RoleBindingLister roleBindingLister = new DefaultRoleBindingLister();
public DefaultRuleResolver(RoleGetter roleGetter) {
this.roleGetter = roleGetter; this.roleGetter = roleGetter;
this.roleBindingLister = roleBindingLister;
} }
@Override @Override
@ -38,26 +39,12 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
@Override @Override
public void visitRulesFor(UserDetails user, RuleAccumulator visitor) { public void visitRulesFor(UserDetails user, RuleAccumulator visitor) {
List<RoleBinding> roleBindings = Collections.emptyList(); Set<String> roleNames = roleBindingLister.listBoundRoleNames();
try {
roleBindings = roleBindingLister.listRoleBindings();
} catch (Exception e) {
if (visitor.visit(null, null, e)) {
return;
}
}
for (RoleBinding roleBinding : roleBindings) { List<PolicyRule> rules = Collections.emptyList();
AppliesResult appliesResult = appliesTo(user, roleBinding.subjects); for (String roleName : roleNames) {
if (!appliesResult.applies) {
continue;
}
Subject subject = roleBinding.subjects.get(appliesResult.subjectIndex);
List<PolicyRule> rules = Collections.emptyList();
try { try {
Role role = roleGetter.getRole(roleBinding.roleRef.name); Role role = roleGetter.getRole(roleName);
rules = role.getRules(); rules = role.getRules();
} catch (Exception e) { } catch (Exception e) {
if (visitor.visit(null, null, e)) { if (visitor.visit(null, null, e)) {
@ -65,7 +52,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
} }
} }
String source = roleBindingDescriber(roleBinding, subject); String source = roleBindingDescriber(roleName, user.getUsername());
for (PolicyRule rule : rules) { for (PolicyRule rule : rules) {
if (!visitor.visit(source, rule, null)) { if (!visitor.visit(source, rule, null)) {
return; return;
@ -74,28 +61,12 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
} }
} }
String roleBindingDescriber(RoleBinding roleBinding, Subject subject) { String roleBindingDescriber(String roleName, String subject) {
String describeSubject = String.format("%s %s", subject.kind, subject.name); return String.format("Binding role [%s] to [%s]", roleName, subject);
return String.format("RoleBinding %s of %s %s to %s", roleBinding.getMetadata().getName(),
roleBinding.roleRef.getKind(), roleBinding.roleRef.getName(), describeSubject);
} }
AppliesResult appliesTo(UserDetails user, List<Subject> bindingSubjects) { public void setRoleBindingLister(RoleBindingLister roleBindingLister) {
for (int i = 0; i < bindingSubjects.size(); i++) { Assert.notNull(roleBindingLister, "The roleBindingLister must not be null.");
if (appliesToUser(user, bindingSubjects.get(i))) { this.roleBindingLister = roleBindingLister;
return new AppliesResult(true, i);
}
}
return new AppliesResult(false, 0);
}
boolean appliesToUser(UserDetails user, Subject subject) {
if (USER_KIND.equals(subject.kind)) {
return StringUtils.equals(user.getUsername(), subject.name);
}
return false;
}
record AppliesResult(boolean applies, int subjectIndex) {
} }
} }

View File

@ -1,8 +0,0 @@
package run.halo.app.identity.authorization;
/**
* @author guqing
* @since 2.0.0
*/
public class NonResourceRuleInfo {
}

View File

@ -1,12 +0,0 @@
package run.halo.app.identity.authorization;
import java.util.List;
/**
* @author guqing
* @since 2.0.0
*/
public class PolicyRuleInfo {
List<ResourceRuleInfo> resourceRules;
List<NonResourceRuleInfo> nonResourceRules;
}

View File

@ -26,9 +26,8 @@ public class RequestInfoAuthorizationManager
private AuthorizationRuleResolver ruleResolver; private AuthorizationRuleResolver ruleResolver;
public RequestInfoAuthorizationManager(RoleGetter roleGetter, public RequestInfoAuthorizationManager(RoleGetter roleGetter) {
RoleBindingLister roleBindingLister) { this.ruleResolver = new DefaultRuleResolver(roleGetter);
this.ruleResolver = new DefaultRuleResolver(roleGetter, roleBindingLister);
} }
@Override @Override

View File

@ -1,8 +0,0 @@
package run.halo.app.identity.authorization;
/**
* @author guqing
* @since 2.0.0
*/
public class ResourceRuleInfo {
}

View File

@ -1,6 +1,6 @@
package run.halo.app.identity.authorization; package run.halo.app.identity.authorization;
import java.util.List; import java.util.Set;
/** /**
* @author guqing * @author guqing
@ -9,5 +9,5 @@ import java.util.List;
@FunctionalInterface @FunctionalInterface
public interface RoleBindingLister { public interface RoleBindingLister {
List<RoleBinding> listRoleBindings(); Set<String> listBoundRoleNames();
} }

View File

@ -2,14 +2,13 @@ package run.halo.app.authorization;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import java.util.List; import java.util.Set;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import run.halo.app.identity.authorization.DefaultRoleBindingLister; import run.halo.app.identity.authorization.DefaultRoleBindingLister;
import run.halo.app.identity.authorization.RoleBinding;
/** /**
* Tests for {@link DefaultRoleBindingLister}. * Tests for {@link DefaultRoleBindingLister}.
@ -30,30 +29,15 @@ public class DefaultRoleBindingListerTest {
@Test @Test
@WithMockUser(username = "test", roles = {"readPost", "readTag"}) @WithMockUser(username = "test", roles = {"readPost", "readTag"})
void listWhenAuthorizedWithTwoRoles() { void listWhenAuthorizedWithTwoRoles() {
List<RoleBinding> roleBindings = roleBindingLister.listRoleBindings(); Set<String> roleBindings = roleBindingLister.listBoundRoleNames();
assertThat(roleBindings).isNotNull(); assertThat(roleBindings).isNotNull();
assertThat(roleBindings.size()).isEqualTo(2); assertThat(roleBindings.size()).isEqualTo(2);
assertThat(roleBindings).containsAll(Set.of("readPost", "readTag"));
RoleBinding readPostRoleBinding = roleBindings.get(0);
assertThat(readPostRoleBinding.getMetadata()).isNotNull();
assertThat(readPostRoleBinding.getMetadata().getName()).isNotNull();
assertThat(readPostRoleBinding.getSubjects()).allMatch(subject ->
"test".equals(subject.getName())
&& "User".equals(subject.getKind()));
assertThat(readPostRoleBinding.getRoleRef().getName()).isEqualTo("readPost");
RoleBinding readTagRoleBinding = roleBindings.get(1);
assertThat(readTagRoleBinding.getMetadata()).isNotNull();
assertThat(readTagRoleBinding.getMetadata().getName()).isNotNull();
assertThat(readTagRoleBinding.getSubjects()).allMatch(subject ->
"test".equals(subject.getName())
&& "User".equals(subject.getKind()));
assertThat(readTagRoleBinding.getRoleRef().getName()).isEqualTo("readTag");
} }
@Test @Test
void listWhenUnauthorizedThenEmpty() { void listWhenUnauthorizedThenEmpty() {
List<RoleBinding> roleBindings = roleBindingLister.listRoleBindings(); Set<String> roleBindings = roleBindingLister.listBoundRoleNames();
assertThat(roleBindings).isEmpty(); assertThat(roleBindings).isEmpty();
} }
} }

View File

@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.RegExUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
@ -21,9 +22,6 @@ import run.halo.app.identity.authorization.RbacRequestEvaluation;
import run.halo.app.identity.authorization.RequestInfo; import run.halo.app.identity.authorization.RequestInfo;
import run.halo.app.identity.authorization.RequestInfoFactory; import run.halo.app.identity.authorization.RequestInfoFactory;
import run.halo.app.identity.authorization.Role; import run.halo.app.identity.authorization.Role;
import run.halo.app.identity.authorization.RoleBinding;
import run.halo.app.identity.authorization.RoleRef;
import run.halo.app.identity.authorization.Subject;
/** /**
* Tests for {@link RequestInfoFactory}. * Tests for {@link RequestInfoFactory}.
@ -135,29 +133,12 @@ public class RequestInfoResolverTest {
metadata.setName("ruleReadPost"); metadata.setName("ruleReadPost");
role.setMetadata(metadata); role.setMetadata(metadata);
return role; return role;
}, () -> {
// role binding lister
RoleBinding roleBinding = new RoleBinding();
Metadata metadata = new Metadata();
metadata.setName("admin_ruleReadPost");
roleBinding.setMetadata(metadata);
Subject subject = new Subject();
subject.setName("admin");
subject.setKind("User");
subject.setApiGroup("");
roleBinding.setSubjects(List.of(subject));
RoleRef roleRef = new RoleRef();
roleRef.setKind("Role");
roleRef.setName("ruleReadPost");
roleRef.setApiGroup("");
roleBinding.setRoleRef(roleRef);
return List.of(roleBinding);
}); });
// list bound role names
ruleResolver.setRoleBindingLister(() -> Set.of("ruleReadPost"));
User user = new User("admin", "123456", AuthorityUtils.createAuthorityList("ruleReadPost")); User user = new User("admin", "123456", AuthorityUtils.createAuthorityList("ruleReadPost"));
// resolve user rules // resolve user rules
List<PolicyRule> rules = ruleResolver.rulesFor(user); List<PolicyRule> rules = ruleResolver.rulesFor(user);
assertThat(rules).isNotNull(); assertThat(rules).isNotNull();
@ -175,7 +156,6 @@ public class RequestInfoResolverTest {
} }
} }
public record NonApiCase(String url, boolean expected){} public record NonApiCase(String url, boolean expected){}
public record ErrorCases(String desc, String url) {} public record ErrorCases(String desc, String url) {}

View File

@ -59,9 +59,6 @@ import run.halo.app.identity.authentication.verifier.TokenAuthenticationManagerR
import run.halo.app.identity.authorization.PolicyRule; import run.halo.app.identity.authorization.PolicyRule;
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager; import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.Role; import run.halo.app.identity.authorization.Role;
import run.halo.app.identity.authorization.RoleBinding;
import run.halo.app.identity.authorization.RoleRef;
import run.halo.app.identity.authorization.Subject;
import run.halo.app.identity.entrypoint.Oauth2LogoutHandler; import run.halo.app.identity.entrypoint.Oauth2LogoutHandler;
import run.halo.app.infra.properties.JwtProperties; import run.halo.app.infra.properties.JwtProperties;
import run.halo.app.infra.utils.HaloUtils; import run.halo.app.infra.utils.HaloUtils;
@ -140,27 +137,6 @@ public class TestWebSecurityConfig {
metadata.setName("ruleReadPost"); metadata.setName("ruleReadPost");
role.setMetadata(metadata); role.setMetadata(metadata);
return role; return role;
}, () -> {
// role binding lister
RoleBinding roleBinding = new RoleBinding();
Metadata metadata = new Metadata();
metadata.setName("userRoleBinding");
roleBinding.setMetadata(metadata);
Subject subject = new Subject();
subject.setName("test_user");
subject.setKind("User");
subject.setApiGroup("");
roleBinding.setSubjects(List.of(subject));
RoleRef roleRef = new RoleRef();
roleRef.setKind("Role");
roleRef.setName("ruleReadPost");
roleRef.setApiGroup("");
roleBinding.setRoleRef(roleRef);
return List.of(roleBinding);
}); });
} }