InfoGrab DocsInfoGrab Docs

GraphQL 구현 가이드

요약

손상된 Personal Access Token(PAT)의 보안 영향을 줄이기 위해, 세분화된(granular) PAT는 사용자가 특정 조직 경계(그룹, 프로젝트, 사용자 또는 인스턴스 수준)에 한정된 세밀한 권한을 가진 토큰을 생성할 수 있도록 합니다.

손상된 Personal Access Token(PAT)의 보안 영향을 줄이기 위해, 세분화된(granular) PAT는 사용자가 특정 조직 경계(그룹, 프로젝트, 사용자 또는 인스턴스 수준)에 한정된 세밀한 권한을 가진 토큰을 생성할 수 있도록 합니다. 이를 통해 사용자는 토큰에 필요한 권한만 부여하는 최소 권한 원칙을 따를 수 있습니다.

Granular PAT는 경계와 특정 리소스 권한으로 구성된 세분화된 스코프를 통해 세밀한 접근 제어를 허용합니다. Granular PAT로 GraphQL 요청을 인증할 때, GitLab은 해당 토큰의 권한에 지정된 경계 수준에서 요청된 리소스에 대한 접근이 포함되어 있는지 검증합니다.

이 문서는 GraphQL 쿼리 및 뮤테이션을 granular PAT 인증과 호환되게 만들려는 커뮤니티 기여자 및 GitLab 개발자를 위해 설계되었습니다.

단계별 구현 가이드#

이 가이드는 GraphQL 유형 및 뮤테이션에 granular PAT 인증을 추가하는 방법을 안내합니다. 시작하기 전에 전반에서 사용되는 용어를 이해하기 위해 권한 명명 규칙 문서를 검토하세요.

Note

이 단계는 GraphQL 유형 및 뮤테이션만 다룹니다. REST API 엔드포인트 보호의 경우 REST API 구현 가이드를 참조하세요.

인증 시스템이 내부적으로 어떻게 작동하는지에 대한 자세한 설명은 GraphQL 아키텍처 문서를 참조하세요.

워크플로우 개요#

구현은 다음 흐름을 따릅니다:

  • 1-2단계: 계획 - 유형/뮤테이션 식별 및 권한 설계

  • 3단계: 원시 권한 만들기 (YAML 파일)

  • 4단계: 원시 권한을 할당 가능한 권한으로 묶기 (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)

  • 리소스를 직접 반환하는 쿼리 필드 (예: QueryTypefield :project)

유형 또는 뮤테이션에 이미 authorize_granular_token 지시자가 있는지 확인합니다. 지시자가 없는 유형/뮤테이션에 지시자를 추가해야 합니다.

2단계: 필요한 권한 결정#

목표: GitLab 명명 규칙에 따라 세분화된 권한을 정의합니다.

명명 규칙에 대해서는 규칙 문서의 권한 명명을 참조하세요.

유형 및 뮤테이션에 대한 권한 이름 결정#

Granular PAT 인증을 구현할 때 GraphQL 스키마 구조가 아닌 유형이 나타내는 것 또는 뮤테이션이 수행하는 것을 기반으로 권한 이름을 지정합니다.

예시:

  • 유형 IssueType → 이슈 읽기를 나타냄 → 권한 이름은 read_issue

  • 뮤테이션 Mutations::Issues::Create → 이슈 생성 → 권한 이름은 create_issue

  • 유형 ProjectType → 프로젝트 데이터 읽기를 나타냄 → 권한 이름은 read_project

일반적인 패턴#

  • 객체 유형: 유형의 모든 필드를 포함하는 read_resource 권한 사용

IssueTyperead_issue

  • ProjectTyperead_project

  • 생성 뮤테이션: create_resource 사용

Mutations::Issues::Createcreate_issue

  • 업데이트 뮤테이션: update_resource 사용

Mutations::Issues::Updateupdate_issue

  • 삭제 뮤테이션: delete_resource 사용

Mutations::Issues::Destroydelete_issue

  • 특수 작업 뮤테이션: 고유한 작업에 대한 특정 권한 만들기

이동, 보관, 전환 등 각각 자체 권한을 가짐

3단계: 권한 정의 파일 만들기#

목표: 아직 존재하지 않는 경우 각 권한에 대한 YAML 정의 파일을 만듭니다.

bin/permission 명령을 사용하여 원시 권한 YAML 파일을 만들려면 권한 정의 파일 섹션의 지침을 따릅니다. 이 단계는 REST API 및 GraphQL 구현 모두에서 동일합니다.

4단계: 할당 가능한 권한 만들기 또는 업데이트#

목표: 더 간단한 사용자 경험을 위해 원시 권한을 할당 가능한 권한으로 묶습니다.

가능한 경우 새 할당 가능한 권한을 만드는 대신 기존 할당 가능한 권한에 원시 권한을 추가하는 것이 좋습니다. 할당 가능한 권한은 사용자에게 노출됩니다. 각 새 권한은 토큰 생성 UI에 표시되며, 해당 이름은 이를 선택하는 모든 토큰에 대해 데이터베이스에 저장됩니다. 나중에 할당 가능한 권한을 제거하거나 이름을 변경하면 이는 호환성이 깨지는 변경이지만, 기존 할당 가능한 권한 내의 원시 권한은 자유롭게 이름을 변경하거나 이동할 수 있습니다. 원시 권한이 사용자가 해당 리소스의 기존 할당 가능한 권한과 별도로 부여할 수 있어야 하는 기능을 나타내는 경우에만 새 할당 가능한 권한을 만드세요. 각 변경 유형의 영향에 대해서는 할당 가능한 권한 유지 관리를 참조하세요.

할당 가능한 권한 YAML 파일을 만들거나 업데이트하려면 할당 가능한 권한 문서의 지침을 따릅니다. 이 단계는 REST API 및 GraphQL 구현 모두에서 동일합니다.

5단계: 유형 및 뮤테이션에 인증 지시자 추가#

목표: GraphQL 유형 및 뮤테이션에 granular 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를 포함할 수 있음. 자세한 내용은 여러 경계를 참조하세요.
traversal 진입점 필드에 대한 필드별 지시자(granular_scope_directive를 통해 전달)에 true로 설정. 유형 수준 authorize_granular_token에 전달하면 ArgumentError가 발생함. 자세한 내용은 진입점 필드를 참조하세요. 현재 적용되지 않음.
skip_reason 유형이 granular-token 인증을 의도적으로 옵트아웃함을 선언하는 심볼. permissions: 및 boundary와 함께가 아니라 그 대신 사용. 자세한 내용은 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_argumentboundary와 결합합니다. 인수는 레코드를 찾고, 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.rbee/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.ownerUser를 반환하므로 프로젝트나 그룹 지시자 모두 일치하지 않으며, 독립형 instance 경계가 적용됩니다. 자세한 내용은 여러 경계를 참조하세요.

진입점 필드#

Note

traversal은 지시자에 선언되지만 현재 GranularScopeAuthorization에 의해 적용되지 않습니다. traversal: true로 표시된 필드는 다른 필드와 마찬가지로 나열된 권한을 적용합니다. 적용은 재구현이 보류 중입니다. Query.groupQuery.project는 현재 지시자를 선언하지 않으며, granular-token 지시자가 없는 필드는 자체적으로 granular 검사를 수행하지 않습니다.

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: trueprojectgroup 경계 유형에만 적용됩니다. 다른 모든 경계 유형의 경우 나열된 권한이 정상적으로 적용됩니다.

인증된 유형 간 탐색#

인증된 유형의 필드가 authorize_granular_token을 선언하는 다른 유형을 반환하면, 두 지시자가 모두 적용됩니다. GranularScopeAuthorization은 각 유형과 필드의 자체 지시자를 독립적으로 평가하므로, 토큰이 자식 유형의 권한뿐만 아니라 소유자 유형의 권한도 필요하다고 가정하여 권한을 계획하세요.

예를 들어, GroupType.groupMembersGroupMemberType을 반환하며, 두 유형 모두 granular-token 지시자를 선언합니다. 토큰이 그룹의 필드를 해석하려면 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: 또는 boundary 인수와 결합할 수 없습니다.

6단계: 인증 테스트 추가#

목표: GraphQL 유형 및 뮤테이션에 granular PAT 권한이 올바르게 적용되는지 확인합니다.

쿼리의 경우#

'authorizing granular token permissions for GraphQL' 공유 예제를 추가합니다:

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_objectboundary_type과 일치해야 합니다:

경계 유형 경계 객체
:project project
:group group
:user :user
:instance :instance

중요: 경계 객체가 :project 또는 :group인 경우, 인증이 부여되려면 user가 해당 네임스페이스(프로젝트 또는 그룹)의 멤버여야 합니다.

이 테스트가 검증하는 것:

  • 레거시(비세분화) personal access token이 계속 접근 권한을 부여함

  • 경계의 최상위 그룹이 세분화된 토큰을 강제할 때 레거시 토큰은 접근이 거부됨

  • Granular PAT에서 필요한 권한이 부여된 사용자가 접근을 허용받음

  • 필요한 권한이 없는 사용자가 접근을 거부당함. 인증되지 않은 쿼리는 200 응답과 함께 null 데이터를 반환하고, 인증되지 않은 뮤테이션은 최상위 GraphQL 오류를 반환함

  • 인증 시스템이 유형/뮤테이션의 권한 요구 사항에 대해 세분화된 스코프를 올바르게 평가함

  • 기능 플래그 granular_personal_access_tokens가 올바르게 적용됨 (비활성화 시 접근 거부)

7단계: 수동 유효성 검사#

목표: 머지 리퀘스트를 만들기 전에 로컬 환경에서 구현을 수동으로 테스트하여 권한이 예상대로 작동하는지 확인합니다.

설정:

Rails 콘솔에서 사용자를 위한 granular PAT를 만듭니다:

# 기능 플래그 활성화
Feature.enable(:granular_personal_access_tokens)

user = User.human.first

# Granular 토큰 만들기
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]

# 적절한 경계 객체 가져오기 (project, group, :user, or :instance)
project = user.projects.first
boundary = Authz::Boundary.for(project)

# 테스트 중인 할당 가능한 권한으로 스코프 만들기.
# 스코프는 할당 가능한 권한 이름을 저장하며, 요청 시점에 원시 권한으로 확장됩니다.
# 아래 예제 쿼리는 ProjectType(원시 권한 read_project, read_project 할당 가능한 권한의 일부)와
# IssueType(원시 권한 read_issue, read_work_item 할당 가능한 권한의 일부) 모두의 필드를 해석하므로,
# 스코프에는 둘 다 필요합니다.
scope = Authz::GranularScope.new(namespace: boundary.namespace, access: boundary.access, permissions: [:read_project, :read_work_item])

# 토큰에 스코프 추가
Authz::GranularScopeService.new(token).add_granular_scopes(scope)

# GraphQL 쿼리 테스트용 curl 명령 복사
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 훅으로도 실행됩니다. 다른 검사 중에서도 다음과 같은 경우 실패합니다:

객체 유형 또는 뮤테이션에 granular-token 지시자도 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 구현 가이드

GitLab v19.2
원문 보기
요약

손상된 Personal Access Token(PAT)의 보안 영향을 줄이기 위해, 세분화된(granular) PAT는 사용자가 특정 조직 경계(그룹, 프로젝트, 사용자 또는 인스턴스 수준)에 한정된 세밀한 권한을 가진 토큰을 생성할 수 있도록 합니다.

