From 84cd2120952e7ee4095cb4b5d7c959f2c11610c5 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 09:37:02 +0200 Subject: Add API support for environments --- lib/api/api.rb | 1 + lib/api/entities.rb | 4 +++ lib/api/environments.rb | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 lib/api/environments.rb (limited to 'lib/api') diff --git a/lib/api/api.rb b/lib/api/api.rb index 3d7d67510a8..9c960d74495 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -32,6 +32,7 @@ module API mount ::API::CommitStatuses mount ::API::Commits mount ::API::DeployKeys + mount ::API::Environments mount ::API::Files mount ::API::GroupMembers mount ::API::Groups diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4eb95d8a215..3e21b7a0b8a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -496,6 +496,10 @@ module API expose :key, :value end + class Environment < Grape::Entity + expose :id, :name, :external_url + end + class RepoLicense < Grape::Entity expose :key, :name, :nickname expose :featured, as: :popular diff --git a/lib/api/environments.rb b/lib/api/environments.rb new file mode 100644 index 00000000000..66a047f72fc --- /dev/null +++ b/lib/api/environments.rb @@ -0,0 +1,87 @@ +module API + # Environments RESTfull API endpoints + class Environments < Grape::API + before { authenticate! } + + resource :projects do + # Get all labels of the project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/environments + get ':id/environments' do + authorize! :read_environment, user_project + + present paginate(user_project.environments), with: Entities::Environment + end + + # Creates a new environment + # + # Parameters: + # id (required) - The ID of a project + # name (required) - The name of the environment to be created + # external_url (optional) - URL on which this deployment is viewable + # + # Example Request: + # POST /projects/:id/labels + post ':id/environments' do + authorize! :create_environment, user_project + required_attributes! [:name] + + attrs = attributes_for_keys [:name, :external_url] + environment = user_project.environments.find_by(name: attrs[:name]) + + conflict!('Environment already exists') if environment + + environment = user_project.environments.create(attrs) + + if environment.valid? + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + # Deletes an existing environment + # + # Parameters: + # id (required) - The ID of a project + # environment_id (required) - The name of the environment to be deleted + # + # Example Request: + # DELETE /projects/:id/environments/:environment_id + delete ':id/environments/:environment_id' do + authorize! :admin_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + present environment.destroy, with: Entities::Environment + end + + # Updates an existing environment + # + # Parameters: + # id (required) - The ID of a project + # environment_id (required) - The ID of the environment + # name (optional) - The name of the label to be deleted + # external_url (optional) - The new name of the label + # + # Example Request: + # PUT /projects/:id/environments/:environment_id + put ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + attrs = attributes_for_keys [:name, :external_url] + + if environment.update(attrs) + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + end + end +end -- cgit v1.2.1 From 76e9b68439510af5c783a81b93944f1c8d96d150 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 14:19:37 +0200 Subject: Incorporate feedback --- lib/api/environments.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'lib/api') diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 66a047f72fc..532baec42c7 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -30,9 +30,6 @@ module API required_attributes! [:name] attrs = attributes_for_keys [:name, :external_url] - environment = user_project.environments.find_by(name: attrs[:name]) - - conflict!('Environment already exists') if environment environment = user_project.environments.create(attrs) @@ -52,7 +49,7 @@ module API # Example Request: # DELETE /projects/:id/environments/:environment_id delete ':id/environments/:environment_id' do - authorize! :admin_environment, user_project + authorize! :update_environment, user_project environment = user_project.environments.find(params[:environment_id]) -- cgit v1.2.1 From 1b72256fa14e65256d78347f81b289d43c44e991 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 29 Jul 2016 12:14:36 +0200 Subject: Use Grape DSL for environment endpoints Also a couple of minor edits for this branch are included --- lib/api/api.rb | 4 +++ lib/api/environments.rb | 91 +++++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 48 deletions(-) (limited to 'lib/api') diff --git a/lib/api/api.rb b/lib/api/api.rb index 9c960d74495..bd16806892b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -7,6 +7,10 @@ module API rack_response({ 'message' => '404 Not found' }.to_json, 404) end + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!({ messages: e.full_messages }, 400) + end + rescue_from :all do |exception| # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 # why is this not wrapped in something reusable? diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 532baec42c7..a50f007d697 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -3,82 +3,77 @@ module API class Environments < Grape::API before { authenticate! } + params do + requires :id, type: String, desc: 'The project ID' + end resource :projects do - # Get all labels of the project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/environments + desc 'Get all environments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end get ':id/environments' do authorize! :read_environment, user_project present paginate(user_project.environments), with: Entities::Environment end - # Creates a new environment - # - # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the environment to be created - # external_url (optional) - URL on which this deployment is viewable - # - # Example Request: - # POST /projects/:id/labels + desc 'Creates a new environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :name, type: String, desc: 'The name of the environment to be created' + optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + end post ':id/environments' do authorize! :create_environment, user_project - required_attributes! [:name] - - attrs = attributes_for_keys [:name, :external_url] - environment = user_project.environments.create(attrs) + create_params = declared(params, include_parent_namespaces: false).to_h + environment = user_project.environments.create(create_params) - if environment.valid? + if environment.persisted? present environment, with: Entities::Environment else render_validation_error!(environment) end end - # Deletes an existing environment - # - # Parameters: - # id (required) - The ID of a project - # environment_id (required) - The name of the environment to be deleted - # - # Example Request: - # DELETE /projects/:id/environments/:environment_id - delete ':id/environments/:environment_id' do - authorize! :update_environment, user_project - - environment = user_project.environments.find(params[:environment_id]) - - present environment.destroy, with: Entities::Environment + desc 'Updates an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + optional :name, type: String, desc: 'The new environment name' + optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' end - - # Updates an existing environment - # - # Parameters: - # id (required) - The ID of a project - # environment_id (required) - The ID of the environment - # name (optional) - The name of the label to be deleted - # external_url (optional) - The new name of the label - # - # Example Request: - # PUT /projects/:id/environments/:environment_id put ':id/environments/:environment_id' do authorize! :update_environment, user_project environment = user_project.environments.find(params[:environment_id]) - attrs = attributes_for_keys [:name, :external_url] - - if environment.update(attrs) + update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h + if environment.update(update_params) present environment, with: Entities::Environment else render_validation_error!(environment) end end + + desc 'Deletes an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + delete ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + present environment.destroy, with: Entities::Environment + end end end end -- cgit v1.2.1 From 34c1c8a3b14ab3b29fbde97532c89404d9573a1d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 1 Aug 2016 08:42:09 +0200 Subject: Minor fixes in the Env API endpoints --- lib/api/environments.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib/api') diff --git a/lib/api/environments.rb b/lib/api/environments.rb index a50f007d697..819f80d8365 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -11,6 +11,10 @@ module API detail 'This feature was introduced in GitLab 8.11.' success Entities::Environment end + params do + optional :page, type: Integer, desc: 'Page number of the current request' + optional :per_page, type: Integer, desc: 'Number of items per page' + end get ':id/environments' do authorize! :read_environment, user_project @@ -51,7 +55,7 @@ module API authorize! :update_environment, user_project environment = user_project.environments.find(params[:environment_id]) - + update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h if environment.update(update_params) present environment, with: Entities::Environment -- cgit v1.2.1 From 020ea32e767b9ad033f9fedcaa902865a01fa944 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 2 Aug 2016 18:06:31 +0800 Subject: Implement pipeline hooks, extracted from !5525 Closes #20115 --- lib/api/entities.rb | 6 ++++-- lib/api/project_hooks.rb | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'lib/api') diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3e21b7a0b8a..b6f6b11d97b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -48,7 +48,8 @@ module API class ProjectHook < Hook expose :project_id, :push_events - expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events + expose :issues_events, :merge_requests_events, :tag_push_events + expose :note_events, :build_events, :pipeline_events expose :enable_ssl_verification end @@ -342,7 +343,8 @@ module API class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active - expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events + expose :push_events, :issues_events, :merge_requests_events + expose :tag_push_events, :note_events, :build_events, :pipeline_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 6bb70bc8bc3..3f63cd678e8 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -45,6 +45,7 @@ module API :tag_push_events, :note_events, :build_events, + :pipeline_events, :enable_ssl_verification ] @hook = user_project.hooks.new(attrs) @@ -78,6 +79,7 @@ module API :tag_push_events, :note_events, :build_events, + :pipeline_events, :enable_ssl_verification ] -- cgit v1.2.1 From c86c1905b5574cac234315598d8d715fcaee3ea7 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 27 Jul 2016 19:00:34 +0200 Subject: switch from diff_file_collection to diffs So we have raw_diffs too --- lib/api/commits.rb | 4 ++-- lib/api/entities.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/api') diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4a11c8e3620..b4eaf1813d4 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -54,7 +54,7 @@ module API sha = params[:sha] commit = user_project.commit(sha) not_found! "Commit" unless commit - commit.diffs.to_a + commit.raw_diffs.to_a end # Get a commit's comments @@ -96,7 +96,7 @@ module API } if params[:path] && params[:line] && params[:line_type] - commit.diffs(all_diffs: true).each do |diff| + commit.raw_diffs(all_diffs: true).each do |diff| next unless diff.new_path == params[:path] lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3e21b7a0b8a..e5b00dc45a5 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -224,7 +224,7 @@ module API class MergeRequestChanges < MergeRequest expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| - compare.diffs(all_diffs: true).to_a + compare.raw_diffs(all_diffs: true).to_a end end -- cgit v1.2.1 From da3d3ba89c19364ca626eb57380e1e33bd344902 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 3 Aug 2016 14:45:32 +0200 Subject: Endpoints to enable and disable deploy keys Resolves #20123 --- lib/api/deploy_keys.rb | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'lib/api') diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 5c570b5e5ca..ab4868eca2d 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -74,6 +74,37 @@ module API end end + desc 'Enable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + post ":id/#{path}/:key_id/enable" do + key = DeployKey.find(params[:key_id]) + + if user_project.deploy_keys << key + present key, with: Entities::SSHKey + else + render_validation_error!(key) + end + end + + desc 'Disable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/#{path}/:key_id/disable" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + key.destroy + + present key.deploy_key, with: Entities::SSHKey + end + # Delete existing deploy key of currently authenticated user # # Example Request: -- cgit v1.2.1 From 2b6bd6a33f765175222cdb88cd927e420864a236 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 3 Aug 2016 16:45:35 +0200 Subject: Use Grape DSL for deploy keys endpoints Also a minor clean up of the post endpoint --- lib/api/deploy_keys.rb | 73 +++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) (limited to 'lib/api') diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index ab4868eca2d..6a0be345667 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -10,6 +10,9 @@ module API present keys, with: Entities::SSHKey end + params do + requires :id, type: String, desc: 'The ID of the project' + end resource :projects do before { authorize_admin_project } @@ -17,52 +20,42 @@ module API # Use "projects/:id/deploy_keys/..." instead. # %w(keys deploy_keys).each do |path| - # Get a specific project's deploy keys - # - # Example Request: - # GET /projects/:id/deploy_keys + desc "Get a specific project's deploy keys" do + success Entities::SSHKey + end get ":id/#{path}" do present user_project.deploy_keys, with: Entities::SSHKey end - # Get single deploy key owned by currently authenticated user - # - # Example Request: - # GET /projects/:id/deploy_keys/:key_id + desc 'Get single deploy key' do + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end get ":id/#{path}/:key_id" do key = user_project.deploy_keys.find params[:key_id] present key, with: Entities::SSHKey end - # Add new deploy key to currently authenticated user - # If deploy key already exists - it will be joined to project - # but only if original one was accessible by same user - # - # Parameters: - # key (required) - New deploy Key - # title (required) - New deploy Key's title - # Example Request: - # POST /projects/:id/deploy_keys + desc 'Add new deploy key to currently authenticated user' do + success Entities::SSHKey + end + params do + requires :key, type: String, desc: "The new deploy key" + requires :title, type: String, desc: 'The title to identify the key from' + end post ":id/#{path}" do - attrs = attributes_for_keys [:title, :key] - - if attrs[:key].present? - attrs[:key].strip! + attrs = declared(params, include_parent_namespaces: false).to_h - # check if key already exist in project - key = user_project.deploy_keys.find_by(key: attrs[:key]) - if key - present key, with: Entities::SSHKey - next - end + key = user_project.deploy_keys.find_by(key: attrs[:key]) + present key, with: Entities::SSHKey if key - # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) - if key - user_project.deploy_keys << key - present key, with: Entities::SSHKey - next - end + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey end key = DeployKey.new attrs @@ -105,12 +98,14 @@ module API present key.deploy_key, with: Entities::SSHKey end - # Delete existing deploy key of currently authenticated user - # - # Example Request: - # DELETE /projects/:id/deploy_keys/:key_id + desc 'Delete existing deploy key of currently authenticated user' do + success Key + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys.find params[:key_id] + key = user_project.deploy_keys.find(params[:key_id]) key.destroy end end -- cgit v1.2.1 From 460065b743a5f28d92bda71b0e778b01cd369d80 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 4 Aug 2016 13:09:10 +0200 Subject: Move deploy_key tests to deploy_key_spec.rb Also, fix the failing test in the process --- lib/api/deploy_keys.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'lib/api') diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 6a0be345667..52f89373ad3 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -38,15 +38,16 @@ module API present key, with: Entities::SSHKey end + # TODO: for 9.0 we should check if params are there with the params block + # grape provides, at this point we'd change behaviour so we can't + # Behaviour now if you don't provide all required params: it renders a + # validation error or two. desc 'Add new deploy key to currently authenticated user' do success Entities::SSHKey end - params do - requires :key, type: String, desc: "The new deploy key" - requires :title, type: String, desc: 'The title to identify the key from' - end post ":id/#{path}" do - attrs = declared(params, include_parent_namespaces: false).to_h + attrs = attributes_for_keys [:title, :key] + attrs[:key].strip! if attrs[:key] key = user_project.deploy_keys.find_by(key: attrs[:key]) present key, with: Entities::SSHKey if key -- cgit v1.2.1 From 554e18ab025fcd86001faa57fab14fe3ca28a672 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 5 Aug 2016 11:35:44 +0200 Subject: Create service for enabling deploy keys --- lib/api/deploy_keys.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/api') diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 52f89373ad3..6dc9beb57ec 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -76,12 +76,12 @@ module API requires :key_id, type: Integer, desc: 'The ID of the deploy key' end post ":id/#{path}/:key_id/enable" do - key = DeployKey.find(params[:key_id]) + key = EnableDeployKeyService.new(user_project, current_user, declared(params)).execute - if user_project.deploy_keys << key + if key present key, with: Entities::SSHKey else - render_validation_error!(key) + not_found!('Deploy Key') end end -- cgit v1.2.1 From 7e47a82899bdb10d2cdc61ce237a25bfa7f8a392 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 8 Aug 2016 20:32:10 +0200 Subject: Namespace EnableDeployKeyService under Projects --- lib/api/deploy_keys.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/api') diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 6dc9beb57ec..825e05fbae3 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -76,7 +76,8 @@ module API requires :key_id, type: Integer, desc: 'The ID of the deploy key' end post ":id/#{path}/:key_id/enable" do - key = EnableDeployKeyService.new(user_project, current_user, declared(params)).execute + key = ::Projects::EnableDeployKeyService.new(user_project, + current_user, declared(params)).execute if key present key, with: Entities::SSHKey -- cgit v1.2.1 From c53b599e61027e910aa0425150373cb30c67b150 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 2 Aug 2016 15:56:27 -0600 Subject: Retain old behavior --- lib/api/api.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/api') diff --git a/lib/api/api.rb b/lib/api/api.rb index bd16806892b..6cd4a853dbe 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -7,8 +7,10 @@ module API rack_response({ 'message' => '404 Not found' }.to_json, 404) end - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!({ messages: e.full_messages }, 400) + # Retain 405 error rather than a 500 error for Grape 0.15.0+. + # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de + rescue_from Grape::Exceptions::Base do |e| + error! e.message, e.status, e.headers end rescue_from :all do |exception| -- cgit v1.2.1 From 4955a47cb1c52168114364e45a2fccf6bc105452 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 6 Aug 2016 07:25:51 -0700 Subject: Clean up project destruction Instead of redirecting from the project service to the service and back to the model, put all destruction code in the service. Also removes a possible source of failure where run_after_commit may not destroy the project. --- lib/api/projects.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/api') diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8fed7db8803..60cfc103afd 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -323,7 +323,7 @@ module API # DELETE /projects/:id delete ":id" do authorize! :remove_project, user_project - ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete! + ::Projects::DestroyService.new(user_project, current_user, {}).async_execute end # Mark this project as forked from another -- cgit v1.2.1 From 29850364eccccc3ce7305f6706cea1d5d073de2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 23 Jun 2016 17:14:31 +0200 Subject: New AccessRequests API endpoints for Group & Project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, mutualize AccessRequests and Members endpoints for Group & Project. New API documentation for the AccessRequests endpoints. Signed-off-by: Rémy Coutable --- lib/api/access_requests.rb | 91 +++++++++++++++++++++++++++ lib/api/api.rb | 8 ++- lib/api/entities.rb | 24 +++---- lib/api/group_members.rb | 87 -------------------------- lib/api/helpers.rb | 25 ++------ lib/api/helpers/members_helpers.rb | 13 ++++ lib/api/members.rb | 124 +++++++++++++++++++++++++++++++++++++ lib/api/project_members.rb | 110 -------------------------------- 8 files changed, 253 insertions(+), 229 deletions(-) create mode 100644 lib/api/access_requests.rb delete mode 100644 lib/api/group_members.rb create mode 100644 lib/api/helpers/members_helpers.rb create mode 100644 lib/api/members.rb delete mode 100644 lib/api/project_members.rb (limited to 'lib/api') diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb new file mode 100644 index 00000000000..9c41d8aaa3e --- /dev/null +++ b/lib/api/access_requests.rb @@ -0,0 +1,91 @@ +module API + class AccessRequests < Grape::API + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + %w[group project].each do |source_type| + resource source_type.pluralize do + # Get a list of group/project access requests viewable by the authenticated user. + # + # Parameters: + # id (required) - The group/project ID + # + # Example Request: + # GET /groups/:id/access_requests + # GET /projects/:id/access_requests + get ":id/access_requests" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + + access_requesters = source.requesters + users = Kaminari.paginate_array(access_requesters.map(&:user)) + + present paginate(users), with: Entities::AccessRequester, source: source + end + + # Request access to the group/project + # + # Parameters: + # id (required) - The group/project ID + # + # Example Request: + # POST /groups/:id/access_requests + # POST /projects/:id/access_requests + post ":id/access_requests" do + source = find_source(source_type, params[:id]) + access_requester = source.request_access(current_user) + + if access_requester.persisted? + present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester + else + render_validation_error!(access_requester) + end + end + + # Approve a group/project access request + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the access requester + # access_level (optional) - Access level + # + # Example Request: + # PUT /groups/:id/access_requests/:user_id/approve + # PUT /projects/:id/access_requests/:user_id/approve + put ':id/access_requests/:user_id/approve' do + required_attributes! [:user_id] + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + + member = source.requesters.find_by!(user_id: params[:user_id]) + if params[:access_level] + member.update(access_level: params[:access_level]) + end + member.accept_request + + status :created + present member.user, with: Entities::Member, member: member + end + + # Deny a group/project access request + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the access requester + # + # Example Request: + # DELETE /groups/:id/access_requests/:user_id + # DELETE /projects/:id/access_requests/:user_id + delete ":id/access_requests/:user_id" do + required_attributes! [:user_id] + source = find_source(source_type, params[:id]) + + access_requester = source.requesters.find_by!(user_id: params[:user_id]) + + ::Members::DestroyService.new(access_requester, current_user).execute + end + end + end + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index 6cd4a853dbe..d43af3f24e9 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,6 +3,10 @@ module API include APIGuard version 'v3', using: :path + rescue_from Gitlab::Access::AccessDeniedError do + rack_response({ 'message' => '403 Forbidden' }.to_json, 403) + end + rescue_from ActiveRecord::RecordNotFound do rack_response({ 'message' => '404 Not found' }.to_json, 404) end @@ -32,6 +36,7 @@ module API # Ensure the namespace is right, otherwise we might load Grape::API::Helpers helpers ::API::Helpers + mount ::API::AccessRequests mount ::API::AwardEmoji mount ::API::Branches mount ::API::Builds @@ -40,19 +45,18 @@ module API mount ::API::DeployKeys mount ::API::Environments mount ::API::Files - mount ::API::GroupMembers mount ::API::Groups mount ::API::Internal mount ::API::Issues mount ::API::Keys mount ::API::Labels mount ::API::LicenseTemplates + mount ::API::Members mount ::API::MergeRequests mount ::API::Milestones mount ::API::Namespaces mount ::API::Notes mount ::API::ProjectHooks - mount ::API::ProjectMembers mount ::API::ProjectSnippets mount ::API::Projects mount ::API::Repositories diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e5b00dc45a5..c5ff4557b4a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -91,9 +91,17 @@ module API end end - class ProjectMember < UserBasic + class Member < UserBasic expose :access_level do |user, options| - options[:project].project_members.find_by(user_id: user.id).access_level + member = options[:member] || options[:source].members.find_by(user_id: user.id) + member.access_level + end + end + + class AccessRequester < UserBasic + expose :requested_at do |user, options| + access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id) + access_requester.requested_at end end @@ -108,12 +116,6 @@ module API expose :shared_projects, using: Entities::Project end - class GroupMember < UserBasic - expose :access_level do |user, options| - options[:group].group_members.find_by(user_id: user.id).access_level - end - end - class RepoBranch < Grape::Entity expose :name @@ -325,7 +327,7 @@ module API expose :id, :path, :kind end - class Member < Grape::Entity + class MemberAccess < Grape::Entity expose :access_level expose :notification_level do |member, options| if member.notification_setting @@ -334,10 +336,10 @@ module API end end - class ProjectAccess < Member + class ProjectAccess < MemberAccess end - class GroupAccess < Member + class GroupAccess < MemberAccess end class ProjectService < Grape::Entity diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb deleted file mode 100644 index dbe5bb08d3f..00000000000 --- a/lib/api/group_members.rb +++ /dev/null @@ -1,87 +0,0 @@ -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]) - users = group.users - 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! :admin_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], current_user) - 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! :admin_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! :admin_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/helpers.rb b/lib/api/helpers.rb index 130509cdad6..f06c262fd4c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -28,7 +28,7 @@ module API # If the sudo is the current user do nothing if identifier && !(@current_user.id == identifier || @current_user.username == identifier) - render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? + forbidden!('Must be admin to use sudo') unless @current_user.is_admin? @current_user = User.by_username_or_id(identifier) not_found!("No user id or username for: #{identifier}") if @current_user.nil? end @@ -47,18 +47,18 @@ module API end end + # Deprecated def user_project @project ||= find_project(params[:id]) - @project || not_found!("Project") end def find_project(id) project = Project.find_with_namespace(id) || Project.find_by(id: id) - if project && can?(current_user, :read_project, project) + if can?(current_user, :read_project, project) project else - nil + not_found!('Project') end end @@ -89,11 +89,7 @@ module API end def find_group(id) - begin - group = Group.find(id) - rescue ActiveRecord::RecordNotFound - group = Group.find_by!(path: id) - end + group = Group.find_by(path: id) || Group.find_by(id: id) if can?(current_user, :read_group, group) group @@ -135,7 +131,7 @@ module API end def authorize!(action, subject) - forbidden! unless abilities.allowed?(current_user, action, subject) + forbidden! unless can?(current_user, action, subject) end def authorize_push_project @@ -197,10 +193,6 @@ module API errors end - def validate_access_level?(level) - Gitlab::Access.options_with_owner.values.include? level.to_i - end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. # @@ -411,11 +403,6 @@ module API File.read(Gitlab.config.gitlab_shell.secret_file).chomp end - def handle_member_errors(errors) - error!(errors[:access_level], 422) if errors[:access_level].any? - not_found!(errors) - end - def send_git_blob(repository, blob) env['api.format'] = :txt content_type 'text/plain' diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb new file mode 100644 index 00000000000..90114f6f667 --- /dev/null +++ b/lib/api/helpers/members_helpers.rb @@ -0,0 +1,13 @@ +module API + module Helpers + module MembersHelpers + def find_source(source_type, id) + public_send("find_#{source_type}", id) + end + + def authorize_admin_source!(source_type, source) + authorize! :"admin_#{source_type}", source + end + end + end +end diff --git a/lib/api/members.rb b/lib/api/members.rb new file mode 100644 index 00000000000..56f8b1ca391 --- /dev/null +++ b/lib/api/members.rb @@ -0,0 +1,124 @@ +module API + class Members < Grape::API + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + %w[group project].each do |source_type| + resource source_type.pluralize do + # Get a list of group/project members viewable by the authenticated user. + # + # Parameters: + # id (required) - The group/project ID + # query - Query string + # + # Example Request: + # GET /groups/:id/members + # GET /projects/:id/members + get ":id/members" do + source = find_source(source_type, params[:id]) + + members = source.members + members = members.joins(:user).merge(User.search(params[:query])) if params[:query] + users = Kaminari.paginate_array(members.map(&:user)) + + present paginate(users), with: Entities::Member, source: source + end + + # Get a group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the member + # + # Example Request: + # GET /groups/:id/members/:user_id + # GET /projects/:id/members/:user_id + get ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + + members = source.members + member = members.find_by!(user_id: params[:user_id]) + + present member.user, with: Entities::Member, member: member + end + + # Add a new group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the new member + # access_level (required) - A valid access level + # + # Example Request: + # POST /groups/:id/members + # POST /projects/:id/members + post ":id/members" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + required_attributes! [:user_id, :access_level] + + access_requester = source.requesters.find_by(user_id: params[:user_id]) + if access_requester + # We pass current_user = access_requester so that the requester doesn't + # receive a "access denied" email + ::Members::DestroyService.new(access_requester, access_requester.user).execute + end + + conflict!('Member already exists') if source.members.exists?(user_id: params[:user_id]) + + source.add_user(params[:user_id], params[:access_level], current_user) + member = source.members.find_by(user_id: params[:user_id]) + if member + present member.user, with: Entities::Member, member: member + else + render_api_error!('400 Bad Request', 400) + end + end + + # Update a group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the member + # access_level (required) - A valid access level + # + # Example Request: + # PUT /groups/:id/members/:user_id + # PUT /projects/:id/members/:user_id + put ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + required_attributes! [:user_id, :access_level] + + member = source.members.find_by!(user_id: params[:user_id]) + + if member.update_attributes(access_level: params[:access_level]) + present member.user, with: Entities::Member, member: member + else + render_validation_error!(member) + end + end + + # Remove a group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the member + # + # Example Request: + # DELETE /groups/:id/members/:user_id + # DELETE /projects/:id/members/:user_id + delete ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + required_attributes! [:user_id] + + member = source.members.find_by!(user_id: params[:user_id]) + + ::Members::DestroyService.new(member, current_user).execute + status :no_content + end + end + end + end +end diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb deleted file mode 100644 index 6a0b3e7d134..00000000000 --- a/lib/api/project_members.rb +++ /dev/null @@ -1,110 +0,0 @@ -module API - # Projects members API - class ProjectMembers < Grape::API - before { authenticate! } - - resource :projects do - # Get a project team members - # - # Parameters: - # id (required) - The ID of a project - # query - Query string - # Example Request: - # GET /projects/:id/members - get ":id/members" do - if params[:query].present? - @members = paginate user_project.users.where("username LIKE ?", "%#{params[:query]}%") - else - @members = paginate user_project.users - end - present @members, with: Entities::ProjectMember, project: user_project - end - - # Get a project team members - # - # Parameters: - # id (required) - The ID of a project - # user_id (required) - The ID of a user - # Example Request: - # GET /projects/:id/members/:user_id - get ":id/members/:user_id" do - @member = user_project.users.find params[:user_id] - present @member, with: Entities::ProjectMember, project: user_project - end - - # Add a new project team member - # - # Parameters: - # id (required) - The ID of a project - # user_id (required) - The ID of a user - # access_level (required) - Project access level - # Example Request: - # POST /projects/:id/members - post ":id/members" do - authorize! :admin_project, user_project - required_attributes! [:user_id, :access_level] - - # either the user is already a team member or a new one - project_member = user_project.project_member(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 project_member.save - @member = project_member.user - present @member, with: Entities::ProjectMember, project: user_project - else - handle_member_errors project_member.errors - end - end - - # Update project team member - # - # Parameters: - # id (required) - The ID of a project - # user_id (required) - The ID of a team member - # access_level (required) - Project access level - # Example Request: - # PUT /projects/:id/members/:user_id - put ":id/members/:user_id" do - authorize! :admin_project, user_project - required_attributes! [:access_level] - - 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 project_member.update_attributes(access_level: params[:access_level]) - @member = project_member.user - present @member, with: Entities::ProjectMember, project: user_project - else - handle_member_errors project_member.errors - end - end - - # Remove a team member from project - # - # Parameters: - # id (required) - The ID of a project - # user_id (required) - The ID of a team member - # Example Request: - # DELETE /projects/:id/members/:user_id - delete ":id/members/:user_id" do - project_member = user_project.project_members.find_by(user_id: params[:user_id]) - - unless current_user.can?(:admin_project, user_project) || - current_user.can?(:destroy_project_member, project_member) - forbidden! - end - - if project_member.nil? - { message: "Access revoked", id: params[:user_id].to_i } - else - project_member.destroy - end - end - end - end -end -- cgit v1.2.1 From 7c1b33b48f8c45b726a513b6809a85919d41c23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 9 Aug 2016 12:14:11 +0200 Subject: Restore back-compatibility for current members API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/api/helpers.rb | 1 - lib/api/members.rb | 45 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 8 deletions(-) (limited to 'lib/api') diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index f06c262fd4c..d0469d6602d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -47,7 +47,6 @@ module API end end - # Deprecated def user_project @project ||= find_project(params[:id]) end diff --git a/lib/api/members.rb b/lib/api/members.rb index 56f8b1ca391..ccb7618360d 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -65,14 +65,29 @@ module API ::Members::DestroyService.new(access_requester, access_requester.user).execute end - conflict!('Member already exists') if source.members.exists?(user_id: params[:user_id]) - - source.add_user(params[:user_id], params[:access_level], current_user) member = source.members.find_by(user_id: params[:user_id]) + + # This is to ensure back-compatibility but 409 behavior should be used + # for both project and group members in 9.0! + conflict!('Member already exists') if source_type == 'group' && member + + unless member + source.add_user(params[:user_id], params[:access_level], current_user) + member = source.members.find_by(user_id: params[:user_id]) + end + if member present member.user, with: Entities::Member, member: member else - render_api_error!('400 Bad Request', 400) + # Since `source.add_user` doesn't return a member object, we have to + # build a new one and populate its errors in order to render them. + member = source.members.build(attributes_for_keys([:user_id, :access_level])) + member.valid? # populate the errors + + # This is to ensure back-compatibility but 400 behavior should be used + # for all validation errors in 9.0! + render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) + render_validation_error!(member) end end @@ -96,6 +111,9 @@ module API if member.update_attributes(access_level: params[:access_level]) present member.user, with: Entities::Member, member: member else + # This is to ensure back-compatibility but 400 behavior should be used + # for all validation errors in 9.0! + render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end @@ -113,10 +131,23 @@ module API source = find_source(source_type, params[:id]) required_attributes! [:user_id] - member = source.members.find_by!(user_id: params[:user_id]) + # This is to ensure back-compatibility but find_by! should be used + # in that casse in 9.0! + member = source.members.find_by(user_id: params[:user_id]) + + # This is to ensure back-compatibility but this should be removed in + # favor of find_by! in 9.0! + not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil? + + # This is to ensure back-compatibility but 204 behavior should be used + # for all DELETE endpoints in 9.0! + if member.nil? + { message: "Access revoked", id: params[:user_id].to_i } + else + ::Members::DestroyService.new(member, current_user).execute - ::Members::DestroyService.new(member, current_user).execute - status :no_content + present member.user, with: Entities::Member, member: member + end end end end -- cgit v1.2.1 From 5010be7766d08a9adee51c7d16ba71c19ff7dede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 9 Aug 2016 12:20:09 +0200 Subject: Improve the performance of the GET /:sources/:id/{access_requests,members} API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The performance was greatly improved by removing two N+1 queries issues for each endpoint. For comparison: 1. `GET /projects/:id/members` With two N+1 queries issues (one was already fxed in the following snippet): ``` ProjectMember Load (0.2ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND "members"."requested_at" IS NULL ORDER BY "members"."id" DESC [["source_type", "Project"], ["source_id", 1], ["source_type", "Project"]] User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 22, 16, 13) ORDER BY "users"."id" DESC ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND "members"."requested_at" IS NULL AND "members"."user_id" = $4 ORDER BY "members"."id" DESC LIMIT 1 [["source_type", "Project"], ["source_id", 1], ["source_type", "Project"], ["user_id", 5]] ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND "members"."requested_at" IS NULL AND "members"."user_id" = $4 ORDER BY "members"."id" DESC LIMIT 1 [["source_type", "Project"], ["source_id", 1], ["source_type", "Project"], ["user_id", 22]] ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND "members"."requested_at" IS NULL AND "members"."user_id" = $4 ORDER BY "members"."id" DESC LIMIT 1 [["source_type", "Project"], ["source_id", 1], ["source_type", "Project"], ["user_id", 16]] ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND "members"."requested_at" IS NULL AND "members"."user_id" = $4 ORDER BY "members"."id" DESC LIMIT 1 [["source_type", "Project"], ["source_id", 1], ["source_type", "Project"], ["user_id", 13]] ``` Without the N+1 queries issues: ``` ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND "members"."requested_at" IS NULL ORDER BY "members"."id" DESC LIMIT 20 OFFSET 0 [["source_type", "Project"], ["source_id", 1], ["source_type", "Project"]] User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 22, 16, 13) ORDER BY "users"."id" DESC ``` 2. `GET /projects/:id/access_requests` With two N+1 queries issues: ``` ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS NOT NULL) ORDER BY "members"."id" DESC [["source_type", "Project"], ["source_id", 8], ["source_type", "Project"]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" DESC LIMIT 1 [["id", 24]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" DESC LIMIT 1 [["id", 23]] ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" ProjectMember Load (0.1ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS NOT NULL) AND "members"."user_id" = $4 ORDER BY "members"."id" DESC LIMIT 1 [["source_type", "Project"], ["source_id", 8], ["source_type", "Project"], ["user_id", 24]] ProjectMember Load (0.2ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS NOT NULL) AND "members"."user_id" = $4 ORDER BY "members"."id" DESC LIMIT 1 [["source_type", "Project"], ["source_id", 8], ["source_type", "Project"], ["user_id", 23]] ``` Without the N+1 queries issues: ``` ProjectMember Load (0.3ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = $1 AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND "members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS NOT NULL) ORDER BY "members"."id" DESC LIMIT 20 OFFSET 0 [["source_type", "Project"], ["source_id", 8], ["source_type", "Project"]] User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (24, 23) ORDER BY "users"."id" DESC ``` Signed-off-by: Rémy Coutable --- lib/api/access_requests.rb | 5 ++--- lib/api/entities.rb | 4 ++-- lib/api/members.rb | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) (limited to 'lib/api') diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 9c41d8aaa3e..d02b469dac8 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -18,10 +18,9 @@ module API source = find_source(source_type, params[:id]) authorize_admin_source!(source_type, source) - access_requesters = source.requesters - users = Kaminari.paginate_array(access_requesters.map(&:user)) + access_requesters = paginate(source.requesters.includes(:user)) - present paginate(users), with: Entities::AccessRequester, source: source + present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters end # Request access to the group/project diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c5ff4557b4a..ae74d14a4bb 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -93,14 +93,14 @@ module API class Member < UserBasic expose :access_level do |user, options| - member = options[:member] || options[:source].members.find_by(user_id: user.id) + member = options[:member] || options[:members].find { |m| m.user_id == user.id } member.access_level end end class AccessRequester < UserBasic expose :requested_at do |user, options| - access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id) + access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id } access_requester.requested_at end end diff --git a/lib/api/members.rb b/lib/api/members.rb index ccb7618360d..2fae83f60b2 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -18,11 +18,11 @@ module API get ":id/members" do source = find_source(source_type, params[:id]) - members = source.members + members = source.members.includes(:user) members = members.joins(:user).merge(User.search(params[:query])) if params[:query] - users = Kaminari.paginate_array(members.map(&:user)) + members = paginate(members) - present paginate(users), with: Entities::Member, source: source + present members.map(&:user), with: Entities::Member, members: members end # Get a group/project member -- cgit v1.2.1 From 6109daf480327581b6e2dcdfffe90464be6c7796 Mon Sep 17 00:00:00 2001 From: Scott Le Date: Thu, 28 Jul 2016 11:04:57 +0700 Subject: api for generating new merge request DRY code + fix rubocop Add more test cases Append to changelog DRY changes list find_url service for merge_requests use GET for getting merge request links remove files rename to get_url_service reduce loop add test case for cross project refactor tiny thing update changelog --- lib/api/internal.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib/api') diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 959b700de78..d8e9ac406c4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -74,6 +74,10 @@ module API response end + get "/merge_request_urls" do + ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) + end + # # Discover user by ssh key # -- cgit v1.2.1 From 1f2253545ba7a902212bace29f144a2246eeedab Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Fri, 8 Jul 2016 18:42:47 +0200 Subject: Use cache for todos counter calling TodoService --- lib/api/todos.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/api') diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 26c24c3baff..a90a667fafe 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -73,7 +73,7 @@ module API # delete do todos = find_todos - todos.each(&:done) + TodoService.new.mark_todos_as_done(todos, current_user) todos.length end -- cgit v1.2.1 From f8b53ba20b74181a46985b0c7dde742239bd54f8 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 11 Aug 2016 18:39:50 +0200 Subject: Recover usage of Todos counter cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’re being kept up to date the counter data but we’re not using it. The only thing which is not real if is the number of projects that the user read changes the number of todos can be stale for some time. The counters will be sync just after the user receives a new todo or mark any as done --- lib/api/todos.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'lib/api') diff --git a/lib/api/todos.rb b/lib/api/todos.rb index a90a667fafe..19df13d8aac 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -61,9 +61,9 @@ module API # delete ':id' do todo = current_user.todos.find(params[:id]) - todo.done + TodoService.new.mark_todos_as_done([todo], current_user) - present todo, with: Entities::Todo, current_user: current_user + present todo.reload, with: Entities::Todo, current_user: current_user end # Mark all todos as done @@ -74,8 +74,6 @@ module API delete do todos = find_todos TodoService.new.mark_todos_as_done(todos, current_user) - - todos.length end end end -- cgit v1.2.1 From 722fc84e3d4785fb3a9db5f1c7d2aabad22e8e01 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Thu, 28 Jul 2016 19:02:56 -0500 Subject: Complete refactor of the `Spammable` concern and tests: - Merged `AkismetSubmittable` into `Spammable` - Clean up `SpamCheckService` - Added tests for `Spammable` - Added submit (ham or spam) options to `AkismetHelper` --- lib/api/issues.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/api') diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c4d3134da6c..7bbfc137c2c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -160,7 +160,7 @@ module API issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute - if issue.spam? + if issue.spam_detected? render_api_error!({ error: 'Spam detected' }, 400) end -- cgit v1.2.1 From 64ab2b3d9f10366249c03a6bcf5e8b1d20010d8f Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Fri, 29 Jul 2016 23:18:32 -0500 Subject: Refactored spam related code even further - Removed unnecessary column from `SpamLog` - Moved creation of SpamLogs out of its own service and into SpamCheckService - Simplified code in SpamCheckService. - Moved move spam related code into Spammable concern --- lib/api/issues.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/api') diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 7bbfc137c2c..c4d3134da6c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -160,7 +160,7 @@ module API issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute - if issue.spam_detected? + if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end -- cgit v1.2.1 From 43e756d4eafd79f4d2f366b646ebb94af78b5a4c Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Fri, 5 Aug 2016 17:10:08 -0500 Subject: Refactored AkismetHelper into AkismetService and cleaned up `Spammable` - Refactored SpamCheckService into SpamService --- lib/api/issues.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'lib/api') diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c4d3134da6c..077258faee1 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -3,8 +3,6 @@ module API class Issues < Grape::API before { authenticate! } - helpers ::Gitlab::AkismetHelper - helpers do def filter_issues_state(issues, state) case state -- cgit v1.2.1 From e805a6470031d942f7de604fdf7acfc7cf4f0b1a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 16 Aug 2016 10:39:13 +0530 Subject: Backport changes from gitlab-org/gitlab-ee!581 to CE. !581 has a lot of changes that would cause merge conflicts if not properly backported to CE. This commit/MR serves as a better foundation for gitlab-org/gitlab-ee!581. = Changes = 1. Move from `has_one {merge,push}_access_level` to `has_many`, with the `length` of the association limited to `1`. This is _effectively_ a `has_one` association, but should cause less conflicts with EE, which is set to `has_many`. This has a number of related changes in the views, specs, and factories. 2. Make `gon` variable loading more consistent (with EE!581) in the `ProtectedBranchesController`. Also use `::` to prefix the `ProtectedBranches` services, because this is required in EE. 3. Extract a `ProtectedBranchAccess` concern from the two access level models. This concern only has a single `humanize` method here, but will have more methods in EE. 4. Add `form_errors` to the protected branches creation form. This is not strictly required for EE compatibility, but was an oversight nonetheless. --- lib/api/entities.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/api') diff --git a/lib/api/entities.rb b/lib/api/entities.rb index ae74d14a4bb..7bce427adf6 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -129,12 +129,12 @@ module API expose :developers_can_push do |repo_branch, options| project = options[:project] - project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER } + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_levels.first.access_level == Gitlab::Access::DEVELOPER } end expose :developers_can_merge do |repo_branch, options| project = options[:project] - project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER } + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_levels.first.access_level == Gitlab::Access::DEVELOPER } end end -- cgit v1.2.1 From 4ddbbcd11a6f03ae36efd4b9016974c34a1465ed Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 16 Aug 2016 12:00:56 +0530 Subject: Improve EE compatibility with protected branch access levels. 1. Change a few incorrect `access_level` to `access_levels.first` that were missed in e805a64. 2. `API::Entities` can iterate over all access levels instead of just the first one. This makes no difference to CE, and makes it more compatible with EE. --- lib/api/entities.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/api') diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 7bce427adf6..ec455e67329 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -129,12 +129,14 @@ module API expose :developers_can_push do |repo_branch, options| project = options[:project] - project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_levels.first.access_level == Gitlab::Access::DEVELOPER } + access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten + access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER } end expose :developers_can_merge do |repo_branch, options| project = options[:project] - project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_levels.first.access_level == Gitlab::Access::DEVELOPER } + access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten + access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER } end end -- cgit v1.2.1 From dd3b738d5b3eb70217d7ac7f9fe441498d2e8e7e Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 16 Aug 2016 13:34:56 +0530 Subject: Fix failing tests relating to backporting ee!581. 1. `GitPushService` was still using `{merge,push}_access_level_attributes` instead of `{merge,push}_access_levels_attributes`. 2. The branches API creates access levels regardless of the state of the `developers_can_{push,merge}` parameters. This is in line with the UI, where Master access is the default for a new protected branch. 3. Use `after(:build)` to create access levels in the `protected_branches` factory, so that `factories_spec` passes. It only builds records, so we need to create access levels on `build` as well. --- lib/api/branches.rb | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) (limited to 'lib/api') diff --git a/lib/api/branches.rb b/lib/api/branches.rb index a77afe634f6..b615703df93 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -61,22 +61,27 @@ module API name: @branch.name } - unless developers_can_merge.nil? - protected_branch_params.merge!({ - merge_access_level_attributes: { - access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - } - }) + # If `developers_can_merge` is switched off, _all_ `DEVELOPER` + # merge_access_levels need to be deleted. + if developers_can_merge == false + protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all end - unless developers_can_push.nil? - protected_branch_params.merge!({ - push_access_level_attributes: { - access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - } - }) + # If `developers_can_push` is switched off, _all_ `DEVELOPER` + # push_access_levels need to be deleted. + if developers_can_push == false + protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all end + protected_branch_params.merge!( + merge_access_levels_attributes: [{ + access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + }], + push_access_levels_attributes: [{ + access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + }] + ) + if protected_branch service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params) service.execute(protected_branch) -- cgit v1.2.1 From 28726729452ef64270534806e75a9595ea1a659d Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 24 Jun 2016 16:43:46 -0300 Subject: Load issues and merge requests templates from repository --- lib/api/templates.rb | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) (limited to 'lib/api') diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 18408797756..b9e718147e1 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -1,21 +1,28 @@ module API class Templates < Grape::API - TEMPLATE_TYPES = { - gitignores: Gitlab::Template::Gitignore, - gitlab_ci_ymls: Gitlab::Template::GitlabCiYml + GLOBAL_TEMPLATE_TYPES = { + gitignores: Gitlab::Template::GitignoreTemplate, + gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate }.freeze - TEMPLATE_TYPES.each do |template, klass| + helpers do + def render_response(template_type, template) + not_found!(template_type.to_s.singularize) unless template + present template, with: Entities::Template + end + end + + GLOBAL_TEMPLATE_TYPES.each do |template_type, klass| # Get the list of the available template # # Example Request: # GET /gitignores # GET /gitlab_ci_ymls - get template.to_s do + get template_type.to_s do present klass.all, with: Entities::TemplatesList end - # Get the text for a specific template + # Get the text for a specific template present in local filesystem # # Parameters: # name (required) - The name of a template @@ -23,13 +30,10 @@ module API # Example Request: # GET /gitignores/Elixir # GET /gitlab_ci_ymls/Ruby - get "#{template}/:name" do + get "#{template_type}/:name" do required_attributes! [:name] - new_template = klass.find(params[:name]) - not_found!(template.to_s.singularize) unless new_template - - present new_template, with: Entities::Template + render_response(template_type, new_template) end end end -- cgit v1.2.1