diff --git a/app/models/member.rb b/app/models/member.rb index 845a711e986cd762e1075cee5bebbffe008da90b..d31016567390f32bf498aa51aecd6c96fdaedb71 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -288,6 +288,14 @@ class Member < ApplicationRecord refresh_member_authorized_projects end + after_create if: :update_organization_user? do + Organizations::OrganizationUser.upsert( + { organization_id: source.organization_id, user_id: user_id, access_level: :default }, + unique_by: [:organization_id, :user_id], + on_duplicate: :skip # Do not change access_level, could make :owner :default + ) + end + attribute :notification_level, default: -> { NotificationSetting.levels[:global] } class << self @@ -657,6 +665,12 @@ def project_bot? user&.project_bot? end + def update_organization_user? + return false unless Feature.enabled?(:update_organization_users, source.root_ancestor, type: :gitlab_com_derisk) + + !invite? && source.organization.present? + end + def log_invitation_token_cleanup return true unless Gitlab.com? && invite? && invite_accepted_at? diff --git a/app/policies/organizations/organization_policy.rb b/app/policies/organizations/organization_policy.rb index afd8c6e144f3470d0cc08bf7ce7855da75f92a90..a203a58b16435716a038d25f7f52dc7a03b83bb8 100644 --- a/app/policies/organizations/organization_policy.rb +++ b/app/policies/organizations/organization_policy.rb @@ -3,6 +3,7 @@ module Organizations class OrganizationPolicy < BasePolicy condition(:organization_user) { @subject.user?(@user) } + condition(:organization_owner) { @subject.owner?(@user) } desc 'Organization is public' condition(:public_organization, scope: :subject, score: 0) { true } @@ -18,11 +19,14 @@ class OrganizationPolicy < BasePolicy enable :read_organization_user end - rule { organization_user }.policy do + rule { organization_owner }.policy do enable :admin_organization - enable :create_group + end + + rule { organization_user }.policy do enable :read_organization enable :read_organization_user + enable :create_group end end end diff --git a/config/feature_flags/gitlab_com_derisk/update_organization_users.yml b/config/feature_flags/gitlab_com_derisk/update_organization_users.yml new file mode 100644 index 0000000000000000000000000000000000000000..090748501505c5c6df13535e27d7d0b28fad33a0 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/update_organization_users.yml @@ -0,0 +1,9 @@ +--- +name: update_organization_users +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419366 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139188 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/435868 +milestone: '16.8' +group: group::tenant scale +type: gitlab_com_derisk +default_enabled: false diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 807df94e1150b7f70f46226976770646938b9e62..2f55c3ab567a24b7cb177b74655fed57ca70b898 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -18,6 +18,10 @@ create(:namespace_settings, namespace: group) unless group.namespace_settings end + trait :with_organization do + association :organization + end + trait :public do visibility_level { Gitlab::VisibilityLevel::PUBLIC } end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 9419bfa76a76cdfe83672b675a200ac0fb575822..bd74af9b7e83846e942cc881a3cfda167387aee8 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -1084,6 +1084,110 @@ end end + context 'for updating organization_users' do + let_it_be(:group) { create(:group, :with_organization) } + let(:member) { create(:group_member, source: group) } + let(:update_organization_users_enabled) { true } + + before do + stub_feature_flags(update_organization_users: update_organization_users_enabled) + end + + context 'when update_organization_users is enabled' do + it 'inserts new record on member creation' do + expect { member }.to change { Organizations::OrganizationUser.count }.by(1) + record_attrs = { organization: group.organization, user: member.user, access_level: :default } + expect(Organizations::OrganizationUser.exists?(record_attrs)).to be(true) + end + + context 'when user already exists in the organization_users' do + context 'for an already existing default organization_user' do + let_it_be(:project) { create(:project, group: group, organization: group.organization) } + + before do + member + end + + it 'does not insert a new record in organization_users' do + expect do + create(:project_member, :owner, source: project, user: member.user) + end.not_to change { Organizations::OrganizationUser.count } + + expect( + Organizations::OrganizationUser.exists?( + organization: project.organization, user: member.user, access_level: :default + ) + ).to be(true) + end + + it 'does not update timestamps' do + travel_to(1.day.from_now) do + expect do + create(:project_member, :owner, source: project, user: member.user) + end.not_to change { Organizations::OrganizationUser.last.updated_at } + end + end + end + + context 'for an already existing owner organization_user' do + let_it_be(:user) { create(:user) } + let_it_be(:common_attrs) { { organization: group.organization, user: user } } + + before_all do + create(:organization_user, :owner, common_attrs) + end + + it 'does not insert a new record in organization_users nor update the access_level' do + expect do + create(:group_member, :owner, source: group, user: user) + end.not_to change { Organizations::OrganizationUser.count } + + expect( + Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :default)) + ).to be(false) + expect( + Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :owner)) + ).to be(true) + end + end + end + + context 'when updating the organization_users is not successful' do + it 'rolls back the member creation' do + allow(Organizations::OrganizationUser).to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout) + + expect { member }.to raise_error(ActiveRecord::StatementTimeout) + expect(Organizations::OrganizationUser.exists?(organization: group.organization)).to be(false) + expect(group.group_members).to be_empty + end + end + end + + shared_examples_for 'does not create an organization_user entry' do + specify do + expect { member }.not_to change { Organizations::OrganizationUser.count } + end + end + + context 'when update_organization_users is disabled' do + let(:update_organization_users_enabled) { false } + + it_behaves_like 'does not create an organization_user entry' + end + + context 'when member is an invite' do + let(:member) { create(:group_member, :invited, source: group) } + + it_behaves_like 'does not create an organization_user entry' + end + + context 'when organization does not exist' do + let(:member) { create(:group_member) } + + it_behaves_like 'does not create an organization_user entry' + end + end + context 'when after_commit :update_highest_role' do let_it_be(:user) { create(:user) } diff --git a/spec/policies/organizations/organization_policy_spec.rb b/spec/policies/organizations/organization_policy_spec.rb index a1a2f1db30590ed5b81bb25356aaedf615be7d47..9660ed578f7147bfd9d433d4cd4273cf9015056c 100644 --- a/spec/policies/organizations/organization_policy_spec.rb +++ b/spec/policies/organizations/organization_policy_spec.rb @@ -32,8 +32,19 @@ end context 'when the user is part of the organization' do - before do - create :organization_user, organization: organization, user: current_user + before_all do + create(:organization_user, organization: organization, user: current_user) + end + + it { is_expected.to be_disallowed(:admin_organization) } + it { is_expected.to be_allowed(:create_group) } + it { is_expected.to be_allowed(:read_organization) } + it { is_expected.to be_allowed(:read_organization_user) } + end + + context 'when the user is an owner of the organization' do + before_all do + create(:organization_user, :owner, organization: organization, user: current_user) end it { is_expected.to be_allowed(:admin_organization) } diff --git a/spec/requests/api/graphql/mutations/organizations/update_spec.rb b/spec/requests/api/graphql/mutations/organizations/update_spec.rb index 4e819c280d02dcc12e57c8799b735c46c1e393bc..33890ae45921d198449cb197db64e6b4d99ab383 100644 --- a/spec/requests/api/graphql/mutations/organizations/update_spec.rb +++ b/spec/requests/api/graphql/mutations/organizations/update_spec.rb @@ -8,7 +8,7 @@ let_it_be(:user) { create(:user) } let_it_be_with_reload(:organization) do - create(:organization) { |org| create(:organization_user, organization: org, user: user) } + create(:organization) { |org| create(:organization_user, :owner, organization: org, user: user) } end let(:mutation) { graphql_mutation(:organization_update, params) } diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index 73bcfe81d76a53a285bd1bbf41e13740c8a5630c..14becd52e93fbb384e6dddcee08028931dca4ec0 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -67,13 +67,13 @@ it 'returns correct organization user fields' do request_organization - organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first + organization_user_nodes = graphql_data_at(:organization, :organizationUsers, :nodes) expected_attributes = { "badges" => [{ "text" => "It's you!", "variant" => 'muted' }], "id" => organization_user.to_global_id.to_s, "user" => { "id" => user.to_global_id.to_s } } - expect(organization_user_node).to match(expected_attributes) + expect(organization_user_nodes).to include(expected_attributes) end it 'avoids N+1 queries for all the fields' do diff --git a/spec/requests/organizations/settings_controller_spec.rb b/spec/requests/organizations/settings_controller_spec.rb index 1d98e59815902c30e65696143baed925ec845558..0177187e3a3f9fa7bd578f38c017b6642bc2343a 100644 --- a/spec/requests/organizations/settings_controller_spec.rb +++ b/spec/requests/organizations/settings_controller_spec.rb @@ -21,13 +21,13 @@ end context 'when the user is signed in' do + let_it_be(:user) { create(:user) } + before do sign_in(user) end context 'with no association to an organization' do - let_it_be(:user) { create(:user) } - it_behaves_like 'organization - not found response' it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' end @@ -39,11 +39,18 @@ it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' end - context 'as an organization user' do - let_it_be(:user) { create :user } + context 'as a default organization user' do + before_all do + create(:organization_user, organization: organization, user: user) + end - before do - create :organization_user, organization: organization, user: user + it_behaves_like 'organization - not found response' + it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag' + end + + context 'as an owner of an organization' do + before_all do + create(:organization_user, :owner, organization: organization, user: user) end it_behaves_like 'organization - successful response' diff --git a/spec/services/organizations/update_service_spec.rb b/spec/services/organizations/update_service_spec.rb index 630bfdfe1d775bbb47fabaff99743850da169b68..30c07ae1d139fa65a63a0997d6821bfd7a88720b 100644 --- a/spec/services/organizations/update_service_spec.rb +++ b/spec/services/organizations/update_service_spec.rb @@ -32,7 +32,7 @@ context 'when user has permission' do before_all do - create(:organization_user, organization: organization, user: current_user) + create(:organization_user, :owner, organization: organization, user: current_user) end shared_examples 'updating an organization' do