손상된 Personal Access Token(PAT)의 보안 영향을 줄이기 위해, 세분화된(granular) PAT는 사용자가 특정 조직 경계(그룹, 프로젝트, 사용자 또는 인스턴스 수준)에 한정된 세밀한 권한을 가진 토큰을 생성할 수 있도록 합니다. 이를 통해 사용자는 토큰에 필요한 권한만 부여하는 최소 권한 원칙을 따를 수 있습니다.

Granular PAT는 경계와 특정 리소스 권한으로 구성된 세분화된 스코프를 통해 세밀한 접근 제어를 허용합니다. Granular PAT로 GraphQL 요청을 인증할 때, GitLab은 해당 토큰의 권한에 지정된 경계 수준에서 요청된 리소스에 대한 접근이 포함되어 있는지 검증합니다.

이 문서는 GraphQL 쿼리 및 뮤테이션을 granular PAT 인증과 호환되게 만들려는 커뮤니티 기여자 및 GitLab 개발자를 위해 설계되었습니다.

단계별 구현 가이드#

이 가이드는 GraphQL 유형 및 뮤테이션에 granular PAT 인증을 추가하는 방법을 안내합니다. 시작하기 전에 전반에서 사용되는 용어를 이해하기 위해 권한 명명 규칙 문서를 검토하세요.

Note

이 단계는 GraphQL 유형 및 뮤테이션만 다룹니다. REST API 엔드포인트 보호의 경우 REST API 구현 가이드를 참조하세요.

인증 시스템이 내부적으로 어떻게 작동하는지에 대한 자세한 설명은 GraphQL 아키텍처 문서를 참조하세요.

워크플로우 개요#

구현은 다음 흐름을 따릅니다:

  • 1-2단계: 계획 - 유형/뮤테이션 식별 및 권한 설계

  • 3단계: 원시 권한 만들기 (YAML 파일)

  • 4단계: 원시 권한을 할당 가능한 권한으로 묶기 (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)

  • 리소스를 직접 반환하는 쿼리 필드 (예: QueryTypefield :project)

유형 또는 뮤테이션에 이미 authorize_granular_token 지시자가 있는지 확인합니다. 지시자가 없는 유형/뮤테이션에 지시자를 추가해야 합니다.

2단계: 필요한 권한 결정#

목표: GitLab 명명 규칙에 따라 세분화된 권한을 정의합니다.

명명 규칙에 대해서는 규칙 문서의 권한 명명을 참조하세요.

유형 및 뮤테이션에 대한 권한 이름 결정#

Granular PAT 인증을 구현할 때 GraphQL 스키마 구조가 아닌 유형이 나타내는 것 또는 뮤테이션이 수행하는 것을 기반으로 권한 이름을 지정합니다.

예시:

  • 유형 IssueType → 이슈 읽기를 나타냄 → 권한 이름은 read_issue

  • 뮤테이션 Mutations::Issues::Create → 이슈 생성 → 권한 이름은 create_issue

  • 유형 ProjectType → 프로젝트 데이터 읽기를 나타냄 → 권한 이름은 read_project

일반적인 패턴#

  • 객체 유형: 유형의 모든 필드를 포함하는 read_resource 권한 사용

IssueTyperead_issue

  • ProjectTyperead_project

  • 생성 뮤테이션: create_resource 사용

Mutations::Issues::Createcreate_issue

  • 업데이트 뮤테이션: update_resource 사용

Mutations::Issues::Updateupdate_issue

  • 삭제 뮤테이션: delete_resource 사용

Mutations::Issues::Destroydelete_issue

  • 특수 작업 뮤테이션: 고유한 작업에 대한 특정 권한 만들기

이동, 보관, 전환 등 각각 자체 권한을 가짐

3단계: 권한 정의 파일 만들기#

목표: 아직 존재하지 않는 경우 각 권한에 대한 YAML 정의 파일을 만듭니다.

bin/permission 명령을 사용하여 원시 권한 YAML 파일을 만들려면 권한 정의 파일 섹션의 지침을 따릅니다. 이 단계는 REST API 및 GraphQL 구현 모두에서 동일합니다.

4단계: 할당 가능한 권한 만들기 또는 업데이트#

목표: 더 간단한 사용자 경험을 위해 원시 권한을 할당 가능한 권한으로 묶습니다.

가능한 경우 새 할당 가능한 권한을 만드는 대신 기존 할당 가능한 권한에 원시 권한을 추가하는 것이 좋습니다. 할당 가능한 권한은 사용자에게 노출됩니다. 각 새 권한은 토큰 생성 UI에 표시되며, 해당 이름은 이를 선택하는 모든 토큰에 대해 데이터베이스에 저장됩니다. 나중에 할당 가능한 권한을 제거하거나 이름을 변경하면 이는 호환성이 깨지는 변경이지만, 기존 할당 가능한 권한 내의 원시 권한은 자유롭게 이름을 변경하거나 이동할 수 있습니다. 원시 권한이 사용자가 해당 리소스의 기존 할당 가능한 권한과 별도로 부여할 수 있어야 하는 기능을 나타내는 경우에만 새 할당 가능한 권한을 만드세요. 각 변경 유형의 영향에 대해서는 할당 가능한 권한 유지 관리를 참조하세요.

할당 가능한 권한 YAML 파일을 만들거나 업데이트하려면 할당 가능한 권한 문서의 지침을 따릅니다. 이 단계는 REST API 및 GraphQL 구현 모두에서 동일합니다.

5단계: 유형 및 뮤테이션에 인증 지시자 추가#

목표: GraphQL 유형 및 뮤테이션에 granular 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를 포함할 수 있음. 자세한 내용은 여러 경계를 참조하세요.
traversal 진입점 필드에 대한 필드별 지시자(granular_scope_directive를 통해 전달)에 true로 설정. 유형 수준 authorize_granular_token에 전달하면 ArgumentError가 발생함. 자세한 내용은 진입점 필드를 참조하세요. 현재 적용되지 않음.
skip_reason 유형이 granular-token 인증을 의도적으로 옵트아웃함을 선언하는 심볼. permissions: 및 boundary와 함께가 아니라 그 대신 사용. 자세한 내용은 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_argumentboundary와 결합합니다. 인수는 레코드를 찾고, 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.rbee/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.ownerUser를 반환하므로 프로젝트나 그룹 지시자 모두 일치하지 않으며, 독립형 instance 경계가 적용됩니다. 자세한 내용은 여러 경계를 참조하세요.

진입점 필드#

Note

traversal은 지시자에 선언되지만 현재 GranularScopeAuthorization에 의해 적용되지 않습니다. traversal: true로 표시된 필드는 다른 필드와 마찬가지로 나열된 권한을 적용합니다. 적용은 재구현이 보류 중입니다. Query.groupQuery.project는 현재 지시자를 선언하지 않으며, granular-token 지시자가 없는 필드는 자체적으로 granular 검사를 수행하지 않습니다.

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: trueprojectgroup 경계 유형에만 적용됩니다. 다른 모든 경계 유형의 경우 나열된 권한이 정상적으로 적용됩니다.

인증된 유형 간 탐색#

인증된 유형의 필드가 authorize_granular_token을 선언하는 다른 유형을 반환하면, 두 지시자가 모두 적용됩니다. GranularScopeAuthorization은 각 유형과 필드의 자체 지시자를 독립적으로 평가하므로, 토큰이 자식 유형의 권한뿐만 아니라 소유자 유형의 권한도 필요하다고 가정하여 권한을 계획하세요.

예를 들어, GroupType.groupMembersGroupMemberType을 반환하며, 두 유형 모두 granular-token 지시자를 선언합니다. 토큰이 그룹의 필드를 해석하려면 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: 또는 boundary 인수와 결합할 수 없습니다.

6단계: 인증 테스트 추가#

