From 0ec4531742b87112a0bc4e0d0dd8889d001a5a27 Mon Sep 17 00:00:00 2001 From: Harsimar Sandhu <hsandhu@gitlab.com> Date: Fri, 19 May 2023 09:12:56 +0000 Subject: [PATCH] Google cloud logging configuration create api This commit implements GraphQL api to create google cloud logging configurations EE: true Changelog: added --- doc/api/graphql/reference/index.md | 38 +++++ ee/app/graphql/ee/types/mutation_type.rb | 1 + .../create.rb | 64 ++++++++ ...google_cloud_logging_configuration_type.rb | 36 +++++ .../google_cloud_logging_configuration.rb | 7 +- ...ogle_cloud_logging_configuration_policy.rb | 7 + ...google_cloud_logging_configuration_spec.rb | 24 +++ .../create_spec.rb | 150 ++++++++++++++++++ 8 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/create.rb create mode 100644 ee/app/graphql/types/audit_events/google_cloud_logging_configuration_type.rb create mode 100644 ee/app/policies/audit_events/google_cloud_logging_configuration_policy.rb create mode 100644 ee/spec/requests/api/graphql/mutations/audit_events/google_cloud_logging_configurations/create_spec.rb diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index aba5ea4de71f4..a517f4bf6a13b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3521,6 +3521,29 @@ Input type: `GitlabSubscriptionActivateInput` | <a id="mutationgitlabsubscriptionactivatefuturesubscriptions"></a>`futureSubscriptions` | [`[SubscriptionFutureEntry!]`](#subscriptionfutureentry) | Array of future subscriptions. | | <a id="mutationgitlabsubscriptionactivatelicense"></a>`license` | [`CurrentLicense`](#currentlicense) | Current license. | +### `Mutation.googleCloudLoggingConfigurationCreate` + +Input type: `GoogleCloudLoggingConfigurationCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationgooglecloudloggingconfigurationcreateclientemail"></a>`clientEmail` | [`String!`](#string) | Client email. | +| <a id="mutationgooglecloudloggingconfigurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationgooglecloudloggingconfigurationcreategoogleprojectidname"></a>`googleProjectIdName` | [`String!`](#string) | Google project ID. | +| <a id="mutationgooglecloudloggingconfigurationcreategrouppath"></a>`groupPath` | [`ID!`](#id) | Group path. | +| <a id="mutationgooglecloudloggingconfigurationcreatelogidname"></a>`logIdName` | [`String`](#string) | Log ID. (defaults to `audit_events`). | +| <a id="mutationgooglecloudloggingconfigurationcreateprivatekey"></a>`privateKey` | [`String!`](#string) | Private key. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationgooglecloudloggingconfigurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationgooglecloudloggingconfigurationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationgooglecloudloggingconfigurationcreategooglecloudloggingconfiguration"></a>`googleCloudLoggingConfiguration` | [`GoogleCloudLoggingConfigurationType`](#googlecloudloggingconfigurationtype) | configuration created. | + ### `Mutation.groupMemberBulkUpdate` Input type: `GroupMemberBulkUpdateInput` @@ -15154,6 +15177,21 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="geonodeuploadregistriesreplicationstate"></a>`replicationState` | [`ReplicationStateEnum`](#replicationstateenum) | Filters registries by their replication state. | | <a id="geonodeuploadregistriesverificationstate"></a>`verificationState` | [`VerificationStateEnum`](#verificationstateenum) | Filters registries by their verification state. | +### `GoogleCloudLoggingConfigurationType` + +Stores Google Cloud Logging configurations associated with IAM service accounts,used for generating access tokens. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="googlecloudloggingconfigurationtypeclientemail"></a>`clientEmail` | [`String!`](#string) | Client email. | +| <a id="googlecloudloggingconfigurationtypegoogleprojectidname"></a>`googleProjectIdName` | [`String!`](#string) | Google project ID. | +| <a id="googlecloudloggingconfigurationtypegroup"></a>`group` | [`Group!`](#group) | Group the configuration belongs to. | +| <a id="googlecloudloggingconfigurationtypeid"></a>`id` | [`ID!`](#id) | ID of the configuration. | +| <a id="googlecloudloggingconfigurationtypelogidname"></a>`logIdName` | [`String!`](#string) | Log ID. | +| <a id="googlecloudloggingconfigurationtypeprivatekey"></a>`privateKey` | [`String!`](#string) | Private key. | + ### `GpgSignature` GPG signature for a signed commit. diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 10c0f7fcf5267..d88d1447fe664 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -115,6 +115,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::InstanceExternalAuditEventDestinations::Create mount_mutation ::Mutations::AuditEvents::InstanceExternalAuditEventDestinations::Destroy mount_mutation ::Mutations::AuditEvents::InstanceExternalAuditEventDestinations::Update + mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Create mount_mutation ::Mutations::Forecasting::BuildForecast, alpha: { milestone: '16.0' } prepend(Types::DeprecatedMutations) diff --git a/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/create.rb b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/create.rb new file mode 100644 index 0000000000000..1bed033a0dfcb --- /dev/null +++ b/ee/app/graphql/mutations/audit_events/google_cloud_logging_configurations/create.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Mutations + module AuditEvents + module GoogleCloudLoggingConfigurations + class Create < BaseMutation + graphql_name 'GoogleCloudLoggingConfigurationCreate' + + authorize :admin_external_audit_events + + argument :group_path, GraphQL::Types::ID, + required: true, + description: 'Group path.' + + argument :google_project_id_name, GraphQL::Types::String, + required: true, + description: 'Google project ID.' + + argument :client_email, GraphQL::Types::String, + required: true, + description: 'Client email.' + + argument :log_id_name, GraphQL::Types::String, + required: false, + description: 'Log ID. (defaults to `audit_events`).', + default_value: 'audit_events' + + argument :private_key, GraphQL::Types::String, + required: true, + description: 'Private key.' + + field :google_cloud_logging_configuration, ::Types::AuditEvents::GoogleCloudLoggingConfigurationType, + null: true, + description: 'configuration created.' + + def resolve(group_path:, google_project_id_name:, client_email:, private_key:, log_id_name: nil) + group = authorized_find!(group_path) + config_attributes = { + group: group, + google_project_id_name: google_project_id_name, + client_email: client_email, + private_key: private_key + } + + config_attributes[:log_id_name] = log_id_name if log_id_name.present? + + config = ::AuditEvents::GoogleCloudLoggingConfiguration.new(config_attributes) + + if config.save + { google_cloud_logging_configuration: config, errors: [] } + else + { google_cloud_logging_configuration: nil, errors: Array(config.errors) } + end + end + + private + + def find_object(group_path) + ::Group.find_by_full_path(group_path) + end + end + end + end +end diff --git a/ee/app/graphql/types/audit_events/google_cloud_logging_configuration_type.rb b/ee/app/graphql/types/audit_events/google_cloud_logging_configuration_type.rb new file mode 100644 index 0000000000000..5d5eacea8a87f --- /dev/null +++ b/ee/app/graphql/types/audit_events/google_cloud_logging_configuration_type.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Types + module AuditEvents + class GoogleCloudLoggingConfigurationType < ::Types::BaseObject + graphql_name 'GoogleCloudLoggingConfigurationType' + description 'Stores Google Cloud Logging configurations associated with IAM service accounts,' \ + 'used for generating access tokens.' + authorize :admin_external_audit_events + + field :group, ::Types::GroupType, + null: false, + description: 'Group the configuration belongs to.' + + field :id, GraphQL::Types::ID, + null: false, + description: 'ID of the configuration.' + + field :google_project_id_name, GraphQL::Types::String, + null: false, + description: 'Google project ID.' + + field :client_email, GraphQL::Types::String, + null: false, + description: 'Client email.' + + field :log_id_name, GraphQL::Types::String, + null: false, + description: 'Log ID.' + + field :private_key, GraphQL::Types::String, + null: false, + description: 'Private key.' + end + end +end diff --git a/ee/app/models/audit_events/google_cloud_logging_configuration.rb b/ee/app/models/audit_events/google_cloud_logging_configuration.rb index b5cd49df8ab37..583e0bbd570e0 100644 --- a/ee/app/models/audit_events/google_cloud_logging_configuration.rb +++ b/ee/app/models/audit_events/google_cloud_logging_configuration.rb @@ -11,6 +11,10 @@ class GoogleCloudLoggingConfiguration < ApplicationRecord GOOGLE_PROJECT_ID_NAME_REGEX = %r{\A[a-z][a-z0-9-]*[a-z0-9]\z} LOG_ID_NAME_REGEX = %r{\A[\w/.-]+\z} + DEFAULT_LOG_ID_NAME = "audit_events" + + attribute :log_id_name, :string, default: DEFAULT_LOG_ID_NAME + belongs_to :group, class_name: '::Group', foreign_key: 'namespace_id', inverse_of: :google_cloud_logging_configurations @@ -18,7 +22,8 @@ class GoogleCloudLoggingConfiguration < ApplicationRecord format: { with: GOOGLE_PROJECT_ID_NAME_REGEX, message: 'must only contain lowercase letters, digits, or hyphens, ' \ 'and must start and end with a letter or digit' }, - length: { in: 6..30 } + length: { in: 6..30 }, + uniqueness: { scope: [:namespace_id, :log_id_name] } validates :log_id_name, presence: true, format: { with: LOG_ID_NAME_REGEX, diff --git a/ee/app/policies/audit_events/google_cloud_logging_configuration_policy.rb b/ee/app/policies/audit_events/google_cloud_logging_configuration_policy.rb new file mode 100644 index 0000000000000..7edd961ccbca8 --- /dev/null +++ b/ee/app/policies/audit_events/google_cloud_logging_configuration_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module AuditEvents + class GoogleCloudLoggingConfigurationPolicy < ::BasePolicy + delegate { @subject.group } + end +end diff --git a/ee/spec/models/audit_events/google_cloud_logging_configuration_spec.rb b/ee/spec/models/audit_events/google_cloud_logging_configuration_spec.rb index f803228742885..7bb9c9622c4f9 100644 --- a/ee/spec/models/audit_events/google_cloud_logging_configuration_spec.rb +++ b/ee/spec/models/audit_events/google_cloud_logging_configuration_spec.rb @@ -45,6 +45,24 @@ it { is_expected.not_to allow_value('#AUDIT_EVENT').for(:log_id_name) } it { is_expected.not_to allow_value('%audit_events/123').for(:log_id_name) } + context 'when the same google_project_id_name for the same namespace and log_id_name exists' do + let(:group) { create(:group) } + let(:google_project_id_name) { 'valid-project-id' } + let(:log_id_name) { 'audit_events' } + + before do + create(:google_cloud_logging_configuration, group: group, google_project_id_name: google_project_id_name, + log_id_name: log_id_name) + end + + it 'is not valid and adds an error message' do + config = build(:google_cloud_logging_configuration, group: group, + google_project_id_name: google_project_id_name, log_id_name: log_id_name) + expect(config).not_to be_valid + expect(config.errors[:google_project_id_name]).to include('has already been taken') + end + end + context 'when the group is a subgroup' do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } @@ -60,6 +78,12 @@ end end + describe 'default values' do + it "uses 'audit_events' as default value for log_id_name" do + expect(described_class.new.log_id_name).to eq('audit_events') + end + end + it_behaves_like 'includes Limitable concern' do subject { build(:google_cloud_logging_configuration, group: create(:group)) } end diff --git a/ee/spec/requests/api/graphql/mutations/audit_events/google_cloud_logging_configurations/create_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/google_cloud_logging_configurations/create_spec.rb new file mode 100644 index 0000000000000..99a38f4af44f7 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/audit_events/google_cloud_logging_configurations/create_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create google cloud logging configuration', feature_category: :audit_events do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:owner) { create(:user) } + let_it_be(:google_project_id_name) { 'test-project' } + let_it_be(:client_email) { 'test-email@example.com' } + let_it_be(:private_key) { OpenSSL::PKey::RSA.new(4096).to_pem } + let_it_be(:default_log_id_name) { 'audit_events' } + + let(:current_user) { owner } + let(:mutation) { graphql_mutation(:google_cloud_logging_configuration_create, input) } + let(:mutation_response) { graphql_mutation_response(:google_cloud_logging_configuration_create) } + + let(:input) do + { + groupPath: group.full_path, + googleProjectIdName: google_project_id_name, + clientEmail: client_email, + privateKey: private_key + } + end + + subject(:mutate) { post_graphql_mutation(mutation, current_user: owner) } + + shared_examples 'a mutation that does not create a configuration' do + it 'does not destroy the configuration' do + expect { mutate } + .not_to change { AuditEvents::GoogleCloudLoggingConfiguration.count } + end + end + + shared_examples 'an unauthorized mutation that does not create a configuration' do + it_behaves_like 'a mutation on an unauthorized resource' + it_behaves_like 'a mutation that does not create a configuration' + end + + context 'when feature is licensed' do + before do + stub_licensed_features(external_audit_events: true) + end + + context 'when current user is a group owner' do + before do + group.add_owner(owner) + end + + it 'resolves group by full path' do + expect(::Group).to receive(:find_by_full_path).with(group.full_path) + + mutate + end + + it 'creates the configuration' do + expect { mutate } + .to change { AuditEvents::GoogleCloudLoggingConfiguration.count }.by(1) + + config = AuditEvents::GoogleCloudLoggingConfiguration.last + expect(config.group).to eq(group) + expect(config.google_project_id_name).to eq(google_project_id_name) + expect(config.client_email).to eq(client_email) + expect(config.log_id_name).to eq(default_log_id_name) + expect(config.private_key).to eq(private_key) + end + + context 'when overriding log id name' do + let_it_be(:log_id_name) { 'test-log-id' } + + let(:input) do + { + groupPath: group.full_path, + googleProjectIdName: google_project_id_name, + clientEmail: client_email, + privateKey: private_key, + logIdName: log_id_name + } + end + + it 'creates the configuration' do + expect { mutate } + .to change { AuditEvents::GoogleCloudLoggingConfiguration.count }.by(1) + + config = AuditEvents::GoogleCloudLoggingConfiguration.last + expect(config.group).to eq(group) + expect(config.google_project_id_name).to eq(google_project_id_name) + expect(config.client_email).to eq(client_email) + expect(config.log_id_name).to eq(log_id_name) + expect(config.private_key).to eq(private_key) + end + end + + context 'when there is error while saving' do + before do + allow_next_instance_of(AuditEvents::GoogleCloudLoggingConfiguration) do |instance| + allow(instance).to receive(:save).and_return(false) + + errors = ActiveModel::Errors.new(instance).tap { |e| e.add(:log_id_name, 'error message') } + allow(instance).to receive(:errors).and_return(errors) + end + end + + it 'does not create the configuration and returns the error' do + expect { mutate } + .not_to change { AuditEvents::GoogleCloudLoggingConfiguration.count } + + expect(mutation_response).to include( + 'googleCloudLoggingConfiguration' => nil, + 'errors' => ["Log id name error message"] + ) + end + end + end + + context 'when current user is a group maintainer' do + before do + group.add_maintainer(owner) + end + + it_behaves_like 'an unauthorized mutation that does not create a configuration' + end + + context 'when current user is a group developer' do + before do + group.add_developer(owner) + end + + it_behaves_like 'an unauthorized mutation that does not create a configuration' + end + + context 'when current user is a group guest' do + before do + group.add_guest(owner) + end + + it_behaves_like 'an unauthorized mutation that does not create a configuration' + end + end + + context 'when feature is unlicensed' do + before do + stub_licensed_features(external_audit_events: false) + end + + it_behaves_like 'an unauthorized mutation that does not create a configuration' + end +end -- GitLab