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,19 +2,21 @@

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


import com.opensource.docgrid.domain.auth.annotation.CurrentUser;
import com.opensource.docgrid.domain.permission.dto.request.GrantPermissionRequest;
import com.opensource.docgrid.domain.permission.dto.response.CollectionPermissionResponse;
import com.opensource.docgrid.domain.permission.dto.response.DocumentPermissionResponse;
import com.opensource.docgrid.domain.permission.dto.response.DocumentPermissionSummaryResponse;
import com.opensource.docgrid.domain.permission.service.command.CollectionPermissionCommandService;
import com.opensource.docgrid.domain.permission.service.command.DocumentPermissionCommandService;
import com.opensource.docgrid.domain.permission.service.query.PermissionQueryService;
import com.opensource.docgrid.global.common.response.ApiResponse;
import com.opensource.docgrid.global.common.response.ResponseUtils;

Expand All @@ -32,6 +34,20 @@ public class PermissionController {

private final CollectionPermissionCommandService collectionPermissionCommandService;
private final DocumentPermissionCommandService documentPermissionCommandService;
private final PermissionQueryService permissionQueryService;

@Operation(
summary = "내 문서 권한 확인",
description = "현재 로그인한 사용자가 특정 문서에 대해 읽기·쓰기·관리 권한을 가지고 있는지, " +
"그리고 그 권한이 어떤 경로(OWNER/PUBLIC/USER_CACHE/ROLE/DEPARTMENT)로 부여됐는지 반환합니다. " +
"sources는 실제 권한이 부여된 경로만 포함됩니다. 권한이 전혀 없으면 sources는 빈 배열입니다."
)
@GetMapping("/documents/{documentId}/me")
public ResponseEntity<ApiResponse<DocumentPermissionSummaryResponse>> getMyDocumentPermission(
@PathVariable Long documentId,
@Parameter(hidden = true) @CurrentUser Long userId) {
return ResponseUtils.ok(permissionQueryService.checkDocumentPermission(userId, documentId));
}

@Operation(
summary = "컬렉션 권한 부여",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.opensource.docgrid.domain.permission.dto.response;

import java.util.List;

import com.opensource.docgrid.domain.permission.enums.PermissionSourceType;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "내 문서 권한 요약")
public record DocumentPermissionSummaryResponse(
@Schema(description = "문서 ID") Long documentId,
@Schema(description = "읽기 권한") boolean canRead,
@Schema(description = "쓰기 권한") boolean canWrite,
@Schema(description = "관리 권한") boolean canAdmin,
@Schema(description = "권한 부여 경로 (OWNER/PUBLIC/USER_CACHE/ROLE/DEPARTMENT)") List<PermissionSourceType> sources
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.opensource.docgrid.domain.permission.enums;

/**
* 문서 권한 확인 API 응답용 — 사용자가 문서에 접근할 수 있는 경로(출처)를 나타낸다.
*/
public enum PermissionSourceType {
OWNER,
PUBLIC,
USER_CACHE,
ROLE,
DEPARTMENT
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.opensource.docgrid.domain.permission.service.query;

import java.util.ArrayList;
import java.util.List;

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

Expand All @@ -8,6 +11,8 @@
import com.opensource.docgrid.domain.document.entity.Document;
import com.opensource.docgrid.domain.document.enums.VisibilityType;
import com.opensource.docgrid.domain.document.repository.DocumentRepository;
import com.opensource.docgrid.domain.permission.dto.response.DocumentPermissionSummaryResponse;
import com.opensource.docgrid.domain.permission.enums.PermissionSourceType;
import com.opensource.docgrid.domain.permission.repository.CollectionPermissionRepository;
import com.opensource.docgrid.domain.permission.repository.DocumentPermissionRepository;
import com.opensource.docgrid.domain.permission.repository.UserDocumentAccessCacheRepository;
Expand Down Expand Up @@ -174,6 +179,72 @@ public boolean canAdminDocument(Long userId, Long documentId) {
return false;
}

// 문서 권한 확인 API용 — read/write/admin 동시 판단 + 접근 경로(sources) 수집
public DocumentPermissionSummaryResponse checkDocumentPermission(Long userId, Long documentId) {
long start = System.nanoTime();
Document document = documentRepository.findById(documentId)
.orElseThrow(() -> new DocGridException(ErrorCode.DOCUMENT_NOT_FOUND));

List<PermissionSourceType> sources = new ArrayList<>();
boolean canRead = false, canWrite = false, canAdmin = false;

// 1단계: 소유자 — 전체 권한 즉시 반환
if (document.getOwner().getId().equals(userId)) {
sources.add(PermissionSourceType.OWNER);
log.info("[PERM] checkDoc owner doc={} user={} elapsed={}ms", documentId, userId, ms(start));
return new DocumentPermissionSummaryResponse(documentId, true, true, true, sources);
}

// 2단계: PUBLIC — 읽기만 허용
if (document.getVisibility() == VisibilityType.PUBLIC) {
canRead = true;
sources.add(PermissionSourceType.PUBLIC);
}

// 3단계: USER 캐시
boolean cacheRead = cacheRepository.existsValidReadCache(userId, documentId);
boolean cacheWrite = cacheRepository.existsValidWriteCache(userId, documentId);
boolean cacheAdmin = cacheRepository.existsValidAdminCache(userId, documentId);
if (cacheRead || cacheWrite || cacheAdmin) {
sources.add(PermissionSourceType.USER_CACHE);
if (cacheRead) canRead = true;
if (cacheWrite) canWrite = true;
if (cacheAdmin) canAdmin = true;
}

// 4단계: ROLE live
boolean roleRead = documentPermissionRepository.existsRoleReadPermission(userId, documentId)
|| collectionPermissionRepository.existsRoleReadPermissionForDocument(userId, documentId);
boolean roleWrite = documentPermissionRepository.existsRoleWritePermission(userId, documentId)
|| collectionPermissionRepository.existsRoleWritePermissionForDocument(userId, documentId);
boolean roleAdmin = documentPermissionRepository.existsRoleAdminPermission(userId, documentId)
|| collectionPermissionRepository.existsRoleAdminPermissionForDocument(userId, documentId);
if (roleRead || roleWrite || roleAdmin) {
sources.add(PermissionSourceType.ROLE);
if (roleRead) canRead = true;
if (roleWrite) canWrite = true;
if (roleAdmin) canAdmin = true;
}

// 5단계: DEPARTMENT live
boolean deptRead = documentPermissionRepository.existsDeptReadPermission(userId, documentId)
|| collectionPermissionRepository.existsDeptReadPermissionForDocument(userId, documentId);
boolean deptWrite = documentPermissionRepository.existsDeptWritePermission(userId, documentId)
|| collectionPermissionRepository.existsDeptWritePermissionForDocument(userId, documentId);
boolean deptAdmin = documentPermissionRepository.existsDeptAdminPermission(userId, documentId)
|| collectionPermissionRepository.existsDeptAdminPermissionForDocument(userId, documentId);
if (deptRead || deptWrite || deptAdmin) {
sources.add(PermissionSourceType.DEPARTMENT);
if (deptRead) canRead = true;
if (deptWrite) canWrite = true;
if (deptAdmin) canAdmin = true;
}

log.info("[PERM] checkDoc doc={} user={} canRead={} canWrite={} canAdmin={} sources={} elapsed={}ms",
documentId, userId, canRead, canWrite, canAdmin, sources, ms(start));
return new DocumentPermissionSummaryResponse(documentId, canRead, canWrite, canAdmin, sources);
}

// 컬렉션 쓰기 권한 판단 (소유자, USER/ROLE/DEPT 직접 권한)
public boolean canWriteCollection(Long userId, Long collectionId) {
DocumentCollection collection = collectionRepository.findById(collectionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.opensource.docgrid.domain.document.entity.Document;
import com.opensource.docgrid.domain.document.enums.VisibilityType;
import com.opensource.docgrid.domain.document.repository.DocumentRepository;
import com.opensource.docgrid.domain.permission.dto.response.DocumentPermissionSummaryResponse;
import com.opensource.docgrid.domain.permission.enums.PermissionSourceType;
import com.opensource.docgrid.domain.permission.repository.CollectionPermissionRepository;
import com.opensource.docgrid.domain.permission.repository.DocumentPermissionRepository;
import com.opensource.docgrid.domain.permission.repository.UserDocumentAccessCacheRepository;
Expand Down Expand Up @@ -393,6 +395,142 @@ void canWriteCollection_noPermission_returnsFalse() {
assertThat(result).isFalse();
}

// ==================== checkDocumentPermission ====================

@Test
@DisplayName("소유자는 canRead/canWrite/canAdmin 모두 true이고 sources에 OWNER만 포함된다")
void checkDocumentPermission_owner_returnsAllTrueWithOwnerSource() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(CollectionFixture.USER_ID, CollectionFixture.DOCUMENT_ID);

assertThat(result.canRead()).isTrue();
assertThat(result.canWrite()).isTrue();
assertThat(result.canAdmin()).isTrue();
assertThat(result.sources()).containsExactly(PermissionSourceType.OWNER);
then(cacheRepository).should(never()).existsValidReadCache(CollectionFixture.USER_ID, CollectionFixture.DOCUMENT_ID);
}

@Test
@DisplayName("PUBLIC 문서는 canRead가 true이고 sources에 PUBLIC이 포함된다")
void checkDocumentPermission_public_returnsReadTrueWithPublicSource() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
org.springframework.test.util.ReflectionTestUtils.setField(document, "visibility", VisibilityType.PUBLIC);
Long otherUserId = 99L;
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(otherUserId, CollectionFixture.DOCUMENT_ID);

assertThat(result.canRead()).isTrue();
assertThat(result.canWrite()).isFalse();
assertThat(result.canAdmin()).isFalse();
assertThat(result.sources()).containsExactly(PermissionSourceType.PUBLIC);
}

@Test
@DisplayName("USER_CACHE로 쓰기 권한이 있으면 canWrite가 true이고 sources에 USER_CACHE가 포함된다")
void checkDocumentPermission_userCache_returnsWriteTrueWithCacheSource() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
Long otherUserId = 99L;
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));
given(cacheRepository.existsValidReadCache(otherUserId, CollectionFixture.DOCUMENT_ID)).willReturn(true);
given(cacheRepository.existsValidWriteCache(otherUserId, CollectionFixture.DOCUMENT_ID)).willReturn(true);

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(otherUserId, CollectionFixture.DOCUMENT_ID);

assertThat(result.canRead()).isTrue();
assertThat(result.canWrite()).isTrue();
assertThat(result.canAdmin()).isFalse();
assertThat(result.sources()).containsExactly(PermissionSourceType.USER_CACHE);
}

