GraphQL 세분화 토큰 인가 아키텍처
GitLab v19.1이 문서는 GranularTokenAuthorization 필드 확장이 GraphQL 쿼리 및 뮤테이션에서 세분화된 개인 액세스 토큰(PAT) 권한을 적용하는 방식을 설명합니다. 세분화 토큰 인가 시스템은 타입, 필드, 뮤테이션에 적용된 지시어(directive)를 기반으로 GraphQL 필드에 세밀한 권한 검사를 추가합니다.
이 문서는 GranularTokenAuthorization 필드 확장이 GraphQL 쿼리 및 뮤테이션에서 세분화된 개인 액세스 토큰(PAT) 권한을 적용하는 방식을 설명합니다. 단계별 구현 가이드는 GraphQL 구현 가이드를 참조하세요.
개요#
세분화 토큰 인가 시스템은 타입, 필드, 뮤테이션에 적용된 지시어(directive)를 기반으로 GraphQL 필드에 세밀한 권한 검사를 추가합니다. 이 시스템은 세분화된 PAT가 특정 프로젝트 또는 그룹 경계 내에서 명시적으로 권한이 부여된 리소스에만 접근할 수 있도록 보장합니다.
기능 플래그: 이 기능은 토큰의 사용자에 대해 granular_personal_access_tokens 기능 플래그가 활성화되어 있어야 합니다. 플래그가 비활성화된 경우 세분화된 PAT는 GraphQL 요청에 대해 작동하지 않습니다.
아키텍처 구성 요소#
1. 필드 확장(Field Extension)#
-
위치:
lib/gitlab/graphql/authz/granular_token_authorization.rb -
목적: 필드 해석(resolution)을 가로채어 인가 검사를 수행합니다.
-
적용 대상:
Types::BaseField를 통해 모든 GraphQL 필드에 적용됩니다.
2. 지시어(Directive)#
-
위치:
app/graphql/directives/authz/granular_scope.rb -
목적: 필요한 권한 및 경계 추출 전략을 선언합니다.
-
인수(Arguments):
permissions: 필요한 권한 문자열의 배열입니다 (예: ['read_issue']).
-
boundary: 해석된 객체에서 경계를 추출하는 메서드 이름입니다. -
boundary_argument: 경계를 포함하는 인수 이름입니다. -
boundary_type: 인가 경계의 타입입니다 (project,group,user,instance). 권한 경계의 유효성 검사 및 문서화에 사용됩니다. -
traversal:true인 경우, 지시어는 토큰이 경계로 스코프가 지정되어 있는지만 확인합니다(read_boundary). 나열된 권한은 필드 자체에서 적용되지 않습니다. 다운스트림 필드가 실제 권한을 적용하는Query.group(fullPath:)와 같은 진입점 필드에 사용합니다.
3. 지시어 파인더(Directive Finder)#
-
위치:
lib/gitlab/graphql/authz/directive_finder.rb -
목적: 우선순위 순서(field, implementing type, return type, owner type)로 적용 가능한 지시어를 찾습니다.
-
포함: GraphQL 타입 래퍼를 언래핑하기 위한
TypeUnwrapper모듈
4. 경계 추출기(Boundary Extractor)#
-
위치:
lib/gitlab/graphql/authz/boundary_extractor.rb -
목적: 다양한 소스에서 인가 경계를 추출합니다.
5. 타입 언래퍼(Type Unwrapper)#
-
위치:
lib/gitlab/graphql/authz/type_unwrapper.rb -
목적: GraphQL 타입 래퍼(List, NonNull, Connection)를 언래핑하기 위한 공유 모듈입니다.
-
사용처: DirectiveFinder 및 SkipRules
6. 헬퍼 모듈(Helper Module)#
-
위치:
lib/gitlab/graphql/authz/authorize_granular_token.rb -
목적: 더 깔끔한 지시어 구문을 위한
authorize_granular_token헬퍼 메서드를 제공합니다. -
포함 위치:
Types::BaseObject,Types::BaseField,Mutations::BaseMutation -
메서드:
authorize_granular_token(permissions:, boundary_type:, boundary: nil, boundary_argument: nil) -
유효성 검사: 권한은
Authz::PermissionGroups::Assignable.all_permissions에 대해gitlab:permissions:validateRake 태스크로 검증됩니다.
요청 흐름 타임라인#
Phase 1: 요청 시작#
1. GraphQL 요청 도착 (쿼리 또는 뮤테이션)
2. GraphQL Ruby가 파싱 및 유효성 검사 시작
3. 루트 필드로 실행 시작
Phase 2: 필드 해석(필드별)#
각 필드가 해석될 때:
1. GraphQL Ruby가 순서대로 필드 확장을 호출합니다
├─ CallsGitaly::FieldExtension (개발/테스트 전용)
├─ Present::FieldExtension
├─ Authorize::FieldExtension
└─ GranularTokenAuthorization ← 여기에 있습니다
Phase 3: 인가 검사#
Step 1: 조기 종료 조건
def authorize_field(object, arguments, context)
return unless authorization_enabled?(context) # 세분화된 PAT만 인가
return if SkipRules.new(@field).should_skip? # 특정 필드 건너뜀
# ...
end
def authorization_enabled?(context)
token = context[:access_token]
token && token.try(:granular?)
end
-
세분화된 PAT를 사용하지 않는 경우 세분화 스코프 인가가 건너뜁니다 (레거시 PAT는 기존 스코프 인가를 사용합니다).
-
특정 필드는 자동으로 건너뜁니다:
뮤테이션 응답 필드 (예: createIssue.issue). 인가는 응답 래퍼가 아닌 뮤테이션 자체에서 수행됩니다.
-
권한 메타데이터 필드 (예:
issue.userPermissions). 이 필드들은 실제 데이터가 아닌 권한 정보를 반환합니다. -
엣지 래퍼 필드 (예:
groupMembers.nodes,groupMembers.cursor). 이 필드들은 자체 필드에서 인가를 적용하는 기본 노드 타입으로 순회합니다. -
인가된 반환 타입으로의 순회. 필드에 자체 지시어가 없고, 소유자 타입이 지시어를 가지며, 언래핑된 반환 타입이 자체 지시어를 가지고 더 깊은 인가된 하위 필드를 가지는 경우, 소유자 수준의 지시어는 건너뜁니다. 반환 타입의 지시어는 자식 객체의 필드가 해석될 때 적용됩니다. 리프(Leaf) 반환 타입(모든 스칼라 필드)은 빈 결과가 검사를 완전히 우회할 수 있으므로 건너뛰지 않습니다. 자세한 내용은 인가된 반환 타입으로의 순회를 참조하세요.
Step 2: 지시어 탐색
directive = DirectiveFinder.new(@field).find(object)
DirectiveFinder는 다음 우선순위 순서로 지시어를 검사하며 첫 번째 일치 항목을 반환합니다:
-
필드 수준 지시어 (
FIELD_DEFINITION): 필드에 직접 적용됩니다. -
구현 타입 지시어 (인터페이스의 경우): 인터페이스를 구현하는 구체 타입에 적용됩니다.
필드 소유자가 인터페이스이고 object가 제공된 경우에만 검사됩니다.
-
GitlabSchema.types에서 실제 모델 타입(예:Issue)을 해석합니다. -
반환 타입 지시어: 필드가 반환하는 타입에 적용됩니다.
GraphQL 타입 래퍼를 언래핑하여 기본 타입을 찾습니다:
List 타입: [Type] → Type
-
NonNull 타입:
Type!→Type -
Connection 타입:
TypeConnection→Type(예:IssueConnection→IssueType) -
boundary_argument및boundary전략 모두에서 동작합니다. -
:id인수와 함께boundary를 사용할 경우 경계 추출을 위한 ID 폴백을 활성화합니다. -
소유자 타입 지시어 (
OBJECT): 필드를 소유하는 타입에 적용됩니다.
리프 인가 타입으로 이어지는 필드(예: project.languages → RepositoryLanguageType)가
포함 타입의 지시어(예: read_project) 대신 해당 타입의 지시어를 사용하도록
마지막에 검사합니다. 이를 통해 결과가 비어 있을 때도 올바른 권한이 적용됩니다.
Step 3: 경계 추출
boundary = BoundaryExtractor.new(object:, arguments:, context:, directive:).extract
permissions = directive.arguments[:permissions]
지시어를 찾을 수 없는 경우, boundary와 permissions는 모두 nil입니다. 인가 서비스는 "Unable to determine boundaries and permissions for authorization"이라는 오류 메시지를 반환합니다.
경계 추출기의 동작:
-
독립형 리소스 (
boundary: 'user'또는boundary: 'instance'):Authz::Boundary::NilBoundary를 반환합니다. -
유효한 프로젝트/그룹 리소스: 래핑된 경계를 반환합니다 (
ProjectBoundary또는GroupBoundary). -
리소스를 찾을 수 없는 경우:
nil을 반환합니다 (NilBoundary로 래핑되지 않음).
지원되는 경계 타입:
-
Authz::Boundary::ProjectBoundary- Project 리소스용 -
Authz::Boundary::GroupBoundary- Group 리소스용 -
Authz::Boundary::NilBoundary- 독립형 리소스용 (사용자 스코프 또는 인스턴스 전체)
추출기는 네 가지 전략 중 하나를 사용합니다:
전략 A: boundary_argument (뮤테이션 및 쿼리 필드용)
# 지시어: boundary_argument: 'project_path'
# 필드 인수: project_path: "gitlab-org/gitlab"
extract_from_argument('project_path')
↓
args[:project_path] = "gitlab-org/gitlab"
↓
resolve_path("gitlab-org/gitlab")
↓
Project.find_by_full_path("gitlab-org/gitlab") || Group.find_by_full_path("gitlab-org/gitlab")
↓
returns Project or Group instance
전략 B: boundary (해석된 객체가 있는 타입 필드용)
경계 메서드는 유효한 접근자 메서드 중 하나여야 합니다: project, group, 또는 itself. 다른 값을 사용하면 ArgumentError가 발생합니다.
# 지시어: boundary: 'project'
# Object: Issue 인스턴스
extract_from_method('project')
↓
unwrap_object(object) # Issue
↓
object_matches_boundary_type?('project') # false (Issue ≠ Project)
↓
VALID_BOUNDARY_ACCESSOR_METHODS.include?('project') # true
↓
object.respond_to?(:project) # true
↓
object.project
↓
returns Project instance
boundary: 'itself'를 사용하면 객체 자신이 경계로 반환됩니다. 이는 자신이 Project 또는 Group인 타입에 유용합니다:
# 지시어: boundary: 'itself'
# Object: Project 인스턴스
extract_from_method('itself')
↓
unwrap_object(object) # Project
↓
object_matches_boundary_type?('itself') # false (Project ≠ Itself)
↓
VALID_BOUNDARY_ACCESSOR_METHODS.include?('itself') # true
↓
object.itself # Ruby의 Object#itself는 self를 반환
↓
returns Project instance
전략 C: ID 폴백 (GlobalID가 있는 쿼리 필드용)
다음 경우에 사용됩니다:
-
지시어가
boundary: 'project'를 지정한 경우 -
객체가 nil이거나 경계 메서드에 응답하지 않는 경우
-
필드에 GlobalID가 있는
:id인수가 있는 경우
# 쿼리: issue(id: "gid://gitlab/Issue/123")
# 지시어: boundary: 'project'
# Object: nil (쿼리 필드, 아직 해석되지 않음)
extract_from_id_argument
↓
args[:id] = "gid://gitlab/Issue/123"
↓
GlobalID.parse("gid://gitlab/Issue/123")
↓
GlobalID::Locator.locate(gid) # Issue.find(123) - 추가 DB 쿼리
↓
extract_boundary_from_object(issue)
↓
issue.project
↓
returns Project instance
성능 참고: 이 전략은 레코드를 두 번 가져옵니다 - 인가를 위해 한 번, 필드 해석 중에 한 번. 단, 쿼리는 캐시됩니다.
전략 D: 독립형 경계 (사용자 스코프 또는 인스턴스 전체 리소스용)
다음 경우에 사용됩니다:
-
지시어가
boundary: 'user'를 지정한 경우 (사용자 스코프 리소스) -
지시어가
boundary: 'instance'를 지정한 경우 (인스턴스 전체 리소스)
# 지시어: boundary: 'user'
# 리소스가 특정 프로젝트/그룹에 속하지 않음
standalone_boundary?('user')
↓
@boundary_accessor.to_sym # :user
↓
Authz::Boundary.for(:user)
↓
returns Authz::Boundary::NilBoundary.new(:user)
↓
Authorization checks token has appropriate permissions
이 전략은 특정 프로젝트 또는 그룹 경계에 속하지 않지만 사용자 스코프이거나 인스턴스 전체인 리소스에 사용됩니다.
Step 4: 인가 검사
authorize_with_cache!(context, boundary, permissions)
이 메서드는 다음을 수행합니다:
-
캐시 확인: 중복 검사를 방지하기 위해
context[:authz_cache]를 확인합니다. -
인가 서비스 호출:
::Authz::Tokens::AuthorizeGranularScopesService.new(
boundaries: boundary,
permissions: permissions,
token: context[:access_token]
).execute
-
검증: 토큰이 경계에 대해 필요한 권한을 가지고 있는지 확인합니다.
-
인가 실패 시 오류 발생:
raise_resource_not_available_error!(response.message). -
결과 캐싱: 중복 검사를 방지하기 위해 결과를 캐시합니다.
매칭된 지시어에 traversal: true가 있는 경우, 확장은 토큰에 경계가 표시되는지만
확인하는 별도의 인가 경로를 사용합니다. 자세한 내용은
traversal: true를 사용하는 진입점 필드를 참조하세요.
Step 5: 필드 해석
yield(object, arguments, **rest)
인가가 통과되면 필드 해석기가 실행되고 값을 반환합니다.
예시 시나리오#
시나리오 1: boundary_argument를 사용한 뮤테이션#
GraphQL 요청:
mutation {
createIssue(input: {
projectPath: "gitlab-org/gitlab",
title: "New issue"
}) {
issue { id }
}
}
지시어:
class Create < BaseMutation
authorize_granular_token permissions: :create_issue, boundary_argument: :project_path, boundary_type: :project
end
타임라인:
-
createIssue필드에 대해 확장이 호출됩니다. -
object=nil(루트 뮤테이션 필드) -
뮤테이션 클래스에서 지시어 발견
-
arguments[:input][:project_path]에서 경계 추출 -
Project.find_by_full_path("gitlab-org/gitlab")→ Project -
인가 서비스 검사: 토큰이 이 프로젝트에 대해
create_issue권한을 가지고 있는가? -
예인 경우: 뮤테이션 실행
-
아닌 경우: 오류 발생, 뮤테이션 실행되지 않음
시나리오 2: boundary를 사용한 타입 (중첩 필드)#
GraphQL 요청:
query {
project(fullPath: "gitlab-org/gitlab") {
issues {
nodes {
title # ← 여기서 인가
description # ← 그리고 여기서도
}
}
}
}
지시어:
class IssueType < BaseObject
authorize_granular_token permissions: :read_issue, boundary: :project, boundary_type: :project
end
타임라인 (title 필드의 경우):
-
title필드에 대해 확장이 호출됩니다. -
object= Issue 인스턴스 (이미 해석됨) -
IssueType에서 지시어 발견 (title필드의 소유자) -
issue.project를 호출하여 경계 추출 -
인가 서비스 검사: 토큰이 이 프로젝트에 대해
read_issue권한을 가지고 있는가? -
후속 필드(
description등)에서 캐시 히트 - 추가 DB 쿼리 없음 -
예인 경우: 필드가 해석되어 title 반환
-
아닌 경우: 오류 발생
시나리오 3: ID 폴백을 사용한 쿼리 필드#
GraphQL 요청:
query {
issue(id: "gid://gitlab/Issue/123") {
title
}
}
지시어:
class IssueType < BaseObject
authorize_granular_token permissions: :read_issue, boundary: :project, boundary_type: :project
end
타임라인:
-
issue필드에 대해 확장이 호출됩니다 (IssueType 반환). -
object=nil(루트 쿼리 필드) -
반환 타입(
IssueType)에서 지시어 발견 -
경계 추출 감지: object가 nil이지만
:id인수가 있음 -
ID 폴백 사용: GlobalID 추출 → Issue 찾기 →
issue.project가져오기 -
인가 서비스 검사: 토큰이 이 프로젝트에 대해
read_issue권한을 가지고 있는가? -
예인 경우: 필드 해석 (Issue가 해석기에 의해 다시 가져옴)
-
아닌 경우: 필드 해석 전에 오류 발생
시나리오 4: traversal: true를 사용한 진입점 필드#
GraphQL 요청:
query {
group(fullPath: "gitlab-org") {
groupMembers {
nodes {
id
}
}
}
}
Query.group에 대한 진입점 지시어:
field :group, Types::GroupType,
resolver: Resolvers::GroupResolver,
directives: granular_scope_directive(
permissions: :read_group, boundary_argument: :full_path, boundary_type: :group,
traversal: true
)
타임라인:
-
group필드에 대해 확장이 호출됩니다. -
traversal: true인 지시어가 필드에서 해석됩니다. -
arguments[:full_path]("gitlab-org")에서 경계 추출. -
인가 서비스가 순회 모드에서 실행되어
token.can?(:read_boundary, boundary)를 검증합니다.read_group권한은 적용되지 않습니다. -
groupMembers필드에 대해 확장이 호출됩니다. 소유자는GroupType이며 (read_group지시어를 가짐), 반환 타입은GroupMemberType입니다 (read_member지시어를 가짐). 순회 건너뜀이 적용되므로 토큰 검사가 실행되지 않습니다. -
nodes필드에 대해 확장이 호출됩니다. 엣지 래퍼로서 건너뜁니다. -
각
GroupMember의id필드에 대해 확장이 호출됩니다. 소유자는GroupMemberType이며read_member가 필요합니다. 그룹 경계에 대해read_member를 토큰으로 검사합니다.
토큰은 read_member만으로 멤버 데이터에 도달하며, 이는 REST 엔드포인트
GET /api/v4/groups/:id/members와 일치합니다.
인가된 반환 타입으로의 순회#
세분화 토큰 인가 타입의 필드는 그렇지 않으면 소유자 타입의 지시어를 상속합니다.
필드의 언래핑된 반환 타입도 세분화 토큰 지시어를 가질 때 소유자 지시어는 중복됩니다.
반환 타입의 지시어는 자식 객체의 필드가 해석될 때 인가를 적용합니다. SkipRules 클래스는
이 경우를 감지하고 소유자 수준 검사를 건너뜁니다.
다음 조건이 모두 충족될 때 건너뜀이 적용됩니다:
-
필드에 자체 세분화 토큰 지시어가 없습니다 (명시적인 필드 수준 지시어는 항상 우선합니다).
-
필드의 소유자 타입이 세분화 토큰 지시어를 가집니다.
-
필드의 언래핑된 반환 타입(list, non-null, connection 래퍼를 제거한 후)이 세분화 토큰 지시어를 가집니다.
-
반환 타입에 자체 언래핑된 반환 타입이 세분화 토큰 지시어를 가지는 필드가 하나 이상 있습니다 (즉, 모든 필드가 일반 스칼라를 반환하는 "리프" 타입이 아닌 경우).
네 번째 조건은 안전을 위해 필요합니다: 반환 타입이 리프인 경우(모든 필드가 스칼라를 반환하는 경우, 예: RepositoryLanguageType 또는 PushRulesType), 빈 컬렉션이나 nil 결과에 대해 항목별 해석기가 실행되지 않습니다. 컬렉션 수준 검사를 건너뛰면 빈 결과가 인가를 완전히 우회할 수 있습니다. 리프 타입의 경우 컬렉션 수준 검사가 유일한 적용 지점이므로 건너뜀이 실행되지 않아야 합니다.
효과: 자식 리소스의 권한만 가진 토큰은 부모의 읽기 권한 없이도 부모를 통해 자식으로 순회할 수 있습니다. 스칼라나 다른 인가되지 않은 타입을 반환하는 부모의 데이터 필드는 여전히 부모의 권한이 필요합니다.
예시: Group.groupMembers는 GroupMemberType을 반환합니다. GroupType과
GroupMemberType 모두 세분화 토큰 지시어를 선언합니다. group.groupMembers를 해석할 때
read_group이 더 이상 필요하지 않습니다. 각 GroupMember의 모든 필드를 해석할 때는
read_member가 필요합니다. group.name (스칼라)을 해석할 때는 여전히 read_group이 필요합니다.
traversal: true를 사용하는 진입점 필드#
Query.group(fullPath:) 및 Query.project(fullPath:)와 같은 최상위 필드는 경로 인수에서 경계를 해석하기 위해 존재합니다. 이 필드들은 자체적으로 데이터를 노출하지 않습니다. 다운스트림 필드가 실제 권한을 적용합니다. 이 의도를 선언하려면 지시어에 traversal: true를 설정하세요.
traversal: true인 경우:
-
경계가 평소와 같이
boundary_argument에서 해석됩니다. -
인가 서비스가 순회 모드에서 실행되어
token.can?(:read_boundary, boundary)만 검사합니다.permissions인수는 적용되지 않습니다. 문서화를 위해 지시어에 남아 있습니다. -
경계가 해석되지 않거나 토큰에게 경계가 표시되지 않으면, 서비스는
404 Not Found를 반환하고 필드는 오류와 함께null을 반환합니다.
순회 캐시 키는 [:traversal, boundary.class, boundary.namespace&.id]이며,
권한 기반 캐시 키와 별개입니다.
traversal: true는 project 및 group 경계 타입에만 적용됩니다. 다른 모든
경계 타입의 경우, 확장은 일반 권한 검사로 폴백합니다.
성능 최적화#
1. 캐싱#
요청별 캐시:
context[:authz_cache] = Set.new
cache_key = [permissions&.sort, boundary&.class, boundary&.namespace&.id]
# `read_issue`에 대한 캐시 키 예시 (프로젝트의 경우):
# [["read_issue"], Authz::Boundary::ProjectBoundary, 123]
-
인가 결과는 Set을 사용하여 요청별로 캐시됩니다.
-
동일한 경계 및 권한에 대한 중복 인가 검사를 방지합니다.
-
예시: 동일한 프로젝트에서 10개의 이슈 필드를 검사할 때 인가 서비스를 한 번만 호출합니다.
-
캐시 키 구성 요소:
permissions&.sort: 소문자 권한 문자열의 정렬된 배열
-
boundary&.class: 경계 래퍼 클래스 (예:Authz::Boundary::ProjectBoundary) -
boundary&.namespace&.id: 네임스페이스 ID (경계 타입에 따라 다름):
ProjectBoundary: project.project_namespace.id
-
GroupBoundary:group.id -
NilBoundary:nil
2. 조기 반환#
return unless authorization_enabled?(context)
return if SkipRules.new(@field).should_skip?
-
세분화되지 않은 토큰은 전체 시스템을 건너뜁니다 (오버헤드 없음)
-
뮤테이션 응답 필드와 권한 메타데이터 필드는 자동으로 건너뜁니다 (자세한 내용은 Phase 3, Step 1 참조)
오류 처리#
인가 실패#
인가가 실패하면:
raise_resource_not_available_error!(response.message)
GraphQL의 경우:
-
errors배열에 서비스 오류를 반환합니다. -
필드가
null을 반환합니다.
응답 예시:
{
"data": { "issue": null },
"errors": [{
"message": "Insufficient permissions",
"path": ["issue"]
}]
}
엣지 케이스 및 오류 시나리오#
설정 누락 오류#
- 지시어를 찾을 수 없는 경우 (세분화된 PAT 사용 시)
동작: boundary: nil, permissions: nil로 인가가 진행됩니다.
-
결과: 인가 서비스가 오류를 반환합니다.
-
오류 메시지:
"Unable to determine boundaries and permissions for authorization" -
참고: 세분화된 PAT로 접근하는 모든 필드에는 지시어가 있어야 합니다.
-
지시어에 빈 권한 배열이 있는 경우
동작: permissions: []로 인가가 진행됩니다 (경계는 제공됨).
-
결과: 인가 서비스가 오류를 반환합니다.
-
오류 메시지:
"Unable to determine permissions for authorization" -
원인:
permissions: []로 지시어가 정의됨
경계 해석 오류#
- 경계 추출이 nil을 반환하는 경우 (리소스를 찾을 수 없음)
동작: boundary: nil로 인가가 진행됩니다 (권한은 여전히 제공됨).
-
결과: 인가 서비스가 오류를 반환합니다.
-
오류 메시지:
"Unable to determine boundaries for authorization" -
원인:
리소스로 해석되지 않는 유효하지 않은 경로/GlobalID
-
예상 연관을 누락한 객체 (예:
issue.project가nil반환) -
지시어에
boundary와boundary_argument모두 설정되지 않은 경우 -
참고: 이는
NilBoundary객체를 반환하는 독립형 경계와 다릅니다. -
유효하지 않은 GlobalID 형식
동작: GlobalID.parse("invalid")가 nil을 반환합니다.
-
결과: 경계 추출이
nil을 반환 → 인가 오류 -
오류 메시지:
"Unable to determine boundaries for authorization" -
참고: 예외를 발생시키지 않고 정상적으로 실패합니다.
-
경계 메서드가 nil을 반환하는 경우
동작: issue.project가 nil을 반환합니다.
-
결과:
nil반환 → 인가 오류 -
오류 메시지:
"Unable to determine boundaries for authorization" -
일반적인 원인: 소프트 삭제된 연관, 고아 레코드
-
GlobalID가 존재하지 않는 레코드를 가리키는 경우
동작: GlobalID::Locator.locate(gid)가 ActiveRecord::RecordNotFound를 발생시키고 구조 처리되어 nil을 반환합니다.
-
결과: 경계 추출이
nil을 반환 → 인가 오류 -
오류 메시지:
"Unable to determine boundaries for authorization"
설정 오류#
- 유효하지 않은 경계 메서드
동작: ArgumentError: "Invalid boundary method: 'foo'"를 발생시킵니다.
-
원인: 유효한 접근자 메서드 (
project,group,itself)에 없는boundary값 사용 -
참고: 이 유효성 검사는 객체가 메서드에 응답하는지 확인하기 전에 실행됩니다.
-
객체가 경계 메서드에 응답하지 않는 경우
동작: ArgumentError: "Boundary method 'project' not found on Project"를 발생시킵니다.
-
원인: 유효한 경계 메서드(예:
boundary: 'project')를 사용하지만 객체에 해당 메서드가 없는 경우 -
예외:
필드에 :id 인수가 있으면 대신 ID 폴백 사용
-
객체 타입이 경계 이름과 일치하면 객체를 직접 반환
-
예시:
# IssueType에는: boundary: 'project'가 있음
# 필드: project.issue(iid: "1")
# object = Project (Issue 아님)
# Project가 'project'와 일치 → Project 반환
- 유효하지 않은 권한 이름
동작: gitlab:permissions:validate Rake 태스크로 감지됩니다.
-
원인:
Authz::PermissionGroups::Assignable.all_permissions에 존재하지 않는 권한 심볼 사용 -
참고: 이 유효성 검사는 CI의 일부로 실행되어 모든 지시어 권한이 유효한 할당 가능한 권한을 참조하는지 확인합니다.
-
여러 지시어가 발견된 경우
동작: 우선순위 순서(field, implementing type, return type, owner)에서 첫 번째 일치 항목을 사용합니다.
-
결과: 여러 개가 적용되면 예상 지시어를 사용하지 않을 수 있습니다.
-
모범 사례: 혼동을 피하기 위해 필드당 한 수준에만 지시어를 적용하세요.
-
참고: 지시어 파인더는 첫 번째 일치에서 멈추며 이후 수준은 검사하지 않습니다. 소유자 수준 지시어는 필드가 더 깊은 인가된 하위 필드를 가진 인가된 반환 타입으로 순회할 때도 건너뜁니다. 자세한 내용은 인가된 반환 타입으로의 순회를 참조하세요.