ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,20 +1,24 @@
 package nextstep.security.authorization.web;
 
 import java.util.Collection;
+import nextstep.security.access.NullRoleHierarchy;
+import nextstep.security.access.RoleHierarchy;
 import nextstep.security.authentication.Authentication;
 import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authorization.AuthorizationDecision;
 import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.core.GrantedAuthority;
 
 public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+    private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
 
     private final Collection<String> authorities;
 
-    public AuthorityAuthorizationManager(Collection<String> authorities) {
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, Collection<String> authorities) {
+        this.roleHierarchy = roleHierarchy;
         this.authorities = authorities;
     }
 
-
     @Override
     public AuthorizationDecision check(Authentication authentication, T object) {
         if (authentication == null) {
@@ -26,13 +30,17 @@ public AuthorizationDecision check(Authentication authentication, T object) {
         return AuthorizationDecision.of(hasAuthority);
     }
 
-
     private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
-        for (String authority : authentication.getAuthorities()) {
-            if (authorities.contains(authority)) {
+        System.out.println(getGrantedAuthorities(authentication));
+        for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
+            if (authorities.contains(grantedAuthority.getAuthority())) {
                 return true;
             }
         }
         return false;
     }
+
+    private Collection<? extends GrantedAuthority> getGrantedAuthorities(Authentication authentication) {
+        return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
+    }
 }

리뷰: ```suggestion
    private final RoleHierarchy roleHierarchy = new NullRoleHierarchy();
```
불변이면 좋을 것 같네요 :)
@@ -1,20 +1,24 @@
 package nextstep.security.authorization.web;
 
 import java.util.Collection;
+import nextstep.security.access.NullRoleHierarchy;
+import nextstep.security.access.RoleHierarchy;
 import nextstep.security.authentication.Authentication;
 import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authorization.AuthorizationDecision;
 import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.core.GrantedAuthority;
 
 public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+    private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
 
     private final Collection<String> authorities;
 
-    public AuthorityAuthorizationManager(Collection<String> authorities) {
+    public AuthorityAuthorizationManager(RoleHierarchy roleHierarchy, Collection<String> authorities) {
+        this.roleHierarchy = roleHierarchy;
         this.authorities = authorities;
     }
 
-
     @Override
     public AuthorizationDecision check(Authentication authentication, T object) {
         if (authentication == null) {
@@ -26,13 +30,17 @@ public AuthorizationDecision check(Authentication authentication, T object) {
         return AuthorizationDecision.of(hasAuthority);
     }
 
-
     private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
-        for (String authority : authentication.getAuthorities()) {
-            if (authorities.contains(authority)) {
+        System.out.println(getGrantedAuthorities(authentication));
+        for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
+            if (authorities.contains(grantedAuthority.getAuthority())) {
                 return true;
             }
         }
         return false;
     }
+
+    private Collection<? extends GrantedAuthority> getGrantedAuthorities(Authentication authentication) {
+        return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
+    }
 }

리뷰: 기능으로 사용하지 않는 출력문인 것 같아요.
@@ -0,0 +1,13 @@
+package nextstep.security.access;
+
+import java.util.Collection;
+import nextstep.security.core.GrantedAuthority;
+
+public final class NullRoleHierarchy implements RoleHierarchy {
+
+    @Override
+    public Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
+            Collection<? extends GrantedAuthority> authorities) {
+        return authorities;
+    }
+}

리뷰: 기존 스프링시큐리티 코드에서 가져온 부분인데,
생각해보니 저희 구조에선 불필요하네요,, 수정해두겠습니다
@@ -3,11 +3,14 @@
 import nextstep.app.domain.Member;
 import nextstep.app.domain.MemberRepository;
 import nextstep.security.authorization.Secured;
+import nextstep.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
+import java.util.Optional;
 
 @RestController
 public class MemberController {
@@ -24,6 +27,19 @@ public ResponseEntity<List<Member>> list() {
         return ResponseEntity.ok(members);
     }
 
+    @GetMapping("/members/me")
+    public ResponseEntity<Member> getMember() {
+        SecurityContext context = SecurityContextHolder.getContext();
+        String email = (String) context.getAuthentication().getPrincipal();
+
+        Optional<Member> memberOpt = memberRepository.findByEmail(email);
+        if (memberOpt.isEmpty()) {
+            throw new IllegalStateException("회원을 찾을 수 없습니다. (email=%s)".formatted(email));
+        }
+
+        return ResponseEntity.ok(memberOpt.get());
+    }
+
     @Secured("ADMIN")
     @GetMapping("/search")
     public ResponseEntity<List<Member>> search() {

리뷰: orElseThrow()를 사용하면 더 간결하고 처리할 수 있겠네요 😄 
```suggestion
        Member member = memberRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalStateException("회원을 찾을 수 없습니다. (email=%s)".formatted(email)));

        return ResponseEntity.ok(member);
    }
```
@@ -0,0 +1,26 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = method.getAnnotation(Secured.class);
+
+            Set<String> authorities = authentication.getAuthorities();
+            if (authorities.contains(secured.value())) {
+                return AuthorizationDecision.grantedOf();
+            }
+        }
+
+        return AuthorizationDecision.deniedOf();
+    }
+}

