Commit 7e9a17a5 authored by Max Woolf's avatar Max Woolf Committed by Dmitry Gruzd

Set audit event HTTP destination

parent 6fa4c0bb
# frozen_string_literal: true
class CreateExternalAuditEventDestinationsTable < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
create_table :audit_events_external_audit_event_destinations do |t|
t.references :namespace, index: false, null: false, foreign_key: { on_delete: :cascade }
t.text :destination_url, null: false, limit: 255 # rubocop:disable Migration/AddLimitToTextColumns
t.timestamps_with_timezone null: false
t.index [:namespace_id, :destination_url], unique: true, name: 'index_external_audit_event_destinations_on_namespace_id'
end
end
end
# frozen_string_literal: true
class AddExternalEventDestinationLimitToPlanLimits < Gitlab::Database::Migration[1.0]
def change
add_column(:plan_limits, :external_audit_event_destinations, :integer, default: 5, null: false)
end
end
eab87cb4abfad7542fcff7c25d984e4a7588c824a13b379cb16c87d0c077cfbb
\ No newline at end of file
cc53e8c85fdb00c0772987516e0c23f5349cc6dc1e21b4124eb50efdaa6a4fcd
\ No newline at end of file
......@@ -10604,6 +10604,24 @@ CREATE SEQUENCE atlassian_identities_user_id_seq
ALTER SEQUENCE atlassian_identities_user_id_seq OWNED BY atlassian_identities.user_id;
CREATE TABLE audit_events_external_audit_event_destinations (
id bigint NOT NULL,
namespace_id bigint NOT NULL,
destination_url text NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT check_2feafb9daf CHECK ((char_length(destination_url) <= 255))
);
CREATE SEQUENCE audit_events_external_audit_event_destinations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE audit_events_external_audit_event_destinations_id_seq OWNED BY audit_events_external_audit_event_destinations.id;
CREATE SEQUENCE audit_events_id_seq
START WITH 1
INCREMENT BY 1
......@@ -17372,7 +17390,8 @@ CREATE TABLE plan_limits (
ci_max_artifact_size_cluster_image_scanning integer DEFAULT 0 NOT NULL,
ci_jobs_trace_size_limit integer DEFAULT 100 NOT NULL,
pages_file_entries integer DEFAULT 200000 NOT NULL,
dast_profile_schedules integer DEFAULT 1 NOT NULL
dast_profile_schedules integer DEFAULT 1 NOT NULL,
external_audit_event_destinations integer DEFAULT 5 NOT NULL
);
CREATE SEQUENCE plan_limits_id_seq
......@@ -20929,6 +20948,8 @@ ALTER TABLE ONLY atlassian_identities ALTER COLUMN user_id SET DEFAULT nextval('
ALTER TABLE ONLY audit_events ALTER COLUMN id SET DEFAULT nextval('audit_events_id_seq'::regclass);
ALTER TABLE ONLY audit_events_external_audit_event_destinations ALTER COLUMN id SET DEFAULT nextval('audit_events_external_audit_event_destinations_id_seq'::regclass);
ALTER TABLE ONLY authentication_events ALTER COLUMN id SET DEFAULT nextval('authentication_events_id_seq'::regclass);
ALTER TABLE ONLY award_emoji ALTER COLUMN id SET DEFAULT nextval('award_emoji_id_seq'::regclass);
......@@ -22300,6 +22321,9 @@ ALTER TABLE ONLY ar_internal_metadata
ALTER TABLE ONLY atlassian_identities
ADD CONSTRAINT atlassian_identities_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY audit_events_external_audit_event_destinations
ADD CONSTRAINT audit_events_external_audit_event_destinations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY audit_events
ADD CONSTRAINT audit_events_pkey PRIMARY KEY (id, created_at);
......@@ -25084,6 +25108,8 @@ CREATE UNIQUE INDEX index_experiments_on_name ON experiments USING btree (name);
CREATE INDEX index_expired_and_not_notified_personal_access_tokens ON personal_access_tokens USING btree (id, expires_at) WHERE ((impersonation = false) AND (revoked = false) AND (expire_notification_delivered = false));
CREATE UNIQUE INDEX index_external_audit_event_destinations_on_namespace_id ON audit_events_external_audit_event_destinations USING btree (namespace_id, destination_url);
CREATE UNIQUE INDEX index_external_pull_requests_on_project_and_branches ON external_pull_requests USING btree (project_id, source_branch, target_branch);
CREATE UNIQUE INDEX index_feature_flag_scopes_on_flag_id_and_environment_scope ON operations_feature_flag_scopes USING btree (feature_flag_id, environment_scope);
......@@ -28276,6 +28302,9 @@ ALTER TABLE ONLY packages_conan_file_metadata
ALTER TABLE ONLY ci_build_pending_states
ADD CONSTRAINT fk_rails_0bbbfeaf9d FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
ALTER TABLE ONLY audit_events_external_audit_event_destinations
ADD CONSTRAINT fk_rails_0bc80a4edc FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY operations_user_lists
ADD CONSTRAINT fk_rails_0c716e079b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......@@ -2433,6 +2433,64 @@ Input type: `ExportRequirementsInput`
| <a id="mutationexportrequirementsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexportrequirementserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.externalAuditEventDestinationCreate`
Input type: `ExternalAuditEventDestinationCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationexternalauditeventdestinationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexternalauditeventdestinationcreatedestinationurl"></a>`destinationUrl` | [`String!`](#string) | Destination URL. |
| <a id="mutationexternalauditeventdestinationcreategrouppath"></a>`groupPath` | [`ID!`](#id) | Group path. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationexternalauditeventdestinationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexternalauditeventdestinationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationexternalauditeventdestinationcreateexternalauditeventdestination"></a>`externalAuditEventDestination` | [`ExternalAuditEventDestination`](#externalauditeventdestination) | Destination created. |
### `Mutation.externalAuditEventDestinationDestroy`
Input type: `ExternalAuditEventDestinationDestroyInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationexternalauditeventdestinationdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexternalauditeventdestinationdestroyid"></a>`id` | [`AuditEventsExternalAuditEventDestinationID!`](#auditeventsexternalauditeventdestinationid) | ID of external audit event destination to destroy. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationexternalauditeventdestinationdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexternalauditeventdestinationdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.externalAuditEventDestinationUpdate`
Input type: `ExternalAuditEventDestinationUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationexternalauditeventdestinationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexternalauditeventdestinationupdatedestinationurl"></a>`destinationUrl` | [`String`](#string) | Destination URL to change. |
| <a id="mutationexternalauditeventdestinationupdateid"></a>`id` | [`AuditEventsExternalAuditEventDestinationID!`](#auditeventsexternalauditeventdestinationid) | ID of external audit event destination to destroy. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationexternalauditeventdestinationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationexternalauditeventdestinationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationexternalauditeventdestinationupdateexternalauditeventdestination"></a>`externalAuditEventDestination` | [`ExternalAuditEventDestination`](#externalauditeventdestination) | Updated destination. |
### `Mutation.gitlabSubscriptionActivate`
Input type: `GitlabSubscriptionActivateInput`
......@@ -5943,6 +6001,29 @@ The edge type for [`Event`](#event).
| <a id="eventedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="eventedgenode"></a>`node` | [`Event`](#event) | The item at the end of the edge. |
#### `ExternalAuditEventDestinationConnection`
The connection type for [`ExternalAuditEventDestination`](#externalauditeventdestination).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="externalauditeventdestinationconnectionedges"></a>`edges` | [`[ExternalAuditEventDestinationEdge]`](#externalauditeventdestinationedge) | A list of edges. |
| <a id="externalauditeventdestinationconnectionnodes"></a>`nodes` | [`[ExternalAuditEventDestination]`](#externalauditeventdestination) | A list of nodes. |
| <a id="externalauditeventdestinationconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ExternalAuditEventDestinationEdge`
The edge type for [`ExternalAuditEventDestination`](#externalauditeventdestination).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="externalauditeventdestinationedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="externalauditeventdestinationedgenode"></a>`node` | [`ExternalAuditEventDestination`](#externalauditeventdestination) | The item at the end of the edge. |
#### `GroupConnection`
The connection type for [`Group`](#group).
......@@ -9837,6 +9918,18 @@ Representing an event.
| <a id="eventid"></a>`id` | [`ID!`](#id) | ID of the event. |
| <a id="eventupdatedat"></a>`updatedAt` | [`Time!`](#time) | When this event was updated. |
### `ExternalAuditEventDestination`
Represents an external resource to send audit events to.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="externalauditeventdestinationdestinationurl"></a>`destinationUrl` | [`String!`](#string) | External destination to send audit events to. |
| <a id="externalauditeventdestinationgroup"></a>`group` | [`Group!`](#group) | Group the destination belongs to. |
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
### `ExternalIssue`
Represents an external issue.
......@@ -10062,6 +10155,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupemailsdisabled"></a>`emailsDisabled` | [`Boolean`](#boolean) | Indicates if a group has email notifications disabled. |
| <a id="groupepicboards"></a>`epicBoards` | [`EpicBoardConnection`](#epicboardconnection) | Find epic boards. (see [Connections](#connections)) |
| <a id="groupepicsenabled"></a>`epicsEnabled` | [`Boolean`](#boolean) | Indicates if Epics are enabled for namespace. |
| <a id="groupexternalauditeventdestinations"></a>`externalAuditEventDestinations` | [`ExternalAuditEventDestinationConnection`](#externalauditeventdestinationconnection) | External locations that receive audit events belonging to the group. Available only when feature flag `ff_external_audit_events_namespace` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) |
| <a id="groupfullname"></a>`fullName` | [`String!`](#string) | Full name of the namespace. |
| <a id="groupfullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace. |
| <a id="groupid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
......@@ -16709,6 +16803,12 @@ A `AnalyticsDevopsAdoptionEnabledNamespaceID` is a global ID. It is encoded as a
An example `AnalyticsDevopsAdoptionEnabledNamespaceID` is: `"gid://gitlab/Analytics::DevopsAdoption::EnabledNamespace/1"`.
### `AuditEventsExternalAuditEventDestinationID`
A `AuditEventsExternalAuditEventDestinationID` is a global ID. It is encoded as a string.
An example `AuditEventsExternalAuditEventDestinationID` is: `"gid://gitlab/AuditEvents::ExternalAuditEventDestination/1"`.
### `AwardableID`
A `AwardableID` is a global ID. It is encoded as a string.
......
# frozen_string_literal: true
module EE
module Types
module AuditEvents
class ExternalAuditEventDestinationType < ::Types::BaseObject
graphql_name 'ExternalAuditEventDestination'
description 'Represents an external resource to send audit events to'
field :id, GraphQL::Types::ID,
null: false,
description: 'ID of the destination.'
field :destination_url, GraphQL::Types::String,
null: false,
description: 'External destination to send audit events to.'
field :group, ::Types::GroupType,
null: false,
description: 'Group the destination belongs to.'
end
end
end
end
......@@ -91,6 +91,13 @@ module EE
null: true,
method: :itself,
description: "Group's DORA metrics."
field :external_audit_event_destinations,
EE::Types::AuditEvents::ExternalAuditEventDestinationType.connection_type,
null: true,
description: 'External locations that receive audit events belonging to the group.',
feature_flag: :ff_external_audit_events_namespace,
authorize: :admin_external_audit_events
end
end
end
......
......@@ -83,6 +83,9 @@ module EE
mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject
mount_mutation ::Mutations::SecurityPolicy::CreateSecurityPolicyProject
mount_mutation ::Mutations::Security::CiConfiguration::ConfigureDependencyScanning
mount_mutation ::Mutations::AuditEvents::ExternalAuditEventDestinations::Create
mount_mutation ::Mutations::AuditEvents::ExternalAuditEventDestinations::Destroy
mount_mutation ::Mutations::AuditEvents::ExternalAuditEventDestinations::Update
prepend(Types::DeprecatedMutations)
end
......
# frozen_string_literal: true
module Mutations
module AuditEvents
module ExternalAuditEventDestinations
class Create < BaseMutation
graphql_name 'ExternalAuditEventDestinationCreate'
authorize :admin_external_audit_events
argument :destination_url, GraphQL::Types::String,
required: true,
description: 'Destination URL.'
argument :group_path, GraphQL::Types::ID,
required: true,
description: 'Group path.'
field :external_audit_event_destination, EE::Types::AuditEvents::ExternalAuditEventDestinationType,
null: true,
description: 'Destination created.'
def resolve(destination_url:, group_path:)
group = authorized_find!(group_path)
destination = ::AuditEvents::ExternalAuditEventDestination.create(group: group, destination_url: destination_url)
{
external_audit_event_destination: destination&.persisted? ? destination : nil,
errors: Array(destination.errors)
}
end
private
def find_object(group_path)
::GroupFinder.new(current_user).execute(path: group_path)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module AuditEvents
module ExternalAuditEventDestinations
class Destroy < BaseMutation
graphql_name 'ExternalAuditEventDestinationDestroy'
authorize :admin_external_audit_events
argument :id, ::Types::GlobalIDType[::AuditEvents::ExternalAuditEventDestination],
required: true,
description: 'ID of external audit event destination to destroy.'
def resolve(id:)
destination = authorized_find!(id)
destination.destroy if destination
{
external_audit_event_destination: nil,
errors: []
}
end
private
def find_object(destination_gid)
GitlabSchema.object_from_id(destination_gid, expected_type: ::AuditEvents::ExternalAuditEventDestination).sync
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module AuditEvents
module ExternalAuditEventDestinations
class Update < BaseMutation
graphql_name 'ExternalAuditEventDestinationUpdate'
authorize :admin_external_audit_events
argument :id, ::Types::GlobalIDType[::AuditEvents::ExternalAuditEventDestination],
required: true,
description: 'ID of external audit event destination to destroy.'
argument :destinationUrl, GraphQL::Types::String,
required: false,
description: 'Destination URL to change.'
field :external_audit_event_destination, EE::Types::AuditEvents::ExternalAuditEventDestinationType,
null: true,
description: 'Updated destination.'
def resolve(id:, destination_url:)
destination = authorized_find!(id)
destination.update(destination_url: destination_url) if destination
{
external_audit_event_destination: destination,
errors: Array(destination.errors)
}
end
private
def find_object(destination_gid)
GitlabSchema.object_from_id(destination_gid, expected_type: ::AuditEvents::ExternalAuditEventDestination).sync
end
end
end
end
end
# frozen_string_literal: true
module AuditEvents
class ExternalAuditEventDestination < ApplicationRecord
include Limitable
self.limit_name = 'external_audit_event_destinations'
self.limit_scope = :group
self.table_name = 'audit_events_external_audit_event_destinations'
belongs_to :group, class_name: '::Group', foreign_key: 'namespace_id'
validates :destination_url, public_url: true, presence: true
validates :destination_url, uniqueness: { scope: :namespace_id }, length: { maximum: 255 }
end
end
......@@ -28,6 +28,7 @@ module EE
has_one :insight, foreign_key: :namespace_id
accepts_nested_attributes_for :insight, allow_destroy: true
has_one :scim_oauth_access_token
has_many :external_audit_event_destinations, class_name: "AuditEvents::ExternalAuditEventDestination", foreign_key: 'namespace_id'
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :saml_group_links, foreign_key: 'group_id'
......
......@@ -163,6 +163,7 @@ class License < ApplicationRecord
enterprise_templates
environment_alerts
evaluate_group_level_compliance_pipeline
external_audit_events
group_ci_cd_analytics
group_level_compliance_dashboard
group_level_devops_adoption
......
# frozen_string_literal: true
module AuditEvents
class ExternalAuditEventDestinationPolicy < ::BasePolicy
delegate { @subject.group }
end
end
......@@ -13,6 +13,9 @@ module EE
condition(:epics_available) { @subject.feature_available?(:epics) }
condition(:iterations_available) { @subject.feature_available?(:iterations) }
condition(:subepics_available) { @subject.feature_available?(:subepics) }
condition(:external_audit_events_available) do
@subject.feature_available?(:external_audit_events) && ::Feature.enabled?(:ff_external_audit_events_namespace, @subject, default_enabled: :yaml)
end
condition(:contribution_analytics_available) do
@subject.feature_available?(:contribution_analytics)
end
......@@ -387,6 +390,9 @@ module EE
rule { can?(:owner_access) & group_membership_export_available }.enable :export_group_memberships
rule { can?(:owner_access) & compliance_framework_available }.enable :admin_compliance_framework
rule { can?(:owner_access) & group_level_compliance_pipeline_available }.enable :admin_compliance_pipeline_configuration
rule { can?(:owner_access) & external_audit_events_available }.policy do
enable :admin_external_audit_events
end
end
override :lookup_access_level!
......
---
name: ff_external_audit_events_namespace
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70706
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338939
milestone: '14.4'
type: development
group: group::compliance
default_enabled: false
# frozen_string_literal: true
FactoryBot.define do
factory :external_audit_event_destination, class: 'AuditEvents::ExternalAuditEventDestination' do
group
destination_url { FFaker::Internet.http_url }
end
end
......@@ -20,6 +20,7 @@ RSpec.describe GitlabSchema.types['Group'] do
it { expect(described_class).to have_graphql_field(:code_coverage_activities) }
it { expect(described_class).to have_graphql_field(:stats) }
it { expect(described_class).to have_graphql_field(:billable_members_count) }
it { expect(described_class).to have_graphql_field(:external_audit_event_destinations) }
describe 'vulnerabilities' do
let_it_be(:group) { create(:group) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuditEvents::ExternalAuditEventDestination do
subject { build(:external_audit_event_destination) }
describe 'Associations' do
it { is_expected.to belong_to(:group) }
end
describe 'Validations' do
it { is_expected.to validate_uniqueness_of(:destination_url).scoped_to(:namespace_id) }
it { is_expected.to validate_length_of(:destination_url).is_at_most(255) }
it { is_expected.to validate_presence_of(:destination_url) }
end
it_behaves_like 'includes Limitable concern' do
subject { build(:external_audit_event_destination, group: create(:group)) }
end
end
......@@ -31,6 +31,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:iterations) }
it { is_expected.to have_many(:iterations_cadences) }
it { is_expected.to have_many(:epic_board_recent_visits).inverse_of(:group) }
it { is_expected.to have_many(:external_audit_event_destinations) }
it_behaves_like 'model with wiki' do
let(:container) { create(:group, :nested, :wiki_repo) }
......
......@@ -1783,4 +1783,39 @@ RSpec.describe GroupPolicy do
end
end
end
context 'external audit events' do
let(:current_user) { owner }
context 'when feature is disabled' do
before do
stub_feature_flags(ff_external_audit_events_namespace: false)
stub_licensed_features(external_audit_events: true)
end
it { is_expected.to(be_disallowed(:admin_external_audit_events)) }
end
context 'when license is disabled' do
before do
stub_licensed_features(external_audit_events: false)
end
it { is_expected.to(be_disallowed(:admin_external_audit_events)) }
end
context 'when license is enabled' do
before do
stub_licensed_features(external_audit_events: true)
end
it { is_expected.to(be_allowed(:admin_external_audit_events)) }
end
context 'when user is not an owner' do
let(:current_user) { build_stubbed(:user, :auditor) }
it { is_expected.to(be_disallowed(:admin_external_audit_events)) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting a list of external audit event destinations for a group' do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:current_user) { create(:user) }
let_it_be(:destination_1) { create(:external_audit_event_destination, group: group) }
let_it_be(:destination_2) { create(:external_audit_event_destination, group: group) }
let(:path) { %i[group external_audit_event_destinations nodes] }
let!(:query) do
graphql_query_for(
:group, { full_path: group.full_path }, query_nodes(:external_audit_event_destinations)
)
end
shared_examples 'a request that returns no destinations' do
it 'returns no destinations' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(:group, :external_audit_event_destinations)).to be_nil
end
end
context 'when authenticated as the group owner' do
before do
stub_licensed_features(external_audit_events: true)
group.add_owner(current_user)
end
it 'returns the groups external audit event destinations' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(*path)).to contain_exactly(
a_hash_including('destinationUrl' => destination_1.destination_url),
a_hash_including('destinationUrl' => destination_2.destination_url)
)
end
end
context 'when authenticated as a group maintainer' do
before do
stub_licensed_features(external_audit_events: true)
group.add_maintainer(current_user)
end
it_behaves_like 'a request that returns no destinations'
end
context 'when authenticated as a group developer' do
before do
stub_licensed_features(external_audit_events: true)
group.add_developer(current_user)
end
it_behaves_like 'a request that returns no destinations'
end
context 'when authenticated as a group guest' do
before do
stub_licensed_features(external_audit_events: true)
group.add_guest(current_user)
end
it_behaves_like 'a request that returns no destinations'
end
context 'when not authenticated' do
before do
stub_licensed_features(external_audit_events: true)
end
let(:current_user) { nil }
it_behaves_like 'a request that returns no destinations'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create an external audit event destination' do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:owner) { create(:user) }
let(:current_user) { owner }
let(:input) do
{
'groupPath': group.full_path,
'destinationUrl': 'https://gitlab.com/example/testendpoint'
}
end
let(:mutation) { graphql_mutation(:external_audit_event_destination_create, input) }
let(:mutation_response) { graphql_mutation_response(:external_audit_event_destination_create) }
shared_examples 'a mutation that does not create a destination' do
it 'does not destroy the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvents::ExternalAuditEventDestination.count }
end
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 'creates the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvents::ExternalAuditEventDestination.count }.by(1)
end
end
context 'when current user is a group owner' do
before do
group.add_owner(owner)
end
it 'creates the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvents::ExternalAuditEventDestination.count }.by(1)
end
end
context 'when current user is a group maintainer' do
before do
group.add_maintainer(owner)
end
it_behaves_like 'a mutation that does not create a destination'
end
context 'when current user is a group developer' do
before do
group.add_developer(owner)
end
it_behaves_like 'a mutation that does not create a destination'
end
context 'when current user is a group guest' do
before do
group.add_guest(owner)
end
it_behaves_like 'a mutation that does not create a destination'
end
context 'when feature is disabled' do
before do
stub_feature_flags(ff_external_audit_events_namespace: false)
end
it_behaves_like 'a mutation on an unauthorized resource'
it 'does not create the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvents::ExternalAuditEventDestination.count }
end
end
end
context 'when feature is unlicensed' do
before do
stub_licensed_features(external_audit_events: false)
end
it_behaves_like 'a mutation on an unauthorized resource'
it 'does not create the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvents::ExternalAuditEventDestination.count }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Destroy an external audit event destination' do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:owner) { create(:user) }
let_it_be(:destination) { create(:external_audit_event_destination, group: group) }
let(:current_user) { owner }
let(:input) do
{
'id': GitlabSchema.id_from_object(destination).to_s
}
end
let(:mutation) { graphql_mutation(:external_audit_event_destination_destroy, input) }
let(:mutation_response) { graphql_mutation_response(:external_audit_event_destination_destroy) }
shared_examples 'a mutation that does not destroy a destination' do
it 'does not destroy the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { AuditEvents::ExternalAuditEventDestination.count }
end
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 but destination belongs to another group' do
before do
group.add_owner(owner)
destination.update!(group: create(:group))
end
it_behaves_like 'a mutation on an unauthorized resource'
it_behaves_like 'a mutation that does not destroy a destination'
end
context 'when current user is a group owner of a different group' do
before do
group_2 = create(:group)
group_2.add_owner(owner)
end
it_behaves_like 'a mutation on an unauthorized resource'
it_behaves_like 'a mutation that does not destroy a destination'
end
context 'when current user is a group owner' do
before do
group.add_owner(owner)
end
it 'destroys the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { AuditEvents::ExternalAuditEventDestination.count }.by(-1)
end
end
context 'when current user is a group maintainer' do
before do
group.add_maintainer(owner)
end
it_behaves_like 'a mutation that does not destroy a destination'
end
context 'when current user is a group developer' do
before do
group.add_developer(owner)
end
it_behaves_like 'a mutation that does not destroy a destination'
end
context 'when current user is a group guest' do
before do
group.add_guest(owner)
end
it_behaves_like 'a mutation that does not destroy a destination'
end
context 'when feature is disabled' do
before do
stub_feature_flags(ff_external_audit_events_namespace: false)
end
it_behaves_like 'a mutation on an unauthorized resource'
it_behaves_like 'a mutation that does not destroy a destination'
end
end
context 'when feature is unlicensed' do
before do
stub_licensed_features(external_audit_events: false)
end
it_behaves_like 'a mutation on an unauthorized resource'
it_behaves_like 'a mutation that does not destroy a destination'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Update an external audit event destination' do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:owner) { create(:user) }
let_it_be(:destination) { create(:external_audit_event_destination, group: group) }
let(:current_user) { owner }
let(:input) do
{
'id': GitlabSchema.id_from_object(destination).to_s,
'destinationUrl': "https://example.com/test"
}
end
let(:mutation) { graphql_mutation(:external_audit_event_destination_update, input) }
let(:mutation_response) { graphql_mutation_response(:external_audit_event_destination_update) }
shared_examples 'a mutation that does not update a destination' do
it 'does not destroy the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { destination.reload.destination_url }
end
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 but destination belongs to another group' do
before do
group.add_owner(owner)
destination.update!(group: create(:group))
end
it_behaves_like 'a mutation on an unauthorized resource'
it_behaves_like 'a mutation that does not update a destination'
end
context 'when current user is a group owner of a different group' do
before do
group_2 = create(:group)
group_2.add_owner(owner)
end
it_behaves_like 'a mutation on an unauthorized resource'
it_behaves_like 'a mutation that does not update a destination'
end
context 'when current user is a group owner' do
before do
group.add_owner(owner)
end
it 'updates the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.to change { destination.reload.destination_url }.to("https://example.com/test")
end
end
context 'when current user is a group maintainer' do
before do
group.add_maintainer(owner)
end
it_behaves_like 'a mutation that does not update a destination'
end
context 'when current user is a group developer' do
before do
group.add_developer(owner)
end
it_behaves_like 'a mutation that does not update a destination'
end
context 'when current user is a group guest' do
before do
group.add_guest(owner)
end
it_behaves_like 'a mutation that does not update a destination'
end
context 'when feature is disabled' do
before do
stub_feature_flags(ff_external_audit_events_namespace: false)
end
it_behaves_like 'a mutation on an unauthorized resource'
it 'does not update the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { destination.reload.destination_url }
end
end
end
context 'when feature is unlicensed' do
before do
stub_licensed_features(external_audit_events: false)
end
it_behaves_like 'a mutation on an unauthorized resource'
it 'does not destroy the destination' do
expect { post_graphql_mutation(mutation, current_user: owner) }
.not_to change { destination.reload.destination_url }
end
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