diff --git a/src/main/java/com/opensource/docgrid/domain/permission/controller/PermissionController.java b/src/main/java/com/opensource/docgrid/domain/permission/controller/PermissionController.java index 7a18a856..a8428a35 100644 --- a/src/main/java/com/opensource/docgrid/domain/permission/controller/PermissionController.java +++ b/src/main/java/com/opensource/docgrid/domain/permission/controller/PermissionController.java @@ -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; @@ -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> getMyDocumentPermission( + @PathVariable Long documentId, + @Parameter(hidden = true) @CurrentUser Long userId) { + return ResponseUtils.ok(permissionQueryService.checkDocumentPermission(userId, documentId)); + } @Operation( summary = "컬렉션 권한 부여", diff --git a/src/main/java/com/opensource/docgrid/domain/permission/dto/response/DocumentPermissionSummaryResponse.java b/src/main/java/com/opensource/docgrid/domain/permission/dto/response/DocumentPermissionSummaryResponse.java new file mode 100644 index 00000000..ee038572 --- /dev/null +++ b/src/main/java/com/opensource/docgrid/domain/permission/dto/response/DocumentPermissionSummaryResponse.java @@ -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 sources +) { +} diff --git a/src/main/java/com/opensource/docgrid/domain/permission/enums/PermissionSourceType.java b/src/main/java/com/opensource/docgrid/domain/permission/enums/PermissionSourceType.java new file mode 100644 index 00000000..b8d28fdd --- /dev/null +++ b/src/main/java/com/opensource/docgrid/domain/permission/enums/PermissionSourceType.java @@ -0,0 +1,12 @@ +package com.opensource.docgrid.domain.permission.enums; + +/** + * 문서 권한 확인 API 응답용 — 사용자가 문서에 접근할 수 있는 경로(출처)를 나타낸다. + */ +public enum PermissionSourceType { + OWNER, + PUBLIC, + USER_CACHE, + ROLE, + DEPARTMENT +} diff --git a/src/main/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryService.java b/src/main/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryService.java index 74941f0e..7a3bf388 100644 --- a/src/main/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryService.java +++ b/src/main/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryService.java @@ -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; @@ -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; @@ -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 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) diff --git a/src/test/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryServiceTest.java b/src/test/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryServiceTest.java index f81fc377..323a75ab 100644 --- a/src/test/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryServiceTest.java +++ b/src/test/java/com/opensource/docgrid/domain/permission/service/query/PermissionQueryServiceTest.java @@ -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; @@ -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