리뷰: Spring AOP 환경에서는 메서드가 프록시 객체로 감싸질 수 있기 때문에, 프록시된 메서드를 조회할 경우 실제 구현체의 메서드에서 선언된 `@Secured` 어노테이션을 찾지 못할 가능성이 있습니다.
method.getAnnotation 와 AopUtils.getMostSpecificMethod() 어떤 차이가 있을까요? 😄
@@ -0,0 +1,11 @@
+package nextstep.security.util.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+public class AnyRequestMatcher implements RequestMatcher {
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return true;
+    }
+}

리뷰: AnyRequestMatcher는 동일한 동작을 수행하는 불변 객체이므로 싱글톤 패턴 적용을 해봐도 좋을 것 같아요 😄 
또한, 외부에서 인스턴스 생성을 방지하도록 private 생성자를 추가하는 것도 좋겠네요😄

``` java
public static final AnyRequestMatcher INSTANCE = new AnyRequestMatcher();

private AnyRequestMatcher() {
}
```
@@ -0,0 +1,40 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.util.matcher.RequestMatcherEntry;
+
+import java.util.List;
+
+/**
+ * - 요청에 대해서 적합한 권한이 있는지만 판단.
+ * - 인가가 되지 않았을 경우 예외를 발생시키지는 않는다. (예외는 AuthorizationFilter에서 던짐)
+ */
+public class RequestAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+
+    private final List<RequestMatcherEntry<AuthorizationManager>> mappings;
+
+    public RequestAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager>> mappings) {
+        this.mappings = mappings;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) {
+        if (!isAuthenticated(authentication)) {
+            return AuthorizationDecision.deniedOf();
+        }
+
+        for (RequestMatcherEntry<AuthorizationManager> entry : mappings) {
+            if (entry.matches(request)) {
+                AuthorizationManager manager = entry.getEntry();
+                return manager.check(authentication, request);
+            }
+        }
+
+        return AuthorizationDecision.deniedOf();
+    }
+
+    private boolean isAuthenticated(Authentication authentication) {
+        return authentication != null && authentication.isAuthenticated();
+    }
+}

리뷰: > Q. 미션을 구현하다보니 RequestAuthorizationManager에서 Genric Raw Use 관련해서 경고가 뜨던데 이 부분을 어떻게 핸들링하기가 어렵더라고요. 시큐리티 코드를 보니까 List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> 같은 형태를 쓰긴 하던데 현재 수준에서는 Genric Raw Use를 어떻게 피할지 감이 잘 안 잡히네요. 😅 힌트 주시면 감사하겠습니다!

A. 제네릭 타입을 명확히 지정하면 더 안전한 코드가 될 것 같아요! 
아래와 같이 `AuthorizationManager<HttpServletRequest>`를 지정하면  
IDE 경고도 사라지고, 타입 안정성이 더 높아집니다.

``` java
private final List<RequestMatcherEntry<AuthorizationManager<HttpServletRequest>>> mappings;
```
@@ -13,15 +13,13 @@
 import nextstep.security.context.SecurityContextHolderFilter;
 import nextstep.security.matcher.AnyRequestMatcher;
 import nextstep.security.matcher.MvcRequestMatcher;
-import nextstep.security.matcher.RequestMatcherEntry;
 import nextstep.security.userdetails.UserDetails;
 import nextstep.security.userdetails.UserDetailsService;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
 import org.springframework.http.HttpMethod;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -50,20 +48,31 @@ public SecuredMethodInterceptor securedMethodInterceptor() {
         return new SecuredMethodInterceptor();
     }
 
+//    @Bean
+//    public SecuredAspect securedAspect() {
+//        return new SecuredAspect();
+//    }
+
+
     @Bean
