Commit c5c14282 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '273501-graphql' into 'master'

Cloud License: GraphQL and policy

See merge request gitlab-org/gitlab!51575
parents f936fba8 d1dd757f
...@@ -9976,6 +9976,36 @@ Identifier of Gitlab::ErrorTracking::DetailedError ...@@ -9976,6 +9976,36 @@ Identifier of Gitlab::ErrorTracking::DetailedError
""" """
scalar GitlabErrorTrackingDetailedErrorID scalar GitlabErrorTrackingDetailedErrorID
"""
Autogenerated input type of GitlabSubscriptionActivate
"""
input GitlabSubscriptionActivateInput {
"""
Activation code received after purchasing a GitLab subscription.
"""
activationCode: String!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
}
"""
Autogenerated return type of GitlabSubscriptionActivate
"""
type GitlabSubscriptionActivatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
type GrafanaIntegration { type GrafanaIntegration {
""" """
Timestamp of the issue's creation Timestamp of the issue's creation
...@@ -15772,6 +15802,7 @@ type Mutation { ...@@ -15772,6 +15802,7 @@ type Mutation {
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload
gitlabSubscriptionActivate(input: GitlabSubscriptionActivateInput!): GitlabSubscriptionActivatePayload
httpIntegrationCreate(input: HttpIntegrationCreateInput!): HttpIntegrationCreatePayload httpIntegrationCreate(input: HttpIntegrationCreateInput!): HttpIntegrationCreatePayload
httpIntegrationDestroy(input: HttpIntegrationDestroyInput!): HttpIntegrationDestroyPayload httpIntegrationDestroy(input: HttpIntegrationDestroyInput!): HttpIntegrationDestroyPayload
httpIntegrationResetToken(input: HttpIntegrationResetTokenInput!): HttpIntegrationResetTokenPayload httpIntegrationResetToken(input: HttpIntegrationResetTokenInput!): HttpIntegrationResetTokenPayload
......
...@@ -27597,6 +27597,94 @@ ...@@ -27597,6 +27597,94 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "GitlabSubscriptionActivateInput",
"description": "Autogenerated input type of GitlabSubscriptionActivate",
"fields": null,
"inputFields": [
{
"name": "activationCode",
"description": "Activation code received after purchasing a GitLab subscription.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GitlabSubscriptionActivatePayload",
"description": "Autogenerated return type of GitlabSubscriptionActivate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "GrafanaIntegration", "name": "GrafanaIntegration",
...@@ -44895,6 +44983,33 @@ ...@@ -44895,6 +44983,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "gitlabSubscriptionActivate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "GitlabSubscriptionActivateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "GitlabSubscriptionActivatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "httpIntegrationCreate", "name": "httpIntegrationCreate",
"description": null, "description": null,
...@@ -1610,6 +1610,15 @@ Represents an external issue. ...@@ -1610,6 +1610,15 @@ Represents an external issue.
| `url` | String | The user-facing URL for this Geo node | | `url` | String | The user-facing URL for this Geo node |
| `verificationMaxCapacity` | Int | The maximum concurrency of repository verification for this secondary node | | `verificationMaxCapacity` | Int | The maximum concurrency of repository verification for this secondary node |
### GitlabSubscriptionActivatePayload
Autogenerated return type of GitlabSubscriptionActivate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### GrafanaIntegration ### GrafanaIntegration
| Field | Type | Description | | Field | Type | Description |
......
...@@ -22,6 +22,7 @@ module EE ...@@ -22,6 +22,7 @@ module EE
mount_mutation ::Mutations::Epics::Create mount_mutation ::Mutations::Epics::Create
mount_mutation ::Mutations::Epics::SetSubscription mount_mutation ::Mutations::Epics::SetSubscription
mount_mutation ::Mutations::Epics::AddIssue mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::GitlabSubscriptions::Activate
mount_mutation ::Mutations::Iterations::Create mount_mutation ::Mutations::Iterations::Create
mount_mutation ::Mutations::Iterations::Update mount_mutation ::Mutations::Iterations::Update
mount_mutation ::Mutations::RequirementsManagement::CreateRequirement mount_mutation ::Mutations::RequirementsManagement::CreateRequirement
......
# frozen_string_literal: true
module Mutations
module GitlabSubscriptions
class Activate < BaseMutation
graphql_name 'GitlabSubscriptionActivate'
authorize :manage_subscription
argument :activation_code, GraphQL::STRING_TYPE,
required: true,
description: 'Activation code received after purchasing a GitLab subscription.'
def resolve(activation_code:)
authorize! :global
result = ::GitlabSubscriptions::ActivateService.new.execute(activation_code)
{ errors: Array(result[:errors]) }
end
end
end
end
...@@ -28,6 +28,7 @@ module EE ...@@ -28,6 +28,7 @@ module EE
enable :destroy_licenses enable :destroy_licenses
enable :read_all_geo enable :read_all_geo
enable :manage_devops_adoption_segments enable :manage_devops_adoption_segments
enable :manage_subscription
end end
rule { admin & pages_size_limit_available }.enable :update_max_pages_size rule { admin & pages_size_limit_available }.enable :update_max_pages_size
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::GitlabSubscriptions::Activate do
include AdminModeHelper
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let_it_be(:user) { create(:admin) }
let(:activation_code) { 'activation_code' }
let(:result) { { success: true } }
describe '#resolve' do
before do
enable_admin_mode!(user)
allow_next_instance_of(::GitlabSubscriptions::ActivateService) do |service|
expect(service).to receive(:execute).with(activation_code).and_return(result)
end
end
context 'when successful' do
it 'adds the issue to the epic' do
result = mutation.resolve(activation_code: activation_code)
expect(result).to eq({ errors: [] })
end
end
context 'when failure' do
let(:result) { { success: false, errors: ['foo'] } }
it 'returns errors' do
result = mutation.resolve(activation_code: activation_code)
expect(result).to eq({ errors: ['foo'] })
end
end
context 'when non-admin' do
let_it_be(:user) { create(:user) }
it 'raises errors' do
expect do
mutation.resolve(activation_code: activation_code)
end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
...@@ -39,17 +39,20 @@ RSpec.describe GlobalPolicy do ...@@ -39,17 +39,20 @@ RSpec.describe GlobalPolicy do
it { is_expected.to be_disallowed(:read_licenses) } it { is_expected.to be_disallowed(:read_licenses) }
it { is_expected.to be_disallowed(:destroy_licenses) } it { is_expected.to be_disallowed(:destroy_licenses) }
it { is_expected.to be_disallowed(:read_all_geo) } it { is_expected.to be_disallowed(:read_all_geo) }
it { is_expected.to be_disallowed(:manage_subscription) }
context 'when admin mode enabled', :enable_admin_mode do context 'when admin mode enabled', :enable_admin_mode do
it { expect(described_class.new(admin, [user])).to be_allowed(:read_licenses) } it { expect(described_class.new(admin, [user])).to be_allowed(:read_licenses) }
it { expect(described_class.new(admin, [user])).to be_allowed(:destroy_licenses) } it { expect(described_class.new(admin, [user])).to be_allowed(:destroy_licenses) }
it { expect(described_class.new(admin, [user])).to be_allowed(:read_all_geo) } it { expect(described_class.new(admin, [user])).to be_allowed(:read_all_geo) }
it { expect(described_class.new(admin, [user])).to be_allowed(:manage_subscription) }
end end
context 'when admin mode disabled' do context 'when admin mode disabled' do
it { expect(described_class.new(admin, [user])).to be_disallowed(:read_licenses) } it { expect(described_class.new(admin, [user])).to be_disallowed(:read_licenses) }
it { expect(described_class.new(admin, [user])).to be_disallowed(:destroy_licenses) } it { expect(described_class.new(admin, [user])).to be_disallowed(:destroy_licenses) }
it { expect(described_class.new(admin, [user])).to be_disallowed(:read_all_geo) } it { expect(described_class.new(admin, [user])).to be_disallowed(:read_all_geo) }
it { expect(described_class.new(admin, [user])).to be_disallowed(:manage_subscription) }
end end
shared_examples 'analytics policy' do |action| shared_examples 'analytics policy' do |action|
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Activate a subscription' do
include GraphqlHelpers
let_it_be(:current_user) { create(:admin) }
let!(:application_setting) do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
create(:application_setting, cloud_license_enabled: true)
end
let(:authentication_token) { 'authentication_token' }
let(:mutation) do
graphql_mutation(:gitlab_subscription_activate, { activation_code: 'abc' })
end
let(:remote_response) do
{
success: true,
data: {
"data" => {
"cloudActivationActivate" => {
"authenticationToken" => authentication_token,
"errors" => []
}
}
}
}
end
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:http_post).and_return(remote_response)
end
it 'persists authentication token' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_mutation_response(:gitlab_subscription_activate)['errors']).to be_empty
expect(application_setting.reload.cloud_license_auth_token).to eq(authentication_token)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment