Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import javax.crypto.SecretKey;
Expand All @@ -19,6 +18,7 @@

@Component
public class JwtProviderAdapter implements JwtProvider {

private final SecretKey secretKey;
private final long accessTokenExpirationSeconds;
private final String issuer;
Expand All @@ -34,7 +34,7 @@ public JwtProviderAdapter(
}

@Override
public String createAccessToken(Long memberId, String email, List<String> roles) {
public String createAccessToken(Long memberId, String email) {
Instant now = Instant.now();
Instant expiration = now.plusSeconds(accessTokenExpirationSeconds);

Expand All @@ -47,7 +47,6 @@ public String createAccessToken(Long memberId, String email, List<String> roles)
.id(UUID.randomUUID().toString())
.claim("member_id", memberId)
.claim("email", email)
.claim("roles", roles)
.signWith(secretKey)
.compact();
}
Expand All @@ -63,11 +62,9 @@ public Claims validateAndGetClaims(String token) {

Long memberId = claims.get("member_id", Long.class);
String email = claims.get("email", String.class);
@SuppressWarnings("unchecked")
List<String> roles = claims.get("roles", List.class);
Instant expiresAt = claims.getExpiration().toInstant();

return new Claims(memberId, email, roles, expiresAt);
return new Claims(memberId, email, expiresAt);

} catch (ExpiredJwtException e) {
throw new DomainException(AuthProblemCode.EXPIRED_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,35 @@

import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.springframework.stereotype.Service;

import me.chan99k.learningmanager.attendance.Attendance;
import me.chan99k.learningmanager.attendance.AttendanceQueryRepository;
import me.chan99k.learningmanager.attendance.CorrectionRequested;
import me.chan99k.learningmanager.course.CourseRole;
import me.chan99k.learningmanager.member.Member;
import me.chan99k.learningmanager.member.MemberQueryRepository;
import me.chan99k.learningmanager.member.SystemRole;
import me.chan99k.learningmanager.session.Session;
import me.chan99k.learningmanager.session.SessionQueryRepository;

@Service("attendanceSecurity")
public class AttendanceSecurity {

private final AttendanceQueryRepository attendanceQueryRepository;
private final SessionQueryRepository sessionQueryRepository;
private final MemberQueryRepository memberQueryRepository;
private final SystemAuthorizationPort systemAuthorizationPort;
private final CourseAuthorizationPort courseAuthorizationPort;

public AttendanceSecurity(AttendanceQueryRepository attendanceQueryRepository,
SessionQueryRepository sessionQueryRepository, MemberQueryRepository memberQueryRepository,
CourseAuthorizationPort courseAuthorizationPort) {
public AttendanceSecurity(
AttendanceQueryRepository attendanceQueryRepository,
SessionQueryRepository sessionQueryRepository,
SystemAuthorizationPort systemAuthorizationPort,
CourseAuthorizationPort courseAuthorizationPort
) {
this.attendanceQueryRepository = attendanceQueryRepository;
this.sessionQueryRepository = sessionQueryRepository;
this.memberQueryRepository = memberQueryRepository;
this.systemAuthorizationPort = systemAuthorizationPort;
this.courseAuthorizationPort = courseAuthorizationPort;
}

Expand All @@ -38,14 +41,7 @@ public boolean canRequestCorrection(String attendanceId, Long memberId) {
}

// SystemRole 우선 확인
Optional<Member> foundMember = memberQueryRepository.findById(memberId);
if (foundMember.isEmpty()) {
return false;
}

var member = foundMember.get();
var systemRole = member.getRole();
if (systemRole == SystemRole.ADMIN || systemRole == SystemRole.REGISTRAR) {
if (systemAuthorizationPort.hasAnyRole(memberId, Set.of(SystemRole.ADMIN, SystemRole.REGISTRAR))) {
return true;
}

Expand Down Expand Up @@ -82,14 +78,8 @@ public boolean canApproveCorrection(String attendanceId, Long memberId) {
return false; // 대기중인 수정 요청 없음
}

Optional<Member> foundMember = memberQueryRepository.findById(memberId);
if (foundMember.isEmpty()) {
return false;
}

var requestedApprover = foundMember.get();
var systemRole = requestedApprover.getRole();
if (systemRole == SystemRole.ADMIN || systemRole == SystemRole.REGISTRAR) {
// SystemRole 우선 확인
if (systemAuthorizationPort.hasAnyRole(memberId, Set.of(SystemRole.ADMIN, SystemRole.REGISTRAR))) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static org.assertj.core.api.Assertions.*;

import java.util.Base64;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -16,7 +15,6 @@ class JwtProviderAdapterTest {

private static final Long MEMBER_ID = 1L;
private static final String EMAIL = "test@example.com";
private static final List<String> ROLES = List.of("MEMBER");
private static final long EXPIRATION_SECONDS = 3600L;
private static final String ISSUER = "test-issuer";

Expand All @@ -42,7 +40,7 @@ class CreateAccessTokenTest {
@Test
@DisplayName("유효한 JWT 문자열을 생성한다")
void creates_valid_jwt_string() {
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL);

assertThat(token).isNotNull();
assertThat(token.split("\\.")).hasSize(3);
Expand All @@ -51,7 +49,7 @@ void creates_valid_jwt_string() {
@Test
@DisplayName("생성된 토큰에 memberId 클레임을 포함한다")
void token_contains_member_id_claim() {
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL);

JwtProvider.Claims claims = jwtProvider.validateAndGetClaims(token);
assertThat(claims.memberId()).isEqualTo(MEMBER_ID);
Expand All @@ -60,20 +58,11 @@ void token_contains_member_id_claim() {
@Test
@DisplayName("생성된 토큰에 email 클레임을 포함한다")
void token_contains_email_claim() {
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL);

JwtProvider.Claims claims = jwtProvider.validateAndGetClaims(token);
assertThat(claims.email()).isEqualTo(EMAIL);
}

@Test
@DisplayName("생성된 토큰에 roles 클레임을 포함한다")
void token_contains_roles_claim() {
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);

JwtProvider.Claims claims = jwtProvider.validateAndGetClaims(token);
assertThat(claims.roles()).containsExactlyElementsOf(ROLES);
}
}

@Nested
Expand All @@ -83,21 +72,20 @@ class ValidateAndGetClaimsTest {
@Test
@DisplayName("유효한 토큰에서 클레임을 추출한다")
void extracts_claims_from_valid_token() {
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL);

JwtProvider.Claims claims = jwtProvider.validateAndGetClaims(token);

assertThat(claims.memberId()).isEqualTo(MEMBER_ID);
assertThat(claims.email()).isEqualTo(EMAIL);
assertThat(claims.roles()).isEqualTo(ROLES);
assertThat(claims.expiresAt()).isNotNull();
}

@Test
@DisplayName("만료된 토큰은 EXPIRED_TOKEN 예외를 던진다")
void throws_expired_token_exception() {
JwtProviderAdapter shortLivedProvider = new JwtProviderAdapter(SECRET, 0L, ISSUER);
String expiredToken = shortLivedProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String expiredToken = shortLivedProvider.createAccessToken(MEMBER_ID, EMAIL);

assertThatThrownBy(() -> jwtProvider.validateAndGetClaims(expiredToken))
.isInstanceOf(DomainException.class)
Expand All @@ -122,7 +110,7 @@ void throws_invalid_token_for_wrong_signature() {
JwtProviderAdapter differentProvider = new JwtProviderAdapter(
DIFFERENT_SECRET, EXPIRATION_SECONDS, ISSUER
);
String tokenWithDifferentSignature = differentProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String tokenWithDifferentSignature = differentProvider.createAccessToken(MEMBER_ID, EMAIL);

assertThatThrownBy(() -> jwtProvider.validateAndGetClaims(tokenWithDifferentSignature))
.isInstanceOf(DomainException.class)
Expand All @@ -138,7 +126,7 @@ class IsValidTest {
@Test
@DisplayName("유효한 토큰은 true를 반환한다")
void returns_true_for_valid_token() {
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String token = jwtProvider.createAccessToken(MEMBER_ID, EMAIL);

boolean result = jwtProvider.isValid(token);

Expand All @@ -149,7 +137,7 @@ void returns_true_for_valid_token() {
@DisplayName("만료된 토큰은 false를 반환한다")
void returns_false_for_expired_token() {
JwtProviderAdapter shortLivedProvider = new JwtProviderAdapter(SECRET, 0L, ISSUER);
String expiredToken = shortLivedProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String expiredToken = shortLivedProvider.createAccessToken(MEMBER_ID, EMAIL);

boolean result = jwtProvider.isValid(expiredToken);

Expand All @@ -170,7 +158,7 @@ void returns_false_for_wrong_signature() {
JwtProviderAdapter differentProvider = new JwtProviderAdapter(
DIFFERENT_SECRET, EXPIRATION_SECONDS, ISSUER
);
String tokenWithDifferentSignature = differentProvider.createAccessToken(MEMBER_ID, EMAIL, ROLES);
String tokenWithDifferentSignature = differentProvider.createAccessToken(MEMBER_ID, EMAIL);

boolean result = jwtProvider.isValid(tokenWithDifferentSignature);

Expand All @@ -190,4 +178,4 @@ void returns_configured_expiration_seconds() {
assertThat(result).isEqualTo(EXPIRATION_SECONDS);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package me.chan99k.learningmanager.authorization;

import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import me.chan99k.learningmanager.member.JpaMemberSystemRoleRepository;
import me.chan99k.learningmanager.member.SystemRole;
import me.chan99k.learningmanager.member.SystemRoleHierarchy;
import me.chan99k.learningmanager.member.entity.MemberSystemRoleEntity;

@Repository
public class JpaSystemAuthorizationAdapter implements SystemAuthorizationPort {

private final JpaMemberSystemRoleRepository roleRepository;
private final SystemRoleHierarchy roleHierarchy;

public JpaSystemAuthorizationAdapter(
JpaMemberSystemRoleRepository roleRepository,
SystemRoleHierarchy roleHierarchy
) {
this.roleRepository = roleRepository;
this.roleHierarchy = roleHierarchy;
}

@Override
public boolean hasRole(Long memberId, SystemRole role) {
return roleRepository.existsByMemberIdAndSystemRole(memberId, role);
}

@Override
public boolean hasAnyRole(Long memberId, Set<SystemRole> roles) {
return roleRepository.existsByMemberIdAndSystemRoleIn(memberId, roles);
}

@Override
public Set<SystemRole> getRoles(Long memberId) {
return roleRepository.findByMemberId(memberId).stream()
.map(MemberSystemRoleEntity::getSystemRole)
.collect(Collectors.toSet());
}

@Override
@Transactional
public void grantRole(Long memberId, SystemRole role) {
if (!hasRole(memberId, role)) {
roleRepository.save(new MemberSystemRoleEntity(memberId, role));
}
}

@Override
@Transactional
public void revokeRole(Long memberId, SystemRole role) {
roleRepository.deleteByMemberIdAndSystemRole(memberId, role);
}

@Override
public boolean hasRoleOrHigher(Long memberId, SystemRole minimumRole) {
Set<SystemRole> memberRoles = getRoles(memberId);
return memberRoles.stream()
.anyMatch(role -> roleHierarchy.isHigherOrEqual(role, minimumRole));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import me.chan99k.learningmanager.member.SystemRoleHierarchy;

@Configuration
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = "me.chan99k.learningmanager")
public class JpaConfig {

@Bean
public AuditorAware<Long> auditorAware() {
return () -> Optional.of(1L);
}

@Bean
public SystemRoleHierarchy systemRoleHierarchy() {
return new SystemRoleHierarchy();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package me.chan99k.learningmanager.member;

import java.util.List;
import java.util.Set;

import org.springframework.data.jpa.repository.JpaRepository;

import me.chan99k.learningmanager.member.entity.MemberSystemRoleEntity;

public interface JpaMemberSystemRoleRepository extends JpaRepository<MemberSystemRoleEntity, Long> {

List<MemberSystemRoleEntity> findByMemberId(Long memberId);

boolean existsByMemberIdAndSystemRole(Long memberId, SystemRole systemRole);

boolean existsByMemberIdAndSystemRoleIn(Long memberId, Set<SystemRole> systemRoles);

void deleteByMemberIdAndSystemRole(Long memberId, SystemRole systemRole);

void deleteByMemberId(Long memberId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import me.chan99k.learningmanager.common.MutableEntity;
import me.chan99k.learningmanager.member.Email;
import me.chan99k.learningmanager.member.MemberStatus;
import me.chan99k.learningmanager.member.SystemRole;

@Entity
@Table(name = "member")
Expand All @@ -31,9 +30,6 @@ public class MemberEntity extends MutableEntity {
@Column(name = "nickname", nullable = false, unique = true, length = 20)
private String nickname;

@Enumerated(EnumType.STRING)
private SystemRole role;

@Enumerated(EnumType.STRING)
private MemberStatus status;

Expand Down Expand Up @@ -73,14 +69,6 @@ public void setNickname(String nickname) {
this.nickname = nickname;
}

public SystemRole getRole() {
return role;
}

public void setRole(SystemRole role) {
this.role = role;
}

public MemberStatus getStatus() {
return status;
}
Expand Down
Loading
Loading