-    public SecuredAspect securedAspect() {
-        return new SecuredAspect();
+    public RoleHierarchy roleHierarchy() {
+       return RoleHierarchyImpl.with()
+               .role("ADMIN")
+               .implies("USER","GUEST")
+               .build();
     }
 
+
     @Bean
     public RequestMatcherDelegatingAuthorizationManager requestAuthorizationManager() {
-        List<RequestMatcherEntry<AuthorizationManager>> 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, "/search"), new PermitAllAuthorizationManager()));
-        mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.POST, "/private"), new DenyAllAuthorizationManager()));
-        mappings.add(new RequestMatcherEntry<>(new AnyRequestMatcher(), new PermitAllAuthorizationManager()));
-        return new RequestMatcherDelegatingAuthorizationManager(mappings);
+        return RequestMatcherAuthorizationManagerBuilder.withRoleHierarchy(roleHierarchy())
+                .authenticated(new MvcRequestMatcher(HttpMethod.GET, "/members/me"))
+                .permitAll(new MvcRequestMatcher(HttpMethod.GET, "/search"))
+                .hasAuthority("ADMIN", new MvcRequestMatcher(HttpMethod.GET, "/members"))
+                .hasAuthority("USER", new MvcRequestMatcher(HttpMethod.POST, "/hierarchy"))
+                .denyAll(new MvcRequestMatcher(HttpMethod.POST, "/private"))
+                .permitAll(new AnyRequestMatcher())
+        .build();
     }
 
     @Bean

리뷰: 요구사항 설정 👍
@@ -13,15 +13,13 @@
 import nextstep.security.context.SecurityContextHolderFilter;
 import nextstep.security.matcher.AnyRequestMatcher;
 import nextstep.security.matcher.MvcRequestMatcher;
-import nextstep.security.matcher.RequestMatcherEntry;
 import nextstep.security.userdetails.UserDetails;
 import nextstep.security.userdetails.UserDetailsService;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
 import org.springframework.http.HttpMethod;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -50,20 +48,31 @@ public SecuredMethodInterceptor securedMethodInterceptor() {
         return new SecuredMethodInterceptor();
     }
 
+//    @Bean
+//    public SecuredAspect securedAspect() {
+//        return new SecuredAspect();
+//    }
+
+
     @Bean
-    public SecuredAspect securedAspect() {
-        return new SecuredAspect();
+    public RoleHierarchy roleHierarchy() {
+       return RoleHierarchyImpl.with()
+               .role("ADMIN")
+               .implies("USER","GUEST")
+               .build();
     }
 
+
     @Bean
     public RequestMatcherDelegatingAuthorizationManager requestAuthorizationManager() {
-        List<RequestMatcherEntry<AuthorizationManager>> 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, "/search"), new PermitAllAuthorizationManager()));
-        mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.POST, "/private"), new DenyAllAuthorizationManager()));
-        mappings.add(new RequestMatcherEntry<>(new AnyRequestMatcher(), new PermitAllAuthorizationManager()));
-        return new RequestMatcherDelegatingAuthorizationManager(mappings);
+        return RequestMatcherAuthorizationManagerBuilder.withRoleHierarchy(roleHierarchy())
+                .authenticated(new MvcRequestMatcher(HttpMethod.GET, "/members/me"))
+                .permitAll(new MvcRequestMatcher(HttpMethod.GET, "/search"))
+                .hasAuthority("ADMIN", new MvcRequestMatcher(HttpMethod.GET, "/members"))
+                .hasAuthority("USER", new MvcRequestMatcher(HttpMethod.POST, "/hierarchy"))
+                .denyAll(new MvcRequestMatcher(HttpMethod.POST, "/private"))
+                .permitAll(new AnyRequestMatcher())
+        .build();
     }
 
     @Bean

리뷰: 권한 계층을 조회하도록 하셨군요!
설정이 잘 되었는지 확인하기 위해 좋은 시도입니다 :)

단, roleHierarchy()을 매번 설정할 때 마다 주입해주는 형태로 구현해주셨는데
한번만 설정하는 방법은 없을 지 고민해보셔도 좋을 것 같아요!
@@ -2,33 +2,43 @@
 
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import nextstep.security.authentication.Authentication;
 import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.filter.GenericFilterBean;
 
 import java.io.IOException;
 
-public class CheckAuthenticationFilter extends OncePerRequestFilter {
+public class CheckAuthenticationFilter extends GenericFilterBean {
 
-    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+    private final AuthorizationManager<HttpServletRequest> authorizationManager;
+    private final AuthorizationDeniedHandler authorizationDeniedHandler;
 
-    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
-        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    public CheckAuthenticationFilter(AuthorizationManager<HttpServletRequest> authorizationManager) {
+        this(authorizationManager, response -> response.setStatus(HttpServletResponse.SC_FORBIDDEN));
+    }
+
+    public CheckAuthenticationFilter(AuthorizationManager<HttpServletRequest> authorizationManager
+            , AuthorizationDeniedHandler authorizationDeniedHandler
+    ) {
+        this.authorizationManager = authorizationManager;
+        this.authorizationDeniedHandler = authorizationDeniedHandler;
     }
 
     @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 
-        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+        AuthorizationDecision authorizationDecision = authorizationManager.check(authentication, (HttpServletRequest) request);
 
         if (authorizationDecision.isDeny()) {
-            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+            authorizationDeniedHandler.handle((HttpServletResponse) response);
             return;
         }
 
-        filterChain.doFilter(request, response);
+        chain.doFilter(request, response);
     }
 }

