Skip to content

[Feat] 권한 부여·회수 API + USER 캐시 갱신 - #19

Merged
kangcheolung merged 2 commits into
developfrom
feature/18
Jul 15, 2026
Merged

[Feat] 권한 부여·회수 API + USER 캐시 갱신#19
kangcheolung merged 2 commits into
developfrom
feature/18

Conversation

@kangcheolung

@kangcheolung kangcheolung commented Jul 15, 2026

Copy link
Copy Markdown
Member

🔍 작업 내용

✨ 상세 설명

구현 내용

  • POST/DELETE /permissions/collections/{collectionId} — 컬렉션 권한 부여·회수
  • POST/DELETE /permissions/documents/{documentId} — 문서 예외 권한 부여·회수
  • USER 대상 권한 부여 시 user_document_access_cache 즉시 갱신
  • USER 대상 권한 회수 시 캐시 soft invalidate (invalidated_at 설정)
  • ROLE/DEPARTMENT 대상은 캐시 미사용 — live predicate로 판단 예정 (Issue 3)

권한 판단 구조

  • targetType(USER/ROLE/DEPARTMENT)과 ID 필드 조합 유효성 검사 (INVALID_TARGET_TYPE)
  • 소유자 체크는 현재 owner 직접 비교, Issue 3 canAdminDocument() 완성 후 대체 예정

추가된 ErrorCode

  • INVALID_TARGET_TYPE, COLLECTION_PERMISSION_NOT_FOUND, DOCUMENT_PERMISSION_NOT_FOUND
  • PERMISSION_DENIED 메시지 범용화 ("관리자만" → "접근 권한이 없습니다")

🛠️ 추후 리팩토링 및 고도화 계획

  • Issue 3: PermissionQueryService 완성 후 owner 직접 비교 → canAdminDocument() 교체

💬 리뷰 요구사항

  • UserDocumentAccessCacheService grant/revoke 흐름 (upsert 방식) 확인 부탁

Summary by CodeRabbit

  • 새 기능
    • 컬렉션 및 문서 단위의 권한 부여·회수 API를 추가했습니다.
    • 사용자, 역할, 부서를 대상으로 읽기·쓰기·관리 권한과 만료 시각을 설정할 수 있습니다.
    • 사용자 권한 변경 시 문서 접근 상태가 자동으로 갱신·무효화됩니다.
  • 문서화
    • 주요 요청·응답 정보가 OpenAPI 문서에 더 명확하게 표시됩니다.
  • 오류 개선
    • 권한 대상 검증 및 권한 없음·권한 미존재 관련 오류 안내를 보강했습니다.

kangcheolung and others added 2 commits July 15, 2026 19:40
- CollectionPermissionRepository, DocumentPermissionRepository, UserDocumentAccessCacheRepository 추가
- UserDocumentAccessCacheService: USER 권한 grant 시 upsert, revoke 시 invalidate
- CollectionPermissionCommandService: POST/DELETE /permissions/collections/{id}
- DocumentPermissionCommandService: POST/DELETE /permissions/documents/{id}
- PermissionController, PermissionConverter, GrantPermissionRequest, PermissionResponse DTO 추가
- UserDocumentAccessCache 엔티티에 grant(), invalidate() 메서드 추가
- CollectionDocumentRepository에 findAllByCollectionId() 추가
- ROLE/DEPARTMENT 권한은 캐시 미적용, USER 권한만 캐시 갱신
- ErrorCode: PERMISSION_DENIED 메시지 범용화, INVALID_TARGET_TYPE/COLLECTION_PERMISSION_NOT_FOUND/DOCUMENT_PERMISSION_NOT_FOUND 추가
- DTO 클래스 레벨 @Schema 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rvice 단위 테스트 추가

- PermissionFixture: Role, Department, CollectionPermission, DocumentPermission 팩토리 메서드
- CollectionPermissionCommandServiceTest: USER 권한 부여(캐시 갱신), ROLE 권한 부여(캐시 없음),
  COLLECTION_NOT_FOUND, PERMISSION_DENIED, INVALID_TARGET_TYPE, 권한 회수(캐시 무효화),
  COLLECTION_PERMISSION_NOT_FOUND
