From 874162c276a75761e9e9085d5efc636ebe7da893 Mon Sep 17 00:00:00 2001 From: Oleksandr Chebotarov Date: Thu, 28 Nov 2024 15:35:09 +0200 Subject: [PATCH] Add permissions params to create API key mutation --- app/graphql/mutations/api_keys/create.rb | 3 +- app/models/api_key.rb | 4 +- app/services/api_keys/create_service.rb | 6 +- ...8132010_add_new_permissions_to_api_keys.rb | 13 ++ db/schema.rb | 2 +- schema.graphql | 1 + schema.json | 12 ++ spec/services/api_keys/create_service_spec.rb | 175 ++++++++++++++++-- 8 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 db/migrate/20241128132010_add_new_permissions_to_api_keys.rb diff --git a/app/graphql/mutations/api_keys/create.rb b/app/graphql/mutations/api_keys/create.rb index 3520e1a93a8..4d5db596479 100644 --- a/app/graphql/mutations/api_keys/create.rb +++ b/app/graphql/mutations/api_keys/create.rb @@ -12,11 +12,12 @@ class Create < BaseMutation description 'Creates a new API key' argument :name, String, required: false + argument :permissions, GraphQL::Types::JSON, required: false type Types::ApiKeys::Object def resolve(**args) - result = ::ApiKeys::CreateService.call(args.merge(organization_id: current_organization.id)) + result = ::ApiKeys::CreateService.call(args.merge(organization: current_organization)) result.success? ? result.api_key : result_error(result) end diff --git a/app/models/api_key.rb b/app/models/api_key.rb index 08f804feae6..4392f495759 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -5,8 +5,8 @@ class ApiKey < ApplicationRecord RESOURCES = %w[ add_on analytic billable_metric coupon applied_coupon credit_note customer_usage - customer fee invoice organization payment_request plan subscription lifetime_usage - tax wallet wallet_transaction webhook_endpoint + customer event fee invoice organization payment_request plan subscription lifetime_usage + tax wallet wallet_transaction webhook_endpoint webhook_jwt_public_key ].freeze MODES = %w[read write].freeze diff --git a/app/services/api_keys/create_service.rb b/app/services/api_keys/create_service.rb index e92ec07ea10..fb82e354143 100644 --- a/app/services/api_keys/create_service.rb +++ b/app/services/api_keys/create_service.rb @@ -10,8 +10,12 @@ def initialize(params) def call return result.forbidden_failure! unless License.premium? + if params[:permissions].present? && !params[:organization].premium_integrations.include?('api_permissions') + return result.forbidden_failure!(code: 'premium_integration_missing') + end + api_key = ApiKey.create!( - params.slice(:organization_id, :name) + params.slice(:organization, :name, :permissions) ) ApiKeyMailer.with(api_key:).created.deliver_later diff --git a/db/migrate/20241128132010_add_new_permissions_to_api_keys.rb b/db/migrate/20241128132010_add_new_permissions_to_api_keys.rb new file mode 100644 index 00000000000..619236634bc --- /dev/null +++ b/db/migrate/20241128132010_add_new_permissions_to_api_keys.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddNewPermissionsToApiKeys < ActiveRecord::Migration[7.1] + def up + ApiKey.update_all(permissions: ApiKey.default_permissions) # rubocop:disable Rails/SkipsModelValidations + end + + def down + ApiKey.update_all( # rubocop:disable Rails/SkipsModelValidations + permissions: ApiKey.default_permissions.without("event", "webhook_jwt_public_key") + ) + end +end diff --git a/db/schema.rb b/db/schema.rb index 436d3f4dc70..cab41d69efb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_11_26_141853) do +ActiveRecord::Schema[7.1].define(version: 2024_11_28_132010) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" diff --git a/schema.graphql b/schema.graphql index b0db7e61528..49c1c169f5e 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1839,6 +1839,7 @@ input CreateApiKeyInput { """ clientMutationId: String name: String + permissions: JSON } """ diff --git a/schema.json b/schema.json index ca5e91ce1e6..c5329f7a3a0 100644 --- a/schema.json +++ b/schema.json @@ -6681,6 +6681,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "enumValues": null diff --git a/spec/services/api_keys/create_service_spec.rb b/spec/services/api_keys/create_service_spec.rb index 9ef0f4d5ba4..dbe9701663d 100644 --- a/spec/services/api_keys/create_service_spec.rb +++ b/spec/services/api_keys/create_service_spec.rb @@ -6,36 +6,175 @@ describe '#call' do subject(:service_result) { described_class.call(params) } - let!(:params) do - { - organization_id: create(:organization).id, - name: Faker::Lorem.words.join(' ') - } - end + let(:name) { Faker::Lorem.words.join(' ') } + let(:organization) { create(:organization) } context 'with premium organization' do around { |test| lago_premium!(&test) } - it 'creates a new API key' do - expect { service_result }.to change(ApiKey, :count).by(1) + context 'when permissions hash is provided' do + let(:params) { {permissions:, name:, organization:} } + let(:permissions) { ApiKey.default_permissions } + + before { organization.update!(premium_integrations:) } + + context 'when organization has api permissions addon' do + let(:premium_integrations) { ['api_permissions'] } + + it 'creates a new API key' do + expect { service_result }.to change(ApiKey, :count).by(1) + end + + it 'sends an API key created email' do + expect { service_result } + .to have_enqueued_mail(ApiKeyMailer, :created) + .with(hash_including(params: {api_key: instance_of(ApiKey)})) + end + end + + context 'when organization has no api permissions addon' do + let(:premium_integrations) { [] } + + it 'does not create an API key' do + expect { service_result }.not_to change(ApiKey, :count) + end + + it 'does not send an API key created email' do + expect { service_result }.not_to have_enqueued_mail(ApiKeyMailer, :created) + end + + it 'returns an error' do + aggregate_failures do + expect(service_result).not_to be_success + expect(service_result.error).to be_a(BaseService::ForbiddenFailure) + expect(service_result.error.code).to eq('premium_integration_missing') + end + end + end end - it 'sends an API key created email' do - expect { service_result } - .to have_enqueued_mail(ApiKeyMailer, :created) - .with(hash_including(params: {api_key: instance_of(ApiKey)})) + context 'when permissions hash is missing' do + let(:params) { {name:, organization:} } + + before { organization.update!(premium_integrations:) } + + context 'when organization has api permissions addon' do + let(:premium_integrations) { ['api_permissions'] } + + it 'creates a new API key' do + expect { service_result }.to change(ApiKey, :count).by(1) + end + + it 'sends an API key created email' do + expect { service_result } + .to have_enqueued_mail(ApiKeyMailer, :created) + .with(hash_including(params: {api_key: instance_of(ApiKey)})) + end + end + + context 'when organization has no api permissions addon' do + let(:premium_integrations) { [] } + + it 'creates a new API key' do + expect { service_result }.to change(ApiKey, :count).by(1) + end + + it 'sends an API key created email' do + expect { service_result } + .to have_enqueued_mail(ApiKeyMailer, :created) + .with(hash_including(params: {api_key: instance_of(ApiKey)})) + end + end end end context 'with free organization' do - it 'does not create an API key' do - expect { service_result }.not_to change(ApiKey, :count) + context 'when permissions hash is provided' do + let(:params) { {permissions:, name:, organization:} } + let(:permissions) { ApiKey.default_permissions } + + before { organization.update!(premium_integrations:) } + + context 'when organization has api permissions addon' do + let(:premium_integrations) { ['api_permissions'] } + + it 'does not create an API key' do + expect { service_result }.not_to change(ApiKey, :count) + end + + it 'does not send an API key created email' do + expect { service_result }.not_to have_enqueued_mail(ApiKeyMailer, :created) + end + + it 'returns an error' do + aggregate_failures do + expect(service_result).not_to be_success + expect(service_result.error).to be_a(BaseService::ForbiddenFailure) + end + end + end + + context 'when organization has no api permissions addon' do + let(:premium_integrations) { [] } + + it 'does not create an API key' do + expect { service_result }.not_to change(ApiKey, :count) + end + + it 'does not send an API key created email' do + expect { service_result }.not_to have_enqueued_mail(ApiKeyMailer, :created) + end + + it 'returns an error' do + aggregate_failures do + expect(service_result).not_to be_success + expect(service_result.error).to be_a(BaseService::ForbiddenFailure) + end + end + end end - it 'returns an error' do - aggregate_failures do - expect(service_result).not_to be_success - expect(service_result.error).to be_a(BaseService::ForbiddenFailure) + context 'when permissions hash is missing' do + let(:params) { {name:, organization:} } + + before { organization.update!(premium_integrations:) } + + context 'when organization has api permissions addon' do + let(:premium_integrations) { ['api_permissions'] } + + it 'does not create an API key' do + expect { service_result }.not_to change(ApiKey, :count) + end + + it 'does not send an API key created email' do + expect { service_result }.not_to have_enqueued_mail(ApiKeyMailer, :created) + end + + it 'returns an error' do + aggregate_failures do + expect(service_result).not_to be_success + expect(service_result.error).to be_a(BaseService::ForbiddenFailure) + end + end + end + + context 'when organization has no api permissions addon' do + let(:premium_integrations) { [] } + + it 'does not create an API key' do + expect { service_result }.not_to change(ApiKey, :count) + end + + it 'does not send an API key created email' do + expect { service_result }.not_to have_enqueued_mail(ApiKeyMailer, :created) + end + + it 'returns an error' do + aggregate_failures do + expect(service_result).not_to be_success + expect(service_result.error).to be_a(BaseService::ForbiddenFailure) + end + end end end end