Add GraphQL granular token authorization
What does this MR do and why?
Add support for authorizing granular tokens for GraphQL queries and mutations.
The included documentation describes all the components that are added in this MR.
This flowchart gives a quick summary of the auth flow:
Click to expand
GraphQL Request
↓
┌─────────────────────────────────────────────────────────────┐
│ GranularTokenAuthorization Extension (on every field) │
│ - Automatically applied via BaseField │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 1. Check Token Type │
│ - Is token granular? (token.granular?) │
│ - If NO → Skip authorization │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Check Skip Rules │
│ - Is this a mutation response field? (field :issue) │
│ - Is this a permission metadata field? (userPermissions) │
│ - If YES → Skip authorization │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Find Directive (DirectiveFinder) │
│ Check in order: │
│ a) Field itself │
│ b) Owner (mutation class, type class) │
│ c) Implementing type (for interfaces) │
│ d) Return type │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Extract Boundary (BoundaryExtractor) │
│ - If boundary_argument → Extract from arguments │
│ - If boundary → Call method on object │
│ - Returns: Project/Group boundary or nil. │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Authorize (AuthorizeGranularScopesService) │
│ - Check feature flag (granular_personal_access_tokens) │
│ - Check token has required permissions │
│ - Check token has access to boundary │
│ - Cache result to avoid duplicate checks │
└─────────────────────────────────────────────────────────────┘
↓
Success → Execute field resolver
Failure → Raise ResourceNotAvailable error
Reference MR that implements an authorization directive on a query and a mutation: !214111
References
Issue: #571510
How to set up and validate locally
-
Copy this patch and execute
pbpaste | git applyClick to expand
diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb index ebe24c2aab5a..5611fa328fe1 100644 --- a/app/graphql/mutations/issues/create.rb +++ b/app/graphql/mutations/issues/create.rb @@ -11,2 +11,4 @@ class Create < BaseMutation + directive Directives::Authz::GranularScope, permissions: ['CREATE_ISSUE'], boundary_argument: 'project_path' + authorize :create_issue diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 07fdc994a5ac..8257d8be71f1 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -12,2 +12,4 @@ class IssueType < BaseObject + directive Directives::Authz::GranularScope, permissions: ['READ_ISSUE'], boundary: 'project' + authorize :read_issue diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 90c4e8867285..49804712e85e 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -157,3 +157,9 @@ def self.authorization_scopes description: "Find a project.", - scopes: [:api, :read_api, :ai_workflows] + scopes: [:api, :read_api, :ai_workflows], + directives: { + Directives::Authz::GranularScope => { + permissions: ['READ_PROJECT'], + boundary_argument: 'full_path' + } + } field :projects, diff --git a/config/authz/permissions/issue/read.yml b/config/authz/permissions/issue/read.yml new file mode 100644 index 000000000000..a8719e0dd3bf --- /dev/null +++ b/config/authz/permissions/issue/read.yml @@ -0,0 +1,5 @@ +--- +name: read_issue +description: Grants the ability to read issues +feature_category: team_planning +available_for_tokens: true diff --git a/config/authz/permissions/project/read.yml b/config/authz/permissions/project/read.yml new file mode 100644 index 000000000000..06e90a82c84d --- /dev/null +++ b/config/authz/permissions/project/read.yml @@ -0,0 +1,5 @@ +--- +name: read_project +description: Grants the ability to read projects +feature_category: team_planning +available_for_tokens: true -
In Rails console, enable the
granular_personal_access_tokensFF and create a granular PAT with a granular scope for a user.
# Enable feature flag
Feature.enable(:granular_personal_access_tokens)
user = User.human.last
# 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 boundary object (project or group)
project = user.projects.first
# Create a granular scope and add it to the granular PAT
scope = Authz::GranularScope.new(namespace: project.project_namespace, permissions: [:read_project, :read_issue, :create_issue])
Authz::GranularScopeService.new(token).add_granular_scopes(scope)
# Copy the PAT to the clipboard
IO.popen('pbcopy', 'w') { |f| f.puts token.token }
-
Verify GraphQL mutations to create issues within the project are working (replace
my-user/my-projectwithproject.full_pathand PAT with the copied token):curl http://gdk.test:3000/api/graphql -X POST \ -H "PRIVATE-TOKEN: PAT" \ -H 'Content-Type: application/json' \ -d '{"query":"mutation { createIssue(input: { projectPath: \"my-user/my-project\", title: \"Test Issue\" }) { issue { iid title } errors } }"}'output:
=> {"data":{"createIssue":{"issue":{"iid":"1","title":"Test Issue"},"errors":[]}}} -
Verify GraphQL queries to lookup issue titles within the project are working (replace
my-user/my-projectwithproject.full_pathand PAT with the copied token):curl http://gdk.test:3000/api/graphql -X POST \ -H "PRIVATE-TOKEN: PAT" \ -H 'Content-Type: application/json' \ -d '{"query":"{ project(fullPath: \"my-user/my-project\") { issues { nodes { title } } } }"}'output:
=> {"data":{"project":{"issues":{"nodes":[{"title":"Test Issue"}]}}}}
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.