BigW Consortium Gitlab

Commit ec0061a9 by Rémy Coutable

Allow Member.add_user to handle access requesters

Changes include: - Ensure Member.add_user is not called directly when not necessary - New GroupMember.add_users_to_group to have the same abstraction level as for Project - Refactor Member.add_user to take a source instead of an array of members - Fix Rubocop offenses - Always use Project#add_user instead of project.team.add_user - Factorize users addition as members in Member.add_users_to_source - Make access_level a keyword argument in GroupMember.add_users_to_group and ProjectMember.add_users_to_projects - Destroy any requester before adding them as a member - Improve the way we handle access requesters in Member.add_user Instead of removing the requester and creating a new member, we now simply accepts their access request. This way, they will receive a "access request granted" email. - Fix error that was previously silently ignored - Stop raising when access level is invalid in Member, let Rails validation do their work Signed-off-by: 's avatarRémy Coutable <remy@rymai.me>
parent 3b206ccb
......@@ -102,40 +102,44 @@ class Group < Namespace
self[:lfs_enabled]
end
def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
Member.add_user(
self.group_members,
user_id,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users_to_group(
self,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
add_users([user], access_level, current_user: current_user, expires_at: expires_at)
GroupMember.add_user(
self,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
def add_guest(user, current_user = nil)
add_user(user, Gitlab::Access::GUEST, current_user: current_user)
add_user(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user = nil)
add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
add_user(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user = nil)
add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
add_user(user, :developer, current_user: current_user)
end
def add_master(user, current_user = nil)
add_user(user, Gitlab::Access::MASTER, current_user: current_user)
add_user(user, :master, current_user: current_user)
end
def add_owner(user, current_user = nil)
add_user(user, Gitlab::Access::OWNER, current_user: current_user)
add_user(user, :owner, current_user: current_user)
end
def has_owner?(user)
......
......@@ -80,49 +80,70 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def user_for_id(user_id)
return user_id if user_id.is_a?(User)
user = User.find_by(id: user_id)
user ||= User.find_by(email: user_id)
user ||= user_id
user
end
def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
def add_user(source, user, access_level, current_user: nil, expires_at: nil)
user = retrieve_user(user)
access_level = retrieve_access_level(access_level)
# `user` can be either a User object or an email to be invited
if user.is_a?(User)
member = members.find_or_initialize_by(user_id: user.id)
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
source.requesters.find_by(user_id: user.id) ||
source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
return member unless can_update_member?(current_user, member)
member.attributes = {
created_by: member.created_by || current_user,
access_level: access_level,
expires_at: expires_at
}
if member.request?
member.accept_request
else
member = members.build
member.invite_email = user
member.save
end
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
member.expires_at = expires_at
member
end
member.save
end
def access_levels
Gitlab::Access.sym_options
end
private
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user)
return user if user.is_a?(User)
User.find_by(id: user) || User.find_by(email: user) || user
end
def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
def can_update_member?(current_user, member)
# There is no current user for bulk actions, in which case anything is allowed
!current_user ||
current_user.can?(:update_group_member, member) ||
current_user.can?(:update_project_member, member)
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
def project_creator?(member, access_level)
member.new_record? && member.owner? &&
access_level.to_i == ProjectMember::MASTER
def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
users.each do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
......
......@@ -12,6 +12,22 @@ class GroupMember < Member
Gitlab::Access.options_with_owner
end
def self.access_levels
Gitlab::Access.sym_options_with_owner
end
def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
add_users_to_source(
group,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def group
source
end
......
......@@ -34,36 +34,20 @@ class ProjectMember < Member
# :master
# )
#
def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
access
else
raise "Non valid access"
end
users = user_ids.map { |user_id| Member.user_for_id(user_id) }
ProjectMember.transaction do
def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
project_ids.each do |project_id|
project = Project.find(project_id)
users.each do |user|
Member.add_user(
project.project_members,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
add_users_to_source(
project,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
true
rescue
false
end
def truncate_teams(project_ids)
......@@ -84,13 +68,15 @@ class ProjectMember < Member
truncate_teams [project.id]
end
def roles_hash
Gitlab::Access.sym_options
end
def access_level_roles
Gitlab::Access.options
end
private
def can_update_member?(current_user, member)
super || (member.owner? && member.new_record?)
end
end
def access_field
......
......@@ -146,6 +146,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
# Validations
validates :creator, presence: true, on: :create
......@@ -1016,10 +1017,6 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
......
......@@ -33,18 +33,24 @@ class ProjectTeam
member
end
def add_users(users, access, current_user: nil, expires_at: nil)
def add_users(users, access_level, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
access,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
def add_user(user, access, current_user: nil, expires_at: nil)
add_users([user], access, current_user: current_user, expires_at: expires_at)
def add_user(user, access_level, current_user: nil, expires_at: nil)
ProjectMember.add_user(
project,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
# Remove all users from project team
......
Gitlab::Seeder.quiet do
Group.all.each do |group|
User.all.sample(4).each do |user|
if group.add_users([user.id], Gitlab::Access.values.sample)
if group.add_user(user, Gitlab::Access.values.sample).persisted?
print '.'
else
print 'F'
......
......@@ -59,13 +59,6 @@ module API
authorize_admin_source!(source_type, source)
required_attributes! [:user_id, :access_level]
access_requester = source.requesters.find_by(user_id: params[:user_id])
if access_requester
# We pass current_user = access_requester so that the requester doesn't
# receive a "access denied" email
::Members::DestroyService.new(access_requester, access_requester.user).execute
end
member = source.members.find_by(user_id: params[:user_id])
# This is to ensure back-compatibility but 409 behavior should be used
......@@ -73,18 +66,12 @@ module API
conflict!('Member already exists') if source_type == 'group' && member
unless member
source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
member = source.members.find_by(user_id: params[:user_id])
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
if member
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
......
......@@ -53,6 +53,10 @@ module Gitlab
}
end
def sym_options_with_owner
sym_options.merge(owner: OWNER)
end
def protection_options
{
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
......
......@@ -13,7 +13,7 @@ describe Projects::TemplatesController do
end
before do
project.team.add_user(user, Gitlab::Access::MASTER)
project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
end
......
......@@ -4,24 +4,9 @@ FactoryGirl.define do
project
master
trait :guest do
access_level ProjectMember::GUEST
end
trait :reporter do
access_level ProjectMember::REPORTER
end
trait :developer do
access_level ProjectMember::DEVELOPER
end
trait :master do
access_level ProjectMember::MASTER
end
trait :owner do
access_level ProjectMember::OWNER
end
trait(:guest) { access_level ProjectMember::GUEST }
trait(:reporter) { access_level ProjectMember::REPORTER }
trait(:developer) { access_level ProjectMember::DEVELOPER }
trait(:master) { access_level ProjectMember::MASTER }
end
end
require 'spec_helper'
feature 'Projects > Members > Owner cannot leave project', feature: true do
let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
project.team << [owner, :owner]
login_as(owner)
login_as(project.owner)
visit namespace_project_path(project.namespace, project)
end
......
require 'spec_helper'
feature 'Projects > Members > Owner cannot request access to his project', feature: true do
let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
project.team << [owner, :owner]
login_as(owner)
login_as(project.owner)
visit namespace_project_path(project.namespace, project)
end
......
......@@ -82,7 +82,7 @@ feature 'Project', feature: true do
before do
login_with(user)
project.team.add_user(user, Gitlab::Access::MASTER)
project.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_path(project.namespace, project)
end
......@@ -101,8 +101,8 @@ feature 'Project', feature: true do
context 'on issues page', js: true do
before do
login_with(user)
project.team.add_user(user, Gitlab::Access::MASTER)
project2.team.add_user(user, Gitlab::Access::MASTER)
project.add_user(user, Gitlab::Access::MASTER)
project2.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_issue_path(project.namespace, project, issue)
end
......
......@@ -43,7 +43,7 @@ describe JoinedGroupsFinder do
context 'if profile visitor is in one of the private group projects' do
before do
project = create(:project, :private, group: private_group, name: 'B', path: 'B')
project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
project.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
end
it 'shows group' do
......
......@@ -38,7 +38,7 @@ describe ProjectsFinder do
describe 'with private projects' do
before do
private_project.team.add_user(user, Gitlab::Access::MASTER)
private_project.add_user(user, Gitlab::Access::MASTER)
end
it do
......
......@@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do
let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
before do
project.team.add_user(user, Gitlab::Access::MASTER)
project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
......@@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do
context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do
templates = subject.by_category('', empty_project)
......@@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do
context "when repo is empty" do
let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do
issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
......
......@@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do
let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
before do
project.team.add_user(user, Gitlab::Access::MASTER)
project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
......@@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do
templates = subject.by_category('', empty_project)
......@@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context "when repo is empty" do
let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do
issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
......
......@@ -492,21 +492,22 @@ describe Notify do
end
end
def invite_to_project(project:, email:, inviter:)
Member.add_user(
project.project_members,
'toto@example.com',
Gitlab::Access::DEVELOPER,
current_user: inviter
def invite_to_project(project, inviter:)
create(
:project_member,
:developer,
project: project,
invite_token: '1234',
invite_email: 'toto@example.com',
user: nil,
created_by: inviter
)
project.project_members.invite.last
end
describe 'project invitation' do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
let(:project_member) { invite_to_project(project, inviter: master) }
subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
......@@ -525,10 +526,10 @@ describe Notify do
describe 'project invitation accepted' do
let(:project) { create(:project) }
let(:invited_user) { create(:user) }
let(:invited_user) { create(:user, name: 'invited user') }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
invitee = invite_to_project(project, inviter: master)
invitee.accept_invite!(invited_user)
invitee
end
......@@ -552,7 +553,7 @@ describe Notify do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
invitee = invite_to_project(project, inviter: master)
invitee.decline_invite!
invitee
end
......@@ -744,21 +745,22 @@ describe Notify do
end
end
def invite_to_group(group:, email:, inviter:)
Member.add_user(
group.group_members,
'toto@example.com',
Gitlab::Access::DEVELOPER,
current_user: inviter
def invite_to_group(group, inviter:)
create(
:group_member,
:developer,
group: group,
invite_token: '1234',
invite_email: 'toto@example.com',
user: nil,
created_by: inviter
)
group.group_members.invite.last
end
describe 'group invitation' do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
let(:group_member) { invite_to_group(group, inviter: owner) }
subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
......@@ -777,10 +779,10 @@ describe Notify do
describe 'group invitation accepted' do
let(:group) { create(:group) }
let(:invited_user) { create(:user) }
let(:invited_user) { create(:user, name: 'invited user') }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
invitee = invite_to_group(group, inviter: owner)
invitee.accept_invite!(invited_user)
invitee
end
......@@ -804,7 +806,7 @@ describe Notify do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
invitee = invite_to_group(group, inviter: owner)
invitee.decline_invite!
invitee
end
......
......@@ -494,7 +494,7 @@ describe Issue, models: true do
context 'with an admin user' do
let(:project) { create(:empty_project) }
let(:user) { create(:user, admin: true) }
let(:user) { create(:admin) }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
......
require 'spec_helper'
describe GroupMember, models: true do
describe '.access_level_roles' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
end
end
describe '.access_levels' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end
end
describe '.add_users_to_group' do
it 'adds the given users to the given group' do
group = create(:group)
users = create_list(:user, 2)
described_class.add_users_to_group(
group,
[users.first.id, users.second],
described_class::MASTER
)
expect(group.users).to include(users.first, users.second)
end
end
describe 'notifications' do
describe "#after_create" do
it "sends email to user" do
......
......@@ -15,6 +15,26 @@ describe ProjectMember, models: true do
it { is_expected.to include_module(Gitlab::ShellAdapter) }
end
describe '.access_level_roles' do
it 'returns Gitlab::Access.options' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
end
end
describe '.add_user' do
context 'when called with the project owner' do
it 'adds the user as a member' do
project = create(:empty_project)
expect(project.users).not_to include(project.owner)
described_class.add_user(project, project.owner, :master, current_user: project.owner)
expect(project.users.reload).to include(project.owner)
end
end
end
describe '#real_source_type' do
subject { create(:project_member).real_source_type }
......@@ -50,7 +70,7 @@ describe ProjectMember, models: true do
end
end
describe :import_team do
describe '.import_team' do
before do
@project_1 = create :project
@project_2 = create :project
......@@ -81,25 +101,21 @@ describe ProjectMember, models: true do
end
describe '.add_users_to_projects' do
before do
@project_1 = create :project
@project_2 = create :project
it 'adds the given users to the given projects' do
projects = create_list(:empty_project, 2)
users = create_list(:user, 2)
@user_1 = create :user
@user_2 = create :user
ProjectMember.add_users_to_projects(
[@project_1.id, @project_2.id],
[@user_1.id, @user_2.id],
ProjectMember::MASTER
)
end
described_class.add_users_to_projects(
[projects.first.id, projects.second],
[users.first.id, users.second],
described_class::MASTER)
it { expect(@project_1.users).to include(@user_1) }
it { expect(@project_1.users).to include(@user_2) }
expect(projects.first.users).to include(users.first)
expect(projects.first.users).to include(users.second)
it { expect(@project_2.users).to include(@user_1) }
it { expect(@project_2.users).to include(@user_2) }
expect(projects.second.users).to include(users.first)
expect(projects.second.users).to include(users.second)
end
end
describe '.truncate_teams' do
......
......@@ -836,7 +836,7 @@ describe Project, models: true do
describe 'when a user has access to a project' do
before do
project.team.add_user(user, Gitlab::Access::MASTER)
project.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([project]) }
......
......@@ -15,7 +15,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
project.team << [user, :reporters]
project.team << [user, :reporter]
end
describe "GET /projects/:id/merge_requests" do
......@@ -299,7 +299,7 @@ describe API::API, api: true do
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each|
fork_project.team << [user2, :reporters]
fork_project.team << [user2, :reporter]
end
it "returns merge_request" do
......
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