Skip to content
Snippets Groups Projects
Commit 937ddb53 authored by Marco Zille's avatar Marco Zille :eyes: Committed by drew stachon
Browse files

Added service and mutation to set user namespace commit email

Changelog: added
parent 5f22e48f
No related branches found
No related tags found
2 merge requests!119439Draft: Prevent file variable content expansion in downstream pipeline,!108236Added service and mutation to set user namespace commit email
# frozen_string_literal: true
module Mutations
module Users
class SetNamespaceCommitEmail < BaseMutation
graphql_name 'UserSetNamespaceCommitEmail'
argument :namespace_id,
::Types::GlobalIDType[::Namespace],
required: true,
description: 'ID of the namespace to set the namespace commit email for.'
argument :email_id,
::Types::GlobalIDType[::Email],
required: false,
description: 'ID of the email to set.'
field :namespace_commit_email,
Types::Users::NamespaceCommitEmailType,
null: true,
description: 'User namespace commit email after mutation.'
authorize :read_namespace
def resolve(args)
namespace = authorized_find!(args[:namespace_id])
args[:email_id] = args[:email_id].model_id
result = ::Users::SetNamespaceCommitEmailService.new(current_user, namespace, args[:email_id], {}).execute
{
namespace_commit_email: result.payload[:namespace_commit_email],
errors: result.errors
}
end
private
def find_object(id)
GitlabSchema.object_from_id(
id, expected_type: [::Namespace, ::Namespaces::UserNamespace, ::Namespaces::ProjectNamespace]).sync
end
end
end
end
......@@ -181,6 +181,7 @@ class MutationType < BaseObject
mount_mutation Mutations::Pages::MarkOnboardingComplete
mount_mutation Mutations::SavedReplies::Destroy
mount_mutation Mutations::Uploads::Delete
mount_mutation Mutations::Users::SetNamespaceCommitEmail
end
end
......
......@@ -2301,6 +2301,12 @@ def abuse_metadata
}
end
def namespace_commit_email_for_namespace(namespace)
return if namespace.nil?
namespace_commit_emails.find_by(namespace: namespace)
end
protected
# override, from Devise::Validatable
......
......@@ -31,6 +31,7 @@ class UserPolicy < BasePolicy
enable :read_user_groups
enable :read_saved_replies
enable :read_user_email_address
enable :admin_user_email_address
end
rule { default }.enable :read_user_profile
......
# frozen_string_literal: true
module Users
class SetNamespaceCommitEmailService
include Gitlab::Allowable
attr_reader :current_user, :target_user, :namespace, :email_id
def initialize(current_user, namespace, email_id, params)
@current_user = current_user
@target_user = params.delete(:user) || current_user
@namespace = namespace
@email_id = email_id
end
def execute
return error(_('Namespace must be provided.')) if namespace.nil?
unless can?(current_user, :admin_user_email_address, target_user)
return error(_("User doesn't exist or you don't have permission to change namespace commit emails."))
end
unless can?(target_user, :read_namespace, namespace)
return error(_("Namespace doesn't exist or you don't have permission."))
end
email = target_user.emails.find_by(id: email_id) unless email_id.nil? # rubocop: disable CodeReuse/ActiveRecord
existing_namespace_commit_email = target_user.namespace_commit_email_for_namespace(namespace)
if existing_namespace_commit_email.nil?
return error(_('Email must be provided.')) if email.nil?
create_namespace_commit_email(email)
elsif email_id.nil?
remove_namespace_commit_email(existing_namespace_commit_email)
else
update_namespace_commit_email(existing_namespace_commit_email, email)
end
end
private
def remove_namespace_commit_email(namespace_commit_email)
namespace_commit_email.destroy
success(nil)
end
def create_namespace_commit_email(email)
namespace_commit_email = ::Users::NamespaceCommitEmail.new(
user: target_user,
namespace: namespace,
email: email
)
save_namespace_commit_email(namespace_commit_email)
end
def update_namespace_commit_email(namespace_commit_email, email)
namespace_commit_email.email = email
save_namespace_commit_email(namespace_commit_email)
end
def save_namespace_commit_email(namespace_commit_email)
if !namespace_commit_email.save
error_in_save(namespace_commit_email)
else
success(namespace_commit_email)
end
end
def success(namespace_commit_email)
ServiceResponse.success(payload: {
namespace_commit_email: namespace_commit_email
})
end
def error(message)
ServiceResponse.error(message: message)
end
def error_in_save(namespace_commit_email)
return error(_('Failed to save namespace commit email.')) if namespace_commit_email.errors.empty?
error(namespace_commit_email.errors.full_messages.to_sentence)
end
end
end
......@@ -6773,6 +6773,26 @@ Input type: `UserPreferencesUpdateInput`
| <a id="mutationuserpreferencesupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationuserpreferencesupdateuserpreferences"></a>`userPreferences` | [`UserPreferences`](#userpreferences) | User preferences after mutation. |
 
### `Mutation.userSetNamespaceCommitEmail`
Input type: `UserSetNamespaceCommitEmailInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationusersetnamespacecommitemailclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationusersetnamespacecommitemailemailid"></a>`emailId` | [`EmailID`](#emailid) | ID of the email to set. |
| <a id="mutationusersetnamespacecommitemailnamespaceid"></a>`namespaceId` | [`NamespaceID!`](#namespaceid) | ID of the namespace to set the namespace commit email for. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationusersetnamespacecommitemailclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationusersetnamespacecommitemailerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationusersetnamespacecommitemailnamespacecommitemail"></a>`namespaceCommitEmail` | [`NamespaceCommitEmail`](#namespacecommitemail) | User namespace commit email after mutation. |
### `Mutation.vulnerabilityConfirm`
 
Input type: `VulnerabilityConfirmInput`
......@@ -26974,6 +26994,12 @@ Duration between two instants, represented as a fractional number of seconds.
 
For example: 12.3334.
 
### `EmailID`
A `EmailID` is a global ID. It is encoded as a string.
An example `EmailID` is: `"gid://gitlab/Email/1"`.
### `EnvironmentID`
 
A `EnvironmentID` is a global ID. It is encoded as a string.
......@@ -16769,6 +16769,9 @@ msgstr ""
msgid "Email display name"
msgstr ""
 
msgid "Email must be provided."
msgstr ""
msgid "Email not verified. Please verify your email in Salesforce."
msgstr ""
 
......@@ -18773,6 +18776,9 @@ msgstr ""
msgid "Failed to save merge conflicts resolutions. Please try again!"
msgstr ""
 
msgid "Failed to save namespace commit email."
msgstr ""
msgid "Failed to save new settings"
msgstr ""
 
......@@ -29660,6 +29666,12 @@ msgstr ""
msgid "Namespace Limits"
msgstr ""
 
msgid "Namespace doesn't exist or you don't have permission."
msgstr ""
msgid "Namespace must be provided."
msgstr ""
msgid "Namespace or group to import repository into does not exist."
msgstr ""
 
......@@ -49403,6 +49415,9 @@ msgstr ""
msgid "User does not have permission to create a Security Policy project."
msgstr ""
 
msgid "User doesn't exist or you don't have permission to change namespace commit emails."
msgstr ""
msgid "User has already been deactivated"
msgstr ""
 
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Users::SetNamespaceCommitEmail, feature_category: :user_profile do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:group) { create(:group) }
let(:email) { create(:email, user: current_user) }
let(:input) { {} }
let(:namespace_id) { group.to_global_id }
let(:email_id) { email.to_global_id }
shared_examples 'success' do
it 'creates namespace commit email with correct values' do
expect(resolve_mutation[:namespace_commit_email])
.to have_attributes({ namespace_id: namespace_id.model_id.to_i, email_id: email_id.model_id.to_i })
end
end
describe '#resolve' do
subject(:resolve_mutation) do
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
namespace_id: namespace_id,
email_id: email_id
)
end
context 'when current_user does not have permission' do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
.with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
context 'when the user has permission' do
before do
group.add_reporter(current_user)
end
context 'when the email does not belong to the target user' do
let(:email_id) { create(:email).to_global_id }
it 'returns the validation error' do
expect(resolve_mutation[:errors]).to contain_exactly("Email must be provided.")
end
end
context 'when namespace is a group' do
it_behaves_like 'success'
end
context 'when namespace is a user' do
let(:namespace_id) { current_user.namespace.to_global_id }
it_behaves_like 'success'
end
context 'when namespace is a project' do
let_it_be(:project) { create(:project) }
let(:namespace_id) { project.project_namespace.to_global_id }
before do
project.add_reporter(current_user)
end
it_behaves_like 'success'
end
end
end
specify { expect(described_class).to require_graphql_authorizations(:read_namespace) }
end
......@@ -253,10 +253,12 @@
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_user_email_address) }
it { is_expected.to be_allowed(:admin_user_email_address) }
end
context 'when admin mode is disabled' do
it { is_expected.not_to be_allowed(:read_user_email_address) }
it { is_expected.not_to be_allowed(:admin_user_email_address) }
end
end
......@@ -265,10 +267,12 @@
subject { described_class.new(current_user, current_user) }
it { is_expected.to be_allowed(:read_user_email_address) }
it { is_expected.to be_allowed(:admin_user_email_address) }
end
context "requesting a different user's" do
it { is_expected.not_to be_allowed(:read_user_email_address) }
it { is_expected.not_to be_allowed(:admin_user_email_address) }
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting namespace commit email', feature_category: :user_profile do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:group) { create(:group, :public) }
let(:email) { create(:email, :confirmed, user: current_user) }
let(:input) { {} }
let(:namespace_id) { group.to_global_id }
let(:email_id) { email.to_global_id }
let(:resource_or_permission_error) do
"The resource that you are attempting to access does not exist or you don't have permission to perform this action"
end
let(:mutation) do
variables = {
namespace_id: namespace_id,
email_id: email_id
}
graphql_mutation(:user_set_namespace_commit_email, variables.merge(input),
<<-QL.strip_heredoc
namespaceCommitEmail {
email {
id
}
}
errors
QL
)
end
def mutation_response
graphql_mutation_response(:user_set_namespace_commit_email)
end
shared_examples 'success' do
it 'creates a namespace commit email' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response.dig('namespaceCommitEmail', 'email', 'id')).to eq(email.to_global_id.to_s)
expect(graphql_errors).to be_nil
end
end
before do
group.add_reporter(current_user)
end
context 'when current_user is nil' do
it 'returns the top level error' do
post_graphql_mutation(mutation, current_user: nil)
expect(graphql_errors.first).to match a_hash_including(
'message' => resource_or_permission_error)
end
end
context 'when the user cannot access the namespace' do
let(:namespace_id) { create(:group).to_global_id }
it 'returns the top level error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).not_to be_empty
expect(graphql_errors.first).to match a_hash_including(
'message' => resource_or_permission_error)
end
end
context 'when the service returns an error' do
let(:email_id) { create(:email).to_global_id }
it 'returns the error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to contain_exactly("Email must be provided.")
expect(mutation_response['namespaceCommitEmail']).to be_nil
end
end
context 'when namespace is a group' do
it_behaves_like 'success'
end
context 'when namespace is a user' do
let(:namespace_id) { current_user.namespace.to_global_id }
it_behaves_like 'success'
end
context 'when namespace is a project' do
let_it_be(:project) { create(:project) }
let(:namespace_id) { project.project_namespace.to_global_id }
before do
project.add_reporter(current_user)
end
it_behaves_like 'success'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::SetNamespaceCommitEmailService, feature_category: :user_profile do
include AfterNextHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:email) { create(:email, user: user) }
let_it_be(:existing_achievement) { create(:achievement, namespace: group) }
let(:namespace) { group }
let(:current_user) { user }
let(:target_user) { user }
let(:email_id) { email.id }
let(:params) { { user: target_user } }
let(:service) { described_class.new(current_user, namespace, email_id, params) }
before_all do
group.add_reporter(user)
end
shared_examples 'success' do
it 'creates namespace commit email' do
result = service.execute
expect(result.payload[:namespace_commit_email]).to be_a(Users::NamespaceCommitEmail)
expect(result.payload[:namespace_commit_email]).to be_persisted
end
end
describe '#execute' do
context 'when current_user is not provided' do
let(:current_user) { nil }
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
context 'when current_user does not have permission to change namespace commit emails' do
let(:target_user) { create(:user) }
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
context 'when target_user does not have permission to access the namespace' do
let(:namespace) { create(:group) }
it 'returns error message' do
expect(service.execute.message).to eq("Namespace doesn't exist or you don't have permission.")
end
end
context 'when namespace is not provided' do
let(:namespace) { nil }
it 'returns error message' do
expect(service.execute.message).to eq('Namespace must be provided.')
end
end
context 'when target user is not current user' do
context 'when current user is an admin' do
let(:current_user) { create(:user, :admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it 'creates namespace commit email' do
result = service.execute
expect(result.payload[:namespace_commit_email]).to be_a(Users::NamespaceCommitEmail)
expect(result.payload[:namespace_commit_email]).to be_persisted
end
end
context 'when admin mode is not enabled' do
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
end
context 'when current user is not an admin' do
let(:current_user) { create(:user) }
it 'returns error message' do
expect(service.execute.message)
.to eq("User doesn't exist or you don't have permission to change namespace commit emails.")
end
end
end
context 'when namespace commit email does not exist' do
context 'when email_id is not provided' do
let(:email_id) { nil }
it 'returns error message' do
expect(service.execute.message).to eq('Email must be provided.')
end
end
context 'when model save fails' do
before do
allow_next(::Users::NamespaceCommitEmail).to receive(:save).and_return(false)
end
it 'returns error message' do
expect(service.execute.message).to eq('Failed to save namespace commit email.')
end
end
context 'when namepsace is a group' do
it_behaves_like 'success'
end
context 'when namespace is a user' do
let(:namespace) { current_user.namespace }
it_behaves_like 'success'
end
context 'when namespace is a project' do
let_it_be(:project) { create(:project) }
let(:namespace) { project.project_namespace }
before do
project.add_reporter(current_user)
end
it_behaves_like 'success'
end
end
context 'when namespace commit email already exists' do
let!(:existing_namespace_commit_email) do
create(:namespace_commit_email,
user: target_user,
namespace: namespace,
email: create(:email, user: target_user))
end
context 'when email_id is not provided' do
let(:email_id) { nil }
it 'destroys the namespace commit email' do
result = service.execute
expect(result.message).to be_nil
expect(result.payload[:namespace_commit_email]).to be_nil
end
end
context 'and email_id is provided' do
let(:email_id) { create(:email, user: current_user).id }
it 'updates namespace commit email' do
result = service.execute
existing_namespace_commit_email.reload
expect(result.payload[:namespace_commit_email]).to eq(existing_namespace_commit_email)
expect(existing_namespace_commit_email.email_id).to eq(email_id)
end
end
context 'when model save fails' do
before do
allow_any_instance_of(::Users::NamespaceCommitEmail).to receive(:save).and_return(false) # rubocop:disable RSpec/AnyInstanceOf
end
it 'returns generic error message' do
expect(service.execute.message).to eq('Failed to save namespace commit email.')
end
context 'with model errors' do
before do
allow_any_instance_of(::Users::NamespaceCommitEmail).to receive_message_chain(:errors, :empty?).and_return(false) # rubocop:disable RSpec/AnyInstanceOf
allow_any_instance_of(::Users::NamespaceCommitEmail).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return('Model error') # rubocop:disable RSpec/AnyInstanceOf
end
it 'returns the model error message' do
expect(service.execute.message).to eq('Model error')
end
end
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment