Skip to content

Conversation

@juanxiu
Copy link
Collaborator

@juanxiu juanxiu commented Oct 15, 2025

테넌트 격리 수준 정책

관련 이슈

#85

개요

목적

멀티 테넌트 환경에서 조직(테넌트) 단위로 리소스 접근 권한을 제어하기 위한 격리 수준 정책을 구현합니다.

핵심 개념

  • 조직 = 테넌트: 조직과 테넌트는 동일한 개념으로 사용
  • Worker: 조직에 속한 사용자 (User)
  • 리소스: 클라우드 리소스 (CloudResource) - AWS, GCP 등 여러 클라우드 계정에서 생성
  • 격리 수준: SHARED, DEDICATED 두 가지 모드

격리 수준 정의

SHARED (공유 모드)

  • 조직 내 모든 Worker가 모든 리소스에 접근 가능
  • 리소스 공유 및 협업에 적합
  • ResourceOwner 테이블 조회 불필요

DEDICATED (전용 모드)

  • Worker는 본인이 생성한 리소스만 접근/관리 가능
  • 리소스 소유권 기반 접근 제어
  • ResourceOwner 테이블에서 소유권 확인 필수

구현 요구사항

기능 요구사항

FR-1: 리소스 생성 시 소유권 관리

  • 리소스 생성 시 자동으로 ResourceOwner 레코드 생성
  • 생성자(User)를 소유자로 등록
  • 트랜잭션 일관성 보장 (리소스 생성 실패 시 소유권도 롤백)

FR-2: 격리 수준별 리소스 조회

  • SHARED 모드: 테넌트의 모든 리소스 조회
  • DEDICATED 모드: 사용자가 소유한 리소스만 조회
  • 격리 수준 변경 시 기존 리소스 접근 영향 최소화

FR-3: 리소스 접근 권한 검증

  • 리소스 조회/수정/삭제 시 접근 권한 검증
  • 테넌트 일치 확인 필수
  • 격리 수준에 따른 접근 제어 적용

FR-4: 격리 수준 관리

  • 테넌트별 격리 수준 조회
  • 격리 수준 설정/변경 기능
  • 격리 수준 미설정 시 안전한 기본값 (접근 거부)

비기능 요구사항

NFR-1: 성능

  • 격리 수준 조회는 캐싱 권장 (변경 빈도 낮음)
  • ResourceOwner 조회 시 인덱스 활용 필수
  • 목록 조회 시 JOIN 쿼리 최적화

NFR-2: 보안

  • 접근 거부 시 상세 로그 기록 (보안 감사)
  • 테넌트 간 데이터 접근 완전 차단
  • 소유권 확인 실패 시 명확한 에러 메시지

NFR-3: 확장성

  • 향후 공동 소유, 권한 레벨 추가 용이
  • 리소스 소유권 이전 기능 확장 가능
  • 격리 수준 추가 확장 가능

엔티티 관계 구조

전체 관계도

image

관계 상세

1. Organization → Tenant (1:1)

  • 하나의 조직은 하나의 테넌트만 가질 수 있음
  • 테넌트는 반드시 하나의 조직에 속함
  • 조직 = 테넌트: 조직과 테넌트는 동일한 개념

2. Tenant → TenantIsolation (1:1)

  • 테넌트당 하나의 격리 정책
  • 격리 수준: SHARED, DEDICATED

3. Tenant → User (1:N)

  • 하나의 테넌트는 여러 사용자를 가질 수 있음
  • 사용자는 반드시 하나의 테넌트에 속함

4. Tenant → CloudResource (1:N)

  • 하나의 테넌트는 여러 리소스를 가질 수 있음
  • 리소스는 반드시 하나의 테넌트에 속함

5. CloudResource ↔ User (M:N via ResourceOwner)

  • 중간 테이블(ResourceOwner)을 통한 다대다 관계
  • DEDICATED 모드에서 소유권 관리에 사용

접근 제어 적용 시점

작업 접근 제어 적용 비고
리소스 생성 생성자는 자동으로 소유자 등록
리소스 조회 단건/목록 모두 적용
리소스 수정 접근 권한 검증 후 수정
리소스 삭제 접근 권한 검증 후 삭제

전체 접근 제어 시퀀스 다이어그램

image

접근 제어 로직 플로우 차트

image

상세 로직

1단계: 테넌트 일치 확인

if (!isSameTenant(user.getTenant(), resource.getTenant())) {
    return false;  // 접근 거부
}

2단계: 격리 수준 조회

IsolationLevel level = tenantIsolationService.getIsolationLevel(resource.getTenant());

3단계: 격리 수준별 접근 제어

SHARED 모드:

if (level == IsolationLevel.SHARED) {
    return true;  // 테넌트 내 모든 사용자 접근 가능
}

DEDICATED 모드:

if (level == IsolationLevel.DEDICATED) {
    return resourceOwnerService.isResourceOwner(user, resource);
}

실행된 테스트 클래스들

  • TenantIsolationServiceTest - 테넌트 격리 수준 관리
  • TenantAuthorizationServiceTest - 테넌트 권한 관리
  • TenantAwareAuthorizationServiceTest - 테넌트 인식 권한 관리
  • TenantPolicyServiceTest - 테넌트 정책 관리
  • TenantDataRetentionServiceTest - 테넌트 데이터 보관 정책
  • TenantCollectorConfigServiceTest - 테넌트 수집기 설정
  • TenantContextTaskDecoratorTest - 테넌트 컨텍스트 태스크 데코레이터
  • 기타 테넌트 관련 테스트들

