# 권한 시스템 > G7의 권한(Permission)과 역할(Role) 시스템 --- ## TL;DR (5초 요약) ```text 1. 구조: User → Role → Permission (기능 레벨) 2. 리소스: User → Role → role_menus 피벗 → Menu 3. 네이밍: [module].[entity].[action] (예: users.read) 4. type 필드: admin(관리자용) 또는 user(사용자용), 기본값: admin 5. scope_type: 역할별 접근 스코프 (null=전체, self=본인, role=소유역할) ``` --- ## 목차 1. [권한 아키텍처](#권한-아키텍처) 2. [권한 타입 (Permission Type)](#권한-타입-permission-type) 3. [모듈 Role/Permission 자동 관리](#모듈-rolepermission-자동-관리) 4. [Role 정의 규칙](#role-정의-규칙) 5. [Permission에 Role 할당](#permission에-role-할당) 6. [Role 삭제 조건](#role-삭제-조건) 7. [콘솔 커맨드 출력](#콘솔-커맨드-출력) 8. [권한 네이밍](#권한-네이밍) 9. [리소스 레벨 권한](#리소스-레벨-권한) 10. [레이아웃 컴포넌트 권한 필터링](#레이아웃-컴포넌트-권한-필터링) --- ## 권한 아키텍처 G7의 권한 시스템은 두 가지 레벨로 구성됩니다. ### 기능 레벨 권한 (Feature-level) ```text User → Role → Permission ``` - **User**: 시스템 사용자 - **Role**: 역할 그룹 (여러 Permission을 묶음) - **Permission**: 개별 권한 (특정 작업 수행 허용) ### 리소스 레벨 권한 (Resource-level) ```text User → Role → role_menus 피벗 테이블 → Menu ``` - **role_menus**: 메뉴 접근 권한 > **참고**: 첨부파일(Attachment)은 기능 레벨 권한(`core.attachments.*` + `scope_type`)으로 제어합니다. `role_attachments` 독립 시스템은 폐기되었습니다. --- ## 권한 타입 (Permission Type) 권한은 `type` 필드로 사용 컨텍스트를 구분합니다. ### PermissionType Enum ```php // app/Enums/PermissionType.php enum PermissionType: string { case Admin = 'admin'; // 관리자 화면용 권한 case User = 'user'; // 사용자(프론트엔드) 화면용 권한 } ``` ### 타입별 용도 | 타입 | 용도 | 예시 | |------|------|------| | `admin` | 관리자 화면에서 사용되는 권한 | 사용자 관리, 설정, 모듈 관리 | | `user` | 사용자(프론트엔드) 화면에서 사용되는 권한 | 내 주문 조회, 프로필 수정 | ### 관리자 판단 기준 ```php // User::isAdmin() 메서드 // 사용자가 보유한 권한 중 type='admin'인 것이 하나라도 있으면 관리자 public function isAdmin(): bool { return $this->roles() ->whereHas('permissions', function ($query) { $query->where('type', PermissionType::Admin); }) ->exists(); } ``` ### 모듈/플러그인에서 권한 타입 지정 ```php // getPermissions() 반환 구조 'permissions' => [ [ 'action' => 'read', 'name' => ['ko' => '조회', 'en' => 'Read'], 'description' => ['ko' => '...', 'en' => '...'], 'type' => 'admin', // admin 또는 user (기본값: admin) 'roles' => ['admin', 'manager'], ], [ 'action' => 'view_own', 'name' => ['ko' => '내 정보 조회', 'en' => 'View Own'], 'type' => 'user', // 사용자 화면용 권한 'roles' => ['user'], ], ], ``` ### 코어 권한에서 타입 지정 (config/core.php) 코어 권한(`config/core.php`의 `categories`)도 모듈/플러그인 매니페스트와 **동일하게 `type` 필드를 지원**합니다. `CoreUpdateService::syncCoreRolesAndPermissions()`가 이 필드를 읽어 admin/user 컨텍스트를 분리합니다. ```php // config/core.php 'categories' => [ [ 'identifier' => 'core.users', 'category' => 'users', 'order' => 1, 'type' => 'admin', // ← 카테고리 타입 명시 'permissions' => [ ['identifier' => 'core.users.read', 'type' => 'admin', ...], // ← 권한 타입 명시 ['identifier' => 'core.users.create', 'type' => 'admin', ...], ], ], [ 'identifier' => 'core.user-notifications', 'category' => 'user-notifications', 'order' => 7.6, 'type' => 'user', // ← 사용자 컨텍스트 카테고리 'permissions' => [ ['identifier' => 'core.user-notifications.read', 'type' => 'user', ...], ], ], ], ``` ```text ✅ 필수: 신규 코어 권한 정의 시 카테고리/개별 권한 모두에 `type` 명시 (가독성 + 의도 명확화) ✅ 카테고리 type은 (1) 명시 필드 (2) 하위 권한이 모두 동일 type일 때 추론 (3) admin 기본값 우선순위로 결정됨 ✅ 권한 type 미지정 시 카테고리 type을 상속 ``` ### CRITICAL: 단일 unique 제약 함정 ```text ⚠️ CRITICAL: permissions.identifier 컬럼은 단일 unique 제약입니다. ⚠️ 같은 식별자로 (identifier='core.notifications.read', type='admin')과 (identifier='core.notifications.read', type='user') 두 행을 동시에 생성할 수 없습니다. ``` | 잘못된 접근 | 올바른 접근 | |---|---| | `core.notifications.*` 하나의 식별자를 admin/user 양쪽에서 사용 | admin은 `core.notifications.*` (`type=admin`), 사용자는 `core.user-notifications.*` (`type=user`)로 **별도 식별자** 분리 | | 라우트 미들웨어에서 admin 식별자를 user 컨텍스트에 사용 | 사용자 라우트(`/api/user/...`)는 반드시 `type=user` 권한 행이 존재하는 식별자를 사용 | `PermissionMiddleware`는 `(identifier, type)` 쌍으로 권한 행을 조회하므로, 라우트 미들웨어 `permission:user,xxx`는 DB에 `(identifier='xxx', type='user')` 행이 존재할 때만 통과합니다. ### 라우트 미들웨어 type 일치 규칙 ```php // routes/api.php // ❌ 잘못된 예: 사용자 라우트에 admin 타입 권한 식별자 사용 Route::get('/api/user/notifications', [UserNotificationController::class, 'index']) ->middleware('permission:user,core.notifications.read'); // 항상 403 — type 불일치 // ✅ 올바른 예: 사용자 라우트에는 user 타입 권한 식별자 사용 Route::get('/api/user/notifications', [UserNotificationController::class, 'index']) ->middleware('permission:user,core.user-notifications.read'); // ✅ 관리자 라우트에는 admin 타입 권한 식별자 사용 Route::get('/api/admin/notifications', [AdminNotificationController::class, 'index']) ->middleware('permission:admin,core.notifications.read'); ``` `Broadcast::channel()` 권한 체크에서도 동일하게 `PermissionType` 인자를 명시해야 합니다: ```php // routes/channels.php Broadcast::channel('core.user.notifications.{uuid}', function ($user, $uuid) { return $user->uuid === $uuid && $user->hasPermission('core.user-notifications.read', PermissionType::User); }); ``` ### Permission 모델 헬퍼 ```php // 타입 확인 메서드 $permission->isAdminPermission(); // type === 'admin' $permission->isUserPermission(); // type === 'user' // 스코프 Permission::adminPermissions()->get(); // admin 타입만 Permission::userPermissions()->get(); // user 타입만 ``` --- ## 모듈 Role/Permission 자동 관리 ```text 중요: 모듈 설치 시 Role과 Permission이 자동으로 생성/연결됨 ✅ 필수: getRoles()와 getPermissions()의 roles 필드 사용 ``` > **플러그인 권한**: 플러그인도 모듈과 동일한 3레벨 계층 구조(`categories`)를 사용합니다. 상세는 [plugin-development.md](plugin-development.md)의 "플러그인 권한 시스템" 섹션을 참조하세요. ### 동작 방식 #### 모듈 설치 시 (module:install) 1. 마이그레이션 실행 2. `getRoles()`에서 정의한 Role 생성 3. `getPermissions()`에서 정의한 Permission 생성 4. Permission의 `roles` 필드에 지정된 Role에 권한 할당 5. 메뉴 생성 6. 시더 실행 #### 모듈 제거 시 (module:uninstall) 1. Role에서 Permission 분리 (detach) 2. Permission 삭제 3. Role 삭제 (조건: `extension_type`이 NULL(사용자 생성) AND 남은 권한 0개) 4. 메뉴 삭제 5. 마이그레이션 롤백 --- ## Role 정의 규칙 ### identifier 네이밍 ```text [vendor-module].[rolename] ``` ### 예시 ```text sirsoft-ecommerce.manager sirsoft-ecommerce.viewer sirsoft-sample.manager ``` ### getRoles() 반환 구조 ```php public function getRoles(): array { return [ [ 'identifier' => 'sirsoft-ecommerce.manager', 'name' => [ 'ko' => '이커머스 관리자', 'en' => 'Ecommerce Manager', ], 'description' => [ 'ko' => '이커머스 모듈의 관리 권한을 가진 역할', 'en' => 'Role with management permissions for ecommerce module', ], ], ]; } ``` **필수 필드**: | 필드 | 타입 | 설명 | | ------------- | ------ | -------------------------------------- | | `identifier` | string | 고유 식별자 (vendor-module.rolename) | | `name` | array | 다국어 역할명 | | `description` | array | 다국어 설명 | --- ## Permission에 Role 할당 ### getPermissions() roles 필드 ```php public function getPermissions(): array { return [ [ 'identifier' => 'sirsoft-ecommerce.products.view', 'name' => ['ko' => '상품 조회', 'en' => 'View Products'], 'description' => ['ko' => '...', 'en' => '...'], 'roles' => ['admin', 'sirsoft-ecommerce.manager'], // Role 지정 ], ]; } ``` ### 규칙 - `roles` 필드에 지정된 Role에 자동으로 권한 할당 - 기존 Role의 권한은 `syncWithoutDetaching`으로 보존 - 여러 Role에 동일 권한 할당 가능 ### syncWithoutDetaching 동작 ```php // 기존 권한은 유지하면서 새 권한만 추가 $role->permissions()->syncWithoutDetaching($permissionIds); ``` - 모듈 A가 `admin` Role에 권한 할당 - 모듈 B도 `admin` Role에 권한 할당 - 모듈 A 제거 시 모듈 B의 권한은 유지됨 --- ## Role 삭제 조건 ### 안전한 삭제를 위한 조건 1. `extension_type`이 NULL (사용자 생성 Role만 삭제 가능) 2. `extension_type = 'core'` → 삭제 불가 (코어 역할 보호) 3. `extension_type = 'module'` 또는 `'plugin'` → 삭제 불가 (확장 소유 역할 보호, `ExtensionOwnedRoleDeleteException` 발생) 4. 남은 권한 개수 = 0 (다른 모듈의 권한이 없어야 함) ### 조건을 만족하지 않는 경우 - `extension_type`이 설정된 Role은 `ExtensionOwnedRoleDeleteException` 또는 403 응답 - 남은 권한이 있는 경우 삭제되지 않고 로그에 경고 기록 ### Extension Ownership 예시 ```php // 코어 Role (extension_type = 'core', extension_identifier = 'core') 'admin' // 최고 관리자 — 삭제 불가 // 모듈 Role (extension_type = 'module', extension_identifier = 'sirsoft-ecommerce') 'sirsoft-ecommerce.manager' // 모듈 소유 — 삭제 불가 // 사용자 생성 Role (extension_type = NULL) 'custom-role' // 사용자 생성 — 삭제 가능 ``` --- ## 콘솔 커맨드 출력 ### 설치 시 ```text ✅ 모듈 "sirsoft-sample" 설치 완료 - 벤더: sirsoft - 버전: 1.0.0 - 1개 역할 생성됨 - 4개 권한 생성됨 - 3개 메뉴 생성됨 ``` ### 제거 시 ```text 모듈 "sirsoft-sample"을(를) 정말 삭제하시겠습니까? - 1개 역할이 삭제됩니다. - 4개 권한이 삭제됩니다. - 3개 메뉴가 삭제됩니다. ``` --- ## 권한 네이밍 ### 코어 권한 ```text core.[entity].[action] ``` **예시**: ```text core.templates.layouts.view core.templates.layouts.create core.templates.layouts.edit core.templates.layouts.delete core.users.view core.users.manage ``` ### 모듈 권한 ```text [vendor-module].[entity].[action] ``` **예시**: ```text sirsoft-ecommerce.products.view sirsoft-ecommerce.products.create sirsoft-ecommerce.products.edit sirsoft-ecommerce.products.delete sirsoft-ecommerce.orders.view sirsoft-ecommerce.orders.manage ``` ### 권한 네이밍 규칙 요약 | 구분 | 패턴 | 예시 | | ------ | ------------------------------- | ------------------------------------ | | 코어 | `core.[entity].[action]` | `core.users.view` | | 모듈 | `[vendor-module].[entity].[action]` | `sirsoft-ecommerce.products.create` | ### 일반적인 action 목록 | Action | 설명 | | -------- | --------------------------------------- | | `view` | 조회 권한 | | `create` | 생성 권한 | | `edit` | 수정 권한 | | `delete` | 삭제 권한 | | `manage` | 관리 권한 (view + create + edit + delete) | --- ## 리소스 레벨 권한 G7은 기능 레벨 권한 외에 **리소스 레벨 권한**을 지원합니다. 특정 리소스(메뉴 등)에 대한 세밀한 접근 제어가 가능합니다. ### 적용된 리소스 | 리소스 | 권한 방식 | 설명 | | ------------ | --------- | ---- | | Menu | `role_menus` 피벗 테이블 | 역할별 메뉴 접근 제어 | | Attachment | `core.attachments.*` + `scope_type` | 기능 레벨 권한으로 제어 | > **참고**: 첨부파일은 `role_attachments` 독립 시스템이 아닌, 기존 `permissions` + `scope_type` 시스템으로 통합되었습니다. > - `core.attachments.create`: 업로드 권한 > - `core.attachments.update`: 수정/순서변경 권한 (`resource_route_key: 'attachment'`, `owner_key: 'created_by'`) > - `core.attachments.delete`: 삭제 권한 (`resource_route_key: 'attachment'`, `owner_key: 'created_by'`) > - 공개 다운로드는 `HookManager::checkHookPermission('core.attachment.download')` 으로 제어 --- ## API 응답 권한 메타 (is_owner + abilities) 모든 API 리소스 응답에 `is_owner`와 `abilities` (can_*) 필드가 표준화되어 포함됩니다. ### 응답 형식 ```json { "id": 1, "name": "상품명", "is_owner": true, "abilities": { "can_update": true, "can_delete": false } } ``` - `is_owner`: 현재 요청 사용자가 리소스 소유자인지 여부 - `abilities`: `can_*` 키로 통합된 권한 플래그 ### 권한 확인 순서 (checkAbility) ```text 1. Admin 역할 → 무조건 true 2. 인증된 사용자 → $user->hasPermission($identifier) 3. 게스트 → PermissionHelper::check($identifier) ``` ### 프론트엔드에서 사용 레이아웃 JSON에서 `abilities.can_*` 키로 접근합니다: ```json { "if": "{{item.abilities.can_update}}", "type": "Button", "props": { "text": "수정" } } ``` > **상세 구현 규칙**: [api-resources.md](../backend/api-resources.md#권한-메타-표준화-is_owner--abilities) 참조 --- ## PermissionHelper — 서비스/컨트롤러에서 권한 체크 `PermissionHelper` 헬퍼 클래스를 사용하여 서비스/컨트롤러 내부에서 권한을 체크합니다. scope_type 기반 스코프 체크 기능이 내장되어 있습니다. ```php use App\Helpers\PermissionHelper; // 기본 권한 체크 PermissionHelper::check('core.users.update'); // 구조화 권한 (OR/AND) PermissionHelper::checkWithLogic(['or' => ['perm.a', 'perm.b']]); // scope_type 기반 상세 접근 체크 PermissionHelper::checkScopeAccess($model, 'core.users.update', $user); // scope_type 기반 목록 필터링 (Repository에서 사용) PermissionHelper::applyPermissionScope($query, 'core.users.read'); // effective scope 조회 PermissionHelper::getEffectiveScope('core.users.read', $user); // null|'self'|'role' // 소유자 정보 조회 (캐싱) PermissionHelper::getOwnerKey('core.users.read'); // 'id' PermissionHelper::getResourceRouteKey('core.users.read'); // 'user' ``` **메서드 시그니처**: ```php public static function check(string $ability, ?User $user = null): bool public static function checkWithLogic(array|object $permissions, ?User $user = null): bool public static function checkScopeAccess(Model $model, string $permission, ?User $user = null): bool public static function applyPermissionScope(Builder $query, string $permission, ?User $user = null): void public static function getEffectiveScope(string $permission, ?User $user = null): ?string public static function getOwnerKey(string $permission): ?string public static function getResourceRouteKey(string $permission): ?string ``` ### 권한 체크 방식 선택 가이드 | 상황 | 사용 방법 | |------|----------| | 라우트 접근 제어 | `permission:` 미들웨어 (scope_type 자동 체크) | | 서비스 내부 조건부 권한 체크 | `PermissionHelper::check()` 직접 호출 | | 상세 엔드포인트 스코프 체크 | `PermissionHelper::checkScopeAccess()` | | 목록 엔드포인트 필터링 | `PermissionHelper::applyPermissionScope()` (Repository에서 호출) | | API 응답 abilities 플래그 | `HasAbilityCheck` 트레이트 (`abilityMap()`) | | 리소스 레벨 접근 제어 (메뉴 등) | `role_*` 피벗 테이블 기반 | | 레이아웃 컴포넌트 필터링 | `permissions` 속성 (서버 사이드 제거) | ### 주의 사항 ```text ❌ AuthServiceProvider::checkPermission() 직접 호출 → PermissionHelper::check() 사용 ❌ 서비스 내부에서 미들웨어에 의존한 권한 체크 생략 → PermissionHelper로 명시적 체크 ❌ except/only/menu 미들웨어 옵션 사용 (제거됨) → scope_type 데이터 기반 시스템 사용 ``` --- ## scope_type 스코프 시스템 역할별 접근 범위를 `role_permissions` 피벗의 `scope_type` 컬럼으로 제어합니다. 모든 라우트 미들웨어 옵션 (`except:`, `only:`, `menu:`)은 **제거**되었습니다. ### DB 구조 | 테이블 | 컬럼 | 타입 | 용도 | |--------|------|------|------| | `permissions` | `resource_route_key` | VARCHAR(50) NULL | 라우트 파라미터명 (예: 'user', 'menu') | | `permissions` | `owner_key` | VARCHAR(50) NULL | 모델의 소유자 식별 컬럼 (예: 'id', 'created_by') | | `role_permissions` | `scope_type` | ENUM('self', 'role') NULL | 역할별 접근 스코프 | ### ScopeType Enum ```php // app/Enums/ScopeType.php enum ScopeType: string { case Self = 'self'; // 본인 리소스만 case Role = 'role'; // 소유 역할 공유 사용자의 리소스 } // null = 전체 접근 (제한 없음) ``` ### scope_type 값 정의 | 값 | 의미 | 목록 필터링 | 상세 접근 체크 | |---|---|---|---| | `null` | 전체 접근 | 필터 미적용 | 항상 통과 | | `'self'` | 본인 리소스만 | `WHERE {owner_key} = {user_id}` | `$model->{owner_key} === $user->id` | | `'role'` | 내 역할 범위 | `WHERE {owner_key} IN (내 역할 공유 사용자)` | 소유자가 내 역할 공유하는지 체크 | ### union 정책 (복수 역할 보유 시) - **우선순위**: `null`(전체) > `'role'`(소유역할) > `'self'`(본인) - 여러 역할 중 하나라도 scope_type=null → 전체 접근 - 전부 non-null이면 가장 넓은 범위 적용 (role > self) - 예: 역할A(scope=self) + 역할B(scope=role) → role 적용 ### 미들웨어 처리 흐름 ```text permission:admin,core.users.read ← 옵션 없음 1. 권한 타입 검증 2. 동적 파라미터 치환 3. 권한 체크 (인증/게스트) 4. scope_type 스코프 체크: a. Permission 조회 (캐싱) → resource_route_key, owner_key b. resource_route_key null → 통과 (시스템 리소스) c. $request->route(resource_route_key) → 모델 resolve d. 모델 없음 → 통과 (목록 엔드포인트) e. 사용자의 effective scope 확인 (union 정책) f. scope=null → 통과 g. scope='self' → $model->{owner_key} === $user->id h. scope='role' → 소유자가 내 역할 공유하는지 체크 ``` ### 목록 엔드포인트 필터링 (Repository 패턴) ```php // 모든 Repository에서 한 줄로 적용 $query = User::query(); PermissionHelper::applyPermissionScope($query, 'core.users.read'); // ... 기존 필터/정렬/페이지네이션 ``` ### 소유자 식별 전체 매핑 | Permission prefix | resource_route_key | owner_key | 비고 | |---|---|---|---| | core.users.* | user | id | self: $user->id === $model->id | | core.menus.* | menu | created_by | | | core.attachments.update/delete | attachment | created_by | create는 scope 불필요 | | core.schedules.* | schedule | created_by | | | core.activity_logs.* | null | null | 소유자 없음 | | sirsoft-board.boards.* | board | created_by | | | sirsoft-board.{slug}.*.posts.* | post | user_id | | | sirsoft-ecommerce.products.* | product | created_by | | | sirsoft-ecommerce.orders.* | order | user_id | | | sirsoft-page.pages.* | page | created_by | | > resource_route_key/owner_key가 null → scope_type UI 미표시, 설정해도 무시 ### getPermissions() 포맷 (모듈) ```php 'categories' => [ [ 'identifier' => 'pages', 'resource_route_key' => 'page', // 리소스 라우트 키 'owner_key' => 'created_by', // 소유자 컬럼 'permissions' => [ [ 'action' => 'read', 'roles' => ['admin', 'manager'], // 문자열 → scope_type=null ], [ 'action' => 'delete', 'roles' => [ ['role' => 'admin', 'scope_type' => null], ['role' => 'manager', 'scope_type' => 'self'], ], ], ], ], ], ``` ### 역할 관리 UI - PermissionTree 컴포넌트에 scope_type 드롭다운 추가 - resource_route_key가 있는 권한 + 체크된 상태에서만 드롭다운 표시 - 선택지: 전체(null), 소유역할('role'), 본인만('self') - 폼 데이터: `permissions: [{id: 1, scope_type: null}, {id: 2, scope_type: 'self'}]` --- ## 레이아웃 컴포넌트 권한 필터링 레이아웃 JSON의 컴포넌트에 `permissions` 속성을 정의하면, **레이아웃 서빙 API에서 해당 컴포넌트를 제거하여 서빙**합니다. 프론트엔드 `if` 조건부 렌더링과 달리 JSON 구조 자체가 클라이언트에 노출되지 않아 보안이 강화됩니다. ### 동작 원리 ```text 파이프라인 위치: [캐시 조회] → [extends/partial 병합] → [applyExtensions] → [★ filterComponentsByPermissions] → [훅] → [응답] ``` - **Post-cache filtering**: 캐시 이후 사용자별 동적 필터링 (캐시 구조 영향 없음, ~1-2ms 추가) - **AND 조건**: `permissions` 배열의 모든 권한을 충족해야 컴포넌트 포함 - **Admin 바이패스**: `admin` 역할은 `Gate::before()`에 의해 모든 권한 통과 ### 필터링 대상 영역 | 영역 | 설명 | |------|------| | `components` | 메인 컴포넌트 트리 (재귀 children 포함) | | `modals[]` | 모달 자체에 permissions → 모달 항목 전체 제거 | | `modals[].components` | 모달 내부 컴포넌트 트리 (재귀) | | `defines` | 재사용 컴포넌트 정의 (재귀) | | partial 내부 | extends/partial 병합 후 필터링이므로 자동 처리 | ### 필터링 규칙 1. 현재 노드의 `permissions` 배열 확인 (없거나 빈 배열이면 통과) 2. `AuthServiceProvider::checkPermissions($permissions, $user)` 호출 (AND 조건) 3. 권한 없으면 해당 노드 및 하위 children **전체 제거** (하위 평가 생략) 4. 권한 있으면 통과 → children을 재귀적으로 독립 평가 5. 필터링 완료 후 모든 노드에서 `permissions` 속성 제거 (클라이언트 노출 방지) ### 상위-하위 중복 선언 ```json { "type": "Div", "permissions": ["core.users.read"], "children": [ { "type": "Button", "permissions": ["core.users.delete"], "text": "삭제" } ] } ``` | 시나리오 | 상위 결과 | 하위 결과 | 최종 | |---------|---------|---------|------| | `read` 없음 | 상위 제거 → 전체 제거 | 평가 안 됨 | 둘 다 없음 | | `read` 있음 + `delete` 없음 | 통과 | 하위만 제거 | Div만 남음 | | `read` 있음 + `delete` 있음 | 통과 | 통과 | 둘 다 남음 | → 각 노드는 자기 permissions만 독립 평가. 별도 병합 정책 없음. ### 확장(Extension)과의 관계 - Extension Point로 주입된 컴포넌트에 `permissions` → 정상 필터링 - Overlay로 교체된 컴포넌트에 `permissions` → 교체된 컴포넌트의 permissions 적용 - 파이프라인 순서: `applyExtensions` → `filterComponentsByPermissions` (확장 적용 후 필터링) ### 레이아웃 최상위 permissions와의 비교 | 항목 | 최상위 `permissions` | 컴포넌트 `permissions` | |------|---------------------|----------------------| | 적용 범위 | 레이아웃 전체 | 개별 컴포넌트 단위 | | 미충족 시 | 403 응답 | 해당 컴포넌트만 제거 | | 조건 방식 | AND | AND | | 처리 시점 | 캐시 조회 전 | 캐시 조회 후 (post-cache) | ### 관련 코드 | 파일 | 역할 | |------|------| | `app/Services/LayoutService.php` | `filterComponentsByPermissions()` 메서드 | | `app/Http/Controllers/Api/Public/PublicLayoutController.php` | 서빙 시 필터링 호출 | | `app/Rules/ValidLayoutStructure.php` | 컴포넌트 `permissions` 검증 | > **레이아웃 JSON 문법 상세**: [layout-json.md](../frontend/layout-json.md#컴포넌트별-권한-component-permissions) 참조 --- ## Model 권한/역할 체크 메서드 User 모델은 권한/역할 체크를 위한 헬퍼 메서드를 제공합니다. ### 메서드 시그니처 ```php // 단일 권한 체크 $user->hasPermission('core.users.read'); $user->hasPermission('core.users.read', PermissionType::Admin); // 복수 권한 체크 $user->hasPermissions(['perm.a', 'perm.b'], requireAll: true); // AND $user->hasPermissions(['perm.a', 'perm.b'], requireAll: false); // OR // 역할 체크 $user->hasRole('admin'); $user->hasRoles(['admin', 'manager'], requireAll: false); // OR // 관리자 판단 (admin 타입 권한 보유 여부) $user->isAdmin(); ``` ### 인스턴스 레벨 캐싱 `getEffectiveScopeForPermission()` 메서드는 동일 요청 내 반복 호출 시 DB 쿼리를 방지하기 위해 인스턴스 레벨 캐싱을 사용합니다: ```php // User 모델 내부 protected array $effectiveScopeCache = []; public function getEffectiveScopeForPermission(string $identifier): ?string { if (array_key_exists($identifier, $this->effectiveScopeCache)) { return $this->effectiveScopeCache[$identifier]; } // ... DB 쿼리 및 union 정책 적용 ... return $this->effectiveScopeCache[$identifier] = $result; } ``` --- ## Permission 동기화 및 추적 ### Role-Permission 동기화 패턴 역할에 권한을 동기화할 때 이전/이후 상태를 추적합니다: ```php public function syncPermissions(Role $role, array $permissions): void { // 동기화 전 권한 식별자 캡처 (Listener diff 계산용) $previousPermIdentifiers = $role->permissions()->pluck('identifier')->toArray(); // 피벗 데이터에 granted_at, granted_by 자동 설정 $pivotData = []; foreach ($permissions as $permission) { $pivotData[$permission['id']] = [ 'scope_type' => $permission['scope_type'] ?? null, 'granted_at' => now(), 'granted_by' => Auth::id(), ]; } $role->permissions()->sync($pivotData); // 동기화 후 권한 식별자 (Listener에서 diff 계산 가능) $currentPermIdentifiers = $role->permissions()->pluck('identifier')->toArray(); HookManager::doAction('core.role.after_sync_permissions', $role, $previousPermIdentifiers, $currentPermIdentifiers); } ``` ### 자기잠금 방지 마지막 admin 역할 보유자가 자신의 admin 역할을 제거하는 것을 방지합니다: ```text - 관리자가 자기 자신의 역할을 변경하려 하면 → 역할 변경 무시 - 마지막 admin이 admin 역할을 삭제하려 하면 → 거부 ``` --- ## 비회원(Guest) 권한 ### Guest 권한 체크 흐름 ```text 비회원 요청 → PermissionMiddleware → Guest 역할 조회 (Role::where('identifier', 'guest')) → 정적 캐시: self::$guestRoleCache (요청 내 재사용) → 권한 매핑 없음 → 기본 허용 → 권한 매핑 있음 → guest 역할이 해당 권한 보유하는지 확인 ``` ### 핵심 규칙 | 상황 | 동작 | | ------ | ------ | | 권한 미매핑 (permission_hooks 없음) | 모든 사용자(guest 포함) 허용 | | 권한 매핑 + guest 역할 보유 | 허용 | | 권한 매핑 + guest 역할 미보유 | 거부 (401/403) | --- ## Role identifier 자동 생성 관리자가 역할 생성 시 `identifier`를 직접 입력하지 않습니다. `RoleService`가 이름에서 자동 생성합니다: ```text generateIdentifier(name) ↓ 배열인 경우 → 영어 이름 우선 (en → ko → 첫 번째 값) ↓ Str::slug($baseName, '_') → 슬러그 변환 (예: "Content Manager" → "content_manager") ↓ 중복 체크 → 중복 시 _1, _2, ... suffix 자동 추가 ↓ 반환: 고유한 identifier ``` **예시**: | 입력 이름 | 생성 identifier | | ---------- | ---------- | | `Content Manager` | `content_manager` | | `Content Manager` (중복) | `content_manager_1` | | `['ko' => '편집자', 'en' => 'Editor']` | `editor` | --- ## 완전 동기화 (Stale Cleanup) 코어/확장 업그레이드 시 권한·역할 동기화는 단순 upsert 가 아니라 **완전 동기화** 를 수행합니다. config/manifest 에서 제거된 권한·역할은 DB 에서도 삭제됩니다. ### 적용 지점 | 경로 | 호출 시점 | cleanup 대상 | |---|---|---| | `CoreUpdateService::syncCoreRolesAndPermissions` 말미 | 코어 업데이트 | `core/core` 소유 권한·역할 | | `ModuleManager::updateModule` 내 sync 블록 말미 | 모듈 업데이트 | 해당 모듈 소유 권한·역할 | | `PluginManager::updatePlugin` 내 sync 블록 말미 | 플러그인 업데이트 | 해당 플러그인 소유 권한·역할 | ### 핵심 정책 - **권한 stale 삭제**: 순수 diff — config 에 없으면 즉시 삭제 (user_overrides 개념 없음) - **역할 stale 삭제**: config 에 없으면 삭제. 단 **`user_roles` 피벗에 사용자가 참조 중인 역할은 삭제 차단 + 경고 로그** (수동 재배정 유도) - **역할-권한 매핑**: `syncAllRoleAssignments` 가 `core/core` 전체 권한을 diff 기준으로 사용하여 이관된 구 식별자도 detach ### Helper (권장) 직접 Model 조작 금지. `ExtensionRoleSyncHelper` 를 통해 sync/cleanup 수행. 자세한 사용법은 [data-sync-helpers.md](../backend/data-sync-helpers.md#4-extensionrolesynchelper) 참조. --- ## 관련 문서 - [menus.md](./menus.md) - 메뉴 권한 시스템 - [hooks.md](./hooks.md) - 훅 권한 시스템 - [module-basics.md](./module-basics.md) - 모듈 기본 구조 - [index.md](./index.md) - 확장 시스템 인덱스 - [../backend/core-config.md](../backend/core-config.md#완전-동기화-원칙) - 완전 동기화 원칙 - [../backend/data-sync-helpers.md](../backend/data-sync-helpers.md) - 데이터 동기화 Helper - [../backend/user-overrides.md](../backend/user-overrides.md) - 사용자 수정 보존