Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
This commit is contained in:
commit
cc41381be4
|
|
@ -1,6 +1,11 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
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 persistent XSS vulnerability around profile website URLs.
|
||||
- Fix project import URL regex to prevent arbitary local repos from being imported.
|
||||
|
|
@ -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)
|
||||
- Allow HTML tags in Markdown input
|
||||
- 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)
|
||||
- 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)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class @UsersSelect
|
|||
@groupId = $(select).data('group-id')
|
||||
showNullUser = $(select).data('null-user')
|
||||
showAnyUser = $(select).data('any-user')
|
||||
showEmailUser = $(select).data('email-user')
|
||||
firstUser = $(select).data('first-user')
|
||||
|
||||
$(select).select2
|
||||
|
|
@ -19,20 +20,6 @@ class @UsersSelect
|
|||
data = { results: users }
|
||||
|
||||
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
|
||||
# Move current user to the front of the list
|
||||
for obj, index in data.results
|
||||
|
|
@ -40,11 +27,34 @@ class @UsersSelect
|
|||
data.results.splice(index, 1)
|
||||
data.results.unshift(obj)
|
||||
break
|
||||
|
||||
if showNullUser
|
||||
nullUser = {
|
||||
name: 'Unassigned',
|
||||
avatar: null,
|
||||
username: 'none',
|
||||
id: 0
|
||||
}
|
||||
data.results.unshift(nullUser)
|
||||
|
||||
if showAnyUser
|
||||
anyUser = {
|
||||
name: 'Any',
|
||||
avatar: null,
|
||||
username: 'none',
|
||||
id: null
|
||||
}
|
||||
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)
|
||||
|
||||
initSelection: (element, callback) =>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
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.'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def repository
|
||||
@repository ||= project.repository
|
||||
rescue Grit::NoSuchPathError(e)
|
||||
rescue Grit::NoSuchPathError => e
|
||||
log_exception(e)
|
||||
nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController
|
|||
|
||||
def after_confirmation_path_for(resource_name, resource)
|
||||
if signed_in?(resource_name)
|
||||
signed_in_root_path(resource)
|
||||
after_sign_in_path_for(resource)
|
||||
else
|
||||
sign_in(resource)
|
||||
if signed_in?(resource_name)
|
||||
signed_in_root_path(resource)
|
||||
after_sign_in_path_for(resource)
|
||||
else
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class Groups::ApplicationController < ApplicationController
|
|||
end
|
||||
|
||||
def authorize_admin_group!
|
||||
unless can?(current_user, :manage_group, group)
|
||||
unless can?(current_user, :admin_group, group)
|
||||
return render_404
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
def index
|
||||
@project = @group.projects.find(params[:project_id]) if params[:project_id]
|
||||
@members = @group.group_members
|
||||
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
|
||||
|
||||
if params[:search].present?
|
||||
users = @group.users.search(params[:search]).to_a
|
||||
|
|
@ -22,7 +23,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
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.'
|
||||
end
|
||||
|
|
@ -38,7 +39,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
|
||||
@group_member.destroy
|
||||
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 }
|
||||
end
|
||||
else
|
||||
|
|
@ -46,12 +47,26 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
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
|
||||
@group_member = @group.group_members.where(user_id: current_user.id).first
|
||||
|
||||
if can?(current_user, :destroy_group_member, @group_member)
|
||||
@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
|
||||
return render_403
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,6 +51,6 @@ class Groups::MilestonesController < ApplicationController
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
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
|
|||
|
||||
def index
|
||||
@project_members = @project.project_members
|
||||
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
|
||||
|
||||
if params[:search].present?
|
||||
users = @project.users.search(params[:search]).to_a
|
||||
|
|
@ -17,6 +18,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
@group = @project.group
|
||||
if @group
|
||||
@group_members = @group.group_members
|
||||
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
|
||||
|
||||
if params[:search].present?
|
||||
users = @group.users.search(params[:search]).to_a
|
||||
|
|
@ -34,30 +36,42 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
users = User.where(id: params[:user_ids].split(','))
|
||||
@project.team << [users, params[:access_level]]
|
||||
@project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
|
||||
|
||||
redirect_to namespace_project_project_members_path(@project.namespace, @project)
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@project_member = @project.project_members.find_by(user_id: member)
|
||||
@project_member = @project.project_members.find(params[:id])
|
||||
@project_member.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to namespace_project_project_members_path(@project.namespace,
|
||||
@project)
|
||||
redirect_to namespace_project_project_members_path(@project.namespace, @project)
|
||||
end
|
||||
format.js { render nothing: true }
|
||||
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
|
||||
@project.project_members.find_by(user_id: current_user).destroy
|
||||
|
||||
|
|
@ -69,7 +83,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
|
||||
def apply_import
|
||||
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"
|
||||
|
||||
redirect_to(namespace_project_project_members_path(project.namespace, project),
|
||||
|
|
@ -78,10 +92,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def member
|
||||
@member ||= User.find_by(username: params[:id])
|
||||
end
|
||||
|
||||
def member_params
|
||||
params.require(:project_member).permit(:user_id, :access_level)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
module GroupsHelper
|
||||
def remove_user_from_group_message(group, user)
|
||||
"Are you sure you want to remove \"#{user.name}\" from \"#{group.name}\"?"
|
||||
def remove_user_from_group_message(group, member)
|
||||
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
|
||||
|
||||
def leave_group_message(group)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
module ProjectsHelper
|
||||
def remove_from_project_team_message(project, user)
|
||||
"You are going to remove #{user.name} from #{project.name} project team. Are you sure?"
|
||||
def remove_from_project_team_message(project, member)
|
||||
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
|
||||
|
||||
def link_to_project(project)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ module SelectsHelper
|
|||
|
||||
null_user = opts[:null_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
|
||||
|
||||
html = {
|
||||
|
|
@ -15,6 +16,7 @@ module SelectsHelper
|
|||
'data-placeholder' => placeholder,
|
||||
'data-null-user' => null_user,
|
||||
'data-any-user' => any_user,
|
||||
'data-email-user' => email_user,
|
||||
'data-first-user' => first_user
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,50 @@ module Emails
|
|||
def group_access_granted_email(group_member_id)
|
||||
@group_member = GroupMember.find(group_member_id)
|
||||
@group = @group_member.group
|
||||
|
||||
@target_url = group_url(@group)
|
||||
@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"))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,14 +1,55 @@
|
|||
module Emails
|
||||
module Projects
|
||||
def project_access_granted_email(user_project_id)
|
||||
@project_member = ProjectMember.find user_project_id
|
||||
def project_access_granted_email(project_member_id)
|
||||
@project_member = ProjectMember.find project_member_id
|
||||
@project = @project_member.project
|
||||
|
||||
@target_url = namespace_project_url(@project.namespace, @project)
|
||||
@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"))
|
||||
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)
|
||||
@current_user = @user = User.find user_id
|
||||
@project = Project.find project_id
|
||||
|
|
@ -84,9 +125,17 @@ module Emails
|
|||
|
||||
@disable_footer = true
|
||||
|
||||
mail(from: sender(author_id, send_from_committer_email),
|
||||
to: recipient,
|
||||
subject: @subject)
|
||||
reply_to =
|
||||
if send_from_committer_email && can_send_from_user_email?(@author)
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -60,20 +60,24 @@ class Notify < ActionMailer::Base
|
|||
address
|
||||
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.
|
||||
# Only the displayed name changes; the actual email address is always the same.
|
||||
def sender(sender_id, send_from_user_email = false)
|
||||
if sender = User.find(sender_id)
|
||||
address = default_sender_address
|
||||
address.display_name = sender.name
|
||||
return unless sender = User.find(sender_id)
|
||||
|
||||
address = default_sender_address
|
||||
address.display_name = sender.name
|
||||
|
||||
sender_domain = sender.email.split("@").last
|
||||
if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain)
|
||||
address.address = sender.email
|
||||
end
|
||||
|
||||
address.format
|
||||
if send_from_user_email && can_send_from_user_email?(sender)
|
||||
address.address = sender.email
|
||||
end
|
||||
|
||||
address.format
|
||||
end
|
||||
|
||||
# Look up a User by their ID and return their email address
|
||||
|
|
|
|||
|
|
@ -198,11 +198,11 @@ class Ability
|
|||
])
|
||||
end
|
||||
|
||||
# Only group owner and administrators can manage group
|
||||
# Only group owner and administrators can admin group
|
||||
if group.has_owner?(user) || user.admin?
|
||||
rules.push(*[
|
||||
:manage_group,
|
||||
:manage_namespace
|
||||
:admin_group,
|
||||
:admin_namespace
|
||||
])
|
||||
end
|
||||
|
||||
|
|
@ -212,11 +212,11 @@ class Ability
|
|||
def namespace_abilities(user, namespace)
|
||||
rules = []
|
||||
|
||||
# Only namespace owner and administrators can manage it
|
||||
# Only namespace owner and administrators can admin it
|
||||
if namespace.owner == user || user.admin?
|
||||
rules.push(*[
|
||||
:create_projects,
|
||||
:manage_namespace
|
||||
:admin_namespace
|
||||
])
|
||||
end
|
||||
|
||||
|
|
@ -254,7 +254,7 @@ class Ability
|
|||
rules = []
|
||||
target_user = subject.user
|
||||
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)
|
||||
rules << :modify_group_member
|
||||
rules << :destroy_group_member
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class Commit
|
|||
|
||||
title_end = title.index("\n")
|
||||
if (!title_end && title.length > 100) || (title_end && title_end > 100)
|
||||
title[0..79] << "…".html_safe
|
||||
title[0..79] << "…"
|
||||
else
|
||||
title.split("\n", 2).first
|
||||
end
|
||||
|
|
@ -90,7 +90,7 @@ class Commit
|
|||
title_end = safe_message.index("\n")
|
||||
@description ||=
|
||||
if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
|
||||
"…".html_safe << safe_message[80..-1]
|
||||
"…" << safe_message[80..-1]
|
||||
else
|
||||
safe_message.split("\n", 2)[1].try(:chomp)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,19 +46,18 @@ class Group < Namespace
|
|||
@owners ||= group_members.owners.map(&:user)
|
||||
end
|
||||
|
||||
def add_users(user_ids, access_level)
|
||||
user_ids.compact.each do |user_id|
|
||||
user = self.group_members.find_or_initialize_by(user_id: user_id)
|
||||
user.update_attributes(access_level: access_level)
|
||||
def add_users(user_ids, access_level, current_user = nil)
|
||||
user_ids.each do |user_id|
|
||||
Member.add_user(self.group_members, user_id, access_level, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def add_user(user, access_level)
|
||||
self.group_members.create(user_id: user.id, access_level: access_level)
|
||||
def add_user(user, access_level, current_user = nil)
|
||||
add_users([user], access_level, current_user)
|
||||
end
|
||||
|
||||
def add_owner(user)
|
||||
self.add_user(user, Gitlab::Access::OWNER)
|
||||
def add_owner(user, current_user = nil)
|
||||
self.add_user(user, Gitlab::Access::OWNER, current_user)
|
||||
end
|
||||
|
||||
def has_owner?(user)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ require 'digest/md5'
|
|||
|
||||
class Key < ActiveRecord::Base
|
||||
include Sortable
|
||||
include Gitlab::Popen
|
||||
|
||||
belongs_to :user
|
||||
|
||||
|
|
@ -79,20 +78,9 @@ class Key < ActiveRecord::Base
|
|||
|
||||
def generate_fingerprint
|
||||
self.fingerprint = nil
|
||||
return unless key.present?
|
||||
|
||||
cmd_status = 0
|
||||
cmd_output = ''
|
||||
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
|
||||
return unless self.key.present?
|
||||
|
||||
if cmd_status.zero?
|
||||
cmd_output.gsub /(\h{2}:)+\h{2}/ do |match|
|
||||
self.fingerprint = match
|
||||
end
|
||||
end
|
||||
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
# type :string(255)
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# created_by_id :integer
|
||||
# invite_email :string
|
||||
# invite_token :string
|
||||
# invite_accepted_at :datetime
|
||||
#
|
||||
|
||||
class Member < ActiveRecord::Base
|
||||
|
|
@ -18,19 +22,151 @@ class Member < ActiveRecord::Base
|
|||
include Notifiable
|
||||
include Gitlab::Access
|
||||
|
||||
attr_accessor :raw_invite_token
|
||||
|
||||
belongs_to :created_by, class_name: "User"
|
||||
belongs_to :user
|
||||
belongs_to :source, polymorphic: true
|
||||
|
||||
validates :user, presence: true
|
||||
validates :user, presence: true, unless: :invite?
|
||||
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 :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 :reporters, -> { where(access_level: REPORTER) }
|
||||
scope :developers, -> { where(access_level: DEVELOPER) }
|
||||
scope :masters, -> { where(access_level: MASTER) }
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@ class GroupMember < Member
|
|||
scope :with_group, ->(group) { where(source_id: group.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
|
||||
Gitlab::Access.options_with_owner
|
||||
end
|
||||
|
|
@ -43,26 +39,37 @@ class GroupMember < Member
|
|||
access_level
|
||||
end
|
||||
|
||||
def post_create_hook
|
||||
notification_service.new_group_member(self)
|
||||
system_hook_service.execute_hooks_for(self, :create)
|
||||
private
|
||||
|
||||
def send_invite
|
||||
notification_service.invite_group_member(self, @raw_invite_token)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def notify_update
|
||||
def post_create_hook
|
||||
notification_service.new_group_member(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def post_update_hook
|
||||
if access_level_changed?
|
||||
notification_service.update_group_member(self)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def post_destroy_hook
|
||||
system_hook_service.execute_hooks_for(self, :destroy)
|
||||
def after_accept_invite
|
||||
notification_service.accept_group_invite(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def system_hook_service
|
||||
SystemHooksService.new
|
||||
end
|
||||
def after_decline_invite
|
||||
notification_service.decline_group_invite(self)
|
||||
|
||||
def notification_service
|
||||
NotificationService.new
|
||||
super
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@ class ProjectMember < Member
|
|||
validates_format_of :source_type, with: /\AProject\z/
|
||||
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_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
|
||||
scope :with_user, ->(user) { where(user_id: user.id) }
|
||||
|
|
@ -55,7 +51,7 @@ class ProjectMember < Member
|
|||
# :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)
|
||||
roles_hash[access]
|
||||
elsif roles_hash.values.include?(access.to_i)
|
||||
|
|
@ -64,12 +60,14 @@ class ProjectMember < Member
|
|||
raise "Non valid access"
|
||||
end
|
||||
|
||||
users = user_ids.map { |user_id| Member.user_for_id(user_id) }
|
||||
|
||||
ProjectMember.transaction do
|
||||
project_ids.each do |project_id|
|
||||
user_ids.each do |user_id|
|
||||
member = ProjectMember.new(access_level: access_level, user_id: user_id)
|
||||
member.source_id = project_id
|
||||
member.save
|
||||
project = Project.find(project_id)
|
||||
|
||||
users.each do |user|
|
||||
Member.add_user(project.project_members, user, access_level, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -82,6 +80,7 @@ class ProjectMember < Member
|
|||
def truncate_teams(project_ids)
|
||||
ProjectMember.transaction do
|
||||
members = ProjectMember.where(source_id: project_ids)
|
||||
|
||||
members.each do |member|
|
||||
member.destroy
|
||||
end
|
||||
|
|
@ -109,41 +108,58 @@ class ProjectMember < Member
|
|||
access_level
|
||||
end
|
||||
|
||||
def project
|
||||
source
|
||||
end
|
||||
|
||||
def owner?
|
||||
project.owner == user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invite
|
||||
notification_service.invite_project_member(self, @raw_invite_token)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def post_create_hook
|
||||
unless owner?
|
||||
event_service.join_project(self.project, self.user)
|
||||
notification_service.new_project_member(self)
|
||||
end
|
||||
|
||||
system_hook_service.execute_hooks_for(self, :create)
|
||||
super
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def post_destroy_hook
|
||||
event_service.leave_project(self.project, self.user)
|
||||
system_hook_service.execute_hooks_for(self, :destroy)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def after_accept_invite
|
||||
notification_service.accept_project_invite(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def after_decline_invite
|
||||
notification_service.decline_project_invite(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def event_service
|
||||
EventCreateService.new
|
||||
end
|
||||
|
||||
def notification_service
|
||||
NotificationService.new
|
||||
end
|
||||
|
||||
def system_hook_service
|
||||
SystemHooksService.new
|
||||
end
|
||||
|
||||
def project
|
||||
source
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ class ProjectTeam
|
|||
# @team << [@users, :master]
|
||||
#
|
||||
def <<(args)
|
||||
users = args.first
|
||||
users, access, current_user = *args
|
||||
|
||||
if users.respond_to?(:each)
|
||||
add_users(users, args.second)
|
||||
add_users(users, access, current_user)
|
||||
else
|
||||
add_user(users, args.second)
|
||||
add_user(users, access, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -43,22 +43,19 @@ class ProjectTeam
|
|||
member
|
||||
end
|
||||
|
||||
def add_user(user, access)
|
||||
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)
|
||||
def add_users(users, access, current_user = nil)
|
||||
ProjectMember.add_users_into_projects(
|
||||
[project.id],
|
||||
user_ids,
|
||||
access
|
||||
users,
|
||||
access,
|
||||
current_user
|
||||
)
|
||||
end
|
||||
|
||||
def add_user(user, access, current_user = nil)
|
||||
add_users([user], access, current_user)
|
||||
end
|
||||
|
||||
# Remove all users from project team
|
||||
def truncate
|
||||
ProjectMember.truncate_team(project)
|
||||
|
|
@ -88,7 +85,7 @@ class ProjectTeam
|
|||
@masters ||= fetch_members(:masters)
|
||||
end
|
||||
|
||||
def import(source_project)
|
||||
def import(source_project, current_user = nil)
|
||||
target_project = project
|
||||
|
||||
source_members = source_project.project_members.to_a
|
||||
|
|
@ -96,13 +93,14 @@ class ProjectTeam
|
|||
|
||||
source_members.reject! do |member|
|
||||
# Skip if user already present in team
|
||||
target_user_ids.include?(member.user_id)
|
||||
!member.invite? && target_user_ids.include?(member.user_id)
|
||||
end
|
||||
|
||||
source_members.map! do |member|
|
||||
new_member = member.dup
|
||||
new_member.id = nil
|
||||
new_member.source = target_project
|
||||
new_member.created_by = current_user
|
||||
new_member
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,18 @@ class NotificationService
|
|||
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)
|
||||
mailer.project_access_granted_email(project_member.id)
|
||||
end
|
||||
|
|
@ -173,6 +185,18 @@ class NotificationService
|
|||
mailer.project_access_granted_email(project_member.id)
|
||||
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)
|
||||
mailer.group_access_granted_email(group_member.id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ module Projects
|
|||
system_hook_service.execute_hooks_for(@project, :create)
|
||||
|
||||
unless @project.group
|
||||
@project.team << [current_user, :master]
|
||||
@project.team << [current_user, :master, current_user]
|
||||
end
|
||||
|
||||
@project.update_column(:last_activity_at, @project.created_at)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ module Projects
|
|||
#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)
|
||||
if project.save
|
||||
project.team << [@current_user, :master]
|
||||
project.team << [@current_user, :master, @current_user]
|
||||
end
|
||||
|
||||
#Now fork the repo
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
|
||||
%div
|
||||
= users_select_tag(:user_ids, multiple: true)
|
||||
= users_select_tag(:user_ids, multiple: true, email_user: true)
|
||||
%div.prepend-top-10
|
||||
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
|
||||
%hr
|
||||
|
|
@ -74,13 +74,18 @@
|
|||
%ul.well-list.group-users-list
|
||||
- @members.each do |member|
|
||||
- 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
|
||||
%strong
|
||||
= link_to user.name, admin_user_path(user)
|
||||
- if user
|
||||
%strong
|
||||
= link_to user.name, admin_user_path(user)
|
||||
- else
|
||||
%strong
|
||||
= member.invite_email
|
||||
(invited)
|
||||
%span.pull-right.light
|
||||
= 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
|
||||
.panel-footer
|
||||
= paginate @members, param_name: 'members_page', theme: 'gitlab'
|
||||
|
|
|
|||
|
|
@ -124,14 +124,19 @@
|
|||
- user = project_member.user
|
||||
%li.project_member
|
||||
.list-item-name
|
||||
%strong
|
||||
= link_to user.name, admin_user_path(user)
|
||||
- if user
|
||||
%strong
|
||||
= link_to user.name, admin_user_path(user)
|
||||
- else
|
||||
%strong
|
||||
= project_member.invite_email
|
||||
(invited)
|
||||
.pull-right
|
||||
- if project_member.owner?
|
||||
%span.light Owner
|
||||
- else
|
||||
%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
|
||||
.panel-footer
|
||||
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@
|
|||
.pull-right
|
||||
%span.light= group_member.human_access
|
||||
- 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
|
||||
- else
|
||||
.nothing-here-block This user has no groups.
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
%span.light= member.human_access
|
||||
|
||||
- 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
|
||||
#ssh-keys.tab-pane
|
||||
= render 'profiles/keys/key_table', admin: true
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
- group = group_member.group
|
||||
%li
|
||||
.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
|
||||
%i.fa.fa-cogs
|
||||
Settings
|
||||
|
|
|
|||
|
|
@ -1,17 +1,32 @@
|
|||
- user = member.user
|
||||
- return unless user
|
||||
- return unless user || member.invite?
|
||||
- show_roles = true if show_roles.nil?
|
||||
|
||||
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
|
||||
%span{class: ("list-item-name" if show_controls)}
|
||||
= image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
|
||||
%strong= user.name
|
||||
%span.cgray= user.username
|
||||
- if user == current_user
|
||||
%span.label.label-success It's you
|
||||
- if user.blocked?
|
||||
%label.label.label-danger
|
||||
%strong Blocked
|
||||
- if member.user
|
||||
= image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
|
||||
%strong= user.name
|
||||
%span.cgray= user.username
|
||||
- if user == current_user
|
||||
%span.label.label-success It's you
|
||||
- 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 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
|
||||
%span.pull-right
|
||||
|
|
@ -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
|
||||
%i.fa.fa-minus.fa-inverse
|
||||
- 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
|
||||
|
||||
.edit-member.hide.js-toggle-content
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
|
||||
.form-group
|
||||
= 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
|
||||
= f.label :access_level, "Group Access", class: 'control-label'
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
= 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
|
||||
= button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
|
||||
Add members
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
|
||||
.pull-right
|
||||
- if can?(current_user, :manage_group, @group)
|
||||
- if can?(current_user, :admin_group, @group)
|
||||
- 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"
|
||||
- else
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
Open
|
||||
Milestone #{@group_milestone.title}
|
||||
.pull-right
|
||||
- if can?(current_user, :manage_group, @group)
|
||||
- if can?(current_user, :admin_group, @group)
|
||||
- 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"
|
||||
- else
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
.panel-heading
|
||||
%strong= @group.name
|
||||
projects:
|
||||
- if can? current_user, :manage_group, @group
|
||||
- if can? current_user, :admin_group, @group
|
||||
.panel-head-actions
|
||||
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
|
||||
%i.fa.fa-plus
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
%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 @@
|
|||
= link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do
|
||||
%i.fa.fa-user
|
||||
%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
|
||||
%li.hidden-xs
|
||||
= link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
%span
|
||||
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
|
||||
= link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do
|
||||
%i.fa.fa-cogs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
%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)}.
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
|
||||
|
||||
<%= group_url(@group) %>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
%p
|
||||
#{@invite_email}
|
||||
has declined your invitation to join group
|
||||
#{link_to @group.name, group_url(@group)}.
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
|
||||
|
||||
<%= group_url(@group) %>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
%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)
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
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) %>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
%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)}.
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= @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) %>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
%p
|
||||
#{@invite_email}
|
||||
has declined your invitation to join project
|
||||
#{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
|
||||
|
||||
<%= namespace_project_url(@project.namespace, @project) %>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
%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)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
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) %>
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
= 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
|
||||
= 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
|
||||
= f.label :access_level, "Project Access", class: 'control-label'
|
||||
|
|
|
|||
|
|
@ -1,16 +1,32 @@
|
|||
- 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)}
|
||||
%span.list-item-name
|
||||
= image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
|
||||
%strong= user.name
|
||||
%span.cgray= user.username
|
||||
- if user == current_user
|
||||
%span.label.label-success It's you
|
||||
- if user.blocked?
|
||||
%label.label.label-danger
|
||||
%strong Blocked
|
||||
- if member.user
|
||||
= image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
|
||||
%strong
|
||||
= link_to user.name, user_path(user)
|
||||
%span.cgray= user.username
|
||||
- if user == current_user
|
||||
%span.label.label-success It's you
|
||||
- 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
|
||||
- unless @project.personal? && user == current_user
|
||||
|
|
@ -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
|
||||
%i.fa.fa-minus.fa-inverse
|
||||
- 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
|
||||
|
||||
.edit-member.hide.js-toggle-content
|
||||
%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
|
||||
= f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
|
||||
.prepend-top-10
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ production: &base
|
|||
# disable this setting, because the userPrincipalName contains an '@'.
|
||||
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
|
||||
#
|
||||
# Ex. ou=People,dc=gitlab,dc=example
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
|
|||
|
||||
Settings.ldap['servers'].each do |key, server|
|
||||
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['active_directory'] = true if server['active_directory'].nil?
|
||||
server['provider_name'] ||= "ldap#{key}".downcase
|
||||
|
|
|
|||
|
|
@ -53,6 +53,16 @@ Gitlab::Application.routes.draw do
|
|||
end
|
||||
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
|
||||
|
|
@ -260,6 +270,7 @@ Gitlab::Application.routes.draw do
|
|||
|
||||
scope module: :groups do
|
||||
resources :group_members, only: [:index, :create, :update, :destroy] do
|
||||
post :resend_invite, on: :member
|
||||
delete :leave, on: :collection
|
||||
end
|
||||
|
||||
|
|
@ -486,6 +497,10 @@ Gitlab::Application.routes.draw do
|
|||
get :import
|
||||
post :apply_import
|
||||
end
|
||||
|
||||
member do
|
||||
post :resend_invite
|
||||
end
|
||||
end
|
||||
|
||||
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
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
|
|||
t.integer "access_level", null: false
|
||||
t.integer "source_id", 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.string "type"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "created_by_id"
|
||||
t.string "invite_email"
|
||||
t.string "invite_token"
|
||||
t.datetime "invite_accepted_at"
|
||||
end
|
||||
|
||||
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", ["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", ["type"], name: "index_members_on_type", 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
|
|||
# disable this setting, because the userPrincipalName contains an '@'.
|
||||
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
|
||||
#
|
||||
# Ex. ou=People,dc=gitlab,dc=example
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ Feature: Groups
|
|||
When I select "Mike" 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
|
||||
|
||||
@javascript
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
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
|
|||
And I select "Mike" 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
|
||||
Scenario: Update user access
|
||||
Given I should see "Sam" in team list as "Developer"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,23 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
|
|||
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
|
||||
Group.find_by(name: "Owned").projects.each do |project|
|
||||
page.should have_link project.name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
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
|
|||
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
|
||||
within ".access-developer" do
|
||||
page.should have_content('Sam')
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ module API
|
|||
# GET /groups/:id/members
|
||||
get ":id/members" do
|
||||
group = find_group(params[:id])
|
||||
members = group.group_members
|
||||
users = (paginate members).collect(&:user)
|
||||
users = group.users
|
||||
present users, with: Entities::GroupMember, group: group
|
||||
end
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ module API
|
|||
# POST /groups/:id/members
|
||||
post ":id/members" do
|
||||
group = find_group(params[:id])
|
||||
authorize! :manage_group, group
|
||||
authorize! :admin_group, group
|
||||
required_attributes! [:user_id, :access_level]
|
||||
|
||||
unless validate_access_level?(params[:access_level])
|
||||
|
|
@ -35,7 +34,7 @@ module API
|
|||
render_api_error!("Already exists", 409)
|
||||
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])
|
||||
present member.user, with: Entities::GroupMember, group: group
|
||||
end
|
||||
|
|
@ -50,7 +49,7 @@ module API
|
|||
# PUT /groups/:id/members/:user_id
|
||||
put ':id/members/:user_id' do
|
||||
group = find_group(params[:id])
|
||||
authorize! :manage_group, group
|
||||
authorize! :admin_group, group
|
||||
required_attributes! [:access_level]
|
||||
|
||||
group_member = group.group_members.find_by(user_id: params[:user_id])
|
||||
|
|
@ -74,7 +73,7 @@ module API
|
|||
# DELETE /groups/:id/members/:user_id
|
||||
delete ":id/members/:user_id" do
|
||||
group = find_group(params[:id])
|
||||
authorize! :manage_group, group
|
||||
authorize! :admin_group, group
|
||||
member = group.group_members.find_by(user_id: params[:user_id])
|
||||
|
||||
if member.nil?
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ module API
|
|||
# DELETE /groups/:id
|
||||
delete ":id" do
|
||||
group = find_group(params[:id])
|
||||
authorize! :manage_group, group
|
||||
authorize! :admin_group, group
|
||||
group.destroy
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
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
|
|||
options['active_directory']
|
||||
end
|
||||
|
||||
def block_auto_created_users
|
||||
options['block_auto_created_users']
|
||||
end
|
||||
|
||||
protected
|
||||
def base_config
|
||||
Gitlab.config.ldap
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def update_user_attributes
|
||||
return unless persisted?
|
||||
|
||||
gl_user.skip_reconfirmation!
|
||||
gl_user.email = auth_hash.email
|
||||
|
||||
|
|
@ -53,13 +55,17 @@ module Gitlab
|
|||
gl_user.changed? || gl_user.identities.any?(&:changed?)
|
||||
end
|
||||
|
||||
def needs_blocking?
|
||||
false
|
||||
def block_after_signup?
|
||||
ldap_config.block_auto_created_users
|
||||
end
|
||||
|
||||
def allowed?
|
||||
Gitlab::LDAP::Access.allowed?(gl_user)
|
||||
end
|
||||
|
||||
def ldap_config
|
||||
Gitlab::LDAP::Config.new(auth_hash.provider)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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
|
||||
{
|
||||
name: 'John',
|
||||
|
|
@ -16,17 +17,17 @@ describe Gitlab::LDAP::User do
|
|||
describe :changed? do
|
||||
it "marks existing ldap user as changed" do
|
||||
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
|
||||
|
||||
it "marks existing non-ldap user if the email matches as changed" do
|
||||
existing_user = create(:user, email: 'john@example.com')
|
||||
expect(gl_user.changed?).to be_truthy
|
||||
expect(ldap_user.changed?).to be_truthy
|
||||
end
|
||||
|
||||
it "dont marks existing ldap user as changed" do
|
||||
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
|
||||
|
||||
|
|
@ -34,12 +35,12 @@ describe Gitlab::LDAP::User do
|
|||
it "finds the user if already existing" do
|
||||
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
|
||||
|
||||
it "connects to existing non-ldap user if the email matches" do
|
||||
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
|
||||
expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
|
||||
|
|
@ -47,7 +48,59 @@ describe Gitlab::LDAP::User do
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -725,6 +725,11 @@ describe Notify do
|
|||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.address).to eq(user.email)
|
||||
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
|
||||
|
||||
context "when the committer email domain is not completely within the GitLab domain" do
|
||||
|
|
@ -738,6 +743,11 @@ describe Notify do
|
|||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
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
|
||||
|
||||
context "when the committer email domain is outside the GitLab domain" do
|
||||
|
|
@ -751,6 +761,11 @@ describe Notify do
|
|||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
||||
allow(commit).to receive(:safe_message).and_return(message)
|
||||
expect(commit.title).to eq("#{message[0..79]}…")
|
||||
expect(commit.title).to eq("#{message[0..79]}…")
|
||||
end
|
||||
|
||||
it "truncates a message with a newline before 80 characters at the newline" do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
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
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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
|
|||
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) }
|
||||
|
||||
before { project.team << [user, :reporter] }
|
||||
|
||||
describe "GET /projects/:id/repository/tags" do
|
||||
it "should return an array of project tags" do
|
||||
get api("/projects/#{project.id}/repository/tags", user)
|
||||
|
|
|
|||
Loading…
Reference in New Issue