✅ 정상 동작 확인

  1. ResourceOwnerService.createResourceOwnership

    • 이미 소유권이 존재하면 저장하지 않고 반환 (idempotent 동작)
    • 새로운 소유권은 정상적으로 생성
  2. ResourceAccessControlService.canAccessResource

    • 테넌트 일치 확인 후 격리 수준에 따라 접근 제어
    • SHARED 모드: 테넌트 일치 시 모든 사용자 접근 허용
    • DEDICATED 모드: 소유권 확인 후 접근 허용/거부
    • 격리 수준이 없으면 접근 거부 (안전한 기본값)
  3. ResourceAccessControlService.filterAccessibleResources

    • 배치 조회를 사용하여 N+1 문제 해결
    • SHARED 모드: 테넌트 일치한 모든 리소스 반환
    • DEDICATED 모드: 배치로 소유권 확인 후 필터링
  4. CloudResourceService 통합

    • 리소스 생성 시 자동으로 소유권 등록
    • 조회/수정/삭제 시 접근 권한 검증
    • 격리 수준에 따라 적절한 리소스 목록 반환

@juanxiu juanxiu changed the base branch from main to develop October 15, 2025 10:50
@juanxiu juanxiu self-assigned this Oct 15, 2025
@juanxiu juanxiu marked this pull request as draft October 15, 2025 10:52
@juanxiu juanxiu requested a review from KwonSunJae October 15, 2025 10:52
*
* @return 클라우드 프로바이더 타입
*/
String getSupportedCloudProvider();

Choose a reason for hiding this comment

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

여기서 반환하는 프로바이더 반환 타입을 CloudProviderType으로 하면 타입 안정성이 더 높아지지 않을까융
String으로 반환하는 이유가 따로 있으려나용? 코드를 다 보진 않아가지구

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오아 좋은 생각이네요!! 감사합니닷 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

코드리뷰 넘나 환영 .. 더 주세요 히

Choose a reason for hiding this comment

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

기능 푸시하고 요청주면 리뷰할게용~~!

@hyobin-yang
Copy link

추상화
CloudProviderService 인터페이스에서 리소스 관리 기능 구현
VPC, 서브넷, 보안 그룹 생성

  • 이거 위해 클라우드 리소스 동기화, 리소스 데이터 동기화가 필요해보임. 지금 구조를 최대한 가져간다면, CloudResourceSyncPort 등의 새로운 패턴을 생성하는 방식도 있을 것 같네용(-> 이렇게 되면 CloudResourceSyncAdapterFactory 라는 별도 팩토리가 필요할 듯?)

  • 추후 AwsResourceSyncAdapter 등의 구현체를 생성해서 CloudResourceSyncPort를 implements

  • 이렇게 되면 격리 정책 + 리소스 동기화는 통합 서비스 계층에서 합쳐지게 되려나

  • CloudResourceService에 클라우드 리소스 저장할 수 있는 메서드 필요할 듯(ex. @transactional saveAll 메서드)

    • CloudProvider랑 Tenant 가져오는 메서드도 있어야 할 것 같은데, Tenant key로 테넌트 반환하는 메서드는 어디 따로 만들어진 곳이 있는지는 모르겠음(테넌트 키 자체는 spring context에서 추출하는 게 좋을 것 같움)
@Service
public class CloudResourceService {
    
    private final CloudResourceRepository cloudResourceRepository;
    private final TenantService tenantService;
    private final CloudProviderService cloudProviderService;
    
    @Transactional
    public List<CloudResource> saveAll(List<CloudResource> resources) {
        return cloudResourceRepository.saveAll(resources);
    }
    
    public Tenant getTenantByKey(String tenantKey) {
        return tenantService.getTenantByKeyOrThrow(tenantKey);
    }
    
    public CloudProvider getProviderByKey(String providerKey) {
        return cloudProviderService.getProviderByKeyOrThrow(providerKey);
    }
}

-> 대충 이런 식이 되려나?

  • 순환 의존성 생기지 않도록 포트-어댑터 패턴 구조 최대한 활용해야 될 듯!!(컨트롤러 -> 포트 -> 어댑터)

참고만 해주세요 화이팅🥹❤️

@juanxiu juanxiu changed the title feat: 테넌트 격리 정책 서비스 [FEATURE]테넌트 격리 정책 서비스 Nov 24, 2025
@juanxiu juanxiu changed the title [FEATURE]테넌트 격리 정책 서비스 [FEATURE] 테넌트 격리 정책 서비스 Nov 24, 2025
@gitguardian
Copy link

gitguardian bot commented Nov 24, 2025

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
21859919 Triggered Generic High Entropy Secret 8a71bcf src/main/resources/application-local.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

…lder에 requestPath/httpMethod 설정 메서드 추가
@juanxiu juanxiu marked this pull request as ready for review November 24, 2025 10:51
…EventBuilder에 requestPath/httpMethod 설정 메서드 추가"

This reverts commit 04d755e.
- OrganizationController: 조직 테넌트 조회 API 통합
- Organization: tenant 필드 중복 제거
- OrganizationRepository: 테넌트 조회 쿼리 통합
- OrganizationService: 조직-테넌트 관계 관리 메서드 통합
- OrganizationTenantServiceTest: 삭제된 테스트 파일 제거
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.

3 participants