Commit cc41381b authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 4c26bcbe 345e32d3
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 7.10.0 (unreleased) v 7.10.0 (unreleased)
- Allow users to be invited by email to join a group or project.
- Don't crash when project repository doesn't exist.
- Add config var to block auto-created LDAP users.
- Don't use HTML ellipsis in EmailsOnPush subject truncated commit message.
- Set EmailsOnPush reply-to address to committer email when enabled.
- Fix broken file browsing with a submodule that contains a relative link (Stan Hu) - Fix broken file browsing with a submodule that contains a relative link (Stan Hu)
- Fix persistent XSS vulnerability around profile website URLs. - Fix persistent XSS vulnerability around profile website URLs.
- Fix project import URL regex to prevent arbitary local repos from being imported. - Fix project import URL regex to prevent arbitary local repos from being imported.
...@@ -16,6 +21,9 @@ v 7.10.0 (unreleased) ...@@ -16,6 +21,9 @@ v 7.10.0 (unreleased)
- Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu)
- Allow HTML tags in Markdown input - Allow HTML tags in Markdown input
- Fix code unfold not working on Compare commits page (Stan Hu) - Fix code unfold not working on Compare commits page (Stan Hu)
- Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik)
- Include missing events and fix save functionality in admin service template settings form (Stan Hu)
- Fix "Import projects from" button to show the correct instructions (Stan Hu)
- Fix dots in Wiki slugs causing errors (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu)
- Make maximum attachment size configurable via Application Settings (Stan Hu) - Make maximum attachment size configurable via Application Settings (Stan Hu)
- Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
......
...@@ -8,6 +8,7 @@ class @UsersSelect ...@@ -8,6 +8,7 @@ class @UsersSelect
@groupId = $(select).data('group-id') @groupId = $(select).data('group-id')
showNullUser = $(select).data('null-user') showNullUser = $(select).data('null-user')
showAnyUser = $(select).data('any-user') showAnyUser = $(select).data('any-user')
showEmailUser = $(select).data('email-user')
firstUser = $(select).data('first-user') firstUser = $(select).data('first-user')
$(select).select2 $(select).select2
...@@ -19,20 +20,6 @@ class @UsersSelect ...@@ -19,20 +20,6 @@ class @UsersSelect
data = { results: users } data = { results: users }
if query.term.length == 0 if query.term.length == 0
anyUser = {
name: 'Any',
avatar: null,
username: 'none',
id: null
}
nullUser = {
name: 'Unassigned',
avatar: null,
username: 'none',
id: 0
}
if firstUser if firstUser
# Move current user to the front of the list # Move current user to the front of the list
for obj, index in data.results for obj, index in data.results
...@@ -40,11 +27,34 @@ class @UsersSelect ...@@ -40,11 +27,34 @@ class @UsersSelect
data.results.splice(index, 1) data.results.splice(index, 1)
data.results.unshift(obj) data.results.unshift(obj)
break break
if showNullUser if showNullUser
nullUser = {
name: 'Unassigned',
avatar: null,
username: 'none',
id: 0
}
data.results.unshift(nullUser) data.results.unshift(nullUser)
if showAnyUser if showAnyUser
anyUser = {
name: 'Any',
avatar: null,
username: 'none',
id: null
}
data.results.unshift(anyUser) data.results.unshift(anyUser)
if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
emailUser = {
name: "Invite \"#{query.term}\"",
avatar: null,
username: query.term,
id: query.term
}
data.results.unshift(emailUser)
query.callback(data) query.callback(data)
initSelection: (element, callback) => initSelection: (element, callback) =>
......
...@@ -41,7 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -41,7 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def members_update def members_update
@group.add_users(params[:user_ids].split(','), params[:access_level]) @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.' redirect_to [:admin, @group], notice: 'Users were successfully added.'
end end
......
...@@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base ...@@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base
def repository def repository
@repository ||= project.repository @repository ||= project.repository
rescue Grit::NoSuchPathError(e) rescue Grit::NoSuchPathError => e
log_exception(e) log_exception(e)
nil nil
end end
......
...@@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(resource_name, resource)
if signed_in?(resource_name) if signed_in?(resource_name)
signed_in_root_path(resource) after_sign_in_path_for(resource)
else else
sign_in(resource) sign_in(resource)
if signed_in?(resource_name) if signed_in?(resource_name)
signed_in_root_path(resource) after_sign_in_path_for(resource)
else else
new_session_path(resource_name) new_session_path(resource_name)
end end
......
...@@ -13,7 +13,7 @@ class Groups::ApplicationController < ApplicationController ...@@ -13,7 +13,7 @@ class Groups::ApplicationController < ApplicationController
end end
def authorize_admin_group! def authorize_admin_group!
unless can?(current_user, :manage_group, group) unless can?(current_user, :admin_group, group)
return render_404 return render_404
end end
end end
......
...@@ -11,6 +11,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -11,6 +11,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index def index
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members @members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present? if params[:search].present?
users = @group.users.search(params[:search]).to_a users = @group.users.search(params[:search]).to_a
...@@ -22,7 +23,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -22,7 +23,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def create def create
@group.add_users(params[:user_ids].split(','), params[:access_level]) @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end end
...@@ -38,7 +39,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -38,7 +39,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
@group_member.destroy @group_member.destroy
respond_to do |format| respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
format.js { render nothing: true } format.js { render nothing: true }
end end
else else
...@@ -46,12 +47,26 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -46,12 +47,26 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
end end
def resend_invite
redirect_path = group_group_members_path(@group)
@group_member = @group.group_members.find(params[:id])
if @group_member.invite?
@group_member.resend_invite
redirect_to redirect_path, notice: 'The invitation was successfully resent.'
else
redirect_to redirect_path, alert: 'The invitation has already been accepted.'
end
end
def leave def leave
@group_member = @group.group_members.where(user_id: current_user.id).first @group_member = @group.group_members.where(user_id: current_user.id).first
if can?(current_user, :destroy_group_member, @group_member) if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy @group_member.destroy
redirect_to(dashboard_groups_path, info: "You left #{group.name} group.") redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else else
return render_403 return render_403
end end
......
...@@ -51,6 +51,6 @@ class Groups::MilestonesController < ApplicationController ...@@ -51,6 +51,6 @@ class Groups::MilestonesController < ApplicationController
end end
def authorize_group_milestone! def authorize_group_milestone!
return render_404 unless can?(current_user, :manage_group, group) return render_404 unless can?(current_user, :admin_group, group)
end end
end end
class InvitesController < ApplicationController
before_filter :member
skip_before_filter :authenticate_user!, only: :decline
respond_to :html
layout 'navless'
def show
end
def accept
if member.accept_invite!(current_user)
label, path = source_info(member.source)
redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}."
else
redirect_to :back, alert: "The invitation could not be accepted."
end
end
def decline
if member.decline_invite!
label, _ = source_info(member.source)
path =
if current_user
dashboard_path
else
new_user_session_path
end
redirect_to path, notice: "You have declined the invitation to join #{label}."
else
redirect_to :back, alert: "The invitation could not be declined."
end
end
private
def member
return @member if defined?(@member)
@token = params[:id]
@member = Member.find_by_invite_token(@token)
unless @member
render_404 and return
end
@member
end
def authenticate_user!
return if current_user
notice = "To accept this invitation, sign in"
notice << " or create an account" if current_application_settings.signup_enabled?
notice << "."
store_location_for :user, request.fullpath
redirect_to new_user_session_path, notice: notice
end
def source_info(source)
case source
when Project
project = member.source
label = "project #{project.name_with_namespace}"
path = namespace_project_path(project.namespace, project)
when Group
group = member.source
label = "group #{group.name}"
path = group_path(group)
else
label = "who knows what"
path = dashboard_path
end
[label, path]
end
end
...@@ -6,6 +6,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -6,6 +6,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def index def index
@project_members = @project.project_members @project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
if params[:search].present? if params[:search].present?
users = @project.users.search(params[:search]).to_a users = @project.users.search(params[:search]).to_a
...@@ -17,6 +18,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -17,6 +18,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group = @project.group @group = @project.group
if @group if @group
@group_members = @group.group_members @group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present? if params[:search].present?
users = @group.users.search(params[:search]).to_a users = @group.users.search(params[:search]).to_a
...@@ -34,30 +36,42 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -34,30 +36,42 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def create def create
users = User.where(id: params[:user_ids].split(',')) @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
@project.team << [users, params[:access_level]]
redirect_to namespace_project_project_members_path(@project.namespace, @project) redirect_to namespace_project_project_members_path(@project.namespace, @project)
end end
def update def update
@project_member = @project.project_members.find_by(user_id: member) @project_member = @project.project_members.find(params[:id])
@project_member.update_attributes(member_params) @project_member.update_attributes(member_params)
end end
def destroy def destroy
@project_member = @project.project_members.find_by(user_id: member) @project_member = @project.project_members.find(params[:id])
@project_member.destroy @project_member.destroy
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to namespace_project_project_members_path(@project.namespace, redirect_to namespace_project_project_members_path(@project.namespace, @project)
@project)
end end
format.js { render nothing: true } format.js { render nothing: true }
end end
end end
def resend_invite
redirect_path = namespace_project_project_members_path(@project.namespace, @project)
@project_member = @project.project_members.find(params[:id])
if @project_member.invite?
@project_member.resend_invite
redirect_to redirect_path, notice: 'The invitation was successfully resent.'
else
redirect_to redirect_path, alert: 'The invitation has already been accepted.'
end
end
def leave def leave
@project.project_members.find_by(user_id: current_user).destroy @project.project_members.find_by(user_id: current_user).destroy
...@@ -69,7 +83,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -69,7 +83,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def apply_import def apply_import
giver = Project.find(params[:source_project_id]) giver = Project.find(params[:source_project_id])
status = @project.team.import(giver) status = @project.team.import(giver, current_user)
notice = status ? "Successfully imported" : "Import failed" notice = status ? "Successfully imported" : "Import failed"
redirect_to(namespace_project_project_members_path(project.namespace, project), redirect_to(namespace_project_project_members_path(project.namespace, project),
...@@ -78,10 +92,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -78,10 +92,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected protected
def member
@member ||= User.find_by(username: params[:id])
end
def member_params def member_params
params.require(:project_member).permit(:user_id, :access_level) params.require(:project_member).permit(:user_id, :access_level)
end end
......
module GroupsHelper module GroupsHelper
def remove_user_from_group_message(group, user) def remove_user_from_group_message(group, member)
"Are you sure you want to remove \"#{user.name}\" from \"#{group.name}\"?" if member.user
"Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
else
"Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
end
end end
def leave_group_message(group) def leave_group_message(group)
......
module ProjectsHelper module ProjectsHelper
def remove_from_project_team_message(project, user) def remove_from_project_team_message(project, member)
"You are going to remove #{user.name} from #{project.name} project team. Are you sure?" if member.user
"You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
else
"You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
end
end end
def link_to_project(project) def link_to_project(project)
......
...@@ -8,6 +8,7 @@ module SelectsHelper ...@@ -8,6 +8,7 @@ module SelectsHelper
null_user = opts[:null_user] || false null_user = opts[:null_user] || false
any_user = opts[:any_user] || false any_user = opts[:any_user] || false
email_user = opts[:email_user] || false
first_user = opts[:first_user] && current_user ? current_user.username : false first_user = opts[:first_user] && current_user ? current_user.username : false
html = { html = {
...@@ -15,6 +16,7 @@ module SelectsHelper ...@@ -15,6 +16,7 @@ module SelectsHelper
'data-placeholder' => placeholder, 'data-placeholder' => placeholder,
'data-null-user' => null_user, 'data-null-user' => null_user,
'data-any-user' => any_user, 'data-any-user' => any_user,
'data-email-user' => email_user,
'data-first-user' => first_user 'data-first-user' => first_user
} }
......
...@@ -3,10 +3,50 @@ module Emails ...@@ -3,10 +3,50 @@ module Emails
def group_access_granted_email(group_member_id) def group_access_granted_email(group_member_id)
@group_member = GroupMember.find(group_member_id) @group_member = GroupMember.find(group_member_id)
@group = @group_member.group @group = @group_member.group
@target_url = group_url(@group) @target_url = group_url(@group)
@current_user = @group_member.user @current_user = @group_member.user
mail(to: @group_member.user.email,
mail(to: @group_member.user.notification_email,
subject: subject("Access to group was granted")) subject: subject("Access to group was granted"))
end end
def group_member_invited_email(group_member_id, token)
@group_member = GroupMember.find group_member_id
@group = @group_member.group
@token = token
@target_url = group_url(@group)
@current_user = @group_member.user
mail(to: @group_member.invite_email,
subject: "Invitation to join group #{@group.name}")
end
def group_invite_accepted_email(group_member_id)
@group_member = GroupMember.find group_member_id
return if @group_member.created_by.nil?
@group = @group_member.group
@target_url = group_url(@group)
@current_user = @group_member.created_by
mail(to: @group_member.created_by.notification_email,
subject: subject("Invitation accepted"))
end
def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
return if created_by_id.nil?
@group = Group.find(group_id)
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = group_url(@group)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
end end
end end
module Emails module Emails
module Projects module Projects
def project_access_granted_email(user_project_id) def project_access_granted_email(project_member_id)
@project_member = ProjectMember.find user_project_id @project_member = ProjectMember.find project_member_id
@project = @project_member.project @project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project) @target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user @current_user = @project_member.user
mail(to: @project_member.user.email,
mail(to: @project_member.user.notification_email,
subject: subject("Access to project was granted")) subject: subject("Access to project was granted"))
end end
def project_member_invited_email(project_member_id, token)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@token = token
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
mail(to: @project_member.invite_email,
subject: "Invitation to join project #{@project.name_with_namespace}")
end
def project_invite_accepted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
return if @project_member.created_by.nil?
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
mail(to: @project_member.created_by.notification_email,
subject: subject("Invitation accepted"))
end
def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
return if created_by_id.nil?
@project = Project.find(project_id)
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = namespace_project_url(@project.namespace, @project)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
def project_was_moved_email(project_id, user_id) def project_was_moved_email(project_id, user_id)
@current_user = @user = User.find user_id @current_user = @user = User.find user_id
@project = Project.find project_id @project = Project.find project_id
...@@ -84,9 +125,17 @@ module Emails ...@@ -84,9 +125,17 @@ module Emails
@disable_footer = true @disable_footer = true
mail(from: sender(author_id, send_from_committer_email), reply_to =
to: recipient, if send_from_committer_email && can_send_from_user_email?(@author)
subject: @subject) @author.email
else
Gitlab.config.gitlab.email_reply_to
end
mail(from: sender(author_id, send_from_committer_email),
reply_to: reply_to,
to: recipient,
subject: @subject)
end end
end end
end end
...@@ -60,20 +60,24 @@ class Notify < ActionMailer::Base ...@@ -60,20 +60,24 @@ class Notify < ActionMailer::Base
address address
end end
def can_send_from_user_email?(sender)
sender_domain = sender.email.split("@").last
self.class.allowed_email_domains.include?(sender_domain)
end
# Return an email address that displays the name of the sender. # Return an email address that displays the name of the sender.
# Only the displayed name changes; the actual email address is always the same. # Only the displayed name changes; the actual email address is always the same.
def sender(sender_id, send_from_user_email = false) def sender(sender_id, send_from_user_email = false)
if sender = User.find(sender_id) return unless sender = User.find(sender_id)
address = default_sender_address
address.display_name = sender.name address = default_sender_address
address.display_name = sender.name
sender_domain = sender.email.split("@").last if send_from_user_email && can_send_from_user_email?(sender)
if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain) address.address = sender.email
address.address = sender.email
end
address.format
end end
address.format
end end
# Look up a User by their ID and return their email address # Look up a User by their ID and return their email address
......
...@@ -198,11 +198,11 @@ class Ability ...@@ -198,11 +198,11 @@ class Ability
]) ])
end end
# Only group owner and administrators can manage group # Only group owner and administrators can admin group
if group.has_owner?(user) || user.admin? if group.has_owner?(user) || user.admin?
rules.push(*[ rules.push(*[
:manage_group, :admin_group,
:manage_namespace :admin_namespace
]) ])
end end
...@@ -212,11 +212,11 @@ class Ability ...@@ -212,11 +212,11 @@ class Ability
def namespace_abilities(user, namespace) def namespace_abilities(user, namespace)
rules = [] rules = []
# Only namespace owner and administrators can manage it # Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin? if namespace.owner == user || user.admin?
rules.push(*[ rules.push(*[
:create_projects, :create_projects,
:manage_namespace :admin_namespace
]) ])
end end
...@@ -254,7 +254,7 @@ class Ability ...@@ -254,7 +254,7 @@ class Ability
rules = [] rules = []
target_user = subject.user target_user = subject.user
group = subject.group group = subject.group
can_manage = group_abilities(user, group).include?(:manage_group) can_manage = group_abilities(user, group).include?(:admin_group)
if can_manage && (user != target_user) if can_manage && (user != target_user)
rules << :modify_group_member rules << :modify_group_member
rules << :destroy_group_member rules << :destroy_group_member
......
...@@ -77,7 +77,7 @@ class Commit ...@@ -77,7 +77,7 @@ class Commit
title_end = title.index("\n") title_end = title.index("\n")
if (!title_end && title.length > 100) || (title_end && title_end > 100) if (!title_end && title.length > 100) || (title_end && title_end > 100)
title[0..79] << "&hellip;".html_safe title[0..79] << "…"
else else
title.split("\n", 2).first title.split("\n", 2).first
end end
...@@ -90,7 +90,7 @@ class Commit ...@@ -90,7 +90,7 @@ class Commit
title_end = safe_message.index("\n") title_end = safe_message.index("\n")
@description ||= @description ||=
if (!title_end && safe_message.length > 100) || (title_end && title_end > 100) if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
"&hellip;".html_safe << safe_message[80..-1] "…" << safe_message[80..-1]
else else
safe_message.split("\n", 2)[1].try(:chomp) safe_message.split("\n", 2)[1].try(:chomp)
end end
......
...@@ -46,19 +46,18 @@ class Group < Namespace ...@@ -46,19 +46,18 @@ class Group < Namespace
@owners ||= group_members.owners.map(&:user) @owners ||= group_members.owners.map(&:user)
end end
def add_users(user_ids, access_level) def add_users(user_ids, access_level, current_user = nil)
user_ids.compact.each do |user_id| user_ids.each do |user_id|
user = self.group_members.find_or_initialize_by(user_id: user_id) Member.add_user(self.group_members, user_id, access_level, current_user)
user.update_attributes(access_level: access_level)
end end
end end
def add_user(user, access_level) def add_user(user, access_level, current_user = nil)
self.group_members.create(user_id: user.id, access_level: access_level) add_users([user], access_level, current_user)
end end
def add_owner(user) def add_owner(user, current_user = nil)
self.add_user(user, Gitlab::Access::OWNER) self.add_user(user, Gitlab::Access::OWNER, current_user)
end end
def has_owner?(user) def has_owner?(user)
......
...@@ -16,7 +16,6 @@ require 'digest/md5' ...@@ -16,7 +16,6 @@ require 'digest/md5'
class Key < ActiveRecord::Base class Key < ActiveRecord::Base
include Sortable include Sortable
include Gitlab::Popen
belongs_to :user belongs_to :user
...@@ -79,20 +78,9 @@ class Key < ActiveRecord::Base ...@@ -79,20 +78,9 @@ class Key < ActiveRecord::Base
def generate_fingerprint def generate_fingerprint
self.fingerprint = nil self.fingerprint = nil
return unless key.present?
return unless self.key.present?
cmd_status = 0
cmd_output = '' self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
Tempfile.open('gitlab_key_file') do |file|
file.puts key
file.rewind
cmd_output, cmd_status = popen(%W(ssh-keygen -lf #{file.path}), '/tmp')
end
if cmd_status.zero?
cmd_output.gsub /(\h{2}:)+\h{2}/ do |match|
self.fingerprint = match
end
end
end end
end end
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# type :string(255) # type :string(255)
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# created_by_id :integer
# invite_email :string
# invite_token :string
# invite_accepted_at :datetime
# #
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
...@@ -18,19 +22,151 @@ class Member < ActiveRecord::Base ...@@ -18,19 +22,151 @@ class Member < ActiveRecord::Base
include Notifiable include Notifiable
include Gitlab::Access include Gitlab::Access
attr_accessor :raw_invite_token
belongs_to :created_by, class_name: "User"
belongs_to :user belongs_to :user
belongs_to :source, polymorphic: true belongs_to :source, polymorphic: true
validates :user, presence: true validates :user, presence: true, unless: :invite?
validates :source, presence: true validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" } validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :invite_email, presence: { if: :invite? },
email: { strict_mode: true, allow_nil: true },
uniqueness: { scope: [:source_type, :source_id], allow_nil: true }
scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") }
scope :guests, -> { where(access_level: GUEST) } scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) } scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) } scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) } scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) } scope :owners, -> { where(access_level: OWNER) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?
after_create :post_create_hook, unless: :invite?
after_update :post_update_hook, unless: :invite?
after_destroy :post_destroy_hook, unless: :invite?
delegate :name, :username, :email, to: :user, prefix: true delegate :name, :username, :email, to: :user, prefix: true
class << self
def find_by_invite_token(invite_token)
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
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)
user = user_for_id(user_id)
# `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)
else
member = members.build
member.invite_email = user
end
member.created_by ||= current_user
member.access_level = access_level
member.save
end
end
def invite?
self.invite_token.present?
end
def accept_invite!(new_user)
return false unless invite?
self.invite_token = nil
self.invite_accepted_at = Time.now.utc
self.user = new_user
saved = self.save
after_accept_invite if saved
saved
end
def decline_invite!
return false unless invite?
destroyed = self.destroy
after_decline_invite if destroyed
destroyed
end
def generate_invite_token
raw, enc = Devise.token_generator.generate(self.class, :invite_token)
@raw_invite_token = raw
self.invite_token = enc
end
def generate_invite_token!
generate_invite_token && save(validate: false)
end
def resend_invite
return unless invite?
generate_invite_token! unless @raw_invite_token
send_invite
end
private
def send_invite
# override in subclass
end
def post_create_hook
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
# override in subclass
end
def post_destroy_hook
system_hook_service.execute_hooks_for(self, :destroy)
end
def after_accept_invite
post_create_hook
end
def after_decline_invite
# override in subclass
end
def system_hook_service
SystemHooksService.new
end
def notification_service
NotificationService.new
end
end end
...@@ -27,10 +27,6 @@ class GroupMember < Member ...@@ -27,10 +27,6 @@ class GroupMember < Member
scope :with_group, ->(group) { where(source_id: group.id) } scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) } scope :with_user, ->(user) { where(user_id: user.id) }
after_create :post_create_hook
after_update :notify_update
after_destroy :post_destroy_hook
def self.access_level_roles def self.access_level_roles
Gitlab::Access.options_with_owner Gitlab::Access.options_with_owner
end end
...@@ -43,26 +39,37 @@ class GroupMember < Member ...@@ -43,26 +39,37 @@ class GroupMember < Member
access_level access_level
end end
private
def send_invite
notification_service.invite_group_member(self, @raw_invite_token)
super
end
def post_create_hook def post_create_hook
notification_service.new_group_member(self) notification_service.new_group_member(self)
system_hook_service.execute_hooks_for(self, :create)
super
end end
def notify_update def post_update_hook
if access_level_changed? if access_level_changed?
notification_service.update_group_member(self) notification_service.update_group_member(self)
end end
end
def post_destroy_hook super
system_hook_service.execute_hooks_for(self, :destroy)
end end
def system_hook_service def after_accept_invite
SystemHooksService.new notification_service.accept_group_invite(self)
super
end end
def notification_service def after_decline_invite
NotificationService.new notification_service.decline_group_invite(self)
super
end end
end end
...@@ -27,10 +27,6 @@ class ProjectMember < Member ...@@ -27,10 +27,6 @@ class ProjectMember < Member
validates_format_of :source_type, with: /\AProject\z/ validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
after_create :post_create_hook
after_update :post_update_hook
after_destroy :post_destroy_hook
scope :in_project, ->(project) { where(source_id: project.id) } scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) } scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
scope :with_user, ->(user) { where(user_id: user.id) } scope :with_user, ->(user) { where(user_id: user.id) }
...@@ -55,7 +51,7 @@ class ProjectMember < Member ...@@ -55,7 +51,7 @@ class ProjectMember < Member
# :master # :master
# ) # )
# #
def add_users_into_projects(project_ids, user_ids, access) def add_users_into_projects(project_ids, user_ids, access, current_user = nil)
access_level = if roles_hash.has_key?(access) access_level = if roles_hash.has_key?(access)
roles_hash[access] roles_hash[access]
elsif roles_hash.values.include?(access.to_i) elsif roles_hash.values.include?(access.to_i)
...@@ -64,12 +60,14 @@ class ProjectMember < Member ...@@ -64,12 +60,14 @@ class ProjectMember < Member
raise "Non valid access" raise "Non valid access"
end end
users = user_ids.map { |user_id| Member.user_for_id(user_id) }
ProjectMember.transaction do ProjectMember.transaction do
project_ids.each do |project_id| project_ids.each do |project_id|
user_ids.each do |user_id| project = Project.find(project_id)
member = ProjectMember.new(access_level: access_level, user_id: user_id)
member.source_id = project_id users.each do |user|
member.save Member.add_user(project.project_members, user, access_level, current_user)
end end
end end
end end
...@@ -82,6 +80,7 @@ class ProjectMember < Member ...@@ -82,6 +80,7 @@ class ProjectMember < Member
def truncate_teams(project_ids) def truncate_teams(project_ids)
ProjectMember.transaction do ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids) members = ProjectMember.where(source_id: project_ids)
members.each do |member| members.each do |member|
member.destroy member.destroy
end end
...@@ -109,41 +108,58 @@ class ProjectMember < Member ...@@ -109,41 +108,58 @@ class ProjectMember < Member
access_level access_level
end end
def project
source
end
def owner? def owner?
project.owner == user project.owner == user
end end
private
def send_invite
notification_service.invite_project_member(self, @raw_invite_token)
super
end
def post_create_hook def post_create_hook
unless owner? unless owner?
event_service.join_project(self.project, self.user) event_service.join_project(self.project, self.user)
notification_service.new_project_member(self) notification_service.new_project_member(self)
end end
system_hook_service.execute_hooks_for(self, :create) super
end end
def post_update_hook def post_update_hook
notification_service.update_project_member(self) if self.access_level_changed? if access_level_changed?
notification_service.update_project_member(self)
end
super
end end
def post_destroy_hook def post_destroy_hook
event_service.leave_project(self.project, self.user) event_service.leave_project(self.project, self.user)
system_hook_service.execute_hooks_for(self, :destroy)
end
def event_service super
EventCreateService.new
end end
def notification_service def after_accept_invite
NotificationService.new notification_service.accept_project_invite(self)
super
end end
def system_hook_service def after_decline_invite
SystemHooksService.new notification_service.decline_project_invite(self)
super
end end
def project def event_service
source EventCreateService.new
end end
end end
...@@ -12,12 +12,12 @@ class ProjectTeam ...@@ -12,12 +12,12 @@ class ProjectTeam
# @team << [@users, :master] # @team << [@users, :master]
# #
def <<(args) def <<(args)
users = args.first users, access, current_user = *args
if users.respond_to?(:each) if users.respond_to?(:each)
add_users(users, args.second) add_users(users, access, current_user)
else else
add_user(users, args.second) add_user(users, access, current_user)
end end
end end
...@@ -43,22 +43,19 @@ class ProjectTeam ...@@ -43,22 +43,19 @@ class ProjectTeam
member member
end end
def add_user(user, access) def add_users(users, access, current_user = nil)
add_users_ids([user.id], access)
end
def add_users(users, access)
add_users_ids(users.map(&:id), access)
end
def add_users_ids(user_ids, access)
ProjectMember.add_users_into_projects( ProjectMember.add_users_into_projects(
[project.id], [project.id],
user_ids, users,
access access,
current_user
) )
end end
def add_user(user, access, current_user = nil)
add_users([user], access, current_user)
end
# Remove all users from project team # Remove all users from project team
def truncate def truncate
ProjectMember.truncate_team(project) ProjectMember.truncate_team(project)
...@@ -88,7 +85,7 @@ class ProjectTeam ...@@ -88,7 +85,7 @@ class ProjectTeam
@masters ||= fetch_members(:masters) @masters ||= fetch_members(:masters)
end end
def import(source_project) def import(source_project, current_user = nil)
target_project = project target_project = project
source_members = source_project.project_members.to_a source_members = source_project.project_members.to_a
...@@ -96,13 +93,14 @@ class ProjectTeam ...@@ -96,13 +93,14 @@ class ProjectTeam
source_members.reject! do |member| source_members.reject! do |member|
# Skip if user already present in team # Skip if user already present in team
target_user_ids.include?(member.user_id) !member.invite? && target_user_ids.include?(member.user_id)
end end
source_members.map! do |member| source_members.map! do |member|
new_member = member.dup new_member = member.dup
new_member.id = nil new_member.id = nil
new_member.source = target_project new_member.source = target_project
new_member.created_by = current_user
new_member new_member
end end
......
...@@ -165,6 +165,18 @@ class NotificationService ...@@ -165,6 +165,18 @@ class NotificationService
end end
end end
def invite_project_member(project_member, token)
mailer.project_member_invited_email(project_member.id, token)
end
def accept_project_invite(project_member)
mailer.project_invite_accepted_email(project_member.id)
end
def decline_project_invite(project_member)
mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id)
end
def new_project_member(project_member) def new_project_member(project_member)
mailer.project_access_granted_email(project_member.id) mailer.project_access_granted_email(project_member.id)
end end
...@@ -173,6 +185,18 @@ class NotificationService ...@@ -173,6 +185,18 @@ class NotificationService
mailer.project_access_granted_email(project_member.id) mailer.project_access_granted_email(project_member.id)
end end
def invite_group_member(group_member, token)
mailer.group_member_invited_email(group_member.id, token)
end
def accept_group_invite(group_member)
mailer.group_invite_accepted_email(group_member.id)
end
def decline_group_invite(group_member)
mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id)
end
def new_group_member(group_member) def new_group_member(group_member)
mailer.group_access_granted_email(group_member.id) mailer.group_access_granted_email(group_member.id)
end end
......
...@@ -83,7 +83,7 @@ module Projects ...@@ -83,7 +83,7 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create) system_hook_service.execute_hooks_for(@project, :create)
unless @project.group unless @project.group
@project.team << [current_user, :master] @project.team << [current_user, :master, current_user]
end end
@project.update_column(:last_activity_at, @project.created_at) @project.update_column(:last_activity_at, @project.created_at)
......
...@@ -38,7 +38,7 @@ module Projects ...@@ -38,7 +38,7 @@ module Projects
#First save the DB entries as they can be rolled back if the repo fork fails #First save the DB entries as they can be rolled back if the repo fork fails
project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id)
if project.save if project.save
project.team << [@current_user, :master] project.team << [@current_user, :master, @current_user]
end end
#Now fork the repo #Now fork the repo
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div %div
= users_select_tag(:user_ids, multiple: true) = users_select_tag(:user_ids, multiple: true, email_user: true)
%div.prepend-top-10 %div.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr %hr
...@@ -74,13 +74,18 @@ ...@@ -74,13 +74,18 @@
%ul.well-list.group-users-list %ul.well-list.group-users-list
- @members.each do |member| - @members.each do |member|
- user = member.user - user = member.user
%li{class: dom_class(member), id: dom_id(user)} %li{class: dom_class(member), id: (dom_id(user) if user)}
.list-item-name .list-item-name
%strong - if user
= link_to user.name, admin_user_path(user) %strong
= link_to user.name, admin_user_path(user)
- else
%strong
= member.invite_email
(invited)
%span.pull-right.light %span.pull-right.light
= member.human_access = member.human_access
= link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse %i.fa.fa-minus.fa-inverse
.panel-footer .panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab' = paginate @members, param_name: 'members_page', theme: 'gitlab'
...@@ -124,14 +124,19 @@ ...@@ -124,14 +124,19 @@
- user = project_member.user - user = project_member.user
%li.project_member %li.project_member
.list-item-name .list-item-name
%strong - if user
= link_to user.name, admin_user_path(user) %strong
= link_to user.name, admin_user_path(user)
- else
%strong
= project_member.invite_email
(invited)
.pull-right .pull-right
- if project_member.owner? - if project_member.owner?
%span.light Owner %span.light Owner
- else - else
%span.light= project_member.human_access %span.light= project_member.human_access
= link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
%i.fa.fa-times %i.fa.fa-times
.panel-footer .panel-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
...@@ -182,7 +182,7 @@ ...@@ -182,7 +182,7 @@
.pull-right .pull-right
%span.light= group_member.human_access %span.light= group_member.human_access
- unless group_member.owner? - unless group_member.owner?
= link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse %i.fa.fa-times.fa-inverse
- else - else
.nothing-here-block This user has no groups. .nothing-here-block This user has no groups.
...@@ -221,7 +221,7 @@ ...@@ -221,7 +221,7 @@
%span.light= member.human_access %span.light= member.human_access
- if member.respond_to? :project - if member.respond_to? :project
= link_to namespace_project_project_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times %i.fa.fa-times
#ssh-keys.tab-pane #ssh-keys.tab-pane
= render 'profiles/keys/key_table', admin: true = render 'profiles/keys/key_table', admin: true
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
- group = group_member.group - group = group_member.group
%li %li
.pull-right .pull-right
- if can?(current_user, :manage_group, group) - if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do
%i.fa.fa-cogs %i.fa.fa-cogs
Settings Settings
......
- user = member.user - user = member.user
- return unless user - return unless user || member.invite?
- show_roles = true if show_roles.nil? - show_roles = true if show_roles.nil?
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)} %span{class: ("list-item-name" if show_controls)}
= image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' - if member.user
%strong= user.name = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
%span.cgray= user.username %strong= user.name
- if user == current_user %span.cgray= user.username
%span.label.label-success It's you - if user == current_user
- if user.blocked? %span.label.label-success It's you
%label.label.label-danger - if user.blocked?
%strong Blocked %label.label.label-danger
%strong Blocked
- else
= image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
%strong
= member.invite_email
%span.cgray
invited
- if member.created_by
by
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- if show_controls && can?(current_user, :admin_group, @group)
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- if show_roles - if show_roles
%span.pull-right %span.pull-right
...@@ -27,7 +42,7 @@ ...@@ -27,7 +42,7 @@
= link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse %i.fa.fa-minus.fa-inverse
- else - else
= link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse %i.fa.fa-minus.fa-inverse
.edit-member.hide.js-toggle-content .edit-member.hide.js-toggle-content
......
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| = form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
.form-group .form-group
= f.label :user_ids, "People", class: 'control-label' = f.label :user_ids, "People", class: 'control-label'
.col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all) .col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
Search for existing users or invite new ones using their email address.
.form-group .form-group
= f.label :access_level, "Group Access", class: 'control-label' = f.label :access_level, "Group Access", class: 'control-label'
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' }
= button_tag 'Search', class: 'btn' = button_tag 'Search', class: 'btn'
- if current_user && current_user.can?(:manage_group, @group) - if current_user && current_user.can?(:admin_group, @group)
.pull-right .pull-right
= button_tag class: 'btn btn-new js-toggle-button', type: 'button' do = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
Add members Add members
......
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
.pull-right .pull-right
- if can?(current_user, :manage_group, @group) - if can?(current_user, :admin_group, @group)
- if milestone.closed? - if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else - else
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
Open Open
Milestone #{@group_milestone.title} Milestone #{@group_milestone.title}
.pull-right .pull-right
- if can?(current_user, :manage_group, @group) - if can?(current_user, :admin_group, @group)
- if @group_milestone.active? - if @group_milestone.active?
= link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
- else - else
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.panel-heading .panel-heading
%strong= @group.name %strong= @group.name
projects: projects:
- if can? current_user, :manage_group, @group - if can? current_user, :admin_group, @group
.panel-head-actions .panel-head-actions
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
%i.fa.fa-plus %i.fa.fa-plus
......
%h3.page-title Invitation
%p
You have been invited
- if inviter = @member.created_by
by
= link_to inviter.name, user_url(inviter)
to join
- case @member.source
- when Project
- project = @member.source
project
%strong
= link_to project.name_with_namespace, namespace_project_url(project.namespace, project)
- when Group
- group = @member.source
group
%strong
= link_to group.name, group_url(group)
as #{@member.human_access}.
- if @member.source.users.include?(current_user)
%p
However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
Sign in using a different account to accept the invitation.
- else
.actions
= link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
= link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
= link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do = link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do
%i.fa.fa-user %i.fa.fa-user
%li %li
= link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do
%i.fa.fa-sign-out %i.fa.fa-sign-out
%li.hidden-xs %li.hidden-xs
= link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do = link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%span %span
Members Members
- if can?(current_user, :manage_group, @group) - if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do = nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do
= link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do = link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do
%i.fa.fa-cogs %i.fa.fa-cogs
......
%p
#{@group_member.invite_email}, now known as
#{link_to @group_member.user.name, user_url(@group_member.user)},
has accepted your invitation to join group
#{link_to @group.name, group_url(@group)}.
<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
<%= group_url(@group) %>
%p
#{@invite_email}
has declined your invitation to join group
#{link_to @group.name, group_url(@group)}.
<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
<%= group_url(@group) %>
%p
You have been invited
- if inviter = @group_member.created_by
by
= link_to inviter.name, user_url(inviter)
to join group
= link_to @group.name, group_url(@group)
as #{@group_member.human_access}.
%p
= link_to 'Accept invitation', invite_url(@token)
or
= link_to 'decline', decline_invite_url(@token)
You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>.
Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %>
%p
#{@project_member.invite_email}, now known as
#{link_to @project_member.user.name, user_url(@project_member.user)},
has accepted your invitation to join project
#{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>.
<%= namespace_project_url(@project.namespace, @project) %>
%p
#{@invite_email}
has declined your invitation to join project
#{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
<%= namespace_project_url(@project.namespace, @project) %>
%p
You have been invited
- if inviter = @project_member.created_by
by
= link_to inviter.name, user_url(inviter)
to join project
= link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)
as #{@project_member.human_access}.
%p
= link_to 'Accept invitation', invite_url(@token)
or
= link_to 'decline', decline_invite_url(@token)
You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>.
Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %>
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f|
.form-group .form-group
= f.label :user_ids, "People", class: 'control-label' = f.label :user_ids, "People", class: 'control-label'
.col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all) .col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
Search for existing users or invite new ones using their email address.
.form-group .form-group
= f.label :access_level, "Project Access", class: 'control-label' = f.label :access_level, "Project Access", class: 'control-label'
......
- user = member.user - user = member.user
- return unless user - return unless user || member.invite?
%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} %li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
%span.list-item-name %span.list-item-name
= image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' - if member.user
%strong= user.name = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
%span.cgray= user.username %strong
- if user == current_user = link_to user.name, user_path(user)
%span.label.label-success It's you %span.cgray= user.username
- if user.blocked? - if user == current_user
%label.label.label-danger %span.label.label-success It's you
%strong Blocked - if user.blocked?
%label.label.label-danger
%strong Blocked
- else
= image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
%strong
= member.invite_email
%span.cgray
invited
- if member.created_by
by
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- if current_user_can_admin_project
= link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- if current_user_can_admin_project - if current_user_can_admin_project
- unless @project.personal? && user == current_user - unless @project.personal? && user == current_user
...@@ -25,12 +41,12 @@ ...@@ -25,12 +41,12 @@
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
%i.fa.fa-minus.fa-inverse %i.fa.fa-minus.fa-inverse
- else - else
= link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
%i.fa.fa-minus.fa-inverse %i.fa.fa-minus.fa-inverse
.edit-member.hide.js-toggle-content .edit-member.hide.js-toggle-content
%br %br
= form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f| = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
.prepend-top-10 .prepend-top-10
= f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control' = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
.prepend-top-10 .prepend-top-10
......
...@@ -146,6 +146,11 @@ production: &base ...@@ -146,6 +146,11 @@ production: &base
# disable this setting, because the userPrincipalName contains an '@'. # disable this setting, because the userPrincipalName contains an '@'.
allow_username_or_email_login: false allow_username_or_email_login: false
# To maintain tight control over the number of active users on your GitLab installation,
# enable this setting to keep new users blocked until they have been cleared by the admin
# (default: false).
block_auto_created_users: false
# Base where we can search for users # Base where we can search for users
# #
# Ex. ou=People,dc=gitlab,dc=example # Ex. ou=People,dc=gitlab,dc=example
......
...@@ -76,6 +76,7 @@ if Settings.ldap['enabled'] || Rails.env.test? ...@@ -76,6 +76,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
Settings.ldap['servers'].each do |key, server| Settings.ldap['servers'].each do |key, server|
server['label'] ||= 'LDAP' server['label'] ||= 'LDAP'
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil?
server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil?
server['active_directory'] = true if server['active_directory'].nil? server['active_directory'] = true if server['active_directory'].nil?
server['provider_name'] ||= "ldap#{key}".downcase server['provider_name'] ||= "ldap#{key}".downcase
......
...@@ -53,6 +53,16 @@ Gitlab::Application.routes.draw do ...@@ -53,6 +53,16 @@ Gitlab::Application.routes.draw do
end end
get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ } get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ }
#
# Invites
#
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
match :decline, via: [:get, :post]
end
end
# #
# Import # Import
...@@ -260,6 +270,7 @@ Gitlab::Application.routes.draw do ...@@ -260,6 +270,7 @@ Gitlab::Application.routes.draw do
scope module: :groups do scope module: :groups do
resources :group_members, only: [:index, :create, :update, :destroy] do resources :group_members, only: [:index, :create, :update, :destroy] do
post :resend_invite, on: :member
delete :leave, on: :collection delete :leave, on: :collection
end end
...@@ -486,6 +497,10 @@ Gitlab::Application.routes.draw do ...@@ -486,6 +497,10 @@ Gitlab::Application.routes.draw do
get :import get :import
post :apply_import post :apply_import
end end
member do
post :resend_invite
end
end end
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
......
class AddInviteDataToMember < ActiveRecord::Migration
def change
add_column :members, :created_by_id, :integer
add_column :members, :invite_email, :string
add_column :members, :invite_token, :string
add_column :members, :invite_accepted_at, :datetime
change_column :members, :user_id, :integer, null: true
add_index :members, :invite_token, unique: true
end
end
...@@ -163,15 +163,20 @@ ActiveRecord::Schema.define(version: 20150413192223) do ...@@ -163,15 +163,20 @@ ActiveRecord::Schema.define(version: 20150413192223) do
t.integer "access_level", null: false t.integer "access_level", null: false
t.integer "source_id", null: false t.integer "source_id", null: false
t.string "source_type", null: false t.string "source_type", null: false
t.integer "user_id", null: false t.integer "user_id"
t.integer "notification_level", null: false t.integer "notification_level", null: false
t.string "type" t.string "type"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "created_by_id"
t.string "invite_email"
t.string "invite_token"
t.datetime "invite_accepted_at"
end end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
add_index "members", ["type"], name: "index_members_on_type", using: :btree add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
......
...@@ -51,6 +51,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server ...@@ -51,6 +51,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
# disable this setting, because the userPrincipalName contains an '@'. # disable this setting, because the userPrincipalName contains an '@'.
allow_username_or_email_login: false allow_username_or_email_login: false
# To maintain tight control over the number of active users on your GitLab installation,
# enable this setting to keep new users blocked until they have been cleared by the admin
# (default: false).
block_auto_created_users: false
# Base where we can search for users # Base where we can search for users
# #
# Ex. ou=People,dc=gitlab,dc=example # Ex. ou=People,dc=gitlab,dc=example
......
...@@ -55,6 +55,13 @@ Feature: Groups ...@@ -55,6 +55,13 @@ Feature: Groups
When I select "Mike" as "Reporter" When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter" Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Invite user to group
When I visit group "Owned" members page
And I click link "Add members"
When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
# Leave # Leave
@javascript @javascript
......
Feature: Invites
Background:
Given "John Doe" is owner of group "Owned"
And "John Doe" has invited "user@example.com" to group "Owned"
Scenario: Viewing invitation when signed out
When I visit the invitation page
Then I should be redirected to the sign in page
And I should see a notice telling me to sign in
Scenario: Signing in to view invitation
When I visit the invitation page
And I sign in as "Mary Jane"
Then I should be redirected to the invitation page
Scenario: Viewing invitation when signed in
Given I sign in as "Mary Jane"
And I visit the invitation page
Then I should see the invitation details
And I should see an "Accept invitation" button
And I should see a "Decline" button
Scenario: Viewing invitation as an existing member
Given I sign in as "John Doe"
And I visit the invitation page
Then I should see a message telling me I'm already a member
Scenario: Accepting the invitation
Given I sign in as "Mary Jane"
And I visit the invitation page
And I click the "Accept invitation" button
Then I should be redirected to the group page
And I should see a notice telling me I have access
Scenario: Declining the application when signed in
Given I sign in as "Mary Jane"
And I visit the invitation page
And I click the "Decline" button
Then I should be redirected to the dashboard
And I should see a notice telling me I have declined
Scenario: Declining the application when signed out
When I visit the invitation's decline page
Then I should be redirected to the sign in page
And I should see a notice telling me I have declined
...@@ -17,6 +17,12 @@ Feature: Project Team Management ...@@ -17,6 +17,12 @@ Feature: Project Team Management
And I select "Mike" as "Reporter" And I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter" Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Invite user to project
Given I click link "Add members"
And I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript @javascript
Scenario: Update user access Scenario: Update user access
Given I should see "Sam" in team list as "Developer" Given I should see "Sam" in team list as "Developer"
......
...@@ -31,6 +31,23 @@ class Spinach::Features::Groups < Spinach::FeatureSteps ...@@ -31,6 +31,23 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end end
end end
step 'I select "sjobs@apple.com" as "Reporter"' do
within ".users-group-form" do
select2("sjobs@apple.com", from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add users to group"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
within '.well-list' do
page.should have_content('sjobs@apple.com')
page.should have_content('invited')
page.should have_content('Reporter')
end
end
step 'I should see group "Owned" projects list' do step 'I should see group "Owned" projects list' do
Group.find_by(name: "Owned").projects.each do |project| Group.find_by(name: "Owned").projects.each do |project|
page.should have_link project.name page.should have_link project.name
......
class Spinach::Features::Invites < Spinach::FeatureSteps
include SharedAuthentication
include SharedUser
include SharedGroup
step '"John Doe" has invited "user@example.com" to group "Owned"' do
user = User.find_by(name: "John Doe")
group = Group.find_by(name: "Owned")
group.add_user("user@example.com", Gitlab::Access::DEVELOPER, user)
end
step 'I visit the invitation page' do
group = Group.find_by(name: "Owned")
invite = group.group_members.invite.last
invite.generate_invite_token!
@raw_invite_token = invite.raw_invite_token
visit invite_path(@raw_invite_token)
end
step 'I should be redirected to the sign in page' do
expect(current_path).to eq(new_user_session_path)
end
step 'I should see a notice telling me to sign in' do
expect(page).to have_content "To accept this invitation, sign in"
end
step 'I should be redirected to the invitation page' do
expect(current_path).to eq(invite_path(@raw_invite_token))
end
step 'I should see the invitation details' do
expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.")
end
step "I should see a message telling me I'm already a member" do
expect(page).to have_content("However, you are already a member of this group.")
end
step 'I should see an "Accept invitation" button' do
expect(page).to have_link("Accept invitation")
end
step 'I should see a "Decline" button' do
expect(page).to have_link("Decline")
end
step 'I click the "Accept invitation" button' do
page.click_link "Accept invitation"
end
step 'I should be redirected to the group page' do
group = Group.find_by(name: "Owned")
expect(current_path).to eq(group_path(group))
end
step 'I should see a notice telling me I have access' do
expect(page).to have_content("You have been granted Developer access to group Owned.")
end
step 'I click the "Decline" button' do
page.click_link "Decline"
end
step 'I should be redirected to the dashboard' do
expect(current_path).to eq(dashboard_path)
end
step 'I should see a notice telling me I have declined' do
expect(page).to have_content("You have declined the invitation to join group Owned.")
end
step "I visit the invitation's decline page" do
group = Group.find_by(name: "Owned")
invite = group.group_members.invite.last
invite.generate_invite_token!
@raw_invite_token = invite.raw_invite_token
visit decline_invite_path(@raw_invite_token)
end
end
...@@ -35,6 +35,22 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -35,6 +35,22 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
end end
step 'I select "sjobs@apple.com" as "Reporter"' do
within ".users-project-form" do
select2("sjobs@apple.com", from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add users to project"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
within ".access-reporter" do
page.should have_content('sjobs@apple.com')
page.should have_content('invited')
page.should have_content('Reporter')
end
end
step 'I should see "Sam" in team list as "Developer"' do step 'I should see "Sam" in team list as "Developer"' do
within ".access-developer" do within ".access-developer" do
page.should have_content('Sam') page.should have_content('Sam')
......
...@@ -9,8 +9,7 @@ module API ...@@ -9,8 +9,7 @@ module API
# GET /groups/:id/members # GET /groups/:id/members
get ":id/members" do get ":id/members" do
group = find_group(params[:id]) group = find_group(params[:id])
members = group.group_members users = group.users
users = (paginate members).collect(&:user)
present users, with: Entities::GroupMember, group: group present users, with: Entities::GroupMember, group: group
end end
...@@ -24,7 +23,7 @@ module API ...@@ -24,7 +23,7 @@ module API
# POST /groups/:id/members # POST /groups/:id/members
post ":id/members" do post ":id/members" do
group = find_group(params[:id]) group = find_group(params[:id])
authorize! :manage_group, group authorize! :admin_group, group
required_attributes! [:user_id, :access_level] required_attributes! [:user_id, :access_level]
unless validate_access_level?(params[:access_level]) unless validate_access_level?(params[:access_level])
...@@ -35,7 +34,7 @@ module API ...@@ -35,7 +34,7 @@ module API
render_api_error!("Already exists", 409) render_api_error!("Already exists", 409)
end end
group.add_users([params[:user_id]], params[:access_level]) group.add_users([params[:user_id]], params[:access_level], current_user)
member = group.group_members.find_by(user_id: params[:user_id]) member = group.group_members.find_by(user_id: params[:user_id])
present member.user, with: Entities::GroupMember, group: group present member.user, with: Entities::GroupMember, group: group
end end
...@@ -50,7 +49,7 @@ module API ...@@ -50,7 +49,7 @@ module API
# PUT /groups/:id/members/:user_id # PUT /groups/:id/members/:user_id
put ':id/members/:user_id' do put ':id/members/:user_id' do
group = find_group(params[:id]) group = find_group(params[:id])
authorize! :manage_group, group authorize! :admin_group, group
required_attributes! [:access_level] required_attributes! [:access_level]
group_member = group.group_members.find_by(user_id: params[:user_id]) group_member = group.group_members.find_by(user_id: params[:user_id])
...@@ -74,7 +73,7 @@ module API ...@@ -74,7 +73,7 @@ module API
# DELETE /groups/:id/members/:user_id # DELETE /groups/:id/members/:user_id
delete ":id/members/:user_id" do delete ":id/members/:user_id" do
group = find_group(params[:id]) group = find_group(params[:id])
authorize! :manage_group, group authorize! :admin_group, group
member = group.group_members.find_by(user_id: params[:user_id]) member = group.group_members.find_by(user_id: params[:user_id])
if member.nil? if member.nil?
......
...@@ -61,7 +61,7 @@ module API ...@@ -61,7 +61,7 @@ module API
# DELETE /groups/:id # DELETE /groups/:id
delete ":id" do delete ":id" do
group = find_group(params[:id]) group = find_group(params[:id])
authorize! :manage_group, group authorize! :admin_group, group
group.destroy group.destroy
end end
......
module Gitlab
class KeyFingerprint
include Gitlab::Popen
attr_accessor :key
def initialize(key)
@key = key
end
def fingerprint
cmd_status = 0
cmd_output = ''
Tempfile.open('gitlab_key_file') do |file|
file.puts key
file.rewind
cmd = []
cmd.push *%W(ssh-keygen)
cmd.push *%W(-E md5) if explicit_fingerprint_algorithm?
cmd.push *%W(-lf #{file.path})
cmd_output, cmd_status = popen(cmd, '/tmp')
end
return nil unless cmd_status.zero?
# 16 hex bytes separated by ':', optionally starting with "MD5:"
fingerprint_matches = cmd_output.match(/(MD5:)?(?<fingerprint>(\h{2}:){15}\h{2})/)
return nil unless fingerprint_matches
fingerprint_matches[:fingerprint]
end
private
def explicit_fingerprint_algorithm?
# OpenSSH 6.8 introduces a new default output format for fingerprints.
# Check the version and decide which command to use.
version_output, version_status = popen(%W(ssh -V))
return false unless version_status.zero?
version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/)
return false unless version_matches
version_info = Gitlab::VersionInfo.new(version_matches[:major].to_i, version_matches[:minor].to_i)
required_version_info = Gitlab::VersionInfo.new(6, 8)
version_info >= required_version_info
end
end
end
...@@ -80,6 +80,10 @@ module Gitlab ...@@ -80,6 +80,10 @@ module Gitlab
options['active_directory'] options['active_directory']
end end
def block_auto_created_users
options['block_auto_created_users']
end
protected protected
def base_config def base_config
Gitlab.config.ldap Gitlab.config.ldap
......
...@@ -39,6 +39,8 @@ module Gitlab ...@@ -39,6 +39,8 @@ module Gitlab
end end
def update_user_attributes def update_user_attributes
return unless persisted?
gl_user.skip_reconfirmation! gl_user.skip_reconfirmation!
gl_user.email = auth_hash.email gl_user.email = auth_hash.email
...@@ -53,13 +55,17 @@ module Gitlab ...@@ -53,13 +55,17 @@ module Gitlab
gl_user.changed? || gl_user.identities.any?(&:changed?) gl_user.changed? || gl_user.identities.any?(&:changed?)
end end
def needs_blocking? def block_after_signup?
false ldap_config.block_auto_created_users
end end
def allowed? def allowed?
Gitlab::LDAP::Access.allowed?(gl_user) Gitlab::LDAP::Access.allowed?(gl_user)
end end
def ldap_config
Gitlab::LDAP::Config.new(auth_hash.provider)
end
end end
end end
end end
require "spec_helper"
describe Gitlab::KeyFingerprint do
let(:key) { "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" }
let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" }
describe "#fingerprint" do
it "generates the key's fingerprint" do
expect(Gitlab::KeyFingerprint.new(key).fingerprint).to eq(fingerprint)
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::LDAP::User do describe Gitlab::LDAP::User do
let(:gl_user) { Gitlab::LDAP::User.new(auth_hash) } let(:ldap_user) { Gitlab::LDAP::User.new(auth_hash) }
let(:gl_user) { ldap_user.gl_user }
let(:info) do let(:info) do
{ {
name: 'John', name: 'John',
...@@ -16,17 +17,17 @@ describe Gitlab::LDAP::User do ...@@ -16,17 +17,17 @@ describe Gitlab::LDAP::User do
describe :changed? do describe :changed? do
it "marks existing ldap user as changed" do it "marks existing ldap user as changed" do
existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
expect(gl_user.changed?).to be_truthy expect(ldap_user.changed?).to be_truthy
end end
it "marks existing non-ldap user if the email matches as changed" do it "marks existing non-ldap user if the email matches as changed" do
existing_user = create(:user, email: 'john@example.com') existing_user = create(:user, email: 'john@example.com')
expect(gl_user.changed?).to be_truthy expect(ldap_user.changed?).to be_truthy
end end
it "dont marks existing ldap user as changed" do it "dont marks existing ldap user as changed" do
existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain')
expect(gl_user.changed?).to be_falsey expect(ldap_user.changed?).to be_falsey
end end
end end
...@@ -34,12 +35,12 @@ describe Gitlab::LDAP::User do ...@@ -34,12 +35,12 @@ describe Gitlab::LDAP::User do
it "finds the user if already existing" do it "finds the user if already existing" do
existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
expect{ gl_user.save }.to_not change{ User.count } expect{ ldap_user.save }.to_not change{ User.count }
end end
it "connects to existing non-ldap user if the email matches" do it "connects to existing non-ldap user if the email matches" do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter") existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter")
expect{ gl_user.save }.to_not change{ User.count } expect{ ldap_user.save }.to_not change{ User.count }
existing_user.reload existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
...@@ -47,7 +48,59 @@ describe Gitlab::LDAP::User do ...@@ -47,7 +48,59 @@ describe Gitlab::LDAP::User do
end end
it "creates a new user if not found" do it "creates a new user if not found" do
expect{ gl_user.save }.to change{ User.count }.by(1) expect{ ldap_user.save }.to change{ User.count }.by(1)
end
end
describe 'blocking' do
context 'signup' do
context 'dont block on create' do
before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false }
it do
ldap_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create' do
before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true }
it do
ldap_user.save
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
end
end
context 'sign-in' do
before do
ldap_user.save
ldap_user.gl_user.activate
end
context 'dont block on create' do
before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false }
it do
ldap_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create' do
before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true }
it do
ldap_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
end end
end end
end end
...@@ -725,6 +725,11 @@ describe Notify do ...@@ -725,6 +725,11 @@ describe Notify do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
expect(sender.address).to eq(user.email) expect(sender.address).to eq(user.email)
end end
it "is set to reply to the committer email" do
sender = subject.header[:reply_to].addrs[0]
expect(sender.address).to eq(user.email)
end
end end
context "when the committer email domain is not completely within the GitLab domain" do context "when the committer email domain is not completely within the GitLab domain" do
...@@ -738,6 +743,11 @@ describe Notify do ...@@ -738,6 +743,11 @@ describe Notify do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
expect(sender.address).to eq(gitlab_sender) expect(sender.address).to eq(gitlab_sender)
end end
it "is set to reply to the default email" do
sender = subject.header[:reply_to].addrs[0]
expect(sender.address).to eq(gitlab_sender_reply_to)
end
end end
context "when the committer email domain is outside the GitLab domain" do context "when the committer email domain is outside the GitLab domain" do
...@@ -751,6 +761,11 @@ describe Notify do ...@@ -751,6 +761,11 @@ describe Notify do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
expect(sender.address).to eq(gitlab_sender) expect(sender.address).to eq(gitlab_sender)
end end
it "is set to reply to the default email" do
sender = subject.header[:reply_to].addrs[0]
expect(sender.address).to eq(gitlab_sender_reply_to)
end
end end
end end
end end
......
...@@ -14,7 +14,7 @@ describe Commit do ...@@ -14,7 +14,7 @@ describe Commit do
message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
allow(commit).to receive(:safe_message).and_return(message) allow(commit).to receive(:safe_message).and_return(message)
expect(commit.title).to eq("#{message[0..79]}&hellip;") expect(commit.title).to eq("#{message[0..79]}")
end end
it "truncates a message with a newline before 80 characters at the newline" do it "truncates a message with a newline before 80 characters at the newline" do
......
require 'spec_helper'
describe Member do
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
describe "Validation" do
subject { Member.new(access_level: Member::GUEST) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
context "when an invite email is provided" do
let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) }
it "doesn't require a user" do
expect(member).to be_valid
end
it "requires a valid invite email" do
member.invite_email = "nope"
expect(member).not_to be_valid
end
it "requires a unique invite email scoped to this source" do
create(:project_member, source: member.source, invite_email: member.invite_email)
expect(member).not_to be_valid
end
it "is valid otherwise" do
expect(member).to be_valid
end
end
context "when an invite email is not provided" do
let(:member) { build(:project_member) }
it "requires a user" do
member.user = nil
expect(member).not_to be_valid
end
it "is valid otherwise" do
expect(member).to be_valid
end
end
end
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
end
describe ".add_user" do
let!(:user) { create(:user) }
let(:project) { create(:project) }
context "when called with a user id" do
it "adds the user as a member" do
Member.add_user(project.project_members, user.id, ProjectMember::MASTER)
expect(project.users).to include(user)
end
end
context "when called with a user object" do
it "adds the user as a member" do
Member.add_user(project.project_members, user, ProjectMember::MASTER)
expect(project.users).to include(user)
end
end
context "when called with a known user email" do
it "adds the user as a member" do
Member.add_user(project.project_members, user.email, ProjectMember::MASTER)
expect(project.users).to include(user)
end
end
context "when called with an unknown user email" do
it "adds a member invite" do
Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER)
expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com")
end
end
end
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
it "resets the invite token" do
member.accept_invite!(user)
expect(member.invite_token).to be_nil
end
it "sets the invite accepted timestamp" do
member.accept_invite!(user)
expect(member.invite_accepted_at).not_to be_nil
end
it "sets the user" do
member.accept_invite!(user)
expect(member.user).to eq(user)
end
it "calls #after_accept_invite" do
expect(member).to receive(:after_accept_invite)
member.accept_invite!(user)
end
end
describe "#decline_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
it "destroys the member" do
member.decline_invite!
expect(member).to be_destroyed
end
it "calls #after_decline_invite" do
expect(member).to receive(:after_decline_invite)
member.decline_invite!
end
end
describe "#generate_invite_token" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
it "sets the invite token" do
expect { member.generate_invite_token }.to change { member.invite_token}
end
end
end
require 'spec_helper'
describe Member do
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
describe "Validation" do
subject { Member.new(access_level: Member::GUEST) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
end
end
...@@ -11,8 +11,6 @@ describe API::API, api: true do ...@@ -11,8 +11,6 @@ describe API::API, api: true do
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
before { project.team << [user, :reporter] }
describe "GET /projects/:id/repository/tags" do describe "GET /projects/:id/repository/tags" do
it "should return an array of project tags" do it "should return an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user) get api("/projects/#{project.id}/repository/tags", user)
......
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