리뷰: 만약 인증이 되지않은 경우 401응답이 필요할 수 있는데 현재는 모든 에로 상황에서 403이 응답이 되는 것 같네요!
@@ -0,0 +1,11 @@
+package nextstep.security.authorization;
+
+import java.util.Collection;
+
+public class NullRoleHierarchy implements RoleHierarchy {
+
+    @Override
+    public Collection<String> getReachableGrantedAuthorities(Collection<String> authorities) {
+        return authorities;
+    }
+}

리뷰: 널 객체 패턴 👍
@@ -0,0 +1,57 @@
+package nextstep.security.authorization;
+
+import java.util.*;
+
+public class RoleHierarchyImpl implements RoleHierarchy {
+    private final Map<String, Set<String>> hierarchyPath;
+
+    private RoleHierarchyImpl(Map<String, Set<String>> hierarchyPath) {
+        this.hierarchyPath = hierarchyPath;
+    }
+
+    @Override
+    public Collection<String> getReachableGrantedAuthorities(Collection<String> authorities) {
+        Set<String> grantedAuthorities = new HashSet<>();
+
+        for (String authority : authorities) {
+            if (hierarchyPath.get(authority) != null) {
+                grantedAuthorities.addAll(hierarchyPath.get(authority));
+            }
+        }
+
+        return grantedAuthorities;
+    }
+
+    public static RoleBuilder with() {
+        return new RoleBuilder();
+    }
+
+    public static class RoleBuilder {
+        private static final Map<String, Set<String>> hierarchy = new HashMap<>();
+        private String role;
+
+        public RoleBuilder role(String role) {
+            this.role = role;
+            hierarchy.put(role, new HashSet<>(Set.of(role)));
+            return this;
+        }
+
+        public ImpliesBuilder implies(String... implies) {
+            hierarchy.get(role).addAll(Set.of(implies));
+
+            for (int i = 0; i < implies.length; i++) {
+                Set<String> paths = hierarchy.getOrDefault(implies[i], new HashSet<>());
+                paths.addAll(List.of(implies).subList(i, implies.length));
+                hierarchy.put(implies[i], paths);
+            }
+
+            return new ImpliesBuilder();
+        }
+
+        public static class ImpliesBuilder {
+            public RoleHierarchyImpl build() {
+                return new RoleHierarchyImpl(hierarchy);
+            }
+        }
+    }
+}

리뷰: RoleHierarchy 구현 잘 해주셨습니다 👍
@@ -1,24 +0,0 @@
-package nextstep.security.authorization;
-
-import nextstep.security.authentication.Authentication;
-
-public class HasAuthorityAuthorizationManager implements AuthorizationManager {
-    private final String allowRole;
-
-    public HasAuthorityAuthorizationManager(String allowRole) {
-        this.allowRole = allowRole;
-    }
-
-    @Override
-    public AuthorizationDecision check(Authentication authentication, Object object) {
-        if (authentication == null || !authentication.isAuthenticated()) {
-            return AuthorizationDecision.deny();
-        }
-
-        if (authentication.getAuthorities().contains(allowRole)) {
-            return AuthorizationDecision.granted();
-        }
-
-        return AuthorizationDecision.deny();
-    }
-}

리뷰: 시큐리티 코드를 보면 AuthorityAuthorizationManager 와 AuthoritiesAuthorizationManager가 있는데요
이 구조를 분석해보시고 HasAuthorityAuthorizationManager 에 대입해보셔도 좋을 것 같아요.
@@ -0,0 +1,38 @@
+package nextstep.security.authorization;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RoleHierarchyImplTest {
+
+    @DisplayName("계층별로 허용되는 권한을 반환한다")
+    @ParameterizedTest
+    @MethodSource("계층별로_허용되는_권한")
+    void reachableGrantedAuthorities(String role, Set<String> grantedAuthorities) {
+        RoleHierarchyImpl roleHierarchy = RoleHierarchyImpl.with()
+                .role("ADMIN")
+                .implies("USER","GUEST")
+                .build();
+
+        Collection<String> reachableGrantedAuthorities = roleHierarchy.getReachableGrantedAuthorities(List.of(role));
+
+        assertThat(reachableGrantedAuthorities).isEqualTo(grantedAuthorities);
+    }
+
+    private static Stream<Arguments> 계층별로_허용되는_권한() {
+        return Stream.of(
+                Arguments.of("ADMIN", Set.of("ADMIN", "USER","GUEST")),
+                Arguments.of("USER", Set.of("USER", "GUEST")),
+                Arguments.of("GUEST", Set.of("GUEST"))
+        );
+    }
+}

리뷰: Role Hierarchy 테스트 👍