목표: GraphQL 유형 및 뮤테이션에 granular PAT 권한이 올바르게 적용되는지 확인합니다.

쿼리의 경우#

'authorizing granular token permissions for GraphQL' 공유 예제를 추가합니다:

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_objectboundary_type과 일치해야 합니다:

경계 유형 경계 객체
:project project
:group group
:user :user
:instance :instance

중요: 경계 객체가 :project 또는 :group인 경우, 인증이 부여되려면 user가 해당 네임스페이스(프로젝트 또는 그룹)의 멤버여야 합니다.

이 테스트가 검증하는 것:

  • 레거시(비세분화) personal access token이 계속 접근 권한을 부여함

  • 경계의 최상위 그룹이 세분화된 토큰을 강제할 때 레거시 토큰은 접근이 거부됨

  • Granular PAT에서 필요한 권한이 부여된 사용자가 접근을 허용받음

  • 필요한 권한이 없는 사용자가 접근을 거부당함. 인증되지 않은 쿼리는 200 응답과 함께 null 데이터를 반환하고, 인증되지 않은 뮤테이션은 최상위 GraphQL 오류를 반환함

  • 인증 시스템이 유형/뮤테이션의 권한 요구 사항에 대해 세분화된 스코프를 올바르게 평가함

  • 기능 플래그 granular_personal_access_tokens가 올바르게 적용됨 (비활성화 시 접근 거부)

7단계: 수동 유효성 검사#

목표: 머지 리퀘스트를 만들기 전에 로컬 환경에서 구현을 수동으로 테스트하여 권한이 예상대로 작동하는지 확인합니다.

설정:

Rails 콘솔에서 사용자를 위한 granular PAT를 만듭니다:

# 기능 플래그 활성화
Feature.enable(:granular_personal_access_tokens)

user = User.human.first

# Granular 토큰 만들기
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]

# 적절한 경계 객체 가져오기 (project, group, :user, or :instance)
project = user.projects.first
boundary = Authz::Boundary.for(project)

# 테스트 중인 할당 가능한 권한으로 스코프 만들기.
# 스코프는 할당 가능한 권한 이름을 저장하며, 요청 시점에 원시 권한으로 확장됩니다.
# 아래 예제 쿼리는 ProjectType(원시 권한 read_project, read_project 할당 가능한 권한의 일부)와
# IssueType(원시 권한 read_issue, read_work_item 할당 가능한 권한의 일부) 모두의 필드를 해석하므로,
# 스코프에는 둘 다 필요합니다.
scope = Authz::GranularScope.new(namespace: boundary.namespace, access: boundary.access, permissions: [:read_project, :read_work_item])

# 토큰에 스코프 추가
Authz::GranularScopeService.new(token).add_granular_scopes(scope)

# GraphQL 쿼리 테스트용 curl 명령 복사
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 훅으로도 실행됩니다. 다른 검사 중에서도 다음과 같은 경우 실패합니다:

객체 유형 또는 뮤테이션에 granular-token 지시자도 skip_reason:도 없는 경우. 단, config/authz/graphql/authorization_todo.txt에 예외로 등록되어 있는 경우는 제외됩니다.

  • 지시자의 권한이 어떤 할당 가능한 권한에도 속하지 않는 경우.

  • 지시자의 boundary_type이 할당 가능한 권한의 boundaries와 일치하지 않는 경우.

  • skip_reason:lib/tasks/gitlab/permissions/graphql/skip_reasons.rb에 정의되어 있지 않은 경우.

  • 지시자가 skip_reason:permissions:를 모두 선언하는 경우.

  • 지시자의 권한에 인증 테스트가 없는 경우. 권한을 선언하는 각 유형, 뮤테이션 또는 필드는 경계 유형별로 자체 테스트가 필요합니다. 이는 예외 없이 엄격하게 적용되므로, 선언과 동일한 머지 리퀘스트에 테스트를 추가하세요.

  • 생성된 참조 문서가 최신 상태가 아닌 경우.

참조#