diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index dba4205..f69f71c 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -86,9 +86,16 @@ public Set getAuthorities() { public RequestAuthorizationManager requestAuthorizationManager() { List> mappings = new ArrayList<>(); mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthenticatedAuthorizationManager())); - mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new HasAuthorityAuthorizationManager("ADMIN"))); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new HasAuthorityAuthorizationManager("ADMIN", roleHierarchy()))); mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new PermitAllAuthorizationManager())); mappings.add(new RequestMatcherEntry<>(new AnyRequestMatcher(), new PermitAllAuthorizationManager())); return new RequestAuthorizationManager(mappings); } + + @Bean + public RoleHierarchy roleHierarchy() { + return RoleHierarchyImpl.with() + .role("ADMIN").implies("USER") + .build(); + } } diff --git a/src/main/java/nextstep/security/authorization/HasAuthorityAuthorizationManager.java b/src/main/java/nextstep/security/authorization/HasAuthorityAuthorizationManager.java index a0e4a30..39c59c5 100644 --- a/src/main/java/nextstep/security/authorization/HasAuthorityAuthorizationManager.java +++ b/src/main/java/nextstep/security/authorization/HasAuthorityAuthorizationManager.java @@ -2,16 +2,28 @@ import nextstep.security.authentication.Authentication; +import java.util.Collection; + public class HasAuthorityAuthorizationManager implements AuthorizationManager { private final String authority; + private final RoleHierarchy roleHierarchy; public HasAuthorityAuthorizationManager(String authority) { this.authority = authority; + this.roleHierarchy = new NullRoleHierarchy(); + } + + public HasAuthorityAuthorizationManager(String authority, RoleHierarchy roleHierarchy) { + this.authority = authority; + this.roleHierarchy = roleHierarchy; } @Override public AuthorizationDecision check(Authentication authentication, Object object) { - return new AuthorizationDecision(authentication.getAuthorities().contains(authority)); + + Collection reachableGrantedAuthorities = roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); + + return new AuthorizationDecision(reachableGrantedAuthorities.contains(authority)); } } diff --git a/src/main/java/nextstep/security/authorization/NullRoleHierarchy.java b/src/main/java/nextstep/security/authorization/NullRoleHierarchy.java new file mode 100644 index 0000000..17f1904 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/NullRoleHierarchy.java @@ -0,0 +1,10 @@ +package nextstep.security.authorization; + +import java.util.Collection; + +public class NullRoleHierarchy implements RoleHierarchy { + @Override + public Collection getReachableGrantedAuthorities(Collection authorities) { + return authorities; + } +} diff --git a/src/main/java/nextstep/security/authorization/RoleHierarchy.java b/src/main/java/nextstep/security/authorization/RoleHierarchy.java new file mode 100644 index 0000000..7c9aea9 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/RoleHierarchy.java @@ -0,0 +1,8 @@ +package nextstep.security.authorization; + +import java.util.Collection; + +public interface RoleHierarchy { + + Collection getReachableGrantedAuthorities(Collection authorities); +} diff --git a/src/main/java/nextstep/security/authorization/RoleHierarchyImpl.java b/src/main/java/nextstep/security/authorization/RoleHierarchyImpl.java new file mode 100644 index 0000000..03ea7d8 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/RoleHierarchyImpl.java @@ -0,0 +1,75 @@ +package nextstep.security.authorization; + +import org.springframework.util.CollectionUtils; + +import java.util.*; + +public class RoleHierarchyImpl implements RoleHierarchy { + + private final Map> rolesReachableInOneOrMoreStepsMap; + + public RoleHierarchyImpl(Map> rolesReachableInOneOrMoreStepsMap) { + this.rolesReachableInOneOrMoreStepsMap = rolesReachableInOneOrMoreStepsMap; + } + + public static Builder with() { + return new Builder(); + } + + @Override + public Collection getReachableGrantedAuthorities(Collection authorities) { + + Set reachableRoles = new HashSet<>(); + + if (CollectionUtils.isEmpty(authorities)) { + return Collections.emptyList(); + } + + for (String authority : authorities) { + addReachableRoles(authority, reachableRoles); + } + + return reachableRoles; + } + + private void addReachableRoles(String authority, Set reachableRoles) { + reachableRoles.add(authority); + Set lowerRoles = rolesReachableInOneOrMoreStepsMap.computeIfAbsent(authority, k -> new HashSet<>()); + reachableRoles.addAll(lowerRoles); + } + + public static class Builder { + private final Map> hierarchy; + + private Builder() { + this.hierarchy = new HashMap<>(); + } + + public RoleHierarchyImpl build() { + return new RoleHierarchyImpl(hierarchy); + } + + public ImpliedRoles role(String role) { + return new ImpliedRoles(role); + } + + private Builder addHierarchy(String role, String... impliedRoles) { + Set impliedRoleSet = hierarchy.computeIfAbsent(role, k -> new HashSet<>()); + impliedRoleSet.addAll(Arrays.asList(impliedRoles)); + return this; + } + + public final class ImpliedRoles { + private final String role; + + public ImpliedRoles(String role) { + this.role = role; + } + + public Builder implies(String... impliedRoles) { + return Builder.this.addHierarchy(this.role, impliedRoles); + } + } + } +} + diff --git a/src/test/java/nextstep/app/RoleHierachyTest.java b/src/test/java/nextstep/app/RoleHierachyTest.java new file mode 100644 index 0000000..beb7e36 --- /dev/null +++ b/src/test/java/nextstep/app/RoleHierachyTest.java @@ -0,0 +1,51 @@ +package nextstep.app; + +import nextstep.security.authorization.NullRoleHierarchy; +import nextstep.security.authorization.RoleHierarchyImpl; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class RoleHierachyTest { + + @DisplayName("상위 권한의 reachable authorities는 사용자의 권한과 하위 권한을 같이 반환한다.") + @Test + public void roleHierarchyTest() { + + // given + RoleHierarchyImpl roleHierarchy = RoleHierarchyImpl.with() + .role("ADMIN").implies("USER") + .build(); + + Set authorites = new HashSet<>(); + authorites.add("ADMIN"); + + // when + Collection reachableGrantedAuthorities = roleHierarchy.getReachableGrantedAuthorities(authorites); + + // then + Assertions.assertThat(reachableGrantedAuthorities).containsOnly("ADMIN", "USER"); + } + + @DisplayName("NullRoleHierarchy의 reachable authorities는 사용자의 권한 리스트를 그대로 반환한다.") + @Test + public void nullRoleHierarchyTest() { + + // given + NullRoleHierarchy nullRoleHierarchy = new NullRoleHierarchy(); + + Set authorites = new HashSet<>(); + authorites.add("ADMIN"); + authorites.add("USER"); + + // when + Collection result = nullRoleHierarchy.getReachableGrantedAuthorities(authorites); + + // then + Assertions.assertThat(result).isSameAs(authorites); + } +}