@Test
@DisplayName("ROLE 권한이 있으면 해당 권한이 true이고 sources에 ROLE이 포함된다")
void checkDocumentPermission_role_returnsPermissionWithRoleSource() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
Long otherUserId = 99L;
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));
given(documentPermissionRepository.existsRoleReadPermission(otherUserId, CollectionFixture.DOCUMENT_ID)).willReturn(true);

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(otherUserId, CollectionFixture.DOCUMENT_ID);

assertThat(result.canRead()).isTrue();
assertThat(result.sources()).contains(PermissionSourceType.ROLE);
assertThat(result.sources()).doesNotContain(PermissionSourceType.DEPARTMENT);
}

@Test
@DisplayName("DEPARTMENT 권한이 있으면 해당 권한이 true이고 sources에 DEPARTMENT가 포함된다")
void checkDocumentPermission_dept_returnsPermissionWithDeptSource() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
Long otherUserId = 99L;
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));
given(documentPermissionRepository.existsDeptWritePermission(otherUserId, CollectionFixture.DOCUMENT_ID)).willReturn(true);

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(otherUserId, CollectionFixture.DOCUMENT_ID);

assertThat(result.canWrite()).isTrue();
assertThat(result.sources()).containsExactly(PermissionSourceType.DEPARTMENT);
}

@Test
@DisplayName("ROLE과 USER_CACHE가 동시에 충족되면 sources에 둘 다 포함된다")
void checkDocumentPermission_multipleSource_returnsBothSources() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
Long otherUserId = 99L;
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));
given(cacheRepository.existsValidWriteCache(otherUserId, CollectionFixture.DOCUMENT_ID)).willReturn(true);
given(documentPermissionRepository.existsRoleReadPermission(otherUserId, CollectionFixture.DOCUMENT_ID)).willReturn(true);

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(otherUserId, CollectionFixture.DOCUMENT_ID);

assertThat(result.canRead()).isTrue();
assertThat(result.canWrite()).isTrue();
assertThat(result.sources()).containsExactlyInAnyOrder(
PermissionSourceType.USER_CACHE, PermissionSourceType.ROLE);
}

@Test
@DisplayName("권한이 전혀 없으면 모두 false이고 sources가 비어있다")
void checkDocumentPermission_noPermission_returnsAllFalseEmptySources() {
User owner = CollectionFixture.createOwner();
Document document = CollectionFixture.createDocument(owner);
Long otherUserId = 99L;
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.of(document));

DocumentPermissionSummaryResponse result =
service.checkDocumentPermission(otherUserId, CollectionFixture.DOCUMENT_ID);

assertThat(result.canRead()).isFalse();
assertThat(result.canWrite()).isFalse();
assertThat(result.canAdmin()).isFalse();
assertThat(result.sources()).isEmpty();
}

@Test
@DisplayName("문서가 없으면 DOCUMENT_NOT_FOUND 예외가 발생한다")
void checkDocumentPermission_documentNotFound_throwsException() {
given(documentRepository.findById(CollectionFixture.DOCUMENT_ID)).willReturn(Optional.empty());

assertThatThrownBy(() ->
service.checkDocumentPermission(CollectionFixture.USER_ID, CollectionFixture.DOCUMENT_ID))
.isInstanceOf(DocGridException.class)
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.DOCUMENT_NOT_FOUND);
}

// ==================== canAdminCollection ====================

@Test
Expand Down