GraphQL 구현 가이드
GitLab v19.2탈취된 개인 액세스 토큰(PAT)의 보안 영향을 줄이기 위해, 세분화된(granular 또는 fine-grained) PAT를 사용하면 사용자가 특정 조직 경계(그룹, 프로젝트, 사용자 또는 인스턴스 수준)로 제한된 세분화된 권한을 가진 토큰을 만들 수 있습니다.
탈취된 개인 액세스 토큰(PAT)의 보안 영향을 줄이기 위해, 세분화된(granular 또는 fine-grained) PAT를 사용하면 사용자가 특정 조직 경계(그룹, 프로젝트, 사용자 또는 인스턴스 수준)로 제한된 세분화된 권한을 가진 토큰을 만들 수 있습니다. 이를 통해 사용자는 토큰에 필요한 권한만 부여함으로써 최소 권한 원칙을 따를 수 있습니다.
세분화된 PAT는 경계(boundary)와 특정 리소스 권한으로 구성된 세분화된 스코프를 통해 세밀한 접근 제어를 제공합니다. 세분화된 PAT로 GraphQL 요청을 인증할 때, GitLab은 토큰의 권한에 지정된 경계 수준에서 요청된 리소스에 대한 접근 권한이 포함되어 있는지 검증합니다.
이 문서는 GraphQL 쿼리와 뮤테이션을 세분화된 PAT 권한 부여에 맞추고자 하는 커뮤니티 기여자 및 GitLab 개발자를 위해 작성되었습니다.
단계별 구현 가이드#
이 가이드는 GraphQL 타입과 뮤테이션에 세분화된 PAT 권한 부여를 추가하는 과정을 안내합니다. 시작하기 전에 권한 명명 규칙 문서를 검토하여 이 가이드 전반에 사용되는 용어를 이해하시기 바랍니다.
이 단계들은 GraphQL 타입과 뮤테이션만을 다룹니다. REST API 엔드포인트 보호에 대해서는 REST API 구현 가이드를 참고하십시오.
권한 부여 시스템이 내부적으로 어떻게 동작하는지에 대한 자세한 설명은 GraphQL 아키텍처 문서를 참고하십시오.
워크플로 개요#
구현은 다음 흐름을 따릅니다.
-
1-2단계: 계획 - 타입/뮤테이션을 식별하고 권한을 설계합니다.
-
3단계: 원시(raw) 권한을 생성합니다(YAML 파일).
-
4단계: 원시 권한을 할당 가능한(assignable) 권한으로 묶습니다(YAML 파일).
-
5단계: 타입/뮤테이션에 권한 부여 디렉티브를 추가합니다(Ruby 코드).
-
6단계: 권한 부여 테스트를 작성합니다(Ruby 스펙).
-
7단계: 로컬에서 테스트합니다(수동 검증).
-
8단계: 문서를 검증하고 재생성합니다(Rake 태스크).
1단계: 보호할 GraphQL 타입과 뮤테이션 식별#
목표: 작업 중인 리소스에 대한 모든 GraphQL 타입과 뮤테이션을 찾습니다.
-
app/graphql/types/에서 해당 리소스의 GraphQL 타입을 찾습니다.예: 이슈 리소스의 경우
app/graphql/types/issue_type.rb를 엽니다. -
app/graphql/mutations/에서 관련 뮤테이션을 찾습니다.예: 이슈의 경우
app/graphql/mutations/issues/를 확인합니다. -
권한 부여가 필요한 타입과 뮤테이션을 식별합니다.
-
사용자가 접근하는 리소스를 나타내는 오브젝트 타입(예:
IssueType,ProjectType) -
리소스를 생성, 업데이트 또는 삭제하는 뮤테이션(예:
Mutations::Issues::Create) -
리소스를 직접 반환하는 쿼리 필드(예:
QueryType의field :project)
-
-
이미
authorize_granular_token디렉티브가 있는 타입이나 뮤테이션이 있는지 확인합니다. 디렉티브가 없는 타입/뮤테이션에는 디렉티브를 추가해야 합니다.
2단계: 필요한 권한 결정#
목표: GitLab 명명 규칙에 따라 세분화된 권한을 정의합니다.
명명 규칙에 대해서는 규칙 문서의 권한 명명을 참고하십시오.
타입과 뮤테이션의 권한 이름 결정#
세분화된 PAT 권한 부여를 구현할 때, 권한 이름은 GraphQL 스키마 구조가 아니라 타입이 나타내는 것 또는 뮤테이션이 수행하는 것을 기준으로 지정합니다.
예시:
-
타입
IssueType→ 이슈 읽기를 나타냄 → 권한 이름은read_issue -
뮤테이션
Mutations::Issues::Create→ 이슈를 생성함 → 권한 이름은create_issue -
타입
ProjectType→ 프로젝트 데이터 읽기를 나타냄 → 권한 이름은read_project
일반적인 패턴#
-
오브젝트 타입: 타입의 모든 필드를 포괄하는
read_resource권한을 사용합니다.-
IssueType→read_issue -
ProjectType→read_project
-
-
생성 뮤테이션:
create_resource를 사용합니다.Mutations::Issues::Create→create_issue
-
업데이트 뮤테이션:
update_resource를 사용합니다.Mutations::Issues::Update→update_issue
-
삭제 뮤테이션:
delete_resource를 사용합니다.Mutations::Issues::Destroy→delete_issue
-
특수 작업 뮤테이션: 고유한 작업에 대해 특정 권한을 만듭니다.
- 이동, 보관, 이전 등은 각각 고유한 권한을 갖습니다.
3단계: 권한 정의 파일 생성#
목표: 아직 존재하지 않는 각 권한에 대해 YAML 정의 파일을 만듭니다.
권한 정의 파일 섹션의 지침에 따라 bin/permission 명령을 사용하여 원시 권한 YAML 파일을 만듭니다. 이 단계는 REST API 구현과 GraphQL 구현에서 동일합니다.
4단계: 할당 가능한 권한 생성 또는 업데이트#
목표: 더 단순한 사용자 경험을 위해 원시 권한을 할당 가능한 권한으로 묶습니다.
가능한 경우, 새 할당 가능한 권한을 만들기보다는 기존 할당 가능한 권한에 원시 권한을 추가하는 것을 선호하십시오. 할당 가능한 권한은 사용자에게 노출됩니다. 새로 만들어진 각 항목은 토큰 생성 UI에 표시되며, 그 이름은 해당 권한을 선택하는 모든 토큰에 대해 데이터베이스에 저장됩니다. 나중에 할당 가능한 권한을 제거하거나 이름을 변경하는 것은 호환성을 깨는 변경(breaking change)인 반면, 기존 할당 가능한 권한 내부의 원시 권한은 자유롭게 이름을 변경하거나 이동할 수 있습니다. 원시 권한이 해당 리소스의 기존 할당 가능한 권한과 별도로 사용자가 부여할 수 있어야 하는 기능을 나타낼 때만 새 할당 가능한 권한을 만드십시오. 각 종류의 변경이 미치는 영향에 대해서는 할당 가능한 권한 유지 관리를 참고하십시오.
할당 가능한 권한 문서의 지침에 따라 할당 가능한 권한 YAML 파일을 만들거나 업데이트하십시오. 이 단계는 REST API 구현과 GraphQL 구현에서 동일합니다.
5단계: 타입과 뮤테이션에 권한 부여 디렉티브 추가#
목표: GraphQL 타입과 뮤테이션에 세분화된 PAT 권한 부여 디렉티브를 추가합니다.
authorize_granular_token 메서드를 사용하여 타입과 뮤테이션에 권한을 선언합니다. 이 메서드는 모든 GraphQL 타입(Types::BaseObject를 통해), 뮤테이션(Mutations::BaseMutation을 통해), 리졸버(Resolvers::BaseResolver를 통해)에서 사용할 수 있습니다.
메서드 시그니처:
authorize_granular_token(permissions:, boundary_type: nil, boundary: nil, boundary_argument: nil, boundaries: nil, skip_reason: nil)
매개변수:
| 매개변수 | 설명 |
|---|---|
| permissions | (필수) 필요한 권한을 나타내는 심볼(예: :read_issue). 권한의 배열일 수도 있습니다. Authz::PermissionGroups::Assignable.all_permissions의 유효한 권한이어야 합니다. gitlab:permissions:validate Rake 태스크가 이를 검증합니다. |
| boundary_type | 권한 부여 경계의 유형을 선언하는 심볼(:project, :group, :user, :instance). boundaries:를 사용하지 않을 때 필수입니다. gitlab:permissions:validate Rake 태스크가 할당 가능한 권한 경계와 대조하여 검증합니다. |
| boundary | 경계를 추출하기 위해 해결된 오브젝트에서 호출할 메서드를 나타내는 심볼(예: :project). 해결된 오브젝트가 Project 또는 Group 자체일 때는 :itself를 사용합니다(예: ProjectType 및 GroupType). 독립형 리소스에는 :user 또는 :instance를 사용합니다. |
| boundary_argument | 경계 경로를 담고 있는 인자 이름을 나타내는 심볼(예: :project_path). |
| boundaries | 여러 경계 유형을 지원하는 리소스를 위한 경계 해시의 배열. 각 해시에는 boundary_type 키가 필요하며 boundary 또는 boundary_argument를 포함할 수 있습니다. 자세한 내용은 여러 경계(Multiple boundaries)를 참고하십시오. |
| traversal | 진입점(entry-point) 필드를 위해 필드별 디렉티브에서 true로 설정합니다(granular_scope_directive를 통해 전달). 타입 수준의 authorize_granular_token에 전달하면 ArgumentError가 발생합니다. 자세한 내용은 진입점 필드(Entry-point fields)를 참고하십시오. 현재는 강제되지 않습니다. |
| skip_reason | 타입이 세분화된 토큰 권한 부여를 의도적으로 선택 해제함을 선언하는 심볼. permissions: 및 boundary와 함께 사용하지 말고 그 대신 사용합니다. 자세한 내용은 skip_reason으로 권한 부여 건너뛰기(Skip authorization with skip_reason)를 참고하십시오. |
오브젝트 타입의 경우:
class IssueType < BaseObject
authorize_granular_token permissions: :read_issue, boundary: :project, boundary_type: :project
end
뮤테이션의 경우:
module Mutations
module Issues
class Create < BaseMutation
authorize_granular_token permissions: :create_issue, boundary_argument: :project_path, boundary_type: :project
end
end
end
인자가 그 자체로 Project나 Group이 아닌 레코드로 해결될 때는 boundary_argument와 boundary를 함께 사용합니다. 인자가 레코드를 찾아내고, boundary가 그로부터 Project나 Group에 도달합니다.
module Mutations
module Notes
module Create
class Base < Mutations::Notes::Base
authorize_granular_token permissions: :create_note,
boundaries: [
{ boundary_argument: :noteable_id, boundary: :resource_parent, boundary_type: :project },
{ boundary_argument: :noteable_id, boundary: :resource_parent, boundary_type: :group }
]
end
end
end
end
인자 noteable_id: "gid://gitlab/Issue/1"의 경우, 추출기는 이슈를 찾은 다음 issue.resource_parent를 호출하여 경계에 도달합니다.
루트 쿼리 필드의 경우:
루트 쿼리 필드에는 부모 오브젝트가 없으므로, 경계는 boundary_argument를 통해 필드 인자에서 읽어옵니다. 디렉티브를 필드의 리졸버에 선언합니다. 리졸버에 선언된 디렉티브는 그 리졸버를 마운트하는 필드에 적용됩니다.
module Resolvers
module Ai
class ToolRulesResolver < BaseResolver
authorize_granular_token permissions: :read_ai_tool_rule, boundary_argument: :full_path, boundary_type: :group
argument :full_path, GraphQL::Types::ID, required: true
end
end
end
토큰에 권한이 없을 때 필드는 null로 해결되며, 이는 타입 수준 권한 부여가 오브젝트를 검열(redact)하는 방식과 일치합니다. 전체 구현과 해당 권한 부여 테스트에 대해서는 ee/app/graphql/resolvers/ai/tool_rules_resolver.rb와 ee/spec/requests/api/graphql/ai/tool_rules_spec.rb를 참고하십시오.
boundary가 적용되는 경우#
-
해결된 오브젝트의 필드(예:
IssueType가 디렉티브를 선언할 때의issue.title). -
해결된 오브젝트가 경계 자체인 타입(
boundary: :itself사용). -
boundary_type: :user또는boundary_type: :instance를 사용하는 독립형 리소스.
오브젝트가 아직 해결되지 않았을 때는 대신 boundary_argument를 사용합니다.
boundary_argument가 적용되는 경우#
-
루트 뮤테이션
-
경로 또는 GlobalID 인자를 받는 루트 쿼리 필드
-
경계를 인자로 받는 모든 필드
독립형 경계#
특정 프로젝트나 그룹에 속하지 않는 리소스에는 boundary_type: :user 또는 boundary_type: :instance를 사용합니다. 이러한 경계 유형의 경우 boundary_type만으로 경계가 결정됩니다. 관례상 boundary:를 동일한 값으로 설정합니다.
module Mutations
module Todos
class SnoozeMany < BaseMany
authorize_granular_token permissions: :update_todo, boundary: :user, boundary_type: :user
end
end
end
여러 경계#
서로 다른 경계 유형에 속할 수 있는 리소스는 각 경계를 boundaries:로 선언합니다. Ci::RunnerType은 러너가 프로젝트에 속하거나, 그룹에 속하거나, 인스턴스 전체에 걸칠 수 있기 때문에 이렇게 합니다.
class RunnerType < BaseObject
authorize_granular_token(
permissions: :read_runner,
boundaries: [
{ boundary: :owner, boundary_type: :project },
{ boundary: :owner, boundary_type: :group },
{ boundary: :instance, boundary_type: :instance }
]
)
end
하나가 해결되면 구체적인 경계(프로젝트 또는 그룹)가 선호됩니다. 해결된 오브젝트가 선언된 boundary_type과 일치하지 않는 디렉티브는 건너뜁니다. 인스턴스 러너의 경우 runner.owner가 User를 반환하므로 프로젝트 디렉티브도 그룹 디렉티브도 일치하지 않으며, 독립형 instance 경계가 적용됩니다. 자세한 내용은 여러 경계를 참고하십시오.
진입점 필드#
traversal은 디렉티브에 선언되어 있지만 현재 GranularScopeAuthorization에 의해 강제되지 않습니다. traversal: true로 표시된 필드는 다른 필드와 마찬가지로 나열된 권한을 강제합니다. 강제 적용은 재구현을 앞두고 있습니다. Query.group과 Query.project는 현재 디렉티브를 선언하지 않으며, 세분화된 토큰 디렉티브가 없는 필드는 세분화된 검사를 스스로 수행하지 않습니다.
Query.group(fullPath:) 및 Query.project(fullPath:)처럼 경로 인자로부터 경계를 해결하는 최상위 필드는 그 자체로는 데이터를 노출하지 않습니다. 다운스트림 필드가 실제 권한을 강제합니다. 디렉티브에 traversal: true를 사용하여 진입점이 나열된 권한이 아니라 토큰이 해당 경계로 스코프되어 있기만 하면 되도록 합니다.
field :group, Types::GroupType,
null: true,
resolver: Resolvers::GroupResolver,
description: "Find a group.",
directives: granular_scope_directive(
permissions: :read_group, boundary_argument: :full_path, boundary_type: :group,
traversal: true
)
이러한 디렉티브에 traversal: true가 없으면, 자식 리소스로 스코프된 토큰(예: read_member)은 동등한 REST 엔드포인트에서는 허용되더라도 GraphQL에서는 부모에 도달할 수 없습니다. traversal: true를 사용하면 토큰이 부모에 도달하고, 사용자가 쿼리하는 다운스트림 필드만 특정 권한을 강제합니다.
permissions: 인자는 필드 자체가 이를 강제하지 않더라도 진입점이 동작하는 경계를 문서화하기 때문에 여전히 필수입니다.
traversal: true는 project 및 group 경계 유형에만 적용됩니다. 그 밖의 모든 경계 유형의 경우 나열된 권한이 정상적으로 강제됩니다.
권한 부여된 타입 간의 순회(traversal)#
권한 부여된 타입의 필드가 마찬가지로 authorize_granular_token을 선언하는 다른 타입을 반환할 때는 두 디렉티브가 모두 강제됩니다. GranularScopeAuthorization은 각 타입과 필드의 고유한 디렉티브를 독립적으로 평가하므로, 토큰이 자식 타입의 권한뿐만 아니라 소유자 타입의 권한도 필요하다고 가정하여 권한을 계획하십시오.
예를 들어 GroupType.groupMembers는 GroupMemberType을 반환하며, 두 타입 모두 세분화된 토큰 디렉티브를 선언합니다. 토큰은 그룹의 필드를 해결하기 위해 read_group이 필요하고, 그 멤버의 필드를 해결하기 위해 read_member가 필요합니다.
query {
group(fullPath: "gitlab-org") {
groupMembers {
nodes { id }
}
}
}
read_member만 가진 토큰이 그룹을 통해 멤버에 도달할 수 있도록 순회 필드에서 소유자 타입의 디렉티브를 자동으로 건너뛰는 기능이 이전에 구현되었으나 현재 재구현을 앞두고 있습니다. 이에 의존하지 마십시오.
skip_reason으로 권한 부여 건너뛰기#
일부 오브젝트 타입은 의도적으로 자체 권한을 선언하지 않습니다. 이러한 타입의 경우 skip_reason:으로 건너뛰기를 선언하여 권한 부여를 생략한 이유를 기록합니다. 이 값은 이유의 이름을 지정하며, 결정을 문서화하고 검증기가 의도적인 건너뛰기와 권한 부여가 누락된 타입을 구분할 수 있게 합니다.
class VulnerabilityIdentifierType < BaseObject
authorize_granular_token skip_reason: :parent_authorizes
end
유효한 이유와 그 의미는 lib/tasks/gitlab/permissions/graphql/skip_reasons.rb에 정의되어 있습니다.
gitlab:permissions:validate Rake 태스크는 모든 오브젝트 타입이 디렉티브 또는 건너뛰기 중 하나를 선언하도록 요구합니다. 이 요구사항 이전에 존재하던 타입은 config/authz/graphql/authorization_todo.txt에 나열되어 있습니다. 해당 파일에 새 항목을 추가하지 마십시오. skip_reason:은 permissions: 또는 경계 인자와 함께 사용할 수 없습니다.
6단계: 권한 부여 테스트 추가#
목표: 세분화된 PAT 권한이 GraphQL 타입과 뮤테이션에서 올바르게 강제되는지 검증합니다.
쿼리의 경우#
'authorizing granular token permissions for GraphQL' 공유 예제(shared example)를 추가합니다.
it_behaves_like 'authorizing granular token permissions for GraphQL', :<permission_name> do
let(:user) { current_user }
let(:boundary_object) { <boundary_object> }
let(:request) { post_graphql(query, token: { personal_access_token: pat }) }
end
예시:
it_behaves_like 'authorizing granular token permissions for GraphQL', :read_issue do
let(:user) { current_user }
let(:boundary_object) { project }
let(:request) { post_graphql(query, token: { personal_access_token: pat }) }
end
뮤테이션의 경우#
it_behaves_like 'authorizing granular token permissions for GraphQL', :<permission_name> do
let(:user) { current_user }
let(:boundary_object) { <boundary_object> }
let(:request) { post_graphql_mutation(mutation, token: { personal_access_token: pat }) }
end
권한 부여를 건너뛰는 타입의 경우#
타입이 skip_reason: :parent_authorizes를 선언할 때는, 부모 타입이 권한 부여되었을 때 해당 데이터가 반환되고 그렇지 않을 때 보류되는지 검증합니다. skipped_data_path를 건너뛴 타입 데이터의 GraphQL 응답 경로로 설정합니다.
it_behaves_like 'authorizing granular token permissions for GraphQL with a skipped child type', :read_vulnerability do
let(:user) { current_user }
let(:boundary_object) { project }
let(:request) { post_graphql(query, current_user: user, token: { personal_access_token: pat }) }
let(:skipped_data_path) { %i[vulnerability identifiers] }
end
경계 오브젝트 매핑#
boundary_object는 boundary_type과 일치해야 합니다.
| 경계 유형 | 경계 오브젝트 |
|---|---|
| :project | project |
| :group | group |
| :user | :user |
| :instance | :instance |
중요: 경계 오브젝트가 :project 또는 :group일 때, 권한이 부여되려면 user가 해당 네임스페이스(프로젝트 또는 그룹)의 멤버여야 합니다.
이 테스트가 검증하는 것:
-
레거시(비 세분화) 개인 액세스 토큰이 계속 접근 권한을 부여합니다.
-
경계의 최상위 그룹이 세분화된 토큰을 강제할 때 레거시 토큰은 접근이 거부됩니다.
-
세분화된 PAT에 필요한 권한이 부여된 사용자는 접근이 허용됩니다.
-
필요한 권한이 없는 사용자는 접근이 거부됩니다. 권한이 없는 쿼리는
200응답과 함께null데이터를 반환하는 반면, 권한이 없는 뮤테이션은 최상위 GraphQL 오류를 반환합니다. -
권한 부여 시스템이 세분화된 스코프를 타입/뮤테이션의 권한 요구사항과 대조하여 올바르게 평가합니다.
-
기능 플래그
granular_personal_access_tokens가 올바르게 강제됩니다(비활성화되면 접근을 거부).
7단계: 수동 검증#
목표: 병합 요청을 생성하기 전에 로컬 환경에서 구현을 수동으로 테스트하여 권한이 예상대로 동작하는지 검증합니다.
설정:
Rails 콘솔에서 사용자를 위한 세분화된 PAT를 생성합니다.
# Enable feature flag
Feature.enable(:granular_personal_access_tokens)
user = User.human.first
# Create granular token
token = PersonalAccessTokens::CreateService.new(
current_user: user,
target_user: user,
organization_id: user.organization_id,
params: { expires_at: 1.month.from_now, scopes: ['granular'], granular: true, name: 'gPAT' }
).execute[:personal_access_token]
# Get the appropriate boundary object (project, group, :user, or :instance)
project = user.projects.first
boundary = Authz::Boundary.for(project)
# Create a scope with the assignable permissions being tested.
# Scopes store assignable permission names, which expand to raw permissions at request time.
# The example query below resolves fields on both ProjectType (raw permission read_project,
# part of the read_project assignable permission) and IssueType (raw permission read_issue,
# part of the read_work_item assignable permission), so the scope needs both.
scope = Authz::GranularScope.new(namespace: boundary.namespace, access: boundary.access, permissions: [:read_project, :read_work_item])
# Add the scope to the token
Authz::GranularScopeService.new(token).add_granular_scopes(scope)
# Copy a curl command for testing a GraphQL query
query = '{ project(fullPath: \"' + project.full_path + '\") { issues { nodes { title } } } }'
IO.popen('pbcopy', 'w') { |f| f.puts "curl \"http://#{Gitlab.host_with_port}/api/graphql\" --request POST --header \"PRIVATE-TOKEN: #{token.token}\" --header \"Content-Type: application/json\" --data '{\"query\": \"#{query}\"}'" }
- 이 명령을 다른 터미널에 붙여넣습니다. 성공해야 합니다.
8단계: 문서 검증 및 재생성#
목표: 모든 권한 정의와 디렉티브가 일관성이 있는지 확인하고, 생성된 참조 문서를 업데이트합니다.
-
세분화된 토큰 참조 문서를 재생성합니다.
bundle exec rake gitlab:permissions:graphql:compile_docs이 명령은
doc/auth/tokens/fine_grained_access_tokens_graphql.md를 업데이트합니다. 이 파일을 직접 편집하지 마십시오. -
권한 검증을 실행합니다.
bundle exec rake gitlab:permissions:validate이 태스크는 Lefthook pre-push 훅으로도 실행됩니다. 다른 검사들 중에서도 다음의 경우에 실패합니다.
-
오브젝트 타입이나 뮤테이션에 세분화된 토큰 디렉티브도
skip_reason:도 없고,config/authz/graphql/authorization_todo.txt에 예외로 등록되어 있지도 않은 경우. -
디렉티브의 권한이 어떤 할당 가능한 권한에도 포함되지 않은 경우.
-
디렉티브의
boundary_type이 할당 가능한 권한의boundaries와 일치하지 않는 경우. -
skip_reason:이lib/tasks/gitlab/permissions/graphql/skip_reasons.rb에 정의되어 있지 않은 경우. -
디렉티브가
skip_reason:과permissions:를 모두 선언하는 경우. -
디렉티브의 권한에 권한 부여 테스트가 없는 경우. 해당 권한을 선언하는 각 타입, 뮤테이션 또는 필드는 경계 유형별로 자체 테스트가 필요합니다. 이는 예외 없이 엄격하게 강제되므로, 선언과 동일한 병합 요청에서 테스트를 추가하십시오.
-
생성된 참조 문서가 최신 상태가 아닌 경우.
-
참고 항목#
-
GraphQL 아키텍처 문서: 권한 부여 시스템이 내부적으로 어떻게 동작하는지에 대한 자세한 설명
-
할당 가능한 권한: 할당 가능한 권한 파일을 만드는 방법
-
권한 명명 규칙: 권한 명명 지침
-
REST API 구현 가이드: REST API 엔드포인트에 세분화된 PAT 권한 부여 추가