diff options
author | Vinnie Okada <vokada@mrvinn.com> | 2015-03-17 20:53:09 -0600 |
---|---|---|
committer | Vinnie Okada <vokada@mrvinn.com> | 2015-03-17 20:53:09 -0600 |
commit | feeffc442618d92040cd1cc38158b689a09988fd (patch) | |
tree | b19c0ac2ddae23d830bbc69b99d920eec1f81363 /lib | |
parent | 1a9c2ddc55cf563ea42d67811a19b2693d7a44e9 (diff) | |
parent | 5bbc70da9cb439342bdbe022988e4e734d891f44 (diff) | |
download | gitlab-ce-feeffc442618d92040cd1cc38158b689a09988fd.tar.gz |
Merge branch 'master' into markdown-tags
Use the latest HTML pipeline gem
Diffstat (limited to 'lib')
111 files changed, 3129 insertions, 786 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index 2c7cd9038c3..60858a39407 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -2,10 +2,11 @@ Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} module API class API < Grape::API + include APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do - rack_response({'message' => '404 Not found'}.to_json, 404) + rack_response({ 'message' => '404 Not found' }.to_json, 404) end rescue_from :all do |exception| @@ -18,7 +19,7 @@ module API message << " " << trace.join("\n ") API.logger.add Logger::FATAL, message - rack_response({'message' => '500 Internal Server Error'}, 500) + rack_response({ 'message' => '500 Internal Server Error' }, 500) end format :json @@ -27,6 +28,7 @@ module API helpers APIHelpers mount Groups + mount GroupMembers mount Users mount Projects mount Repositories diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb new file mode 100644 index 00000000000..b9994fcefda --- /dev/null +++ b/lib/api/api_guard.rb @@ -0,0 +1,172 @@ +# Guard API with OAuth 2.0 Access Token + +require 'rack/oauth2' + +module APIGuard + extend ActiveSupport::Concern + + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string + + # Must yield access token to store it in the env + request.access_token + end + + helpers HelperMethods + + install_error_responders(base) + end + + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. + # + # If token is presented and valid, then it sets @current_user. + # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError + + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + end + + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish + end + end + end + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes + end + end +end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 14f8b20f6b2..b52d786e020 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -14,7 +14,8 @@ module API # Example Request: # GET /projects/:id/repository/branches get ":id/repository/branches" do - present user_project.repository.branches.sort_by(&:name), with: Entities::RepoObject, project: user_project + branches = user_project.repository.branches.sort_by(&:name) + present branches, with: Entities::RepoObject, project: user_project end # Get a single branch @@ -26,7 +27,7 @@ module API # GET /projects/:id/repository/branches/:branch get ':id/repository/branches/:branch', requirements: { branch: /.*/ } do @branch = user_project.repository.branches.find { |item| item.name == params[:branch] } - not_found!("Branch does not exist") if @branch.nil? + not_found!("Branch") unless @branch present @branch, with: Entities::RepoObject, project: user_project end @@ -43,7 +44,7 @@ module API authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) - not_found! unless @branch + not_found!("Branch") unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) user_project.protected_branches.create(name: @branch.name) unless protected_branch @@ -63,7 +64,7 @@ module API authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) - not_found! unless @branch + not_found!("Branch does not exist") unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch.destroy if protected_branch @@ -82,6 +83,7 @@ module API authorize_push_project result = CreateBranchService.new(user_project, current_user). execute(params[:branch_name], params[:ref]) + if result[:status] == :success present result[:branch], with: Entities::RepoObject, @@ -104,7 +106,9 @@ module API execute(params[:branch]) if result[:status] == :success - true + { + branch_name: params[:branch] + } else render_api_error!(result[:message], result[:return_code]) end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4a67313430a..0de4e720ffe 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -50,6 +50,67 @@ module API not_found! "Commit" unless commit commit.diffs end + + # Get a commit's comments + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # Examples: + # GET /projects/:id/repository/commits/:sha/comments + get ':id/repository/commits/:sha/comments' do + sha = params[:sha] + commit = user_project.repository.commit(sha) + not_found! 'Commit' unless commit + notes = Note.where(commit_id: commit.id) + present paginate(notes), with: Entities::CommitNote + end + + # Post comment to commit + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # note (required) - Text of comment + # path (optional) - The file path + # line (optional) - The line number + # line_type (optional) - The type of line (new or old) + # Examples: + # POST /projects/:id/repository/commits/:sha/comments + post ':id/repository/commits/:sha/comments' do + required_attributes! [:note] + + sha = params[:sha] + commit = user_project.repository.commit(sha) + not_found! 'Commit' unless commit + opts = { + note: params[:note], + noteable_type: 'Commit', + commit_id: commit.id + } + + if params[:path] && params[:line] && params[:line_type] + commit.diffs.each do |diff| + next unless diff.new_path == params[:path] + lines = Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a) + + lines.each do |line| + next unless line.new_pos == params[:line].to_i && line.type == params[:line_type] + break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + end + + break if opts[:line_code] + end + end + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute + + if note.save + present note, with: Entities::CommitNote + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 80e9470195e..489be210784 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -14,9 +14,14 @@ module API expose :bio, :skype, :linkedin, :twitter, :website_url end + class Identity < Grape::Entity + expose :provider, :extern_uid + end + class UserFull < User expose :email - expose :theme_id, :color_scheme_id, :extern_uid, :provider + expose :theme_id, :color_scheme_id, :projects_limit + expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project end @@ -50,7 +55,8 @@ module API expose :path, :path_with_namespace expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, :if => lambda{ | project, options | project.forked? } + expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } + expose :avatar_url end class ProjectMember < UserBasic @@ -60,7 +66,7 @@ module API end class Group < Grape::Entity - expose :id, :name, :path, :owner_id + expose :id, :name, :path, :description end class GroupDetail < Group @@ -73,6 +79,25 @@ module API end end + class RepoTag < Grape::Entity + expose :name + expose :message do |repo_obj, _options| + if repo_obj.respond_to?(:message) + repo_obj.message + else + nil + end + end + + expose :commit do |repo_obj, options| + if repo_obj.respond_to?(:commit) + repo_obj.commit + elsif options[:project] + options[:project].repository.commit(repo_obj.target) + end + end + end + class RepoObject < Grape::Entity expose :name @@ -118,11 +143,16 @@ module API class ProjectEntity < Grape::Entity expose :id, :iid - expose (:project_id) { |entity| entity.project.id } + expose(:project_id) { |entity| entity.project.id } expose :title, :description expose :state, :created_at, :updated_at end + class RepoDiff < Grape::Entity + expose :old_path, :new_path, :a_mode, :b_mode, :diff + expose :new_file, :renamed_file, :deleted_file + end + class Milestone < ProjectEntity expose :due_date end @@ -142,6 +172,12 @@ module API expose :milestone, using: Entities::Milestone end + class MergeRequestChanges < MergeRequest + expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| + compare.diffs + end + end + class SSHKey < Grape::Entity expose :id, :title, :key, :created_at end @@ -159,11 +195,25 @@ module API expose :author, using: Entities::UserBasic end + class CommitNote < Grape::Entity + expose :note + expose(:path) { |note| note.diff_file_name } + expose(:line) { |note| note.diff_new_line } + expose(:line_type) { |note| note.diff_line_type } + expose :author, using: Entities::UserBasic + end + class Event < Grape::Entity expose :title, :project_id, :action_name expose :target_id, :target_type, :author_id expose :data, :target_title expose :created_at + + expose :author_username do |event, options| + if event.author + event.author.username + end + end end class Namespace < Grape::Entity @@ -198,11 +248,6 @@ module API expose :name, :color end - class RepoDiff < Grape::Entity - expose :old_path, :new_path, :a_mode, :b_mode, :diff - expose :new_file, :renamed_file, :deleted_file - end - class Compare < Grape::Entity expose :commit, using: Entities::RepoCommit do |compare, options| Commit.decorate(compare.commits).last @@ -226,5 +271,9 @@ module API class Contributor < Grape::Entity expose :name, :email, :commits, :additions, :deletions end + + class BroadcastMessage < Grape::Entity + expose :message, :starts_at, :ends_at, :color, :font + end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index e63e635a4d3..3176ef0e256 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -35,7 +35,7 @@ module API file_path = attrs.delete(:file_path) commit = user_project.repository.commit(ref) - not_found! "Commit" unless commit + not_found! 'Commit' unless commit blob = user_project.repository.blob_at(commit.sha, file_path) @@ -53,7 +53,7 @@ module API commit_id: commit.id, } else - render_api_error!('File not found', 404) + not_found! 'File' end end @@ -85,7 +85,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end @@ -117,7 +117,8 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + http_status = result[:http_status] || 400 + render_api_error!(result[:message], http_status) end end @@ -149,7 +150,7 @@ module API branch_name: branch_name } else - render_api_error!(result[:error], 400) + render_api_error!(result[:message], 400) end end end diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb new file mode 100644 index 00000000000..ed54c7f6ff0 --- /dev/null +++ b/lib/api/group_members.rb @@ -0,0 +1,88 @@ +module API + class GroupMembers < Grape::API + before { authenticate! } + + resource :groups do + # Get a list of group members viewable by the authenticated user. + # + # Example Request: + # GET /groups/:id/members + get ":id/members" do + group = find_group(params[:id]) + members = group.group_members + users = (paginate members).collect(&:user) + present users, with: Entities::GroupMember, group: group + end + + # Add a user to the list of group members + # + # Parameters: + # id (required) - group id + # user_id (required) - the users id + # access_level (required) - Project access level + # Example Request: + # POST /groups/:id/members + post ":id/members" do + group = find_group(params[:id]) + authorize! :manage_group, group + required_attributes! [:user_id, :access_level] + + unless validate_access_level?(params[:access_level]) + render_api_error!("Wrong access level", 422) + end + + if group.group_members.find_by(user_id: params[:user_id]) + render_api_error!("Already exists", 409) + end + + group.add_users([params[:user_id]], params[:access_level]) + member = group.group_members.find_by(user_id: params[:user_id]) + present member.user, with: Entities::GroupMember, group: group + end + + # Update group member + # + # Parameters: + # id (required) - The ID of a group + # user_id (required) - The ID of a group member + # access_level (required) - Project access level + # Example Request: + # PUT /groups/:id/members/:user_id + put ':id/members/:user_id' do + group = find_group(params[:id]) + authorize! :manage_group, group + required_attributes! [:access_level] + + group_member = group.group_members.find_by(user_id: params[:user_id]) + not_found!('User can not be found') if group_member.nil? + + if group_member.update_attributes(access_level: params[:access_level]) + @member = group_member.user + present @member, with: Entities::GroupMember, group: group + else + handle_member_errors group_member.errors + end + end + + # Remove member. + # + # Parameters: + # id (required) - group id + # user_id (required) - the users id + # + # Example Request: + # DELETE /groups/:id/members/:user_id + delete ":id/members/:user_id" do + group = find_group(params[:id]) + authorize! :manage_group, group + member = group.group_members.find_by(user_id: params[:user_id]) + + if member.nil? + render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404) + else + member.destroy + end + end + end + end +end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 4841e04689d..a92abd4b690 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -4,32 +4,19 @@ module API before { authenticate! } resource :groups do - helpers do - def find_group(id) - group = Group.find(id) - - if can?(current_user, :read_group, group) - group - else - render_api_error!("403 Forbidden - #{current_user.username} lacks sufficient access to #{group.name}", 403) - end - end - - def validate_access_level?(level) - Gitlab::Access.options_with_owner.values.include? level.to_i - end - end - # Get a groups list # # Example Request: # GET /groups get do - if current_user.admin - @groups = paginate Group - else - @groups = paginate current_user.groups - end + @groups = if current_user.admin + Group.all + else + current_user.groups + end + + @groups = @groups.search(params[:search]) if params[:search].present? + @groups = paginate @groups present @groups, with: Entities::Group end @@ -44,14 +31,14 @@ module API authenticated_as_admin! required_attributes! [:name, :path] - attrs = attributes_for_keys [:name, :path] + attrs = attributes_for_keys [:name, :path, :description] @group = Group.new(attrs) - @group.owner = current_user if @group.save + @group.add_owner(current_user) present @group, with: Entities::Group else - not_found! + render_api_error!("Failed to save group #{@group.errors.messages}", 400) end end @@ -94,58 +81,7 @@ module API if result present group else - not_found! - end - end - - # Get a list of group members viewable by the authenticated user. - # - # Example Request: - # GET /groups/:id/members - get ":id/members" do - group = find_group(params[:id]) - members = group.group_members - users = (paginate members).collect(&:user) - present users, with: Entities::GroupMember, group: group - end - - # Add a user to the list of group members - # - # Parameters: - # id (required) - group id - # user_id (required) - the users id - # access_level (required) - Project access level - # Example Request: - # POST /groups/:id/members - post ":id/members" do - required_attributes! [:user_id, :access_level] - unless validate_access_level?(params[:access_level]) - render_api_error!("Wrong access level", 422) - end - group = find_group(params[:id]) - if group.group_members.find_by(user_id: params[:user_id]) - render_api_error!("Already exists", 409) - end - group.add_users([params[:user_id]], params[:access_level]) - member = group.group_members.find_by(user_id: params[:user_id]) - present member.user, with: Entities::GroupMember, group: group - end - - # Remove member. - # - # Parameters: - # id (required) - group id - # user_id (required) - the users id - # - # Example Request: - # DELETE /groups/:id/members/:user_id - delete ":id/members/:user_id" do - group = find_group(params[:id]) - member = group.group_members.find_by(user_id: params[:user_id]) - if member.nil? - render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404) - else - member.destroy + render_api_error!("Failed to transfer project #{project.errors.messages}", 400) end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3262884f6d3..a6e77002a01 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -11,7 +11,7 @@ module API def current_user private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s - @current_user ||= User.find_by(authentication_token: private_token) + @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard) unless @current_user && Gitlab::UserAccess.allowed?(@current_user) return nil @@ -42,7 +42,7 @@ module API def user_project @project ||= find_project(params[:id]) - @project || not_found! + @project || not_found!("Project") end def find_project(id) @@ -55,6 +55,21 @@ module API end end + def find_group(id) + begin + group = Group.find(id) + rescue ActiveRecord::RecordNotFound + group = Group.find_by!(path: id) + end + + if can?(current_user, :read_group, group) + group + else + forbidden!("#{current_user.username} lacks sufficient "\ + "access to #{group.name}") + end + end + def paginate(relation) per_page = params[:per_page].to_i paginated = relation.page(params[:page]).per(per_page) @@ -67,6 +82,13 @@ module API unauthorized! unless current_user end + def authenticate_by_gitlab_shell_token! + input = params['secret_token'].try(:chomp) + unless Devise.secure_compare(secret_token, input) + unauthorized! + end + end + def authenticated_as_admin! forbidden! unless current_user.is_admin? end @@ -131,10 +153,32 @@ module API errors end + def validate_access_level?(level) + Gitlab::Access.options_with_owner.values.include? level.to_i + end + + def issuable_order_by + if params["order_by"] == 'updated_at' + 'updated_at' + else + 'created_at' + end + end + + def issuable_sort + if params["sort"] == 'asc' + :asc + else + :desc + end + end + # error helpers - def forbidden! - render_api_error!('403 Forbidden', 403) + def forbidden!(reason = nil) + message = ['403 Forbidden'] + message << " - #{reason}" if reason + render_api_error!(message.join(' '), 403) end def bad_request!(attribute) @@ -163,13 +207,13 @@ module API end def render_validation_error!(model) - unless model.valid? + if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) end end def render_api_error!(message, status) - error!({'message' => message}, status) + error!({ 'message' => message }, status) end private @@ -193,5 +237,14 @@ module API abilities end end + + def secret_token + File.read(Rails.root.join('.gitlab_shell_secret')).chomp + end + + def handle_member_errors(errors) + error!(errors[:access_level], 422) if errors[:access_level].any? + not_found!(errors) + end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9ac659f50fd..753d0fcbd98 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,6 +1,8 @@ module API # Internal access API class Internal < Grape::API + before { authenticate_by_gitlab_shell_token! } + namespace 'internal' do # Check if git command is allowed to project # @@ -14,6 +16,17 @@ module API # post "/allowed" do status 200 + + actor = if params[:key_id] + Key.find_by(id: params[:key_id]) + elsif params[:user_id] + User.find_by(id: params[:user_id]) + end + + unless actor + return Gitlab::GitAccessStatus.new(false, 'No such user or key') + end + project_path = params[:project] # Check for *.wiki repositories. @@ -21,30 +34,29 @@ module API # project. This applies the correct project permissions to # the wiki repository as well. access = - if project_path =~ /\.wiki\Z/ - project_path.sub!(/\.wiki\Z/, '') + if project_path.end_with?('.wiki') + project_path.chomp!('.wiki') Gitlab::GitAccessWiki.new else Gitlab::GitAccess.new end project = Project.find_with_namespace(project_path) - return false unless project - actor = if params[:key_id] - Key.find(params[:key_id]) - elsif params[:user_id] - User.find(params[:user_id]) - end + if project + status = access.check( + actor, + params[:action], + project, + params[:changes] + ) + end - return false unless actor - - access.allowed?( - actor, - params[:action], - project, - params[:changes] - ) + if project && status && status.allowed? + status + else + Gitlab::GitAccessStatus.new(false, 'No such project') + end end # @@ -62,6 +74,14 @@ module API gitlab_rev: Gitlab::REVISION, } end + + get "/broadcast_message" do + if message = BroadcastMessage.current + present message, with: Entities::BroadcastMessage + else + {} + end + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index d2828b24c36..ff062be6040 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -27,7 +27,9 @@ module API # Parameters: # state (optional) - Return "opened" or "closed" issues # labels (optional) - Comma-separated list of label names - + # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` + # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` + # # Example Requests: # GET /issues # GET /issues?state=opened @@ -39,8 +41,7 @@ module API issues = current_user.issues issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = issues.order('issues.id DESC') - + issues.reorder(issuable_order_by => issuable_sort) present paginate(issues), with: Entities::Issue end end @@ -53,6 +54,8 @@ module API # state (optional) - Return "opened" or "closed" issues # labels (optional) - Comma-separated list of label names # milestone (optional) - Milestone title + # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` + # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` # # Example Requests: # GET /projects/:id/issues @@ -67,11 +70,12 @@ module API issues = user_project.issues issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? + unless params[:milestone].nil? issues = filter_issues_milestone(issues, params[:milestone]) end - issues = issues.order('issues.id DESC') + issues.reorder(issuable_order_by => issuable_sort) present paginate(issues), with: Entities::Issue end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a365f1db00f..25b7857f4b1 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -25,6 +25,8 @@ module API # Parameters: # id (required) - The ID of a project # state (optional) - Return requests "merged", "opened" or "closed" + # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` + # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` # # Example: # GET /projects/:id/merge_requests @@ -37,25 +39,18 @@ module API # get ":id/merge_requests" do authorize! :read_merge_request, user_project + merge_requests = user_project.merge_requests + + merge_requests = + case params["state"] + when "opened" then merge_requests.opened + when "closed" then merge_requests.closed + when "merged" then merge_requests.merged + else merge_requests + end - mrs = case params["state"] - when "opened" then user_project.merge_requests.opened - when "closed" then user_project.merge_requests.closed - when "merged" then user_project.merge_requests.merged - else user_project.merge_requests - end - - sort = case params["sort"] - when 'desc' then 'DESC' - else 'ASC' - end - - mrs = case params["order_by"] - when 'updated_at' then mrs.order("updated_at #{sort}") - else mrs.order("created_at #{sort}") - end - - present paginate(mrs), with: Entities::MergeRequest + merge_requests.reorder(issuable_order_by => issuable_sort) + present paginate(merge_requests), with: Entities::MergeRequest end # Show MR @@ -75,6 +70,22 @@ module API present merge_request, with: Entities::MergeRequest end + # Show MR changes + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_request/:merge_request_id/changes + # + get ':id/merge_request/:merge_request_id/changes' do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request, with: Entities::MergeRequestChanges + end + # Create MR # # Parameters: @@ -167,13 +178,9 @@ module API put ":id/merge_request/:merge_request_id/merge" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - action = if user_project.protected_branch?(merge_request.target_branch) - :push_code_to_protected_branches - else - :push_code - end + allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, user_project, merge_request.target_branch) - if can?(current_user, action, user_project) + if allowed if merge_request.unchecked? merge_request.check_if_can_be_merged end @@ -233,7 +240,7 @@ module API if note.save present note, with: Entities::MRNote else - render_validation_error!(note) + render_api_error!("Failed to save note #{note.errors.messages}", 400) end end end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index a4fdb752d69..c5cd73943fb 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -48,7 +48,7 @@ module API if milestone.valid? present milestone, with: Entities::Milestone else - not_found! + render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400) end end @@ -72,9 +72,24 @@ module API if milestone.valid? present milestone, with: Entities::Milestone else - not_found! + render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) end end + + # Get all issues for a single project milestone + # + # Parameters: + # id (required) - The ID of a project + # milestone_id (required) - The ID of a project milestone + # Example Request: + # GET /projects/:id/milestones/:milestone_id/issues + get ":id/milestones/:milestone_id/issues" do + authorize! :read_milestone, user_project + + @milestone = user_project.milestones.find(params[:milestone_id]) + present paginate(@milestone.issues), with: Entities::Issue + end + end end end diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index f9f2ed90ccc..b90ed6af5fb 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,10 +1,10 @@ module API # namespaces API class Namespaces < Grape::API - before { + before do authenticate! authenticated_as_admin! - } + end resource :namespaces do # Get a namespaces list diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 0ef9a3c4beb..3726be7c537 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -61,9 +61,42 @@ module API if @note.valid? present @note, with: Entities::Note else - not_found! + not_found!("Note #{@note.errors.messages}") end end + + # Modify existing +noteable+ note + # + # Parameters: + # id (required) - The ID of a project + # noteable_id (required) - The ID of an issue or snippet + # node_id (required) - The ID of a note + # body (required) - New content of a note + # Example Request: + # PUT /projects/:id/issues/:noteable_id/notes/:note_id + # PUT /projects/:id/snippets/:noteable_id/notes/:node_id + put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do + required_attributes! [:body] + + authorize! :admin_note, user_project.notes.find(params[:note_id]) + + opts = { + note: params[:body], + note_id: params[:note_id], + noteable_type: noteables_str.classify, + noteable_id: params[noteable_id_str] + } + + @note = ::Notes::UpdateService.new(user_project, current_user, + opts).execute + + if @note.valid? + present @note, with: Entities::Note + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end + end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 7d056b9bf58..be9850367b9 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -53,7 +53,7 @@ module API if @hook.errors[:url].present? error!("Invalid url given", 422) end - not_found! + not_found!("Project hook #{@hook.errors.messages}") end end @@ -82,7 +82,7 @@ module API if @hook.errors[:url].present? error!("Invalid url given", 422) end - not_found! + not_found!("Project hook #{@hook.errors.messages}") end end diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb index 1595ed0bc36..c756bb479fc 100644 --- a/lib/api/project_members.rb +++ b/lib/api/project_members.rb @@ -4,14 +4,6 @@ module API before { authenticate! } resource :projects do - helpers do - def handle_project_member_errors(errors) - if errors[:access_level].any? - error!(errors[:access_level], 422) - end - not_found! - end - end # Get a project team members # @@ -54,19 +46,19 @@ module API required_attributes! [:user_id, :access_level] # either the user is already a team member or a new one - team_member = user_project.team_member_by_id(params[:user_id]) - if team_member.nil? - team_member = user_project.project_members.new( + project_member = user_project.project_member_by_id(params[:user_id]) + if project_member.nil? + project_member = user_project.project_members.new( user_id: params[:user_id], access_level: params[:access_level] ) end - if team_member.save - @member = team_member.user + if project_member.save + @member = project_member.user present @member, with: Entities::ProjectMember, project: user_project else - handle_project_member_errors team_member.errors + handle_member_errors project_member.errors end end @@ -82,14 +74,14 @@ module API authorize! :admin_project, user_project required_attributes! [:access_level] - team_member = user_project.project_members.find_by(user_id: params[:user_id]) - not_found!("User can not be found") if team_member.nil? + project_member = user_project.project_members.find_by(user_id: params[:user_id]) + not_found!("User can not be found") if project_member.nil? - if team_member.update_attributes(access_level: params[:access_level]) - @member = team_member.user + if project_member.update_attributes(access_level: params[:access_level]) + @member = project_member.user present @member, with: Entities::ProjectMember, project: user_project else - handle_project_member_errors team_member.errors + handle_member_errors project_member.errors end end @@ -102,11 +94,11 @@ module API # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do authorize! :admin_project, user_project - team_member = user_project.project_members.find_by(user_id: params[:user_id]) - unless team_member.nil? - team_member.destroy + project_member = user_project.project_members.find_by(user_id: params[:user_id]) + unless project_member.nil? + project_member.destroy else - {message: "Access revoked", id: params[:user_id].to_i} + { message: "Access revoked", id: params[:user_id].to_i } end end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 0c2d282f785..54f2555903f 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -42,21 +42,22 @@ module API # title (required) - The title of a snippet # file_name (required) - The name of a snippet file # code (required) - The content of a snippet + # visibility_level (required) - The snippet's visibility # Example Request: # POST /projects/:id/snippets post ":id/snippets" do authorize! :write_project_snippet, user_project - required_attributes! [:title, :file_name, :code] + required_attributes! [:title, :file_name, :code, :visibility_level] - attrs = attributes_for_keys [:title, :file_name] + attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - @snippet = user_project.snippets.new attrs - @snippet.author = current_user + @snippet = CreateSnippetService.new(user_project, current_user, + attrs).execute - if @snippet.save - present @snippet, with: Entities::ProjectSnippet - else + if @snippet.errors.any? render_validation_error!(@snippet) + else + present @snippet, with: Entities::ProjectSnippet end end @@ -68,19 +69,22 @@ module API # title (optional) - The title of a snippet # file_name (optional) - The name of a snippet file # code (optional) - The content of a snippet + # visibility_level (optional) - The snippet's visibility # Example Request: # PUT /projects/:id/snippets/:snippet_id put ":id/snippets/:snippet_id" do @snippet = user_project.snippets.find(params[:snippet_id]) authorize! :modify_project_snippet, @snippet - attrs = attributes_for_keys [:title, :file_name] + attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - if @snippet.update_attributes attrs - present @snippet, with: Entities::ProjectSnippet - else + UpdateSnippetService.new(user_project, current_user, @snippet, + attrs).execute + if @snippet.errors.any? render_validation_error!(@snippet) + else + present @snippet, with: Entities::ProjectSnippet end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 7f7d2f8e9a8..83f65eec6cc 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -11,23 +11,46 @@ module API attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true attrs end + + def filter_projects(projects) + # If the archived parameter is passed, limit results accordingly + if params[:archived].present? + projects = projects.where(archived: parse_boolean(params[:archived])) + end + + if params[:search].present? + projects = projects.search(params[:search]) + end + + projects.reorder(project_order_by => project_sort) + end + + def project_order_by + order_fields = %w(id name path created_at updated_at last_activity_at) + + if order_fields.include?(params['order_by']) + params['order_by'] + else + 'created_at' + end + end + + def project_sort + if params["sort"] == 'asc' + :asc + else + :desc + end + end end # Get a projects list for authenticated user # - # Parameters: - # archived (optional) - if passed, limit by archived status - # # Example Request: # GET /projects get do @projects = current_user.authorized_projects - - # If the archived parameter is passed, limit results accordingly - if params[:archived].present? - @projects = @projects.where(archived: parse_boolean(params[:archived])) - end - + @projects = filter_projects(@projects) @projects = paginate @projects present @projects, with: Entities::Project end @@ -37,7 +60,9 @@ module API # Example Request: # GET /projects/owned get '/owned' do - @projects = paginate current_user.owned_projects + @projects = current_user.owned_projects + @projects = filter_projects(@projects) + @projects = paginate @projects present @projects, with: Entities::Project end @@ -47,7 +72,9 @@ module API # GET /projects/all get '/all' do authenticated_as_admin! - @projects = paginate Project + @projects = Project.all + @projects = filter_projects(@projects) + @projects = paginate @projects present @projects, with: Entities::Project end @@ -66,7 +93,7 @@ module API # Parameters: # id (required) - The ID of a project # Example Request: - # GET /projects/:id + # GET /projects/:id/events get ":id/events" do limit = (params[:per_page] || 20).to_i offset = (params[:page] || 0).to_i * limit @@ -170,6 +197,49 @@ module API end end + # Update an existing project + # + # Parameters: + # id (required) - the id of a project + # name (optional) - name of a project + # path (optional) - path of a project + # description (optional) - short project description + # issues_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # public (optional) - if true same as setting visibility_level = 20 + # visibility_level (optional) - visibility level of a project + # Example Request + # PUT /projects/:id + put ':id' do + attrs = attributes_for_keys [:name, + :path, + :description, + :default_branch, + :issues_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :public, + :visibility_level] + attrs = map_public_to_visibility_level(attrs) + authorize_admin_project + authorize! :rename_project, user_project if attrs[:name].present? + if attrs[:visibility_level].present? + authorize! :change_visibility_level, user_project + end + + ::Projects::UpdateService.new(user_project, + current_user, attrs).execute + + if user_project.errors.any? + render_validation_error!(user_project) + else + present user_project, with: Entities::Project + end + end + # Remove project # # Parameters: @@ -178,7 +248,7 @@ module API # DELETE /projects/:id delete ":id" do authorize! :remove_project, user_project - user_project.destroy + ::Projects::DestroyService.new(user_project, current_user, {}).execute end # Mark this project as forked from another @@ -198,7 +268,7 @@ module API render_api_error!("Project already forked", 409) end else - not_found! + not_found!("Source Project") end end @@ -227,6 +297,16 @@ module API ids = current_user.authorized_projects.map(&:id) visibility_levels = [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ] projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%") + sort = params[:sort] == 'desc' ? 'desc' : 'asc' + + projects = case params["order_by"] + when 'id' then projects.order("id #{sort}") + when 'name' then projects.order("name #{sort}") + when 'created_at' then projects.order("created_at #{sort}") + when 'last_activity_at' then projects.order("last_activity_at #{sort}") + else projects + end + present paginate(projects), with: Entities::Project end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 626d99c2649..b259914a01c 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -23,7 +23,8 @@ module API # Example Request: # GET /projects/:id/repository/tags get ":id/repository/tags" do - present user_project.repo.tags.sort_by(&:name).reverse, with: Entities::RepoObject, project: user_project + present user_project.repo.tags.sort_by(&:name).reverse, + with: Entities::RepoTag, project: user_project end # Create tag @@ -43,7 +44,7 @@ module API if result[:status] == :success present result[:tag], - with: Entities::RepoObject, + with: Entities::RepoTag, project: user_project else render_api_error!(result[:message], 400) @@ -57,11 +58,13 @@ module API # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used # Example Request: # GET /projects/:id/repository/tree - get ":id/repository/tree" do + get ':id/repository/tree' do ref = params[:ref_name] || user_project.try(:default_branch) || 'master' path = params[:path] || nil commit = user_project.repository.commit(ref) + not_found!('Tree') unless commit + tree = user_project.repository.tree(commit.id, path) present tree.sorted_entries, with: Entities::RepoTreeObject @@ -99,14 +102,18 @@ module API # sha (required) - The blob's sha # Example Request: # GET /projects/:id/repository/raw_blobs/:sha - get ":id/repository/raw_blobs/:sha" do + get ':id/repository/raw_blobs/:sha' do ref = params[:sha] repo = user_project.repository - blob = Gitlab::Git::Blob.raw(repo, ref) + begin + blob = Gitlab::Git::Blob.raw(repo, ref) + rescue + not_found! 'Blob' + end - not_found! "Blob" unless blob + not_found! 'Blob' unless blob env['api.format'] = :txt @@ -121,18 +128,28 @@ module API # sha (optional) - the commit sha to download defaults to the tip of the default branch # Example Request: # GET /projects/:id/repository/archive - get ":id/repository/archive", requirements: { format: Gitlab::Regex.archive_formats_regex } do + get ':id/repository/archive', + requirements: { format: Gitlab::Regex.archive_formats_regex } do authorize! :download_code, user_project - file_path = ArchiveRepositoryService.new.execute(user_project, params[:sha], params[:format]) + + begin + file_path = ArchiveRepositoryService.new.execute( + user_project, + params[:sha], + params[:format]) + rescue + not_found!('File') + end if file_path && File.exists?(file_path) data = File.open(file_path, 'rb').read - header["Content-Disposition"] = "attachment; filename=\"#{File.basename(file_path)}\"" + basename = File.basename(file_path) + header['Content-Disposition'] = "attachment; filename=\"#{basename}\"" content_type MIME::Types.type_for(file_path).first.content_type env['api.format'] = :binary present data else - not_found! + not_found!('File') end end @@ -160,7 +177,12 @@ module API get ':id/repository/contributors' do authorize! :download_code, user_project - present user_project.repository.contributors, with: Entities::Contributor + begin + present user_project.repository.contributors, + with: Entities::Contributor + rescue + not_found! + end end end end diff --git a/lib/api/services.rb b/lib/api/services.rb index bde502e32e1..3ad59cf3adf 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -28,7 +28,7 @@ module API # Delete GitLab CI service settings # # Example Request: - # DELETE /projects/:id/keys/:id + # DELETE /projects/:id/services/gitlab-ci delete ":id/services/gitlab-ci" do if user_project.gitlab_ci_service user_project.gitlab_ci_service.update_attributes( @@ -38,7 +38,41 @@ module API ) end end + + # Set Hipchat service for project + # + # Parameters: + # token (required) - Hipchat token + # room (required) - Hipchat room name + # + # Example Request: + # PUT /projects/:id/services/hipchat + put ':id/services/hipchat' do + required_attributes! [:token, :room] + attrs = attributes_for_keys [:token, :room] + user_project.build_missing_services + + if user_project.hipchat_service.update_attributes( + attrs.merge(active: true)) + true + else + not_found! + end + end + + # Delete Hipchat service settings + # + # Example Request: + # DELETE /projects/:id/services/hipchat + delete ':id/services/hipchat' do + if user_project.hipchat_service + user_project.hipchat_service.update_attributes( + active: false, + token: nil, + room: nil + ) + end + end end end end - diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 3e239c5afe7..518964db50d 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -1,10 +1,10 @@ module API # Hooks API class SystemHooks < Grape::API - before { + before do authenticate! authenticated_as_admin! - } + end resource :hooks do # Get the list of system hooks diff --git a/lib/api/users.rb b/lib/api/users.rb index d07815a8a97..7c8b3250cd0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -54,15 +54,24 @@ module API # bio - Bio # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false + # confirm - Require user confirmation - true (default) or false # Example Request: # POST /users post do authenticated_as_admin! required_attributes! [:email, :password, :name, :username] - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm] user = User.build_user(attrs) admin = attrs.delete(:admin) user.admin = admin unless admin.nil? + confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) + user.skip_confirmation! unless confirm + + identity_attrs = attributes_for_keys [:provider, :extern_uid] + if identity_attrs.any? + user.identities.build(identity_attrs) + end + if user.save present user, with: Entities::UserFull else @@ -89,8 +98,6 @@ module API # twitter - Twitter account # website_url - Website url # projects_limit - Limit projects each user can create - # extern_uid - External authentication provider UID - # provider - External provider # bio - Bio # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false @@ -99,7 +106,7 @@ module API put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :extern_uid, :provider, :bio, :can_create_group, :admin] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin] user = User.find(params[:id]) not_found!('User') unless user diff --git a/lib/backup/database.rb b/lib/backup/database.rb index d12d30a9110..9ab6aca276d 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -13,10 +13,10 @@ module Backup def dump success = case config["adapter"] when /^mysql/ then - print "Dumping MySQL database #{config['database']} ... " + $progress.print "Dumping MySQL database #{config['database']} ... " system('mysqldump', *mysql_args, config['database'], out: db_file_name) when "postgresql" then - print "Dumping PostgreSQL database #{config['database']} ... " + $progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env system('pg_dump', config['database'], out: db_file_name) end @@ -27,13 +27,14 @@ module Backup def restore success = case config["adapter"] when /^mysql/ then - print "Restoring MySQL database #{config['database']} ... " + $progress.print "Restoring MySQL database #{config['database']} ... " system('mysql', *mysql_args, config['database'], in: db_file_name) when "postgresql" then - print "Restoring PostgreSQL database #{config['database']} ... " + $progress.print "Restoring PostgreSQL database #{config['database']} ... " # Drop all tables because PostgreSQL DB dumps do not contain DROP TABLE # statements like MySQL. Rake::Task["gitlab:db:drop_all_tables"].invoke + Rake::Task["gitlab:db:drop_all_postgres_sequences"].invoke pg_env system('psql', config['database'], '-f', db_file_name) end @@ -68,9 +69,9 @@ module Backup def report_success(success) if success - puts '[DONE]'.green + $progress.puts '[DONE]'.green else - puts '[FAILED]'.red + $progress.puts '[FAILED]'.red end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 03fe0f0b02f..ab8db4e9837 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -18,11 +18,11 @@ module Backup end # create archive - print "Creating backup archive: #{tar_file} ... " + $progress.print "Creating backup archive: #{tar_file} ... " if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) - puts "done".green + $progress.puts "done".green else - puts "failed".red + puts "creating archive #{tar_file} failed".red abort 'Backup failed' end @@ -31,37 +31,37 @@ module Backup def upload(tar_file) remote_directory = Gitlab.config.backup.upload.remote_directory - print "Uploading backup archive to remote storage #{remote_directory} ... " + $progress.print "Uploading backup archive to remote storage #{remote_directory} ... " connection_settings = Gitlab.config.backup.upload.connection if connection_settings.blank? - puts "skipped".yellow + $progress.puts "skipped".yellow return end connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) - puts "done".green + $progress.puts "done".green else - puts "failed".red + puts "uploading backup to #{remote_directory} failed".red abort 'Backup failed' end end def cleanup - print "Deleting tmp directories ... " + $progress.print "Deleting tmp directories ... " if Kernel.system('rm', '-rf', *BACKUP_CONTENTS) - puts "done".green + $progress.puts "done".green else - puts "failed".red + puts "deleting tmp directory failed".red abort 'Backup failed' end end def remove_old # delete backups - print "Deleting old backups ... " + $progress.print "Deleting old backups ... " keep_time = Gitlab.config.backup.keep_time.to_i path = Gitlab.config.backup.path @@ -76,9 +76,9 @@ module Backup end end end - puts "done. (#{removed} removed)".green + $progress.puts "done. (#{removed} removed)".green else - puts "skipping".yellow + $progress.puts "skipping".yellow end end @@ -101,12 +101,12 @@ module Backup exit 1 end - print "Unpacking backup ... " + $progress.print "Unpacking backup ... " unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "failed".red + puts "unpacking backup failed".red exit 1 else - puts "done".green + $progress.puts "done".green end settings = YAML.load_file("backup_information.yml") diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 4e99d4bbe5c..e18bc804437 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -8,19 +8,21 @@ module Backup prepare Project.find_each(batch_size: 1000) do |project| - print " * #{project.path_with_namespace} ... " + $progress.print " * #{project.path_with_namespace} ... " # Create namespace dir if missing FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace if project.empty_repo? - puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".cyan else - output, status = Gitlab::Popen.popen(%W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all)) + cmd = %W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all) + output, status = Gitlab::Popen.popen(cmd) if status.zero? - puts "[DONE]".green + $progress.puts "[DONE]".green else puts "[FAILED]".red + puts "failed: #{cmd.join(' ')}" puts output abort 'Backup failed' end @@ -29,15 +31,17 @@ module Backup wiki = ProjectWiki.new(project) if File.exists?(path_to_repo(wiki)) - print " * #{wiki.path_with_namespace} ... " - if wiki.empty? - puts " [SKIPPED]".cyan + $progress.print " * #{wiki.path_with_namespace} ... " + if wiki.repository.empty? + $progress.puts " [SKIPPED]".cyan else - output, status = Gitlab::Popen.popen(%W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)) + cmd = %W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) + output, status = Gitlab::Popen.popen(cmd) if status.zero? - puts " [DONE]".green + $progress.puts " [DONE]".green else puts " [FAILED]".red + puts "failed: #{cmd.join(' ')}" abort 'Backup failed' end end @@ -55,35 +59,52 @@ module Backup FileUtils.mkdir_p(repos_path) Project.find_each(batch_size: 1000) do |project| - print "#{project.path_with_namespace} ... " + $progress.print " * #{project.path_with_namespace} ... " project.namespace.ensure_dir_exist if project.namespace - if system(*%W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)}), silent) - puts "[DONE]".green + if File.exists?(path_to_bundle(project)) + cmd = %W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)}) + else + cmd = %W(git init --bare #{path_to_repo(project)}) + end + + if system(*cmd, silent) + $progress.puts "[DONE]".green else puts "[FAILED]".red + puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end wiki = ProjectWiki.new(project) if File.exists?(path_to_bundle(wiki)) - print " * #{wiki.path_with_namespace} ... " - if system(*%W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}), silent) - puts " [DONE]".green + $progress.print " * #{wiki.path_with_namespace} ... " + + # If a wiki bundle exists, first remove the empty repo + # that was initialized with ProjectWiki.new() and then + # try to restore with 'git clone --bare'. + FileUtils.rm_rf(path_to_repo(wiki)) + cmd = %W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) + + if system(*cmd, silent) + $progress.puts " [DONE]".green else puts " [FAILED]".red + puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end end end - print 'Put GitLab hooks in repositories dirs'.yellow - if system("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks") - puts " [DONE]".green + $progress.print 'Put GitLab hooks in repositories dirs'.yellow + cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks" + if system(cmd) + $progress.puts " [DONE]".green else puts " [FAILED]".red + puts "failed: #{cmd}" end end @@ -91,7 +112,7 @@ module Backup protected def path_to_repo(project) - File.join(repos_path, project.path_with_namespace + '.git') + project.repository.path_to_repo end def path_to_bundle(project) diff --git a/lib/disable_email_interceptor.rb b/lib/disable_email_interceptor.rb new file mode 100644 index 00000000000..1b80be112a4 --- /dev/null +++ b/lib/disable_email_interceptor.rb @@ -0,0 +1,8 @@ +# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails +class DisableEmailInterceptor + + def self.delivering_email(message) + message.perform_deliveries = false + Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" + end +end diff --git a/lib/email_validator.rb b/lib/email_validator.rb index 0a67ebcd795..f509f0a5843 100644 --- a/lib/email_validator.rb +++ b/lib/email_validator.rb @@ -1,5 +1,5 @@ # Based on https://github.com/balexand/email_validator -# +# # Extended to use only strict mode with following allowed characters: # ' - apostrophe # diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index e51cb30bdd9..6e4ed01e079 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -1,17 +1,9 @@ # Module providing methods for dealing with separating a tree-ish string and a # file path string when combined in a request parameter module ExtractsPath - extend ActiveSupport::Concern - # Raised when given an invalid file path class InvalidPathError < StandardError; end - included do - if respond_to?(:before_filter) - before_filter :assign_ref_vars - end - end - # Given a string containing both a Git tree-ish, such as a branch or tag, and # a filesystem path joined by forward slashes, attempts to separate the two. # @@ -110,7 +102,8 @@ module ExtractsPath raise InvalidPathError unless @commit @hex_path = Digest::SHA1.hexdigest(@path) - @logs_path = logs_file_project_ref_path(@project, @ref, @path) + @logs_path = logs_file_namespace_project_ref_path(@project.namespace, + @project, @ref, @path) rescue RuntimeError, NoMethodError, InvalidPathError not_found! diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 411b2b9a3cc..424541b4a04 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -11,6 +11,11 @@ module Gitlab MASTER = 40 OWNER = 50 + # Branch protection settings + PROTECTION_NONE = 0 + PROTECTION_DEV_CAN_PUSH = 1 + PROTECTION_FULL = 2 + class << self def values options.values @@ -43,6 +48,18 @@ module Gitlab master: MASTER, } end + + def protection_options + { + "Not protected, developers and masters can (force) push and delete the branch" => PROTECTION_NONE, + "Partially protected, developers can also push but prevent all force pushes and deletion" => PROTECTION_DEV_CAN_PUSH, + "Fully protected, only masters can push and prevent all force pushes and deletion" => PROTECTION_FULL, + } + end + + def protection_values + protection_options.values + end end def human_access diff --git a/lib/gitlab/app_logger.rb b/lib/gitlab/app_logger.rb index 8e4717b46e6..dddcb2538f9 100644 --- a/lib/gitlab/app_logger.rb +++ b/lib/gitlab/app_logger.rb @@ -1,7 +1,7 @@ module Gitlab class AppLogger < Gitlab::Logger - def self.file_name - 'application.log' + def self.file_name_noext + 'application' end def format_message(severity, timestamp, progname, msg) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 955abc1bedd..30509528b8b 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,24 +1,18 @@ module Gitlab class Auth def find(login, password) - user = User.find_by(email: login) || User.find_by(username: login) + user = User.by_login(login) + # If no user is found, or it's an LDAP server, try LDAP. + # LDAP users are only authenticated via LDAP if user.nil? || user.ldap_user? # Second chance - try LDAP authentication - return nil unless ldap_conf.enabled + return nil unless Gitlab::LDAP::Config.enabled? - Gitlab::LDAP::User.authenticate(login, password) + Gitlab::LDAP::Authentication.login(login, password) else user if user.valid_password?(password) end end - - def log - Gitlab::AppLogger - end - - def ldap_conf - @ldap_conf ||= Gitlab.config.ldap - end end end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index c2f3b851c07..ee877e099b1 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -10,8 +10,9 @@ module Grack @request = Rack::Request.new(env) @auth = Request.new(env) - # Need this patch due to the rails mount + @gitlab_ci = false + # Need this patch due to the rails mount # Need this if under RELATIVE_URL_ROOT unless Gitlab.config.gitlab.relative_url_root.empty? # If website is mounted using relative_url_root need to remove it first @@ -22,8 +23,12 @@ module Grack @env['SCRIPT_NAME'] = "" - if project - auth! + auth! + + if project && authorized_request? + @app.call(env) + elsif @user.nil? && !@gitlab_ci + unauthorized else render_not_found end @@ -32,35 +37,30 @@ module Grack private def auth! - if @auth.provided? - return bad_request unless @auth.basic? - - # Authentication with username and password - login, password = @auth.credentials + return unless @auth.provided? - # Allow authentication for GitLab CI service - # if valid token passed - if gitlab_ci_request?(login, password) - return @app.call(env) - end + return bad_request unless @auth.basic? - @user = authenticate_user(login, password) + # Authentication with username and password + login, password = @auth.credentials - if @user - Gitlab::ShellEnv.set_env(@user) - @env['REMOTE_USER'] = @auth.username - end + # Allow authentication for GitLab CI service + # if valid token passed + if gitlab_ci_request?(login, password) + @gitlab_ci = true + return end - if authorized_request? - @app.call(env) - else - unauthorized + @user = authenticate_user(login, password) + + if @user + Gitlab::ShellEnv.set_env(@user) + @env['REMOTE_USER'] = @auth.username end end def gitlab_ci_request?(login, password) - if login == "gitlab-ci-token" && project.gitlab_ci? + if login == "gitlab-ci-token" && project && project.gitlab_ci? token = project.gitlab_ci_service.token if token.present? && token == password && git_cmd == 'git-upload-pack' @@ -71,16 +71,48 @@ module Grack false end + def oauth_access_token_check(login, password) + if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present? + token = Doorkeeper::AccessToken.by_token(password) + token && token.accessible? && User.find_by(id: token.resource_owner_id) + end + end + def authenticate_user(login, password) - auth = Gitlab::Auth.new - auth.find(login, password) + user = Gitlab::Auth.new.find(login, password) + + unless user + user = oauth_access_token_check(login, password) + end + + return user if user.present? + + # At this point, we know the credentials were wrong. We let Rack::Attack + # know there was a failed authentication attempt from this IP. This + # information is stored in the Rails cache (Redis) and will be used by + # the Rack::Attack middleware to decide whether to block requests from + # this IP. + config = Gitlab.config.rack_attack.git_basic_auth + Rack::Attack::Allow2Ban.filter(@request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(@request.ip) + false + else + true + end + end + + nil # No user was found end def authorized_request? + return true if @gitlab_ci + case git_cmd when *Gitlab::GitAccess::DOWNLOAD_COMMANDS if user - Gitlab::GitAccess.new.download_allowed?(user, project) + Gitlab::GitAccess.new.download_access_check(user, project).allowed? elsif project.public? # Allow clone/fetch for public projects true @@ -90,7 +122,7 @@ module Grack when *Gitlab::GitAccess::PUSH_COMMANDS if user # Skip user authorization on upload request. - # It will be serverd by update hook in repository + # It will be done by the pre-receive hook in the repository. true else false @@ -111,7 +143,9 @@ module Grack end def project - @project ||= project_by_path(@request.path_info) + return @project if defined?(@project) + + @project = project_by_path(@request.path_info) end def project_by_path(path) @@ -119,12 +153,13 @@ module Grack path_with_namespace = m.last path_with_namespace.gsub!(/\.wiki$/, '') + path_with_namespace[0] = '' if path_with_namespace.start_with?('/') Project.find_with_namespace(path_with_namespace) end end def render_not_found - [404, {"Content-Type" => "text/plain"}, ["Not Found"]] + [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end end end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index f95bbde5b39..aabc7f1e69a 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -8,6 +8,13 @@ module Gitlab end end + class << self + def version_required + @version_required ||= File.read(Rails.root. + join('GITLAB_SHELL_VERSION')).strip + end + end + # Init new repository # # name - project path with namespace @@ -16,7 +23,8 @@ module Gitlab # add_repository("gitlab/gitlab-ci") # def add_repository(name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "add-project", "#{name}.git" + Gitlab::Utils.system_silent([gitlab_shell_projects_path, + 'add-project', "#{name}.git"]) end # Import repository @@ -27,7 +35,8 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - system "#{gitlab_shell_path}/bin/gitlab-projects", "import-project", "#{name}.git", url, '240' + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project', + "#{name}.git", url, '240']) end # Move repository @@ -39,7 +48,8 @@ module Gitlab # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new.git") # def mv_repository(path, new_path) - system "#{gitlab_shell_path}/bin/gitlab-projects", "mv-project", "#{path}.git", "#{new_path}.git" + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project', + "#{path}.git", "#{new_path}.git"]) end # Update HEAD for repository @@ -51,7 +61,8 @@ module Gitlab # update_repository_head("gitlab/gitlab-ci", "3-1-stable") # def update_repository_head(path, branch) - system "#{gitlab_shell_path}/bin/gitlab-projects", "update-head", "#{path}.git", branch + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'update-head', + "#{path}.git", branch]) end # Fork repository to new namespace @@ -63,7 +74,8 @@ module Gitlab # fork_repository("gitlab/gitlab-ci", "randx") # def fork_repository(path, fork_namespace) - system "#{gitlab_shell_path}/bin/gitlab-projects", "fork-project", "#{path}.git", fork_namespace + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project', + "#{path}.git", fork_namespace]) end # Remove repository from file system @@ -74,7 +86,8 @@ module Gitlab # remove_repository("gitlab/gitlab-ci") # def remove_repository(name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "rm-project", "#{name}.git" + Gitlab::Utils.system_silent([gitlab_shell_projects_path, + 'rm-project', "#{name}.git"]) end # Add repository branch from passed ref @@ -87,7 +100,8 @@ module Gitlab # add_branch("gitlab/gitlab-ci", "4-0-stable", "master") # def add_branch(path, branch_name, ref) - system "#{gitlab_shell_path}/bin/gitlab-projects", "create-branch", "#{path}.git", branch_name, ref + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'create-branch', + "#{path}.git", branch_name, ref]) end # Remove repository branch @@ -99,7 +113,8 @@ module Gitlab # rm_branch("gitlab/gitlab-ci", "4-0-stable") # def rm_branch(path, branch_name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "rm-branch", "#{path}.git", branch_name + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-branch', + "#{path}.git", branch_name]) end # Add repository tag from passed ref @@ -117,7 +132,7 @@ module Gitlab cmd = %W(#{gitlab_shell_path}/bin/gitlab-projects create-tag #{path}.git #{tag_name} #{ref}) cmd << message unless message.nil? || message.empty? - system *cmd + Gitlab::Utils.system_silent(cmd) end # Remove repository tag @@ -129,7 +144,8 @@ module Gitlab # rm_tag("gitlab/gitlab-ci", "v4.0") # def rm_tag(path, tag_name) - system "#{gitlab_shell_path}/bin/gitlab-projects", "rm-tag", "#{path}.git", tag_name + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-tag', + "#{path}.git", tag_name]) end # Add new key to gitlab-shell @@ -138,7 +154,8 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) - system "#{gitlab_shell_path}/bin/gitlab-keys", "add-key", key_id, key_content + Gitlab::Utils.system_silent([gitlab_shell_keys_path, + 'add-key', key_id, key_content]) end # Batch-add keys to authorized_keys @@ -157,7 +174,8 @@ module Gitlab # remove_key("key-342", "sha-rsa ...") # def remove_key(key_id, key_content) - system "#{gitlab_shell_path}/bin/gitlab-keys", "rm-key", key_id, key_content + Gitlab::Utils.system_silent([gitlab_shell_keys_path, + 'rm-key', key_id, key_content]) end # Remove all ssh keys from gitlab shell @@ -166,7 +184,7 @@ module Gitlab # remove_all_keys # def remove_all_keys - system "#{gitlab_shell_path}/bin/gitlab-keys", "clear" + Gitlab::Utils.system_silent([gitlab_shell_keys_path, 'clear']) end # Add empty directory for storing repositories @@ -249,5 +267,13 @@ module Gitlab def exists?(dir_name) File.exists?(full_path(dir_name)) end + + def gitlab_shell_projects_path + File.join(gitlab_shell_path, 'bin', 'gitlab-projects') + end + + def gitlab_shell_keys_path + File.join(gitlab_shell_path, 'bin', 'gitlab-keys') + end end end diff --git a/lib/gitlab/backend/shell_adapter.rb b/lib/gitlab/backend/shell_adapter.rb index f247f4593d7..fbe2a7a0d72 100644 --- a/lib/gitlab/backend/shell_adapter.rb +++ b/lib/gitlab/backend/shell_adapter.rb @@ -9,4 +9,3 @@ module Gitlab end end end - diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb new file mode 100644 index 00000000000..7298152e7e9 --- /dev/null +++ b/lib/gitlab/bitbucket_import.rb @@ -0,0 +1,6 @@ +module Gitlab + module BitbucketImport + mattr_accessor :public_key + @public_key = nil + end +end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb new file mode 100644 index 00000000000..1e4906c9e31 --- /dev/null +++ b/lib/gitlab/bitbucket_import/client.rb @@ -0,0 +1,99 @@ +module Gitlab + module BitbucketImport + class Client + attr_reader :consumer, :api + + def initialize(access_token = nil, access_token_secret = nil) + @consumer = ::OAuth::Consumer.new( + config.app_id, + config.app_secret, + bitbucket_options + ) + + if access_token && access_token_secret + @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret) + end + end + + def request_token(redirect_uri) + request_token = consumer.get_request_token(oauth_callback: redirect_uri) + + { + oauth_token: request_token.token, + oauth_token_secret: request_token.secret, + oauth_callback_confirmed: request_token.callback_confirmed?.to_s + } + end + + def authorize_url(request_token, redirect_uri) + request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) + + if request_token.callback_confirmed? + request_token.authorize_url + else + request_token.authorize_url(oauth_callback: redirect_uri) + end + end + + def get_token(request_token, oauth_verifier, redirect_uri) + request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) + + if request_token.callback_confirmed? + request_token.get_access_token(oauth_verifier: oauth_verifier) + else + request_token.get_access_token(oauth_callback: redirect_uri) + end + end + + def user + JSON.parse(api.get("/api/1.0/user").body) + end + + def issues(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body) + end + + def issue_comments(project_identifier, issue_id) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) + end + + def project(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) + end + + def find_deploy_key(project_identifier, key) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| + deploy_key["key"].chomp == key.chomp + end + end + + def add_deploy_key(project_identifier, key) + deploy_key = find_deploy_key(project_identifier, key) + return if deploy_key + + JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body) + end + + def delete_deploy_key(project_identifier, key) + deploy_key = find_deploy_key(project_identifier, key) + return unless deploy_key + + api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204" + end + + def projects + JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + end + + private + + def config + Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} + end + + def bitbucket_options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].dup + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb new file mode 100644 index 00000000000..42c93707caa --- /dev/null +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -0,0 +1,52 @@ +module Gitlab + module BitbucketImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.bitbucket_access_token, project.creator.bitbucket_access_token_secret) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + project_identifier = project.import_source + + return true unless client.project(project_identifier)["has_issues"] + + #Issues && Comments + issues = client.issues(project_identifier) + + issues["issues"].each do |issue| + body = @formatter.author_line(issue["reported_by"]["username"], issue["content"]) + + comments = client.issue_comments(project_identifier, issue["local_id"]) + + if comments.any? + body += @formatter.comments_header + end + + comments.each do |comment| + body += @formatter.comment(comment["author_info"]["username"], comment["utc_created_on"], comment["content"]) + end + + project.issues.create!( + description: body, + title: issue["title"], + state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', + author_id: gl_user_id(project, issue["reported_by"]["username"]) + ) + end + + true + end + + private + + def gl_user_id(project, bitbucket_id) + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb new file mode 100644 index 00000000000..9931aa7e029 --- /dev/null +++ b/lib/gitlab/bitbucket_import/key_adder.rb @@ -0,0 +1,23 @@ +module Gitlab + module BitbucketImport + class KeyAdder + attr_reader :repo, :current_user, :client + + def initialize(repo, current_user) + @repo, @current_user = repo, current_user + @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def execute + return false unless BitbucketImport.public_key.present? + + project_identifier = "#{repo["owner"]}/#{repo["slug"]}" + client.add_deploy_key(project_identifier, BitbucketImport.public_key) + + true + rescue + false + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb new file mode 100644 index 00000000000..1a24a86fc37 --- /dev/null +++ b/lib/gitlab/bitbucket_import/key_deleter.rb @@ -0,0 +1,23 @@ +module Gitlab + module BitbucketImport + class KeyDeleter + attr_reader :project, :current_user, :client + + def initialize(project) + @project = project + @current_user = project.creator + @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def execute + return false unless BitbucketImport.public_key.present? + + client.delete_deploy_key(project.import_source, BitbucketImport.public_key) + + true + rescue + false + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb new file mode 100644 index 00000000000..db33af2c2da --- /dev/null +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -0,0 +1,39 @@ +module Gitlab + module BitbucketImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo["name"], + path: repo["slug"], + description: repo["description"], + namespace: namespace, + creator: current_user, + visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + import_type: "bitbucket", + import_source: "#{repo["owner"]}/#{repo["slug"]}", + import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git" + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 401e6e047b1..a9fd59f03d9 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -3,14 +3,19 @@ module Gitlab ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern) def self.closed_by_message_in_project(message, project) - md = ISSUE_CLOSING_REGEX.match(message) - if md - extractor = Gitlab::ReferenceExtractor.new - extractor.analyze(md[0], project) - extractor.issues_for(project) - else - [] + issues = [] + + unless message.nil? + md = message.scan(ISSUE_CLOSING_REGEX) + + md.each do |ref| + extractor = Gitlab::ReferenceExtractor.new + extractor.analyze(ref[0], project) + issues += extractor.issues_for(project) + end end + + issues.uniq end end end diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb new file mode 100644 index 00000000000..2f30d238e6b --- /dev/null +++ b/lib/gitlab/commits_calendar.rb @@ -0,0 +1,33 @@ +module Gitlab + class CommitsCalendar + attr_reader :timestamps + + def initialize(projects, user) + @timestamps = {} + date_timestamps = [] + + projects.reject(&:forked?).each do |project| + date_timestamps << ProjectContributions.new(project, user).commits_log + end + + # Sumarrize commits from all projects per days + date_timestamps = date_timestamps.inject do |collection, date| + collection.merge(date) { |k, old_v, new_v| old_v + new_v } + end + + date_timestamps ||= [] + date_timestamps.each do |date, commits| + timestamp = Date.parse(date).to_time.to_i.to_s rescue nil + @timestamps[timestamp] = commits if timestamp + end + end + + def starting_year + (Time.now - 1.year).strftime("%Y") + end + + def starting_month + Date.today.strftime("%m").to_i + end + end +end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb new file mode 100644 index 00000000000..0ebebfa09c4 --- /dev/null +++ b/lib/gitlab/current_settings.rb @@ -0,0 +1,27 @@ +module Gitlab + module CurrentSettings + def current_application_settings + key = :current_application_settings + + RequestStore.store[key] ||= begin + if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('application_settings') + ApplicationSetting.current || ApplicationSetting.create_from_defaults + else + fake_application_settings + end + end + end + + def fake_application_settings + OpenStruct.new( + default_projects_limit: Settings.gitlab['default_projects_limit'], + default_branch_protection: Settings.gitlab['default_branch_protection'], + signup_enabled: Settings.gitlab['signup_enabled'], + signin_enabled: Settings.gitlab['signin_enabled'], + gravatar_enabled: Settings.gravatar['enabled'], + sign_in_text: Settings.extra['sign_in_text'], + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'] + ) + end + end +end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index f7c1f20d762..c1d9520ddf1 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -4,7 +4,7 @@ module Gitlab include Enumerable def parse(lines) - @lines = lines, + @lines = lines lines_obj = [] line_obj_index = 0 line_old = 1 @@ -27,7 +27,7 @@ module Gitlab line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 - next if line_old == 1 && line_new == 1 #top of file + next if line_old <= 1 && line_new <= 1 #top of file lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next @@ -74,7 +74,7 @@ module Gitlab def html_escape(str) replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - str.gsub(/[&"'><]/, replacements) + str.gsub(/[&"'><]/, replacements) end end end diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb new file mode 100644 index 00000000000..eae9773a067 --- /dev/null +++ b/lib/gitlab/force_push_check.rb @@ -0,0 +1,14 @@ +module Gitlab + class ForcePushCheck + def self.force_push?(project, oldrev, newrev) + return false if project.empty_repo? + + if oldrev != Gitlab::Git::BLANK_SHA && newrev != Gitlab::Git::BLANK_SHA + missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})) + missed_refs.split("\n").size > 0 + else + false + end + end + end +end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb new file mode 100644 index 00000000000..0c350d7c675 --- /dev/null +++ b/lib/gitlab/git.rb @@ -0,0 +1,25 @@ +module Gitlab + module Git + BLANK_SHA = '0' * 40 + TAG_REF_PREFIX = "refs/tags/" + BRANCH_REF_PREFIX = "refs/heads/" + + class << self + def ref_name(ref) + ref.gsub(/\Arefs\/(tags|heads)\//, '') + end + + def tag_ref?(ref) + ref.start_with?(TAG_REF_PREFIX) + end + + def branch_ref?(ref) + ref.start_with?(BRANCH_REF_PREFIX) + end + + def blank_ref?(ref) + ref == BLANK_SHA + end + end + end +end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index b768a99a0e8..cb69e4b13d3 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -5,95 +5,129 @@ module Gitlab attr_reader :params, :project, :git_cmd, :user - def allowed?(actor, cmd, project, changes = nil) + def self.can_push_to_branch?(user, project, ref) + return false unless user + + if project.protected_branch?(ref) && + !(project.developers_can_push_to_protected_branch?(ref) && project.team.developer?(user)) + user.can?(:push_code_to_protected_branches, project) + else + user.can?(:push_code, project) + end + end + + def check(actor, cmd, project, changes = nil) case cmd when *DOWNLOAD_COMMANDS + download_access_check(actor, project) + when *PUSH_COMMANDS if actor.is_a? User - download_allowed?(actor, project) + push_access_check(actor, project, changes) elsif actor.is_a? DeployKey - actor.projects.include?(project) + return build_status_object(false, "Deploy key not allowed to push") elsif actor.is_a? Key - download_allowed?(actor.user, project) + push_access_check(actor.user, project, changes) else raise 'Wrong actor' end - when *PUSH_COMMANDS - if actor.is_a? User - push_allowed?(actor, project, changes) - elsif actor.is_a? DeployKey - # Deploy key not allowed to push - return false - elsif actor.is_a? Key - push_allowed?(actor.user, project, changes) + else + return build_status_object(false, "Wrong command") + end + end + + def download_access_check(actor, project) + if actor.is_a?(User) + user_download_access_check(actor, project) + elsif actor.is_a?(DeployKey) + if actor.projects.include?(project) + build_status_object(true) else - raise 'Wrong actor' + build_status_object(false, "Deploy key not allowed to access this project") end + elsif actor.is_a? Key + user_download_access_check(actor.user, project) else - false + raise 'Wrong actor' end end - def download_allowed?(user, project) - if user && user_allowed?(user) - user.can?(:download_code, project) + def user_download_access_check(user, project) + if user && user_allowed?(user) && user.can?(:download_code, project) + build_status_object(true) else - false + build_status_object(false, "You don't have access") end end - def push_allowed?(user, project, changes) - return false unless user && user_allowed?(user) - return true if changes.blank? + def push_access_check(user, project, changes) + unless user && user_allowed?(user) + return build_status_object(false, "You don't have access") + end + + if changes.blank? + return build_status_object(true) + end + + unless project.repository.exists? + return build_status_object(false, "Repository does not exist") + end changes = changes.lines if changes.kind_of?(String) # Iterate over all changes to find if user allowed all of them to be applied - changes.each do |change| - unless change_allowed?(user, project, change) + changes.map(&:strip).reject(&:blank?).each do |change| + status = change_access_check(user, project, change) + unless status.allowed? # If user does not have access to make at least one change - cancel all push - return false + return status end end - # If user has access to make all changes - true + return build_status_object(true) end - def change_allowed?(user, project, change) + def change_access_check(user, project, change) oldrev, newrev, ref = change.split(' ') action = if project.protected_branch?(branch_name(ref)) - # we dont allow force push to protected branch - if forced_push?(project, oldrev, newrev) - :force_push_code_to_protected_branches - # and we dont allow remove of protected branch - elsif newrev =~ /0000000/ - :remove_protected_branches - else - :push_code_to_protected_branches - end - elsif project.repository && project.repository.tag_names.include?(tag_name(ref)) + protected_branch_action(project, oldrev, newrev, branch_name(ref)) + elsif protected_tag?(project, tag_name(ref)) # Prevent any changes to existing git tag unless user has permissions :admin_project else :push_code end - user.can?(action, project) + if user.can?(action, project) + build_status_object(true) + else + build_status_object(false, "You don't have permission") + end end def forced_push?(project, oldrev, newrev) - return false if project.empty_repo? + Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev) + end - if oldrev !~ /00000000/ && newrev !~ /00000000/ - missed_refs = IO.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})).read - missed_refs.split("\n").size > 0 + private + + def protected_branch_action(project, oldrev, newrev, branch_name) + # we dont allow force push to protected branch + if forced_push?(project, oldrev, newrev) + :force_push_code_to_protected_branches + elsif Gitlab::Git.blank_ref?(newrev) + # and we dont allow remove of protected branch + :remove_protected_branches + elsif project.developers_can_push_to_protected_branch?(branch_name) + :push_code else - false + :push_code_to_protected_branches end end - private + def protected_tag?(project, tag_name) + project.repository.tag_names.include?(tag_name) + end def user_allowed?(user) Gitlab::UserAccess.allowed?(user) @@ -101,8 +135,8 @@ module Gitlab def branch_name(ref) ref = ref.to_s - if ref.start_with?('refs/heads') - ref.sub(%r{\Arefs/heads/}, '') + if Gitlab::Git.branch_ref?(ref) + Gitlab::Git.ref_name(ref) else nil end @@ -110,11 +144,17 @@ module Gitlab def tag_name(ref) ref = ref.to_s - if ref.start_with?('refs/tags') - ref.sub(%r{\Arefs/tags/}, '') + if Gitlab::Git.tag_ref?(ref) + Gitlab::Git.ref_name(ref) else nil end end + + protected + + def build_status_object(status, message = '') + GitAccessStatus.new(status, message) + end end end diff --git a/lib/gitlab/git_access_status.rb b/lib/gitlab/git_access_status.rb new file mode 100644 index 00000000000..5a806ff6e0d --- /dev/null +++ b/lib/gitlab/git_access_status.rb @@ -0,0 +1,15 @@ +module Gitlab + class GitAccessStatus + attr_accessor :status, :message + alias_method :allowed?, :status + + def initialize(status, message = '') + @status = status + @message = message + end + + def to_json + { status: @status, message: @message }.to_json + end + end +end diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index 9f0eb3be20f..a2177c8d548 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -1,7 +1,11 @@ module Gitlab class GitAccessWiki < GitAccess - def change_allowed?(user, project, change) - user.can?(:write_wiki, project) + def change_access_check(user, project, change) + if user.can?(:write_wiki, project) + build_status_object(true) + else + build_status_object(false, "You don't have access") + end end end end diff --git a/lib/gitlab/git_logger.rb b/lib/gitlab/git_logger.rb index fbfed205a0f..9e02ccc0f44 100644 --- a/lib/gitlab/git_logger.rb +++ b/lib/gitlab/git_logger.rb @@ -1,7 +1,7 @@ module Gitlab class GitLogger < Gitlab::Logger - def self.file_name - 'githost.log' + def self.file_name_noext + 'githost' end def format_message(severity, timestamp, progname, msg) diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 13cb08948bb..39d17def930 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -5,7 +5,8 @@ module Gitlab # # Returns true for a valid reference name, false otherwise def validate(ref_name) - system *%W(git check-ref-format refs/#{ref_name}) + Gitlab::Utils.system_silent( + %W(git check-ref-format refs/#{ref_name})) end end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb new file mode 100644 index 00000000000..7fe076b333b --- /dev/null +++ b/lib/gitlab/github_import/client.rb @@ -0,0 +1,53 @@ +module Gitlab + module GithubImport + class Client + attr_reader :client, :api + + def initialize(access_token) + @client = ::OAuth2::Client.new( + config.app_id, + config.app_secret, + github_options + ) + + if access_token + ::Octokit.auto_paginate = true + @api = ::Octokit::Client.new(access_token: access_token) + end + end + + def authorize_url(redirect_uri) + client.auth_code.authorize_url({ + redirect_uri: redirect_uri, + scope: "repo, user, user:email" + }) + end + + def get_token(code) + client.auth_code.get_token(code).token + end + + def method_missing(method, *args, &block) + if api.respond_to?(method) + api.send(method, *args, &block) + else + super(method, *args, &block) + end + end + + def respond_to?(method) + api.respond_to?(method) || super + end + + private + + def config + Gitlab.config.omniauth.providers.find{|provider| provider.name == "github"} + end + + def github_options + OmniAuth::Strategies::GitHub.default_options[:client_options].dup + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb new file mode 100644 index 00000000000..23832b3233c --- /dev/null +++ b/lib/gitlab/github_import/importer.rb @@ -0,0 +1,46 @@ +module Gitlab + module GithubImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.github_access_token) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + #Issues && Comments + client.list_issues(project.import_source, state: :all).each do |issue| + if issue.pull_request.nil? + + body = @formatter.author_line(issue.user.login, issue.body) + + if issue.comments > 0 + body += @formatter.comments_header + + client.issue_comments(project.import_source, issue.number).each do |c| + body += @formatter.comment(c.user.login, c.created_at, c.body) + end + end + + project.issues.create!( + description: body, + title: issue.title, + state: issue.state == 'closed' ? 'closed' : 'opened', + author_id: gl_user_id(project, issue.user.id) + ) + end + end + end + + private + + def gl_user_id(project, github_id) + user = User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb new file mode 100644 index 00000000000..9439ca6cbf4 --- /dev/null +++ b/lib/gitlab/github_import/project_creator.rb @@ -0,0 +1,39 @@ +module Gitlab + module GithubImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo.name, + path: repo.name, + description: repo.description, + namespace: namespace, + creator: current_user, + visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + import_type: "github", + import_source: repo.full_name, + import_url: repo.clone_url.sub("https://", "https://#{current_user.github_access_token}@") + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb new file mode 100644 index 00000000000..2236439c6ce --- /dev/null +++ b/lib/gitlab/gitlab_import/client.rb @@ -0,0 +1,78 @@ +module Gitlab + module GitlabImport + class Client + attr_reader :client, :api + + PER_PAGE = 100 + + def initialize(access_token) + @client = ::OAuth2::Client.new( + config.app_id, + config.app_secret, + gitlab_options + ) + + if access_token + @api = OAuth2::AccessToken.from_hash(@client, access_token: access_token) + end + end + + def authorize_url(redirect_uri) + client.auth_code.authorize_url({ + redirect_uri: redirect_uri, + scope: "api" + }) + end + + def get_token(code, redirect_uri) + client.auth_code.get_token(code, redirect_uri: redirect_uri).token + end + + def issues(project_identifier) + lazy_page_iterator(PER_PAGE) do |page| + api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed + end + end + + def issue_comments(project_identifier, issue_id) + lazy_page_iterator(PER_PAGE) do |page| + api.get("/api/v3/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed + end + end + + def project(id) + api.get("/api/v3/projects/#{id}").parsed + end + + def projects + lazy_page_iterator(PER_PAGE) do |page| + api.get("/api/v3/projects?per_page=#{PER_PAGE}&page=#{page}").parsed + end + end + + private + + def lazy_page_iterator(per_page) + Enumerator.new do |y| + page = 1 + loop do + items = yield(page) + items.each do |item| + y << item + end + break if items.empty? || items.size < per_page + page += 1 + end + end + end + + def config + Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"} + end + + def gitlab_options + OmniAuth::Strategies::GitLab.default_options[:client_options].dup + end + end + end +end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb new file mode 100644 index 00000000000..c5304a0699b --- /dev/null +++ b/lib/gitlab/gitlab_import/importer.rb @@ -0,0 +1,50 @@ +module Gitlab + module GitlabImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.gitlab_access_token) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + project_identifier = URI.encode(project.import_source, '/') + + #Issues && Comments + issues = client.issues(project_identifier) + + issues.each do |issue| + body = @formatter.author_line(issue["author"]["name"], issue["description"]) + + comments = client.issue_comments(project_identifier, issue["id"]) + + if comments.any? + body += @formatter.comments_header + end + + comments.each do |comment| + body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"]) + end + + project.issues.create!( + description: body, + title: issue["title"], + state: issue["state"], + author_id: gl_user_id(project, issue["author"]["id"]) + ) + end + + true + end + + private + + def gl_user_id(project, gitlab_id) + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb new file mode 100644 index 00000000000..6424d56f8f1 --- /dev/null +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -0,0 +1,39 @@ +module Gitlab + module GitlabImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo["name"], + path: repo["path"], + description: repo["description"], + namespace: namespace, + creator: current_user, + visibility_level: repo["visibility_level"], + import_type: "gitlab", + import_source: repo["path_with_namespace"], + import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{current_user.gitlab_access_token}@") + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb new file mode 100644 index 00000000000..5043f6a2ebd --- /dev/null +++ b/lib/gitlab/gitorious_import/client.rb @@ -0,0 +1,63 @@ +module Gitlab + module GitoriousImport + GITORIOUS_HOST = "https://gitorious.org" + + class Client + attr_reader :repo_list + + def initialize(repo_list) + @repo_list = repo_list + end + + def authorize_url(redirect_uri) + "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}" + end + + def repos + @repos ||= repo_names.map { |full_name| Repository.new(full_name) } + end + + def repo(id) + repos.find { |repo| repo.id == id } + end + + private + + def repo_names + repo_list.to_s.split(',').map(&:strip).reject(&:blank?) + end + end + + Repository = Struct.new(:full_name) do + def id + Digest::SHA1.hexdigest(full_name) + end + + def namespace + segments.first + end + + def path + segments.last + end + + def name + path.titleize + end + + def description + "" + end + + def import_url + "#{GITORIOUS_HOST}/#{full_name}.git" + end + + private + + def segments + full_name.split('/') + end + end + end +end diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb new file mode 100644 index 00000000000..3cbebe53997 --- /dev/null +++ b/lib/gitlab/gitorious_import/project_creator.rb @@ -0,0 +1,39 @@ +module Gitlab + module GitoriousImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo.name, + path: repo.path, + description: repo.description, + namespace: namespace, + creator: current_user, + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + import_type: "gitorious", + import_source: repo.full_name, + import_url: repo.import_url + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb new file mode 100644 index 00000000000..72e041a90b1 --- /dev/null +++ b/lib/gitlab/import_formatter.rb @@ -0,0 +1,15 @@ +module Gitlab + class ImportFormatter + def comment(author, date, body) + "\n\n*By #{author} on #{date}*\n\n#{body}" + end + + def comments_header + "\n\n\n**Imported comments:**\n" + end + + def author_line(author, body) + "*Created by: #{author}*\n\n#{body}" + end + end +end diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index 0d34976736f..1bec6088292 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -15,7 +15,6 @@ module Gitlab { title: "support", color: yellow }, { title: "discussion", color: blue }, { title: "suggestion", color: blue }, - { title: "feature", color: green }, { title: "enhancement", color: green } ] diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index d2235d2e3bc..960fb3849b4 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -1,18 +1,21 @@ +# LDAP authorization model +# +# * Check if we are allowed access (not blocked) +# module Gitlab module LDAP class Access - attr_reader :adapter + attr_reader :adapter, :provider, :user - def self.open(&block) - Gitlab::LDAP::Adapter.open do |adapter| - block.call(self.new(adapter)) + def self.open(user, &block) + Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| + block.call(self.new(user, adapter)) end end def self.allowed?(user) - self.open do |access| - if access.allowed?(user) - # GitLab EE LDAP code goes here + self.open(user) do |access| + if access.allowed? user.last_credential_check_at = Time.now user.save true @@ -22,14 +25,23 @@ module Gitlab end end - def initialize(adapter=nil) + def initialize(user, adapter=nil) @adapter = adapter + @user = user + @provider = user.ldap_identity.provider end - def allowed?(user) - if Gitlab::LDAP::Person.find_by_dn(user.extern_uid, adapter) - if Gitlab.config.ldap.active_directory - !Gitlab::LDAP::Person.disabled_via_active_directory?(user.extern_uid, adapter) + def allowed? + if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) + return true unless ldap_config.active_directory + + # Block user in GitLab if he/she was blocked in AD + if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) + user.block unless user.blocked? + false + else + user.activate if user.blocked? + true end else false @@ -37,6 +49,14 @@ module Gitlab rescue false end + + def adapter + @adapter ||= Gitlab::LDAP::Adapter.new(provider) + end + + def ldap_config + Gitlab::LDAP::Config.new(provider) + end end end end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 68ac1b22909..577a890a7d9 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -1,55 +1,28 @@ module Gitlab module LDAP class Adapter - attr_reader :ldap + attr_reader :provider, :ldap - def self.open(&block) - Net::LDAP.open(adapter_options) do |ldap| - block.call(self.new(ldap)) + def self.open(provider, &block) + Net::LDAP.open(config(provider).adapter_options) do |ldap| + block.call(self.new(provider, ldap)) end end - def self.config - Gitlab.config.ldap + def self.config(provider) + Gitlab::LDAP::Config.new(provider) end - def self.adapter_options - encryption = - case config['method'].to_s - when 'ssl' - :simple_tls - when 'tls' - :start_tls - else - nil - end - - options = { - host: config['host'], - port: config['port'], - encryption: encryption - } - - auth_options = { - auth: { - method: :simple, - username: config['bind_dn'], - password: config['password'] - } - } - - if config['password'] || config['bind_dn'] - options.merge!(auth_options) - end - options + def initialize(provider, ldap=nil) + @provider = provider + @ldap = ldap || Net::LDAP.new(config.adapter_options) end - - def initialize(ldap=nil) - @ldap = ldap || Net::LDAP.new(self.class.adapter_options) + def config + Gitlab::LDAP::Config.new(provider) end - def users(field, value) + def users(field, value, limit = nil) if field.to_sym == :dn options = { base: value, @@ -57,13 +30,13 @@ module Gitlab } else options = { - base: config['base'], + base: config.base, filter: Net::LDAP::Filter.eq(field, value) } end - if config['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(config['user_filter']) + if config.user_filter.present? + user_filter = Net::LDAP::Filter.construct(config.user_filter) options[:filter] = if options[:filter] Net::LDAP::Filter.join(options[:filter], user_filter) @@ -72,12 +45,16 @@ module Gitlab end end + if limit.present? + options.merge!(size: limit) + end + entries = ldap_search(options).select do |entry| entry.respond_to? config.uid end entries.map do |entry| - Gitlab::LDAP::Person.new(entry) + Gitlab::LDAP::Person.new(entry, provider) end end @@ -86,8 +63,10 @@ module Gitlab end def dn_matches_filter?(dn, filter) - ldap_search(base: dn, filter: filter, - scope: Net::LDAP::SearchScope_BaseObject, attributes: %w{dn}).any? + ldap_search(base: dn, + filter: filter, + scope: Net::LDAP::SearchScope_BaseObject, + attributes: %w{dn}).any? end def ldap_search(*args) @@ -105,12 +84,6 @@ module Gitlab results end end - - private - - def config - @config ||= self.class.config - end end end end diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb new file mode 100644 index 00000000000..649cf3194b8 --- /dev/null +++ b/lib/gitlab/ldap/authentication.rb @@ -0,0 +1,71 @@ +# This calls helps to authenticate to LDAP by providing username and password +# +# Since multiple LDAP servers are supported, it will loop through all of them +# until a valid bind is found +# + +module Gitlab + module LDAP + class Authentication + def self.login(login, password) + return unless Gitlab::LDAP::Config.enabled? + return unless login.present? && password.present? + + auth = nil + # loop through providers until valid bind + providers.find do |provider| + auth = new(provider) + auth.login(login, password) # true will exit the loop + end + + # If (login, password) was invalid for all providers, the value of auth is now the last + # Gitlab::LDAP::Authentication instance we tried. + auth.user + end + + def self.providers + Gitlab::LDAP::Config.providers + end + + attr_accessor :provider, :ldap_user + + def initialize(provider) + @provider = provider + end + + def login(login, password) + @ldap_user = adapter.bind_as( + filter: user_filter(login), + size: 1, + password: password + ) + end + + def adapter + OmniAuth::LDAP::Adaptor.new(config.options.symbolize_keys) + end + + def config + Gitlab::LDAP::Config.new(provider) + end + + def user_filter(login) + filter = Net::LDAP::Filter.equals(config.uid, login) + + # Apply LDAP user filter if present + if config.user_filter.present? + filter = Net::LDAP::Filter.join( + filter, + Net::LDAP::Filter.construct(config.user_filter) + ) + end + filter + end + + def user + return nil unless ldap_user + Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider) + end + end + end +end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb new file mode 100644 index 00000000000..0cb24d0ccc1 --- /dev/null +++ b/lib/gitlab/ldap/config.rb @@ -0,0 +1,120 @@ +# Load a specific server configuration +module Gitlab + module LDAP + class Config + attr_accessor :provider, :options + + def self.enabled? + Gitlab.config.ldap.enabled + end + + def self.servers + Gitlab.config.ldap.servers.values + end + + def self.providers + servers.map {|server| server['provider_name'] } + end + + def self.valid_provider?(provider) + providers.include?(provider) + end + + def self.invalid_provider(provider) + raise "Unknown provider (#{provider}). Available providers: #{providers}" + end + + def initialize(provider) + if self.class.valid_provider?(provider) + @provider = provider + elsif provider == 'ldap' + @provider = self.class.providers.first + else + self.class.invalid_provider(provider) + end + @options = config_for(@provider) # Use @provider, not provider + end + + def enabled? + base_config.enabled + end + + def adapter_options + { + host: options['host'], + port: options['port'], + encryption: encryption + }.tap do |options| + options.merge!(auth_options) if has_auth? + end + end + + def base + options['base'] + end + + def uid + options['uid'] + end + + def sync_ssh_keys? + sync_ssh_keys.present? + end + + # The LDAP attribute in which the ssh keys are stored + def sync_ssh_keys + options['sync_ssh_keys'] + end + + def user_filter + options['user_filter'] + end + + def group_base + options['group_base'] + end + + def admin_group + options['admin_group'] + end + + def active_directory + options['active_directory'] + end + + protected + def base_config + Gitlab.config.ldap + end + + def config_for(provider) + base_config.servers.values.find { |server| server['provider_name'] == provider } + end + + def encryption + case options['method'].to_s + when 'ssl' + :simple_tls + when 'tls' + :start_tls + else + nil + end + end + + def auth_options + { + auth: { + method: :simple, + username: options['bind_dn'], + password: options['password'] + } + } + end + + def has_auth? + options['password'] || options['bind_dn'] + end + end + end +end diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 87c3d711db4..3c426179375 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -6,24 +6,26 @@ module Gitlab # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2") - def self.find_by_uid(uid, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new - adapter.user(config.uid, uid) + attr_accessor :entry, :provider + + def self.find_by_uid(uid, adapter) + uid = Net::LDAP::Filter.escape(uid) + adapter.user(adapter.config.uid, uid) end - def self.find_by_dn(dn, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new + def self.find_by_dn(dn, adapter) + dn = Net::LDAP::Filter.escape(dn) adapter.user('dn', dn) end - def self.disabled_via_active_directory?(dn, adapter=nil) - adapter ||= Gitlab::LDAP::Adapter.new + def self.disabled_via_active_directory?(dn, adapter) adapter.dn_matches_filter?(dn, AD_USER_DISABLED) end - def initialize(entry) + def initialize(entry, provider) Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry + @provider = provider end def name @@ -38,6 +40,10 @@ module Gitlab uid end + def email + entry.try(:mail) + end + def dn entry.dn end @@ -48,12 +54,8 @@ module Gitlab @entry end - def adapter - @adapter ||= Gitlab::LDAP::Adapter.new - end - def config - @config ||= Gitlab.config.ldap + @config ||= Gitlab::LDAP::Config.new(provider) end end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 25b5a702f9a..cfa8692659d 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -10,77 +10,55 @@ module Gitlab module LDAP class User < Gitlab::OAuth::User class << self - def find_or_create(auth_hash) - self.auth_hash = auth_hash - find(auth_hash) || find_and_connect_by_email(auth_hash) || create(auth_hash) - end - - def find_and_connect_by_email(auth_hash) - self.auth_hash = auth_hash - user = model.find_by(email: self.auth_hash.email) - - if user - user.update_attributes(extern_uid: auth_hash.uid, provider: auth_hash.provider) - Gitlab::AppLogger.info("(LDAP) Updating legacy LDAP user #{self.auth_hash.email} with extern_uid => #{auth_hash.uid}") - return user - end - end - - def authenticate(login, password) - # Check user against LDAP backend if user is not authenticated - # Only check with valid login and password to prevent anonymous bind results - return nil unless ldap_conf.enabled && login.present? && password.present? - - ldap_user = adapter.bind_as( - filter: user_filter(login), - size: 1, - password: password - ) - - find_by_uid(ldap_user.dn) if ldap_user + def find_by_uid_and_provider(uid, provider) + # LDAP distinguished name is case-insensitive + identity = ::Identity. + where(provider: [provider, :ldap]). + where('lower(extern_uid) = ?', uid.downcase).last + identity && identity.user end + end - def adapter - @adapter ||= OmniAuth::LDAP::Adaptor.new(ldap_conf) - end + def initialize(auth_hash) + super + update_user_attributes + end - protected + # instance methods + def gl_user + @gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user + end - def find_by_uid_and_provider - find_by_uid(auth_hash.uid) - end + def find_by_uid_and_provider + self.class.find_by_uid_and_provider( + auth_hash.uid.downcase, auth_hash.provider) + end - def find_by_uid(uid) - # LDAP distinguished name is case-insensitive - model.where("provider = ? and lower(extern_uid) = ?", provider, uid.downcase).last - end + def find_by_email + ::User.find_by(email: auth_hash.email) + end - def provider - 'ldap' - end + def update_user_attributes + gl_user.email = auth_hash.email - def raise_error(message) - raise OmniAuth::Error, "(LDAP) " + message - end + # Build new identity only if we dont have have same one + gl_user.identities.find_or_initialize_by(provider: auth_hash.provider, + extern_uid: auth_hash.uid) - def ldap_conf - Gitlab.config.ldap - end + gl_user + end - def user_filter(login) - filter = Net::LDAP::Filter.eq(adapter.uid, login) - # Apply LDAP user filter if present - if ldap_conf['user_filter'].present? - user_filter = Net::LDAP::Filter.construct(ldap_conf['user_filter']) - filter = Net::LDAP::Filter.join(filter, user_filter) - end - filter - end + def changed? + gl_user.changed? || gl_user.identities.any?(&:changed?) end def needs_blocking? false end + + def allowed? + Gitlab::LDAP::Access.allowed?(gl_user) + end end end end diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb index 8a73ec5038a..59b21149a9a 100644 --- a/lib/gitlab/logger.rb +++ b/lib/gitlab/logger.rb @@ -1,5 +1,9 @@ module Gitlab class Logger < ::Logger + def self.file_name + file_name_noext + '.log' + end + def self.error(message) build.error(message) end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 464b88d07ea..32f04c866e3 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -14,6 +14,7 @@ module Gitlab # * !123 for merge requests # * $123 for snippets # * 123456 for commits + # * 123456...7890123 for commit ranges (comparisons) # # It also parses Emoji codes to insert images. See # http://www.emoji-cheat-sheet.com/ for a list of the supported icons. @@ -33,17 +34,23 @@ module Gitlab attr_reader :html_options - def gfm_with_tasks(text, project = @project, html_options = {}) - text = gfm(text, project, html_options) - parse_tasks(text) + # Public: Parse the provided text with GitLab-Flavored Markdown + # + # text - the source text + # project - extra options for the reference links as given to link_to + # html_options - extra options for the reference links as given to link_to + def gfm(text, project = @project, html_options = {}) + gfm_with_options(text, {}, project, html_options) end # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text + # options - parse_tasks: true - render tasks + # - xhtml: true - output XHTML instead of HTML # project - extra options for the reference links as given to link_to # html_options - extra options for the reference links as given to link_to - def gfm(text, project = @project, html_options = {}) + def gfm_with_options(text, options = {}, project = @project, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str @@ -70,15 +77,27 @@ module Gitlab insert_piece($1) end - # Context passed to the markdoqwn pipeline + # Used markdown pipelines in GitLab: + # GitlabEmojiFilter - performs emoji replacement. + # + # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters + filters = [ + HTML::Pipeline::Gitlab::GitlabEmojiFilter + ] + markdown_context = { - asset_root: File.join(root_url, - Gitlab::Application.config.assets.prefix) + asset_root: Gitlab.config.gitlab.url, + asset_host: Gitlab::Application.config.asset_host } - result = HTML::Pipeline::Gitlab::MarkdownPipeline.call(text, - markdown_context) - text = result[:output].to_html(save_with: 0) + markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline + + result = markdown_pipeline.call(text, markdown_context) + saveoptions = 0 + if options[:xhtml] + saveoptions |= Nokogiri::XML::Node::SaveOptions::AS_XHTML + end + text = result[:output].to_html(save_with: saveoptions) sanitize_html(text) end @@ -88,9 +107,13 @@ module Gitlab allowed_attributes = ActionView::Base.sanitized_allowed_attributes allowed_tags = ActionView::Base.sanitized_allowed_tags - sanitize text.html_safe, - attributes: allowed_attributes + %w(id class), - tags: allowed_tags + %w(table tr td th) + text = sanitize text.html_safe, + attributes: allowed_attributes + %w(id class style), + tags: allowed_tags + %w(table tr td th) + if options[:parse_tasks] + text = parse_tasks(text) + end + text end private @@ -118,24 +141,26 @@ module Gitlab text end - NAME_STR = '[a-zA-Z][a-zA-Z0-9_\-\.]*' + NAME_STR = '[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*' PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})" REFERENCE_PATTERN = %r{ (?<prefix>\W)? # Prefix ( # Reference @(?<user>#{NAME_STR}) # User name + |~(?<label>\d+) # Label ID |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID |\$(?<snippet>\d+) # Snippet ID + |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit ) (?<suffix>\W)? # Suffix }x.freeze - TYPES = [:user, :issue, :merge_request, :snippet, :commit].freeze + TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze def parse_references(text, project = @project) # parse reference links @@ -194,18 +219,38 @@ module Gitlab def reference_user(identifier, project = @project, _ = nil) options = html_options.merge( - class: "gfm gfm-team_member #{html_options[:class]}" + class: "gfm gfm-project_member #{html_options[:class]}" ) if identifier == "all" - link_to("@all", project_url(project), options) - elsif user = User.find_by(username: identifier) - link_to("@#{identifier}", user_url(identifier), options) + link_to("@all", namespace_project_url(project.namespace, project), options) + elsif namespace = Namespace.find_by(path: identifier) + url = + if namespace.type == "Group" + group_url(identifier) + else + user_url(identifier) + end + + link_to("@#{identifier}", url, options) + end + end + + def reference_label(identifier, project = @project, _ = nil) + if label = project.labels.find_by(id: identifier) + options = html_options.merge( + class: "gfm gfm-label #{html_options[:class]}" + ) + link_to( + render_colored_label(label), + namespace_project_issues_path(project.namespace, project, label_name: label.name), + options + ) end end def reference_issue(identifier, project = @project, prefix_text = nil) - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? + if project.default_issues_tracker? if project.issue_exists? identifier url = url_for_issue(identifier, project) title = title_for_issue(identifier, project) @@ -217,10 +262,8 @@ module Gitlab link_to("#{prefix_text}##{identifier}", url, options) end else - config = Gitlab.config - external_issue_tracker = config.issues_tracker[project.issues_tracker] - if external_issue_tracker.present? - reference_external_issue(identifier, external_issue_tracker, project, + if project.external_issue_tracker.present? + reference_external_issue(identifier, project, prefix_text) end end @@ -233,7 +276,8 @@ module Gitlab title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}" ) - url = project_merge_request_url(project, merge_request) + url = namespace_project_merge_request_url(project.namespace, project, + merge_request) link_to("#{prefix_text}!#{identifier}", url, options) end end @@ -244,8 +288,11 @@ module Gitlab title: "Snippet: #{snippet.title}", class: "gfm gfm-snippet #{html_options[:class]}" ) - link_to("$#{identifier}", project_snippet_url(project, snippet), - options) + link_to( + "$#{identifier}", + namespace_project_snippet_url(project.namespace, project, snippet), + options + ) end end @@ -258,16 +305,40 @@ module Gitlab prefix_text = "#{prefix_text}@" if prefix_text link_to( "#{prefix_text}#{identifier}", - project_commit_url(project, commit), + namespace_project_commit_url(project.namespace, project, commit), + options + ) + end + end + + def reference_commit_range(identifier, project = @project, prefix_text = nil) + from_id, to_id = identifier.split(/\.{2,3}/, 2) + + inclusive = identifier !~ /\.{3}/ + from_id << "^" if inclusive + + if project.valid_repo? && + from = project.repository.commit(from_id) && + to = project.repository.commit(to_id) + + options = html_options.merge( + title: "Commits #{from_id} through #{to_id}", + class: "gfm gfm-commit_range #{html_options[:class]}" + ) + prefix_text = "#{prefix_text}@" if prefix_text + + link_to( + "#{prefix_text}#{identifier}", + namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id), options ) end end - def reference_external_issue(identifier, issue_tracker, project = @project, + def reference_external_issue(identifier, project = @project, prefix_text = nil) url = url_for_issue(identifier, project) - title = issue_tracker['title'] + title = project.external_issue_tracker.title options = html_options.merge( title: "Issue in #{title}", diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb new file mode 100644 index 00000000000..85ffa8aca68 --- /dev/null +++ b/lib/gitlab/middleware/static.rb @@ -0,0 +1,13 @@ +module Gitlab + module Middleware + class Static < ActionDispatch::Static + UPLOADS_REGEX = /\A\/uploads(\/|\z)/.freeze + + def call(env) + return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX + + super + end + end + end +end diff --git a/lib/gitlab/middleware/timeout.rb b/lib/gitlab/middleware/timeout.rb new file mode 100644 index 00000000000..015600392b9 --- /dev/null +++ b/lib/gitlab/middleware/timeout.rb @@ -0,0 +1,13 @@ +module Gitlab + module Middleware + class Timeout < Rack::Timeout + GRACK_REGEX = /[-\/\w\.]+\.git\//.freeze + + def call(env) + return @app.call(env) if env['PATH_INFO'] =~ GRACK_REGEX + + super + end + end + end +end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb new file mode 100644 index 00000000000..644dec45dca --- /dev/null +++ b/lib/gitlab/note_data_builder.rb @@ -0,0 +1,77 @@ +module Gitlab + class NoteDataBuilder + class << self + # Produce a hash of post-receive data + # + # For all notes: + # + # data = { + # object_kind: "note", + # user: { + # name: String, + # username: String, + # avatar_url: String + # } + # project_id: Integer, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # } + # object_attributes: { + # <hook data for note> + # } + # <note-specific data>: { + # } + # note-specific data is a hash with one of the following keys and contains + # the hook data for that type. + # - commit + # - issue + # - merge_request + # - snippet + # + def build(note, user) + project = note.project + data = build_base_data(project, user, note) + + if note.for_commit? + data[:commit] = build_data_for_commit(project, user, note) + elsif note.for_issue? + data[:issue] = note.noteable.hook_attrs + elsif note.for_merge_request? + data[:merge_request] = note.noteable.hook_attrs + elsif note.for_project_snippet? + data[:snippet] = note.noteable.hook_attrs + end + + data + end + + def build_base_data(project, user, note) + base_data = { + object_kind: "note", + user: user.hook_attrs, + project_id: project.id, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + }, + object_attributes: note.hook_attrs + } + + base_data[:object_attributes][:url] = + Gitlab::UrlBuilder.new(:note).build(note.id) + base_data + end + + def build_data_for_commit(project, user, note) + # commit_id is the SHA hash + commit = project.repository.commit(note.commit_id) + commit.hook_attrs(project) + end + end + end +end diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/oauth/auth_hash.rb index 0198f61f427..ce52beec78e 100644 --- a/lib/gitlab/oauth/auth_hash.rb +++ b/lib/gitlab/oauth/auth_hash.rb @@ -21,7 +21,7 @@ module Gitlab end def name - (info.name || full_name).to_s.force_encoding('utf-8') + (info.try(:name) || full_name).to_s.force_encoding('utf-8') end def full_name diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index b768eda185f..c023d275703 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -5,67 +5,92 @@ # module Gitlab module OAuth + class ForbiddenAction < StandardError; end + class User - class << self - attr_reader :auth_hash + attr_accessor :auth_hash, :gl_user - def find(auth_hash) - self.auth_hash = auth_hash - find_by_uid_and_provider - end + def initialize(auth_hash) + self.auth_hash = auth_hash + end - def create(auth_hash) - user = new(auth_hash) - user.save_and_trigger_callbacks - end + def persisted? + gl_user.try(:persisted?) + end - def model - ::User - end + def new? + !persisted? + end + + def valid? + gl_user.try(:valid?) + end + + def save + unauthorized_to_create unless gl_user - def auth_hash=(auth_hash) - @auth_hash = AuthHash.new(auth_hash) + if needs_blocking? + gl_user.save! + gl_user.block + else + gl_user.save! end - protected - def find_by_uid_and_provider - model.where(provider: auth_hash.provider, extern_uid: auth_hash.uid).last + log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" + gl_user + rescue ActiveRecord::RecordInvalid => e + log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}" + return self, e.record.errors + end + + def gl_user + @user ||= find_by_uid_and_provider + + if signup_enabled? + @user ||= build_new_user end + + @user end - # Instance methods - attr_accessor :auth_hash, :user + protected - def initialize(auth_hash) - self.auth_hash = auth_hash - self.user = self.class.model.new(user_attributes) - user.skip_confirmation! + def needs_blocking? + new? && block_after_signup? + end + + def signup_enabled? + Gitlab.config.omniauth.allow_single_sign_on + end + + def block_after_signup? + Gitlab.config.omniauth.block_auto_created_users end def auth_hash=(auth_hash) @auth_hash = AuthHash.new(auth_hash) end - def save_and_trigger_callbacks - user.save! - log.info "(OAuth) Creating user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" - user.block if needs_blocking? + def find_by_uid_and_provider + identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid) + identity && identity.user + end + def build_new_user + user = ::User.new(user_attributes) + user.skip_confirmation! + user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) user - rescue ActiveRecord::RecordInvalid => e - log.info "(OAuth) Email #{e.record.errors[:email]}. Username #{e.record.errors[:username]}" - return nil, e.record.errors end def user_attributes { - extern_uid: auth_hash.uid, - provider: auth_hash.provider, - name: auth_hash.name, - username: auth_hash.username, - email: auth_hash.email, - password: auth_hash.password, - password_confirmation: auth_hash.password, + name: auth_hash.name, + username: ::User.clean_username(auth_hash.username), + email: auth_hash.email, + password: auth_hash.password, + password_confirmation: auth_hash.password, + password_automatically_set: true } end @@ -73,12 +98,8 @@ module Gitlab Gitlab::AppLogger end - def raise_error(message) - raise OmniAuth::Error, "(OAuth) " + message - end - - def needs_blocking? - Gitlab.config.omniauth['block_auto_created_users'] + def unauthorized_to_create + raise ForbiddenAction.new("Unauthorized to create user, signup disabled for #{auth_hash.provider}") end end end diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index e2fbafb3899..fea4d2d55d2 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -21,6 +21,9 @@ module Gitlab @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # We are not using stdin so we should close it, in case the command we + # are running waits for input. + stdin.close @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus diff --git a/lib/gitlab/production_logger.rb b/lib/gitlab/production_logger.rb new file mode 100644 index 00000000000..89ce7144b1b --- /dev/null +++ b/lib/gitlab/production_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class ProductionLogger < Gitlab::Logger + def self.file_name_noext + 'production' + end + end +end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb new file mode 100644 index 00000000000..ea9012b8844 --- /dev/null +++ b/lib/gitlab/push_data_builder.rb @@ -0,0 +1,89 @@ +module Gitlab + class PushDataBuilder + class << self + # Produce a hash of post-receive data + # + # data = { + # before: String, + # after: String, + # ref: String, + # user_id: String, + # user_name: String, + # user_email: String + # project_id: String, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # }, + # commits: Array, + # total_commits_count: Fixnum + # } + # + def build(project, user, oldrev, newrev, ref, commits = []) + # Total commits count + commits_count = commits.size + + # Get latest 20 commits ASC + commits_limited = commits.last(20) + + type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" + # Hash to be passed as post_receive_data + data = { + object_kind: type, + before: oldrev, + after: newrev, + ref: ref, + checkout_sha: checkout_sha(project.repository, newrev, ref), + user_id: user.id, + user_name: user.name, + user_email: user.email, + project_id: project.id, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + git_http_url: project.http_url_to_repo, + git_ssh_url: project.ssh_url_to_repo, + visibility_level: project.visibility_level + }, + commits: [], + total_commits_count: commits_count + } + + # For performance purposes maximum 20 latest commits + # will be passed as post receive hook data. + commits_limited.each do |commit| + data[:commits] << commit.hook_attrs(project) + end + + data[:commits] = "" if data[:commits].count == 0 + data + end + + # This method provide a sample data generated with + # existing project and commits to test web hooks + def build_sample(project, user) + commits = project.repository.commits(project.default_branch, nil, 3) + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" + build(project, user, commits.last.id, commits.first.id, ref, commits) + end + + def checkout_sha(repository, newrev, ref) + if newrev != Gitlab::Git::BLANK_SHA && Gitlab::Git.tag_ref?(ref) + tag_name = Gitlab::Git.ref_name(ref) + tag = repository.find_tag(tag_name) + + if tag + commit = repository.commit(tag.target) + commit.try(:sha) + end + else + newrev + end + end + end + end +end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 99165950aef..5b9772de168 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,12 +1,13 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :users, :issues, :merge_requests, :snippets, :commits + attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits, :commit_ranges include Markdown def initialize - @users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], [] + @users, @labels, @issues, @merge_requests, @snippets, @commits, @commit_ranges = + [], [], [], [], [], [], [] end def analyze(string, project) @@ -22,6 +23,12 @@ module Gitlab end.reject(&:nil?) end + def labels_for(project = nil) + labels.map do |entry| + project.labels.where(id: entry[:id]).first + end.reject(&:nil?) + end + def issues_for(project = nil) issues.map do |entry| if should_lookup?(project, entry[:project]) @@ -53,6 +60,16 @@ module Gitlab end.reject(&:nil?) end + def commit_ranges_for(project = nil) + commit_ranges.map do |entry| + repo = entry[:project].repository if entry[:project] + if repo && should_lookup?(project, entry[:project]) + from_id, to_id = entry[:id].split(/\.{2,3}/, 2) + [repo.commit(from_id), repo.commit(to_id)] + end + end.reject(&:nil?) + end + private def reference_link(type, identifier, project, _) @@ -64,7 +81,7 @@ module Gitlab if entry_project.nil? false else - project.nil? || project.id == entry_project.id + project.nil? || entry_project.default_issues_tracker? end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 4b8038843b0..cf6e260f257 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -11,7 +11,7 @@ module Gitlab end def project_name_regex - /\A[a-zA-Z0-9_][a-zA-Z0-9_\-\. ]*\z/ + /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/ end def project_regex_message @@ -67,8 +67,7 @@ module Gitlab def default_regex_message "can contain only letters, digits, '_', '-' and '.'. " \ - "It must start with letter, digit or '_', optionally preceeded by '.'. " \ - "It must not end in '.git'." + "Cannot start with '-' or end in '.git'" \ end def default_regex diff --git a/lib/gitlab/satellite/action.rb b/lib/gitlab/satellite/action.rb index be45cb5c98e..4890ccf21e6 100644 --- a/lib/gitlab/satellite/action.rb +++ b/lib/gitlab/satellite/action.rb @@ -44,7 +44,7 @@ module Gitlab end def default_options(options = {}) - {raise: true, timeout: true}.merge(options) + { raise: true, timeout: true }.merge(options) end def handle_exception(exception) diff --git a/lib/gitlab/satellite/files/delete_file_action.rb b/lib/gitlab/satellite/files/delete_file_action.rb index 30462999aa3..0d37b9dea85 100644 --- a/lib/gitlab/satellite/files/delete_file_action.rb +++ b/lib/gitlab/satellite/files/delete_file_action.rb @@ -13,7 +13,7 @@ module Gitlab prepare_satellite!(repo) # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") # update the file in the satellite's working dir file_path_in_satellite = File.join(repo.working_dir, file_path) @@ -36,7 +36,7 @@ module Gitlab # push commit back to bare repo # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) + repo.git.push({ raise: true, timeout: true }, :origin, ref) # everything worked true diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb index cbdf70f7d12..3cb9c0b5ecb 100644 --- a/lib/gitlab/satellite/files/edit_file_action.rb +++ b/lib/gitlab/satellite/files/edit_file_action.rb @@ -10,12 +10,16 @@ module Gitlab # Returns false if committing the change fails # Returns false if pushing from the satellite to bare repo failed or was rejected # Returns true otherwise - def commit!(content, commit_message, encoding) + def commit!(content, commit_message, encoding, new_branch = nil) in_locked_and_timed_satellite do |repo| prepare_satellite!(repo) # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + begin + repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") + rescue Grit::Git::CommandFailed => ex + log_and_raise(CheckoutFailed, ex.message) + end # update the file in the satellite's working dir file_path_in_satellite = File.join(repo.working_dir, file_path) @@ -31,19 +35,33 @@ module Gitlab # commit the changes # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + begin + repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + rescue Grit::Git::CommandFailed => ex + log_and_raise(CommitFailed, ex.message) + end + target_branch = new_branch.present? ? "#{ref}:#{new_branch}" : ref + # push commit back to bare repo # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) + begin + repo.git.push({ raise: true, timeout: true }, :origin, target_branch) + rescue Grit::Git::CommandFailed => ex + log_and_raise(PushFailed, ex.message) + end # everything worked true end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false + end + + private + + def log_and_raise(errorClass, message) + Gitlab::GitLogger.error(message) + raise(errorClass, message) end end end diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb index 15e9b7a6f77..724dfa0d042 100644 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ b/lib/gitlab/satellite/files/new_file_action.rb @@ -9,12 +9,19 @@ module Gitlab # Returns false if committing the change fails # Returns false if pushing from the satellite to bare repo failed or was rejected # Returns true otherwise - def commit!(content, commit_message, encoding) + def commit!(content, commit_message, encoding, new_branch = nil) in_locked_and_timed_satellite do |repo| prepare_satellite!(repo) # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + current_ref = + if @project.empty_repo? + # skip this step if we want to add first file to empty repo + Satellite::PARKING_BRANCH + else + repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") + ref + end file_path_in_satellite = File.join(repo.working_dir, file_path) dir_name_in_satellite = File.dirname(file_path_in_satellite) @@ -38,10 +45,15 @@ module Gitlab # will raise CommandFailed when commit fails repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + target_branch = if new_branch.present? && !@project.empty_repo? + "#{ref}:#{new_branch}" + else + "#{current_ref}:#{ref}" + end # push commit back to bare repo # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) + repo.git.push({ raise: true, timeout: true }, :origin, target_branch) # everything worked true diff --git a/lib/gitlab/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb index e9141f735aa..25122666f5e 100644 --- a/lib/gitlab/satellite/merge_action.rb +++ b/lib/gitlab/satellite/merge_action.rb @@ -86,7 +86,7 @@ module Gitlab in_locked_and_timed_satellite do |merge_repo| prepare_satellite!(merge_repo) update_satellite_source_and_target!(merge_repo) - patch = merge_repo.git.format_patch(default_options({stdout: true}), "origin/#{merge_request.target_branch}..source/#{merge_request.source_branch}") + patch = merge_repo.git.format_patch(default_options({ stdout: true }), "origin/#{merge_request.target_branch}..source/#{merge_request.source_branch}") end rescue Grit::Git::CommandFailed => ex handle_exception(ex) @@ -128,7 +128,7 @@ module Gitlab # merge the source branch into the satellite # will raise CommandFailed when merge fails - repo.git.merge(default_options({no_ff: true}), "-m#{message}", "source/#{merge_request.source_branch}") + repo.git.merge(default_options({ no_ff: true }), "-m#{message}", "source/#{merge_request.source_branch}") rescue Grit::Git::CommandFailed => ex handle_exception(ex) end @@ -137,7 +137,7 @@ module Gitlab def update_satellite_source_and_target!(repo) repo.remote_add('source', merge_request.source_project.repository.path_to_repo) repo.remote_fetch('source') - repo.git.checkout(default_options({b: true}), merge_request.target_branch, "origin/#{merge_request.target_branch}") + repo.git.checkout(default_options({ b: true }), merge_request.target_branch, "origin/#{merge_request.target_branch}") rescue Grit::Git::CommandFailed => ex handle_exception(ex) end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index 1de84309d15..70125d539da 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -1,5 +1,9 @@ module Gitlab module Satellite + class CheckoutFailed < StandardError; end + class CommitFailed < StandardError; end + class PushFailed < StandardError; end + class Satellite include Gitlab::Popen @@ -98,13 +102,13 @@ module Gitlab if heads.include? PARKING_BRANCH repo.git.checkout({}, PARKING_BRANCH) else - repo.git.checkout(default_options({b: true}), PARKING_BRANCH) + repo.git.checkout(default_options({ b: true }), PARKING_BRANCH) end # remove the parking branch from the list of heads ... heads.delete(PARKING_BRANCH) # ... and delete all others - heads.each { |head| repo.git.branch(default_options({D: true}), head) } + heads.each { |head| repo.git.branch(default_options({ D: true }), head) } end # Deletes all remotes except origin @@ -126,7 +130,7 @@ module Gitlab end def default_options(options = {}) - {raise: true, timeout: true}.merge(options) + { raise: true, timeout: true }.merge(options) end # Create directory for storing diff --git a/lib/gitlab/sidekiq_logger.rb b/lib/gitlab/sidekiq_logger.rb new file mode 100644 index 00000000000..c1dab87a432 --- /dev/null +++ b/lib/gitlab/sidekiq_logger.rb @@ -0,0 +1,7 @@ +module Gitlab + class SidekiqLogger < Gitlab::Logger + def self.file_name_noext + 'sidekiq' + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb new file mode 100644 index 00000000000..0f2db50e98c --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -0,0 +1,53 @@ +module Gitlab + module SidekiqMiddleware + class MemoryKiller + # Default the RSS limit to 0, meaning the MemoryKiller is disabled + MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i + # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit + GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i + # Wait 30 seconds for running jobs to finish during graceful shutdown + SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i + + # Create a mutex used to ensure there will be only one thread waiting to + # shut Sidekiq down + MUTEX = Mutex.new + + def call(worker, job, queue) + yield + current_rss = get_rss + + return unless MAX_RSS > 0 && current_rss > MAX_RSS + + Thread.new do + # Return if another thread is already waiting to shut Sidekiq down + return unless MUTEX.try_lock + + Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ + "#{MAX_RSS}" + Sidekiq.logger.warn "spawned thread that will shut down PID "\ + "#{Process.pid} in #{GRACE_TIME} seconds" + sleep(GRACE_TIME) + + Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" + Process.kill('SIGUSR1', Process.pid) + + Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\ + "SIGTERM to PID #{Process.pid}" + sleep(SHUTDOWN_WAIT) + + Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}" + Process.kill('SIGTERM', Process.pid) + end + end + + private + + def get_rss + output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{Process.pid})) + return 0 unless status.zero? + + output.to_i + end + end + end +end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index b7c50cb734d..9799e54de5d 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -5,6 +5,7 @@ module Gitlab MODERN = 3 unless const_defined?(:MODERN) GRAY = 4 unless const_defined?(:GRAY) COLOR = 5 unless const_defined?(:COLOR) + BLUE = 6 unless const_defined?(:BLUE) def self.css_class_by_id(id) themes = { @@ -12,12 +13,27 @@ module Gitlab MARS => "ui_mars", MODERN => "ui_modern", GRAY => "ui_gray", - COLOR => "ui_color" + COLOR => "ui_color", + BLUE => "ui_blue" } id ||= Gitlab.config.gitlab.default_theme return themes[id] end + + def self.type_css_class_by_id(id) + types = { + BASIC => 'light_theme', + MARS => 'dark_theme', + MODERN => 'dark_theme', + GRAY => 'dark_theme', + COLOR => 'dark_theme' + } + + id ||= Gitlab.config.gitlab.default_theme + + types[id] + end end end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index 74b049b5143..0570c2fbeb5 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -62,7 +62,7 @@ module Gitlab end def env - {'RAILS_ENV' => 'production'} + { 'RAILS_ENV' => 'production' } end def upgrade diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index de7e0404086..11b0d44f340 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,6 +1,7 @@ module Gitlab class UrlBuilder include Rails.application.routes.url_helpers + include GitlabRoutingHelper def initialize(type) @type = type @@ -9,17 +10,51 @@ module Gitlab def build(id) case @type when :issue - issue_url(id) + build_issue_url(id) + when :merge_request + build_merge_request_url(id) + when :note + build_note_url(id) + end end private - def issue_url(id) + def build_issue_url(id) issue = Issue.find(id) - project_issue_url(id: issue.iid, - project_id: issue.project, - host: Settings.gitlab['url']) + issue_url(issue, host: Gitlab.config.gitlab['url']) + end + + def build_merge_request_url(id) + merge_request = MergeRequest.find(id) + merge_request_url(merge_request, host: Gitlab.config.gitlab['url']) + end + + def build_note_url(id) + note = Note.find(id) + if note.for_commit? + namespace_project_commit_url(namespace_id: note.project.namespace, + id: note.commit_id, + project_id: note.project, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_issue? + issue = Issue.find(note.noteable_id) + issue_url(issue, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_merge_request? + merge_request = MergeRequest.find(note.noteable_id) + merge_request_url(merge_request, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_project_snippet? + snippet = Snippet.find(note.noteable_id) + project_snippet_url(snippet, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + end end end end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb new file mode 100644 index 00000000000..bd184c27187 --- /dev/null +++ b/lib/gitlab/utils.rb @@ -0,0 +1,13 @@ +module Gitlab + module Utils + extend self + + # Run system command without outputting to stdout. + # + # @param cmd [Array<String>] + # @return [Boolean] + def system_silent(cmd) + Popen::popen(cmd).last.zero? + end + end +end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index d0b6cde3c7e..1851e76067c 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -5,6 +5,8 @@ # module Gitlab module VisibilityLevel + extend CurrentSettings + PRIVATE = 0 unless const_defined?(:PRIVATE) INTERNAL = 10 unless const_defined?(:INTERNAL) PUBLIC = 20 unless const_defined?(:PUBLIC) @@ -23,21 +25,21 @@ module Gitlab end def allowed_for?(user, level) - user.is_admin? || allowed_level?(level) + user.is_admin? || allowed_level?(level.to_i) end - # Level can be a string `"public"` or a value `20`, first check if valid, - # then check if the corresponding string appears in the config + # Return true if the specified level is allowed for the current user. + # Level should be a numeric value, e.g. `20`. def allowed_level?(level) - if options.has_key?(level.to_s) - non_restricted_level?(level) - elsif options.has_value?(level.to_i) - non_restricted_level?(options.key(level.to_i).downcase) - end + valid_level?(level) && non_restricted_level?(level) end def non_restricted_level?(level) - ! Gitlab.config.gitlab.restricted_visibility_levels.include?(level) + ! current_application_settings.restricted_visibility_levels.include?(level) + end + + def valid_level?(level) + options.has_value?(level) end end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index c3378d6a18f..10efff2ae9f 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -3,30 +3,47 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML attr_reader :template alias_method :h, :template - def initialize(template, options = {}) + def initialize(template, color_scheme, options = {}) @template = template + @color_scheme = color_scheme @project = @template.instance_variable_get("@project") @options = options.dup super options end + def preprocess(full_document) + # Redcarpet doesn't allow SMB links when `safe_links_only` is enabled. + # FTP links are allowed, so we trick Redcarpet. + full_document.gsub("smb://", "ftp://smb:") + end + + # If project has issue number 39, apostrophe will be linked in + # regular text to the issue as Redcarpet will convert apostrophe to + # #39; + # We replace apostrophe with right single quote before Redcarpet + # does the processing and put the apostrophe back in postprocessing. + # This only influences regular text, code blocks are untouched. + def normal_text(text) + return text unless text.present? + text.gsub("'", "’") + end + + # Stolen from Rugments::Plugins::Redcarpet as this module is not required + # from Rugments's gem root. def block_code(code, language) - # New lines are placed to fix an rendering issue - # with code wrapped inside <h1> tag for next case: - # - # # Title kinda h1 - # - # ruby code here - # - <<-HTML - -<div class="highlighted-data #{h.user_color_scheme_class}"> - <div class="highlight"> - <pre><code class="#{language}">#{h.send(:html_escape, code)}</code></pre> - </div> -</div> - - HTML + lexer = Rugments::Lexer.find_fancy(language, code) || Rugments::Lexers::PlainText + + # XXX HACK: Redcarpet strips hard tabs out of code blocks, + # so we assume you're not using leading spaces that aren't tabs, + # and just replace them here. + if lexer.tag == 'make' + code.gsub! /^ /, "\t" + end + + formatter = Rugments::Formatters::HTML.new( + cssclass: "code highlight #{@color_scheme} #{lexer.tag}" + ) + formatter.format(lexer.lex(code)) end def link(link, title, content) @@ -44,13 +61,12 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end def postprocess(full_document) + full_document.gsub!("ftp://smb:", "smb://") + + full_document.gsub!("’", "'") unless @template.instance_variable_get("@project_wiki") || @project.nil? full_document = h.create_relative_links(full_document) end - if @options[:parse_tasks] - h.gfm_with_tasks(full_document) - else - h.gfm(full_document) - end + h.gfm_with_options(full_document, @options) end end diff --git a/lib/repository_cache.rb b/lib/repository_cache.rb new file mode 100644 index 00000000000..fa016a170cd --- /dev/null +++ b/lib/repository_cache.rb @@ -0,0 +1,21 @@ +# Interface to the Redis-backed cache store used by the Repository model +class RepositoryCache + attr_reader :namespace, :backend + + def initialize(namespace, backend = Rails.cache) + @namespace = namespace + @backend = backend + end + + def cache_key(type) + "#{type}:#{namespace}" + end + + def expire(key) + backend.delete(cache_key(key)) + end + + def fetch(key, &block) + backend.fetch(cache_key(key), &block) + end +end diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh index 4684957233a..adea4c7a747 100755 --- a/lib/support/deploy/deploy.sh +++ b/lib/support/deploy/deploy.sh @@ -4,7 +4,7 @@ # If any command return non-zero status - stop deploy set -e -echo 'Deploy: Stoping sidekiq..' +echo 'Deploy: Stopping sidekiq..' cd /home/git/gitlab/ && sudo -u git -H bundle exec rake sidekiq:stop RAILS_ENV=production echo 'Deploy: Show deploy index page' diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 49a68c62293..62a4276536c 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -1,5 +1,5 @@ ## GitLab -## Maintainer: @randx +## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller, DouweM ## ## Lines starting with two hashes (##) are comments with information. ## Lines starting with one hash (#) are configuration parameters that can be uncommented. @@ -15,7 +15,7 @@ ## - installing an old version of Nginx with the chunkin module [2] compiled in, or ## - using a newer version of Nginx. ## -## At the time of writing we do not know if either of these theoretical solutions works. +## At the time of writing we do not know if either of these theoretical solutions works. ## As a workaround users can use Git over SSH to push large files. ## ## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 @@ -26,6 +26,7 @@ ## configuration ## ################################### ## +## See installation.md#using-https for additional HTTPS configuration details. upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; @@ -33,7 +34,8 @@ upstream gitlab { ## Normal HTTP host server { - listen *:80 default_server; + listen 0.0.0.0:80 default_server; + listen [::]:80 default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; @@ -42,6 +44,8 @@ server { ## Or if you want to accept large git objects over http client_max_body_size 20m; + ## See app/controllers/application_controller.rb for headers set + ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; @@ -52,6 +56,27 @@ server { try_files $uri $uri/index.html $uri.html @gitlab; } + ## We route uploads through GitLab to prevent XSS and enforce access control. + location /uploads/ { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + # gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + + proxy_pass http://gitlab; + } + ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index d3fb467ef27..2aefc944698 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -1,5 +1,5 @@ ## GitLab -## Contributors: randx, yin8086, sashkab, orkoden, axilleas +## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller, DouweM ## ## Modified from nginx http version ## Modified from http://blog.phusion.nl/2012/04/21/tutorial-setting-up-gitlab-on-debian-6/ @@ -26,9 +26,8 @@ ## [1] https://github.com/agentzh/chunkin-nginx-module#status ## [2] https://github.com/agentzh/chunkin-nginx-module ## -## ################################### -## SSL configuration ## +## configuration ## ################################### ## ## See installation.md#using-https for additional HTTPS configuration details. @@ -37,22 +36,24 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } -## Normal HTTP host +## Redirects all HTTP traffic to the HTTPS host server { - listen *:80 default_server; + listen 0.0.0.0:80; + listen [::]:80 ipv6only=on default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice - - ## Redirects all traffic to the HTTPS host - root /nowhere; ## root doesn't have to be a valid path since we are redirecting - rewrite ^ https://$server_name$request_uri? permanent; + return 301 https://$server_name$request_uri; + access_log /var/log/nginx/gitlab_access.log; + error_log /var/log/nginx/gitlab_error.log; } + ## HTTPS host server { - listen 443 ssl; + listen 0.0.0.0:443 ssl; + listen [::]:443 ipv6only=on ssl default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com - server_tokens off; + server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; ## Increase this if you want to upload large attachments @@ -60,24 +61,19 @@ server { client_max_body_size 20m; ## Strong SSL Security - ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ ssl on; ssl_certificate /etc/nginx/ssl/gitlab.crt; ssl_certificate_key /etc/nginx/ssl/gitlab.key; # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs - ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4'; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; - ssl_prefer_server_ciphers on; - - ## [WARNING] The following header states that the browser should only communicate - ## with your server over a secure connection for the next 24 months. - add_header Strict-Transport-Security max-age=63072000; - add_header X-Frame-Options SAMEORIGIN; - add_header X-Content-Type-Options nosniff; + ## See app/controllers/application_controller.rb for headers set ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. ## Replace with your ssl_trusted_certificate. For more info see: @@ -88,11 +84,10 @@ server { # ssl_stapling_verify on; # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired - # resolver_timeout 10s; + # resolver_timeout 5s; ## [Optional] Generate a stronger DHE parameter: - ## cd /etc/ssl/certs - ## sudo openssl dhparam -out dhparam.pem 4096 + ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 ## # ssl_dhparam /etc/ssl/certs/dhparam.pem; @@ -106,6 +101,28 @@ server { try_files $uri $uri/index.html $uri.html @gitlab; } + ## We route uploads through GitLab to prevent XSS and enforce access control. + location /uploads/ { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + + proxy_pass http://gitlab; + } + ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake new file mode 100644 index 00000000000..abcb5f0ae46 --- /dev/null +++ b/lib/tasks/brakeman.rake @@ -0,0 +1,9 @@ +desc 'Security check via brakeman' +task :brakeman do + if system("brakeman --skip-files lib/backup/repository.rb -w3 -z") + exit 0 + else + puts 'Security check failed' + exit 1 + end +end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 2eff1260b61..0230fbb010b 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -6,6 +6,7 @@ namespace :gitlab do desc "GITLAB | Create a backup of the GitLab system" task create: :environment do warn_user_is_not_gitlab + configure_cron_mode Rake::Task["gitlab:backup:db:create"].invoke Rake::Task["gitlab:backup:repo:create"].invoke @@ -21,6 +22,7 @@ namespace :gitlab do desc "GITLAB | Restore a previously created backup" task restore: :environment do warn_user_is_not_gitlab + configure_cron_mode backup = Backup::Manager.new backup.unpack @@ -35,43 +37,54 @@ namespace :gitlab do namespace :repo do task create: :environment do - puts "Dumping repositories ...".blue + $progress.puts "Dumping repositories ...".blue Backup::Repository.new.dump - puts "done".green + $progress.puts "done".green end task restore: :environment do - puts "Restoring repositories ...".blue + $progress.puts "Restoring repositories ...".blue Backup::Repository.new.restore - puts "done".green + $progress.puts "done".green end end namespace :db do task create: :environment do - puts "Dumping database ... ".blue + $progress.puts "Dumping database ... ".blue Backup::Database.new.dump - puts "done".green + $progress.puts "done".green end task restore: :environment do - puts "Restoring database ... ".blue + $progress.puts "Restoring database ... ".blue Backup::Database.new.restore - puts "done".green + $progress.puts "done".green end end namespace :uploads do task create: :environment do - puts "Dumping uploads ... ".blue + $progress.puts "Dumping uploads ... ".blue Backup::Uploads.new.dump - puts "done".green + $progress.puts "done".green end task restore: :environment do - puts "Restoring uploads ... ".blue + $progress.puts "Restoring uploads ... ".blue Backup::Uploads.new.restore - puts "done".green + $progress.puts "done".green + end + end + + def configure_cron_mode + if ENV['CRON'] + # We need an object we can say 'puts' and 'print' to; let's use a + # StringIO. + require 'stringio' + $progress = StringIO.new + else + $progress = $stdout end end end # namespace end: backup diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 9ec368254ac..d791b7155f9 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -29,6 +29,7 @@ namespace :gitlab do check_redis_version check_ruby_version check_git_version + check_active_users finished_checking "GitLab" end @@ -328,16 +329,20 @@ namespace :gitlab do if correct_options.all? puts "yes".green else - puts "no".red - try_fixing_it( - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") - ) - for_more_information( - see_installation_guide_section "GitLab" - ) - fix_and_rerun + print "Trying to fix Git error automatically. ..." + if auto_fix_git_config(options) + puts "Success".green + else + puts "Failed".red + try_fixing_it( + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + end end end end @@ -574,24 +579,16 @@ namespace :gitlab do Gitlab::Shell.new.version end - def required_gitlab_shell_version - File.read(File.join(Rails.root, "GITLAB_SHELL_VERSION")).strip - end - def gitlab_shell_major_version - required_gitlab_shell_version.split(".")[0].to_i + Gitlab::Shell.version_required.split('.')[0].to_i end def gitlab_shell_minor_version - required_gitlab_shell_version.split(".")[1].to_i + Gitlab::Shell.version_required.split('.')[1].to_i end def gitlab_shell_patch_version - required_gitlab_shell_version.split(".")[2].to_i - end - - def has_gitlab_shell3? - gitlab_shell_version.try(:start_with?, "v3.") + Gitlab::Shell.version_required.split('.')[2].to_i end end @@ -664,7 +661,7 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "LDAP" - if ldap_config.enabled + if Gitlab::LDAP::Config.enabled? print_users(args.limit) else puts 'LDAP is disabled in config/gitlab.yml' @@ -675,39 +672,19 @@ namespace :gitlab do def print_users(limit) puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" - ldap.search(attributes: attributes, filter: filter, size: limit, return_result: false) do |entry| - puts "DN: #{entry.dn}\t#{ldap_config.uid}: #{entry[ldap_config.uid]}" - end - end - - def attributes - [ldap_config.uid] - end - def filter - uid_filter = Net::LDAP::Filter.present?(ldap_config.uid) - if user_filter - Net::LDAP::Filter.join(uid_filter, user_filter) - else - uid_filter - end - end + servers = Gitlab::LDAP::Config.providers - def user_filter - if ldap_config['user_filter'] && ldap_config.user_filter.present? - Net::LDAP::Filter.construct(ldap_config.user_filter) - else - nil + servers.each do |server| + puts "Server: #{server}" + Gitlab::LDAP::Adapter.open(server) do |adapter| + users = adapter.users(adapter.config.uid, '*', 100) + users.each do |user| + puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + end + end end end - - def ldap - @ldap ||= OmniAuth::LDAP::Adaptor.new(ldap_config).connection - end - - def ldap_config - @ldap_config ||= Gitlab.config.ldap - end end # Helper methods @@ -809,19 +786,23 @@ namespace :gitlab do end end + def check_active_users + puts "Active users: #{User.active.count}" + end + def omnibus_gitlab? Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails' end def sanitized_message(project) - if sanitize + if should_sanitize? "#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... " else "#{project.name_with_namespace.yellow} ... " end end - def sanitize + def should_sanitize? if ENV['SANITIZE'] == "true" true else @@ -829,3 +810,4 @@ namespace :gitlab do end end end + diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 63dcdc52370..189ad6090a4 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -92,11 +92,11 @@ namespace :gitlab do User.ldap.each do |ldap_user| print "#{ldap_user.name} (#{ldap_user.extern_uid}) ..." - if Gitlab::LDAP::Access.open { |access| access.allowed?(ldap_user) } + if Gitlab::LDAP::Access.allowed?(ldap_user) puts " [OK]".green else if block_flag - ldap_user.block! + ldap_user.block! unless ldap_user.blocked? puts " [BLOCKED]".red else puts " [NOT IN LDAP]".yellow diff --git a/lib/tasks/gitlab/db/drop_all_postgres_sequences.rake b/lib/tasks/gitlab/db/drop_all_postgres_sequences.rake new file mode 100644 index 00000000000..e9cf0a9b5e8 --- /dev/null +++ b/lib/tasks/gitlab/db/drop_all_postgres_sequences.rake @@ -0,0 +1,10 @@ +namespace :gitlab do + namespace :db do + task drop_all_postgres_sequences: :environment do + connection = ActiveRecord::Base.connection + connection.execute("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';").each do |sequence| + connection.execute("DROP SEQUENCE #{sequence['relname']}") + end + end + end +end diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index b6ed874e11a..20abb2fa500 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -15,26 +15,17 @@ namespace :gitlab do git_base_path = Gitlab.config.gitlab_shell.repos_path repos_to_import = Dir.glob(git_base_path + '/**/*.git') - namespaces = Namespace.pluck(:path) - repos_to_import.each do |repo_path| # strip repo base path repo_path[0..git_base_path.length] = '' path = repo_path.sub(/\.git$/, '') - name = File.basename path - group_name = File.dirname path + group_name, name = File.split(path) group_name = nil if group_name == '.' - # Skip if group or user - if namespaces.include?(name) - puts "Skipping #{project.name} due to namespace conflict with group or user".yellow - next - end - puts "Processing #{repo_path}".yellow - if path =~ /.wiki\Z/ + if path.end_with?('.wiki') puts " * Skipping wiki repo" next end @@ -53,9 +44,9 @@ namespace :gitlab do # find group namespace if group_name - group = Group.find_by(path: group_name) + group = Namespace.find_by(path: group_name) # create group namespace - if !group + unless group group = Group.new(:name => group_name) group.path = group_name group.owner = user @@ -75,6 +66,7 @@ namespace :gitlab do puts " * Created #{project.name} (#{repo_path})".green else puts " * Failed trying to create #{project.name} (#{repo_path})".red + puts " Validation Errors: #{project.errors.messages}".red end end end diff --git a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake new file mode 100644 index 00000000000..102c6ae55d5 --- /dev/null +++ b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake @@ -0,0 +1,73 @@ +require "#{Rails.root}/app/helpers/emails_helper" +require 'action_view/helpers' +extend ActionView::Helpers + +include ActionView::Context +include EmailsHelper + +namespace :gitlab do + desc "Email google whitelisting email with example email for actions in inbox" + task mail_google_schema_whitelisting: :environment do + subject = "Rails | Implemented feature" + url = "#{Gitlab.config.gitlab.url}/base/rails-project/issues/#{rand(1..100)}#note_#{rand(10..1000)}" + schema = email_action(url) + body = email_template(schema, url) + mail = Notify.test_email("schema.whitelisting+sample@gmail.com", subject, body.html_safe) + if send_now + mail.deliver + else + puts "WOULD SEND:" + end + puts mail + end + + def email_template(schema, url) + "<html lang='en'> + <head> + <meta content='text/html; charset=utf-8' http-equiv='Content-Type'> + <title> + GitLab + </title> + </meta> + </head> + <style> + img { + max-width: 100%; + height: auto; + } + p.details { + font-style:italic; + color:#777 + } + .footer p { + font-size:small; + color:#777 + } + </style> + <body> + <div class='content'> + <div> + <p>I like it :+1: </p> + </div> + </div> + + <div class='footer' style='margin-top: 10px;'> + <p> + <br> + <a href=\"#{url}\">View it on GitLab</a> + You're receiving this notification because you are a member of the Base / Rails Project project team. + #{schema} + </p> + </div> + </body> + </html>" + end + + def send_now + if ENV['SEND'] == "true" + true + else + false + end + end +end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index a8f26a7c029..9af93300e08 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -4,28 +4,32 @@ namespace :gitlab do task :install, [:tag, :repo] => :environment do |t, args| warn_user_is_not_gitlab - default_version = File.read(File.join(Rails.root, "GITLAB_SHELL_VERSION")).strip + default_version = Gitlab::Shell.version_required args.with_defaults(tag: 'v' + default_version, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git") - user = Settings.gitlab.user - home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Settings.gitlab.user_home - gitlab_url = Settings.gitlab.url + user = Gitlab.config.gitlab.user + home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home + gitlab_url = Gitlab.config.gitlab.url # gitlab-shell requires a / at the end of the url - gitlab_url += "/" unless gitlab_url.match(/\/$/) + gitlab_url += '/' unless gitlab_url.end_with?('/') repos_path = Gitlab.config.gitlab_shell.repos_path target_dir = Gitlab.config.gitlab_shell.path # Clone if needed unless File.directory?(target_dir) - sh "git clone '#{args.repo}' '#{target_dir}'" + system(*%W(git clone -- #{args.repo} #{target_dir})) end # Make sure we're on the right tag Dir.chdir(target_dir) do # First try to checkout without fetching # to avoid stalling tests if the Internet is down. - reset = "git reset --hard $(git describe #{args.tag} || git describe origin/#{args.tag})" - sh "#{reset} || git fetch origin && #{reset}" + reseted = reset_to_commit(args) + + unless reseted + system(*%W(git fetch origin)) + reset_to_commit(args) + end config = { user: user, @@ -54,7 +58,7 @@ namespace :gitlab do File.open("config.yml", "w+") {|f| f.puts config.to_yaml} # Launch installation process - sh "bin/install" + system(*%W(bin/install)) end # Required for debian packaging with PKGR: Setup .ssh/environment with @@ -76,7 +80,7 @@ namespace :gitlab do desc "GITLAB | Build missing projects" task build_missing_projects: :environment do Project.find_each(batch_size: 1000) do |project| - path_to_repo = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git") + path_to_repo = project.repository.path_to_repo if File.exists?(path_to_repo) print '-' else @@ -118,5 +122,16 @@ namespace :gitlab do puts "Quitting...".red exit 1 end + + def reset_to_commit(args) + tag, status = Gitlab::Popen.popen(%W(git describe -- #{args.tag})) + + unless status.zero? + tag, status = Gitlab::Popen.popen(%W(git describe -- origin/#{args.tag})) + end + + tag = tag.strip + system(*%W(git reset --hard #{tag})) + end end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index da61c6e007f..14a130be2ca 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -112,4 +112,20 @@ namespace :gitlab do @warned_user_not_gitlab = true end end + + # Tries to configure git itself + # + # Returns true if all subcommands were successfull (according to their exit code) + # Returns false if any or all subcommands failed. + def auto_fix_git_config(options) + if !@warned_user_not_gitlab && options['user.email'] != 'example@example.com' # default email should be overridden? + command_success = options.map do |name, value| + system(%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) + end + + command_success.all? + else + false + end + end end diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index c01b00bd1c0..b4076f8238f 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -2,6 +2,7 @@ namespace :gitlab do desc "GITLAB | Run all tests" task :test do cmds = [ + %W(rake rubocop), %W(rake spinach), %W(rake spec), %W(rake jasmine:ci) diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake new file mode 100644 index 00000000000..ddfaf5d51f2 --- /dev/null +++ b/lib/tasks/rubocop.rake @@ -0,0 +1,4 @@ +unless Rails.env.production? + require 'rubocop/rake_task' + RuboCop::RakeTask.new +end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index 507b315759d..4aefc18ce14 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -2,9 +2,15 @@ Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach') desc "GITLAB | Run spinach" task :spinach do + tags = if ENV['SEMAPHORE'] + '~@tricky' + else + '~@semaphore' + end + cmds = [ %W(rake gitlab:setup), - %W(spinach), + %W(spinach --tags #{tags}), ] run_commands(cmds) end diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 583f4a876da..a39d9649876 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -9,5 +9,5 @@ unless Rails.env.production? require 'coveralls/rake/task' Coveralls::RakeTask.new desc "GITLAB | Run all tests on CI with simplecov" - task :test_ci => [:spinach, :spec, 'coveralls:push'] + task :test_ci => [:rubocop, :brakeman, 'jasmine:ci', :spinach, :spec, 'coveralls:push'] end |