- DocumentPermissionCommandServiceTest: USER/ROLE 권한 부여, DOCUMENT_NOT_FOUND,
  PERMISSION_DENIED, INVALID_TARGET_TYPE, 권한 회수(캐시 무효화),
  DOCUMENT_PERMISSION_NOT_FOUND

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

컬렉션·문서 권한 부여 및 회수 API가 추가되었습니다. 사용자 대상 권한은 문서 접근 캐시에 반영·무효화되며, 요청 검증·응답 변환·오류 코드와 서비스 테스트가 함께 구현되었습니다.

Changes

권한 기능

Layer / File(s) Summary
권한 계약과 API 진입점
src/main/java/com/opensource/docgrid/domain/permission/dto/*, src/main/java/com/opensource/docgrid/domain/permission/controller/*, src/main/java/com/opensource/docgrid/domain/permission/converter/*, src/main/java/com/opensource/docgrid/global/exception/ErrorCode.java, src/main/java/com/opensource/docgrid/domain/collection/dto/*
권한 요청·응답 DTO, 엔티티 변환기, 컬렉션·문서 권한 부여·회수 엔드포인트와 OpenAPI 설명 및 오류 코드가 추가되었습니다.
권한 저장소와 캐시 갱신 기반
src/main/java/com/opensource/docgrid/domain/permission/repository/*, src/main/java/com/opensource/docgrid/domain/permission/entity/UserDocumentAccessCache.java, src/main/java/com/opensource/docgrid/domain/permission/service/command/UserDocumentAccessCacheService.java, src/main/java/com/opensource/docgrid/domain/collection/repository/CollectionDocumentRepository.java
권한 저장소와 사용자 문서 캐시 저장소가 추가되었고, 컬렉션 문서 조회 및 캐시 갱신·무효화 동작이 구현되었습니다.
컬렉션·문서 권한 명령 서비스
src/main/java/com/opensource/docgrid/domain/permission/service/command/*
대상 ID 조합과 소유자를 검증하고, 권한 유형을 플래그로 변환하여 권한을 저장·삭제하며 USER 대상 캐시를 갱신합니다.
권한 서비스 검증 테스트
src/test/java/com/opensource/docgrid/domain/permission/*
컬렉션·문서 권한의 정상 처리, 캐시 호출, 소유자 검증 및 주요 오류 코드를 검증하는 테스트와 픽스처가 추가되었습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PermissionController
  participant PermissionCommandService
  participant UserDocumentAccessCacheService
  Client->>PermissionController: 권한 부여 또는 회수 요청
  PermissionController->>PermissionCommandService: 권한 명령 호출
  PermissionCommandService->>UserDocumentAccessCacheService: USER 캐시 갱신 또는 무효화
  PermissionCommandService-->>PermissionController: 권한 결과 반환
  PermissionController-->>Client: 201 또는 204 응답
Loading

Possibly related PRs

  • DocGrid/backend#2: 공통 응답 유틸리티와 ErrorCode 확장에 연결됩니다.
  • DocGrid/backend#6: 권한 엔티티와 데이터베이스 구조를 기반으로 동작합니다.
  • DocGrid/backend#13: @CurrentUser 인증 주입을 사용하는 API를 추가합니다.

Suggested labels: ✨ Feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 권한 부여·회수 API와 USER 캐시 갱신이라는 핵심 변경을 짧고 명확하게 요약합니다.
Description check ✅ Passed 필수 작업 내용, 상세 설명, 후속 계획, 리뷰 요구사항을 포함해 템플릿을 대부분 충족합니다.
Linked Issues check ✅ Passed #18의 저장소, 서비스, 컨트롤러, DTO, 캐시 갱신, 오류코드, 검증 및 테스트 요구를 모두 반영했습니다.
Out of Scope Changes check ✅ Passed 요약된 변경은 모두 권한 부여·회수와 USER 캐시 갱신 범위에 맞고, 명확한 무관 변경은 보이지 않습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/18

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/main/java/com/opensource/docgrid/domain/permission/service/command/DocumentPermissionCommandService.java (1)

70-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

boolean 배열 대신 레코드(Record)나 명시적 객체를 사용해 가독성과 타입 안정성을 높이는 것을 권장합니다.

resolvePermissionsboolean[]을 반환하고 이를 인덱스(permissions[0], permissions[1])로 접근하는 방식은 클린 코드 관점에서 실수를 유발하기 쉽고 가독성이 떨어집니다. 반환 타입을 명시적인 Record로 변경하면 각 플래그의 의미가 명확해집니다.

♻️ 레코드를 활용한 리팩토링 제안
-        boolean[] permissions = resolvePermissions(request.permissionType());
+        PermissionFlags flags = resolvePermissions(request.permissionType());
         User grantor = userRepository.getReferenceById(grantorId);
 
         DocumentPermission permission = DocumentPermission.builder()
                 .document(document)
                 .targetType(request.targetType())
                 .user(targetUser)
                 .role(targetRole)
                 .department(targetDepartment)
                 .permissionType(request.permissionType())
-                .canRead(permissions[0])
-                .canWrite(permissions[1])
-                .canAdmin(permissions[2])
+                .canRead(flags.canRead())
+                .canWrite(flags.canWrite())
+                .canAdmin(flags.canAdmin())
                 .grantedBy(grantor)
                 .grantedAt(LocalDateTime.now())
                 .expiresAt(request.expiresAt())
                 .build();

아래 캐시 서비스 호출과 메서드 선언부도 다음과 같이 변경합니다:

         if (request.targetType() == PermissionTargetType.USER) {
             cacheService.grantUserPermission(targetUser, document,
-                    permissions[0], permissions[1], permissions[2],
+                    flags.canRead(), flags.canWrite(), flags.canAdmin(),
                     AccessSourceType.DIRECT_DOCUMENT_PERMISSION, permission.getId(), request.expiresAt());
         }
+    private record PermissionFlags(boolean canRead, boolean canWrite, boolean canAdmin) {}
+
-    private boolean[] resolvePermissions(PermissionType type) {
+    private PermissionFlags resolvePermissions(PermissionType type) {
         return switch (type) {
-            case READ  -> new boolean[]{true, false, false};
-            case WRITE -> new boolean[]{true, true, false};
-            case ADMIN -> new boolean[]{true, true, true};
+            case READ  -> new PermissionFlags(true, false, false);
+            case WRITE -> new PermissionFlags(true, true, false);
+            case ADMIN -> new PermissionFlags(true, true, true);
         };
     }

Also applies to: 134-140

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/DocumentPermissionCommandService.java`
around lines 70 - 82, Replace the boolean[] contract returned by
resolvePermissions with an explicit record or value object whose fields
represent read, write, and admin permissions; update all callers, including the
cache-service path, to use named accessors instead of permissions[0],
permissions[1], and permissions[2].
src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java (1)

141-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

불리언 배열 대신 레코드(Record) 반환 (가독성 향상)

boolean[] 배열을 반환하여 인덱스([0], [1], [2])로 권한을 가져오는 방식은 매직 넘버를 유발하며 각 인덱스의 의미를 한눈에 파악하기 어렵게 만듭니다.

의미가 명확하게 드러나도록 내부 레코드(Record)나 DTO를 정의하여 반환하는 방식을 권장합니다.

💡 레코드 적용 예시
    private record ResolvedPermission(boolean canRead, boolean canWrite, boolean canAdmin) {}

    private ResolvedPermission resolvePermissions(PermissionType type) {
        return switch (type) {
            case READ  -> new ResolvedPermission(true, false, false);
            case WRITE -> new ResolvedPermission(true, true, false);
            case ADMIN -> new ResolvedPermission(true, true, true);
        };
    }

호출부에서는 permissions.canRead()와 같이 직관적으로 속성에 접근할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java`
around lines 141 - 147, Update resolvePermissions to return a named internal
record such as ResolvedPermission with canRead, canWrite, and canAdmin
components instead of boolean[]. Adjust its callers to use the record accessors
rather than positional array indexes, preserving the existing permission values
for READ, WRITE, and ADMIN.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java`:
- Around line 152-157: Replace the per-document grantUserPermission loop in
CollectionPermissionCommandService with a bulk operation on
UserDocumentAccessCacheService that accepts the collection’s Document list and
shared permission metadata, performs a single IN-based lookup, and batch-updates
existing entries while inserting missing entries via saveAll.
- Around line 118-123: Replace the per-document lookup and loop in the
collection-permission revocation flow with a bulk invalidation operation on
UserDocumentAccessCacheRepository. Add or reuse a single `@Modifying` query that
updates invalidatedAt for records matching
AccessSourceType.DIRECT_COLLECTION_PERMISSION and permissionId, then invoke it
from CollectionPermissionCommandService without calling findAllByCollectionId or
cacheService.revokeUserPermission for each document.

---

Nitpick comments:
In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java`:
- Around line 141-147: Update resolvePermissions to return a named internal
record such as ResolvedPermission with canRead, canWrite, and canAdmin
components instead of boolean[]. Adjust its callers to use the record accessors
rather than positional array indexes, preserving the existing permission values
for READ, WRITE, and ADMIN.

In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/DocumentPermissionCommandService.java`:
- Around line 70-82: Replace the boolean[] contract returned by
resolvePermissions with an explicit record or value object whose fields
represent read, write, and admin permissions; update all callers, including the
cache-service path, to use named accessors instead of permissions[0],
permissions[1], and permissions[2].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 953d6d48-9821-40e8-a401-9e282502c704

📥 Commits

Reviewing files that changed from the base of the PR and between 27599cc and d22611d.

📒 Files selected for processing (21)
  • src/main/java/com/opensource/docgrid/domain/collection/dto/request/AddDocumentRequest.java
  • src/main/java/com/opensource/docgrid/domain/collection/dto/request/CreateCollectionRequest.java
  • src/main/java/com/opensource/docgrid/domain/collection/dto/response/CollectionDocumentResponse.java
  • src/main/java/com/opensource/docgrid/domain/collection/dto/response/CollectionResponse.java
  • src/main/java/com/opensource/docgrid/domain/collection/repository/CollectionDocumentRepository.java
  • src/main/java/com/opensource/docgrid/domain/permission/controller/PermissionController.java
  • src/main/java/com/opensource/docgrid/domain/permission/converter/PermissionConverter.java
  • src/main/java/com/opensource/docgrid/domain/permission/dto/request/GrantPermissionRequest.java
  • src/main/java/com/opensource/docgrid/domain/permission/dto/response/CollectionPermissionResponse.java
  • src/main/java/com/opensource/docgrid/domain/permission/dto/response/DocumentPermissionResponse.java
  • src/main/java/com/opensource/docgrid/domain/permission/entity/UserDocumentAccessCache.java
  • src/main/java/com/opensource/docgrid/domain/permission/repository/CollectionPermissionRepository.java
  • src/main/java/com/opensource/docgrid/domain/permission/repository/DocumentPermissionRepository.java
  • src/main/java/com/opensource/docgrid/domain/permission/repository/UserDocumentAccessCacheRepository.java
  • src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java
  • src/main/java/com/opensource/docgrid/domain/permission/service/command/DocumentPermissionCommandService.java
  • src/main/java/com/opensource/docgrid/domain/permission/service/command/UserDocumentAccessCacheService.java
  • src/main/java/com/opensource/docgrid/global/exception/ErrorCode.java
  • src/test/java/com/opensource/docgrid/domain/permission/fixture/PermissionFixture.java
  • src/test/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandServiceTest.java
  • src/test/java/com/opensource/docgrid/domain/permission/service/command/DocumentPermissionCommandServiceTest.java

Comment on lines +118 to +123
List<CollectionDocument> docs = collectionDocumentRepository.findAllByCollectionId(collectionId);
for (CollectionDocument cd : docs) {
cacheService.revokeUserPermission(targetUserId, cd.getDocument().getId(),
AccessSourceType.DIRECT_COLLECTION_PERMISSION, permissionId);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

단일 쿼리로 캐시 무효화 (불필요한 조회 및 N+1 문제 개선)

컬렉션 권한을 회수할 때 컬렉션 내의 전체 문서를 조회하고 반복문을 돌며 개별적으로 캐시를 무효화하면 대량의 SELECT/UPDATE 쿼리가 발생(N+1 문제)하여 성능이 크게 저하됩니다.

sourceTypesourceId(컬렉션 권한 ID)를 활용하면 해당 컬렉션 권한을 통해 생성된 모든 캐시 레코드를 고유하게 식별할 수 있습니다. 문서 목록을 DB에서 모두 조회할 필요 없이, UserDocumentAccessCacheRepository에 단일 @Modifying UPDATE 쿼리를 추가하여 일괄 무효화하는 방식으로 개선을 적극 권장합니다.

💡 단일 쿼리 무효화 리팩토링 예시

UserDocumentAccessCacheRepository.java 쿼리 추가:

`@Modifying`
`@Query`("UPDATE UserDocumentAccessCache c SET c.invalidatedAt = CURRENT_TIMESTAMP WHERE c.sourceType = :sourceType AND c.sourceId = :sourceId")
void invalidateBySource(AccessSourceType sourceType, Long sourceId);

CollectionPermissionCommandService.java 적용:

-            Long targetUserId = permission.getUser().getId();
-            List<CollectionDocument> docs = collectionDocumentRepository.findAllByCollectionId(collectionId);
-            for (CollectionDocument cd : docs) {
-                cacheService.revokeUserPermission(targetUserId, cd.getDocument().getId(),
-                        AccessSourceType.DIRECT_COLLECTION_PERMISSION, permissionId);
-            }
+            cacheService.revokeUserPermissionBySource(AccessSourceType.DIRECT_COLLECTION_PERMISSION, permissionId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java`
around lines 118 - 123, Replace the per-document lookup and loop in the
collection-permission revocation flow with a bulk invalidation operation on
UserDocumentAccessCacheRepository. Add or reuse a single `@Modifying` query that
updates invalidatedAt for records matching
AccessSourceType.DIRECT_COLLECTION_PERMISSION and permissionId, then invoke it
from CollectionPermissionCommandService without calling findAllByCollectionId or
cacheService.revokeUserPermission for each document.

Comment on lines +152 to +157
List<CollectionDocument> docs = collectionDocumentRepository.findAllByCollectionId(collectionId);
for (CollectionDocument cd : docs) {
cacheService.grantUserPermission(targetUser, cd.getDocument(),
permissions[0], permissions[1], permissions[2],
AccessSourceType.DIRECT_COLLECTION_PERMISSION, sourceId, expiresAt);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

일괄 처리를 통한 캐시 갱신 (N+1 문제 개선)

컬렉션 권한 부여 시 반복문을 돌며 grantUserPermission을 호출하면 컬렉션 내의 문서 개수만큼 개별적인 SELECT와 INSERT/UPDATE가 발생하게 됩니다.

문서가 많은 컬렉션의 경우 심각한 DB I/O 병목이 발생할 수 있습니다. UserDocumentAccessCacheService에 대상 Document 리스트를 넘겨받아 일괄 처리(Bulk Update/Insert)를 수행하는 전용 메서드를 추가하여 사용하는 것을 권장합니다. (예: 기존 캐시는 IN 절로 한 번에 조회하여 상태를 갱신하고, 존재하지 않는 항목은 saveAll을 통해 배치로 삽입)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/opensource/docgrid/domain/permission/service/command/CollectionPermissionCommandService.java`
around lines 152 - 157, Replace the per-document grantUserPermission loop in
CollectionPermissionCommandService with a bulk operation on
UserDocumentAccessCacheService that accepts the collection’s Document list and
shared permission metadata, performs a single IN-based lookup, and batch-updates
existing entries while inserting missing entries via saveAll.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 권한 부여·회수 API + USER 캐시 갱신

1 participant