Skip to content

Commit

Permalink
Merge pull request #161 from seungki1011/feat/#159-workspacemember-au…
Browse files Browse the repository at this point in the history
…ditor-aware

Feat/#159 `WorkspaceMember`의 `id`를 기록하는 `WebRequestAuditorAware` 구현
  • Loading branch information
seungki1011 authored Dec 26, 2024
2 parents 21e39ad + ab24ebf commit 32a23d7
Show file tree
Hide file tree
Showing 20 changed files with 174 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public abstract class BaseEntity extends BaseDateEntity {
@CreatedBy
@Column(updatable = false)
private Long createdBy;

@LastModifiedBy
private Long lastModifiedBy;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tissue.api.common.entity;

import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class WorkspaceBaseEntity extends BaseDateEntity {

@CreatedBy
@Column(updatable = false)
private Long createdByMember;

@LastModifiedBy
private Long lastModifiedByWorkspaceMember;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tissue.api.common.entity;

import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class WorkspaceContextBaseEntity extends BaseDateEntity {

@CreatedBy
@Column(updatable = false)
private Long createdByWorkspaceMember;

@LastModifiedBy
private Long lastModifiedByWorkspaceMember;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.tissue.api.global.audit;

import java.util.Optional;

import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

import com.tissue.api.security.authorization.interceptor.AuthorizationInterceptor;
import com.tissue.api.security.session.SessionAttributes;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class WebRequestAuditorAware implements AuditorAware<Long> {

private static final String WORKSPACE_API_PREFIX = "/api/v1/workspaces";

private final HttpSession session;
private final HttpServletRequest request;

@Override
public Optional<Long> getCurrentAuditor() {

String requestUri = request.getRequestURI();

// workspace API인 경우 workspaceMemberId 반환, workspaceMemberId가 null이면 넘어가기
if (requestUri.startsWith(WORKSPACE_API_PREFIX)) {
Long workspaceMemberId = (Long)request.getAttribute(
AuthorizationInterceptor.CURRENT_WORKSPACE_MEMBER_ID
);

if (workspaceMemberId != null) {
return Optional.of(workspaceMemberId);
}
}

// 그 외의 경우 memberId 반환
return Optional.ofNullable(
(Long)session.getAttribute(SessionAttributes.LOGIN_MEMBER_ID)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
4 changes: 2 additions & 2 deletions backend/src/main/java/com/tissue/api/issue/domain/Issue.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.ArrayList;
import java.util.List;

import com.tissue.api.common.entity.BaseEntity;
import com.tissue.api.common.entity.WorkspaceContextBaseEntity;
import com.tissue.api.issue.domain.enums.IssuePriority;
import com.tissue.api.issue.domain.enums.IssueStatus;
import com.tissue.api.issue.domain.enums.IssueType;
Expand Down Expand Up @@ -37,7 +37,7 @@
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "type")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class Issue extends BaseEntity {
public abstract class Issue extends WorkspaceContextBaseEntity {

@OneToMany(mappedBy = "parentIssue")
private final List<Issue> childIssues = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static CreateBugResponse from(Bug bug) {
.issueId(bug.getId())
.issueKey(bug.getIssueKey())
.workspaceCode(bug.getWorkspaceCode())
.reporterId(bug.getCreatedBy())
.reporterId(bug.getCreatedByWorkspaceMember())
.title(bug.getTitle())
.content(bug.getContent())
.summary(bug.getSummary())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static CreateEpicResponse from(Epic epic) {
.issueId(epic.getId())
.issueKey(epic.getIssueKey())
.workspaceCode(epic.getWorkspaceCode())
.reporterId(epic.getCreatedBy())
.reporterId(epic.getCreatedByWorkspaceMember())
.title(epic.getTitle())
.content(epic.getContent())
.summary(epic.getSummary())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static CreateStoryResponse from(Story story) {
.issueId(story.getId())
.issueKey(story.getIssueKey())
.workspaceCode(story.getWorkspaceCode())
.reporterId(story.getCreatedBy())
.reporterId(story.getCreatedByWorkspaceMember())
.title(story.getTitle())
.content(story.getContent())
.summary(story.getSummary())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static CreateSubTaskResponse from(SubTask subTask) {
.issueId(subTask.getId())
.issueKey(subTask.getIssueKey())
.workspaceCode(subTask.getWorkspaceCode())
.reporterId(subTask.getCreatedBy())
.reporterId(subTask.getCreatedByWorkspaceMember())
.title(subTask.getTitle())
.content(subTask.getContent())
.summary(subTask.getSummary())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static CreateTaskResponse from(Task task) {
.issueId(task.getId())
.issueKey(task.getIssueKey())
.workspaceCode(task.getWorkspaceCode())
.reporterId(task.getCreatedBy())
.reporterId(task.getCreatedByWorkspaceMember())
.title(task.getTitle())
.content(task.getContent())
.summary(task.getSummary())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.List;

import com.tissue.api.common.ColorType;
import com.tissue.api.common.entity.BaseEntity;
import com.tissue.api.common.entity.WorkspaceContextBaseEntity;
import com.tissue.api.workspace.domain.Workspace;
import com.tissue.api.workspacemember.domain.WorkspaceMember;

Expand All @@ -29,13 +29,11 @@
@Entity
@Getter
@Table(uniqueConstraints = {
@UniqueConstraint(
name = "UK_WORKSPACE_POSITION_NAME",
columnNames = {"workspace_code", "name"}
)
@UniqueConstraint(name = "UK_WORKSPACE_POSITION_NAME",
columnNames = {"workspace_code", "name"})
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Position extends BaseEntity {
public class Position extends WorkspaceContextBaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "POSITION_ID")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@
@RequiredArgsConstructor
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

private final SessionValidator sessionValidator;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
if (isNotHandlerMethod(handler)) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ public boolean supportsParameter(MethodParameter parameter) {
* @return LoginMemberDto
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {

HttpSession session = sessionManager.getSession(webRequest);
return sessionManager.getLoginMemberId(session)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@
@Component
@RequiredArgsConstructor
public class AuthorizationInterceptor implements HandlerInterceptor {
public static final String CURRENT_WORKSPACE_MEMBER_ID = "currentWorkspaceMemberId";
private static final String WORKSPACE_PREFIX = "/api/v1/workspaces/";
private static final int WORKSPACE_PREFIX_LENGTH = 19;

private final SessionManager sessionManager;
private final WorkspaceRepository workspaceRepository;
private final WorkspaceMemberRepository workspaceMemberRepository;

private static boolean isNotHandlerMethod(Object handler) {
return !(handler instanceof HandlerMethod);
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {

if (isNotHandlerMethod(handler)) {
return true;
Expand Down Expand Up @@ -64,45 +73,35 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons

validateRole(workspaceMember, roleRequired);

return true;
}
request.setAttribute(CURRENT_WORKSPACE_MEMBER_ID, workspaceMember.getId());

private static boolean isNotHandlerMethod(Object handler) {
return !(handler instanceof HandlerMethod);
return true;
}

private Optional<RoleRequired> getRoleRequired(Object handler) {
HandlerMethod handlerMethod = (HandlerMethod)handler;
return Optional.ofNullable(handlerMethod.getMethodAnnotation(RoleRequired.class));
}

private void validateRole(WorkspaceMember workspaceMember, RoleRequired roleRequired) {
private void validateRole(
WorkspaceMember workspaceMember,
RoleRequired roleRequired
) {
if (isAccessDenied(workspaceMember.getRole(), roleRequired.roles())) {
throw new InsufficientWorkspaceRoleException();
}
}

/**
* 권한에 integer level을 부여해서 부등호 형식으로 비교한다
* 권한 수준: OWNER(4) > MANAGER(3) > COLLABORATOR(2) > VIEWER(1)
*
* @param userRole - 사용자의 권한
* @param requiredRoles - 권한 리스트
* @return boolean - 접근 거부 여부(예시: 권한이 부족한 true 반환)
*/
private boolean isAccessDenied(WorkspaceRole userRole, WorkspaceRole[] requiredRoles) {
int userRoleLevel = userRole.getLevel();
return Arrays.stream(requiredRoles)
.map(WorkspaceRole::getLevel)
.noneMatch(requiredRoleLevel -> userRoleLevel >= requiredRoleLevel);
}

/**
* URI에서 WORKSPACE_PREFIX_LENGTH을 시작 인덱스로 시작해서 8자리 문자열을 추출한다
* 만약 URI의 길이를 계산해서 코드가 8자리가 아니라면 예외 발생
*
* @param uri - 현재 HTTP 요청의 URI
* @return String - URI에서 추출된 워크스페이스 코드
* <br>
* Todo
* - 유틸 클래스로 분리, 테스트 목적으로 public으로 여는 설계는 피하자
* - extractWorkspaceCodeFromUri가 현재 클래스의 책임인지 고민해보자
*/
public String extractWorkspaceCodeFromUri(String uri) {
return Optional.of(uri.indexOf(WORKSPACE_PREFIX))
Expand All @@ -118,6 +117,24 @@ public String extractWorkspaceCodeFromUri(String uri) {
.orElseThrow(InvalidWorkspaceCodeInUriException::new);
}

/**
* 권한에 integer level을 부여해서 부등호 형식으로 비교한다
* 권한 수준: OWNER(4) > MANAGER(3) > COLLABORATOR(2) > VIEWER(1)
*
* @param userRole - 사용자의 권한
* @param requiredRoles - 권한 리스트
* @return boolean - 접근 거부 여부(예시: 권한이 부족한 true 반환)
*/
private boolean isAccessDenied(
WorkspaceRole userRole,
WorkspaceRole[] requiredRoles
) {
int userRoleLevel = userRole.getLevel();
return Arrays.stream(requiredRoles)
.map(WorkspaceRole::getLevel)
.noneMatch(requiredRoleLevel -> userRoleLevel >= requiredRoleLevel);
}

private boolean isNotEmpty(String code) {
return !code.isEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.List;

import com.tissue.api.common.ColorType;
import com.tissue.api.common.entity.BaseEntity;
import com.tissue.api.common.entity.WorkspaceBaseEntity;
import com.tissue.api.invitation.domain.Invitation;
import com.tissue.api.issue.domain.Issue;
import com.tissue.api.member.domain.Member;
Expand All @@ -28,7 +28,7 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Workspace extends BaseEntity {
public class Workspace extends WorkspaceBaseEntity {

// Todo: 추후 낙관적 락 적용
// @Version
Expand Down Expand Up @@ -113,7 +113,6 @@ public void setCode(String code) {
}

public void updateKeyPrefix(String keyPrefix) {
// TODO: 굳이 null 검증이 필요한가? 이미 요청 DTO에서 null이 아닌 값이 들어온다는 것은 보장됨
this.keyPrefix = toUpperCaseOrDefault(keyPrefix);
}

Expand Down
Loading

0 comments on commit 32a23d7

Please sign in to comment.