From cb275a33e394d4f2327f443552b04f6d1c8287d8 Mon Sep 17 00:00:00 2001 From: Artur Martsinkovskyi Date: Fri, 19 Oct 2018 15:38:30 +0000 Subject: Update best_practices.md let section to also reference let! variables. --- doc/development/testing_guide/best_practices.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index acbfa1850b4..e124a768c2e 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -158,12 +158,13 @@ instead of 30+ seconds in case of a regular `spec_helper`. ### `let` variables -GitLab's RSpec suite has made extensive use of `let` variables to reduce -duplication. However, this sometimes [comes at the cost of clarity][lets-not], +GitLab's RSpec suite has made extensive use of `let`(along with it strict, non-lazy +version `let!`) variables to reduce duplication. However, this sometimes [comes at the cost of clarity][lets-not], so we need to set some guidelines for their use going forward: -- `let` variables are preferable to instance variables. Local variables are - preferable to `let` variables. +- `let!` variables are preferable to instance variables. `let` variables + are preferable to `let!` variables. Local variables are preferable to + `let` variables. - Use `let` to reduce duplication throughout an entire spec file. - Don't use `let` to define variables used by a single test; define them as local variables inside the test's `it` block. @@ -173,6 +174,9 @@ so we need to set some guidelines for their use going forward: - Try to avoid overriding the definition of one `let` variable with another. - Don't define a `let` variable that's only used by the definition of another. Use a helper method instead. +- `let!` variables should be used only in case if strict evaluation with defined + order is required, otherwise `let` will suffice. Remember that `let` is lazy and won't + be evaluated until it is referenced. [lets-not]: https://robots.thoughtbot.com/lets-not -- cgit v1.2.1 From 1911283d5c6d55f1fbef30309fb458e9785a3e83 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 14 Nov 2018 09:44:16 +0100 Subject: Clarify wiki permissions --- doc/user/permissions.md | 3 ++- doc/user/project/wiki/index.md | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 1fd230a41aa..0ccc934183f 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -42,6 +42,8 @@ The following table depicts the various user permission levels in a project. | See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | | Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | | View wiki pages | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Create and edit wiki pages | | | ✓ | ✓ | ✓ | +| Delete wiki pages | | | | ✓ | ✓ | | View license management reports **[ULTIMATE]** | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | View Security reports **[ULTIMATE]** | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | Pull project code | [^1] | ✓ | ✓ | ✓ | ✓ | @@ -70,7 +72,6 @@ The following table depicts the various user permission levels in a project. | Force push to non-protected branches | | | ✓ | ✓ | ✓ | | Remove non-protected branches | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ | -| Write a wiki | | | ✓ | ✓ | ✓ | | Cancel and retry jobs | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ | diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index 127a30d6669..532247a98cd 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -12,10 +12,6 @@ You can create Wiki pages in the web interface or [locally using Git](#adding-and-editing-wiki-pages-locally) since every Wiki is a separate Git repository. ->**Note:** -A [permission level][permissions] of **Guest** is needed to view a Wiki and -**Developer** is needed to create and edit Wiki pages. - ## First time creating the Home page The first time you visit a Wiki, you will be directed to create the Home page. @@ -28,6 +24,9 @@ message. ## Creating a new wiki page +NOTE: **Note:** +A [permission level][permissions] of **Developer** is needed to create Wiki pages. + Create a new page by clicking the **New page** button that can be found in all wiki pages. You will be asked to fill in the page name from which GitLab will create the path to the page. You can specify a full path for the new file @@ -58,12 +57,18 @@ repository, you will have to upload them again. ## Editing a wiki page +NOTE: **Note:** +A [permission level][permissions] of **Developer** is needed to edit Wiki pages. + To edit a page, simply click on the **Edit** button. From there on, you can change its content. When done, click **Save changes** for the changes to take effect. ## Deleting a wiki page +NOTE: **Note:** +A [permission level][permissions] of **Maintainer** is needed to delete Wiki pages. + You can find the **Delete** button only when editing a page. Click on it and confirm you want the page to be deleted. -- cgit v1.2.1 From 89a2985e4d3da41baab484678a92c77a8bae2a1f Mon Sep 17 00:00:00 2001 From: Menna Elkashef Date: Sat, 15 Dec 2018 23:51:02 +0200 Subject: [Fix] Individual contributor graph When the project has only one contributor, the individual contribution graph is cropped because the container with the id 'contributors' is not taking the full width --- app/assets/stylesheets/pages/stat_graph.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index d331edaa302..79186480605 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -25,6 +25,8 @@ } #contributors { + flex: 1; + .contributors-list { margin: 0 0 10px; list-style: none; -- cgit v1.2.1 From 351586876ffc217f757032d477f871da4ece3b77 Mon Sep 17 00:00:00 2001 From: David Planella Date: Sun, 16 Dec 2018 17:50:43 +0100 Subject: Fix leading whitespace in translatable string --- app/views/import/bitbucket_server/status.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index ef69197e453..c4e27efc76f 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -29,7 +29,7 @@ %tr %th= _('From Bitbucket Server') %th= _('To GitLab') - %th= _(' Status') + %th= _('Status') %tbody - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } -- cgit v1.2.1 From 38e55d6b96e2bee43e7773182679fbcfe6f41ff3 Mon Sep 17 00:00:00 2001 From: David Planella Date: Sun, 16 Dec 2018 17:59:02 +0100 Subject: Update .pot file with changed strings --- locale/gitlab.pot | 3 --- 1 file changed, 3 deletions(-) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0584d2006f7..902cf366dc0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16,9 +16,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -msgid " Status" -msgstr "" - msgid "%d addition" msgid_plural "%d additions" msgstr[0] "" -- cgit v1.2.1 From 7a58eb2e1612cef1178c9b35df9aaea71cbe04aa Mon Sep 17 00:00:00 2001 From: Frederic Van Espen Date: Wed, 26 Dec 2018 15:33:11 +0100 Subject: Allow to override part of the backup filename --- doc/raketasks/backup_restore.md | 11 +++++++++++ lib/backup/manager.rb | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 57bc71d2903..4238685b54f 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -195,6 +195,17 @@ To use the `copy` strategy instead of the default streaming strategy, specify sudo gitlab-rake gitlab:backup:create STRATEGY=copy ``` +### Backup filename + +By default a backup file is created according to the specification in [the Backup timestamp](#backup-timestamp) section above. You can however override the `[TIMESTAMP]` part of the filename by setting the `BACKUP` environment variable. For example: + +```sh +sudo gitlab-rake gitlab:backup:create BACKUP=dump +``` + +The resulting file will then be `dump_gitlab_backup.tar`. This is useful for systems that make use of rsync and incremental backups, and will result in considerably faster transfer speeds. + + ### Excluding specific directories from the backup You can choose what should be exempt from the backup up by adding the environment variable `SKIP`. diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index a0434a66ef1..792a25422ee 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -234,7 +234,11 @@ module Backup end def tar_file - @tar_file ||= "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" + if ENV['BACKUP'] + @tar_file ||= ENV['BACKUP'] + "#{FILE_NAME_SUFFIX}" + else + @tar_file ||= "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" + end end def backup_information -- cgit v1.2.1 From e8419fb5bba0e5ba9247983ad24ee5bc2e7b8115 Mon Sep 17 00:00:00 2001 From: Frederic Van Espen Date: Wed, 26 Dec 2018 15:35:20 +0100 Subject: set the --rsyncable option for gzip --- lib/backup/database.rb | 2 +- lib/backup/files.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/backup/database.rb b/lib/backup/database.rb index e6bf3d1856f..8baca500546 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -17,7 +17,7 @@ module Backup FileUtils.mkdir_p(File.dirname(db_file_name)) FileUtils.rm_f(db_file_name) compress_rd, compress_wr = IO.pipe - compress_pid = spawn(*%w(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600]) + compress_pid = spawn(*%w(gzip --rsyncable -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600]) compress_rd.close dump_pid = diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 0032ae8f84b..998fa5a1a92 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -31,10 +31,10 @@ module Backup raise Backup::Error, 'Backup failed' end - run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip --rsyncable -c -1)], out: [backup_tarball, 'w', 0600]) FileUtils.rm_rf(@backup_files_dir) else - run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip --rsyncable -c -1)], out: [backup_tarball, 'w', 0600]) end end -- cgit v1.2.1 From 3bb3f7ddd073e67fafbc210d0e18c76a87b96dcb Mon Sep 17 00:00:00 2001 From: Frederic Van Espen Date: Thu, 27 Dec 2018 12:37:43 +0100 Subject: fix code style --- lib/backup/manager.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 792a25422ee..12121920c67 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -234,11 +234,11 @@ module Backup end def tar_file - if ENV['BACKUP'] - @tar_file ||= ENV['BACKUP'] + "#{FILE_NAME_SUFFIX}" - else - @tar_file ||= "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" - end + @tar_file ||= if ENV['BACKUP'] + ENV['BACKUP'] + "#{FILE_NAME_SUFFIX}" + else + "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" + end end def backup_information -- cgit v1.2.1 From ee832780876add739e16fe173d8157e48501cb49 Mon Sep 17 00:00:00 2001 From: Katrin Leinweber Date: Fri, 11 Jan 2019 20:04:07 +0000 Subject: Add @katrinleinweber as German proofreader --- doc/development/i18n/proofreader.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index ac910e80a89..4ef8bdf7466 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -40,6 +40,7 @@ are very appreciative of the work done by translators and proofreaders! - Pedro Garcia - [GitLab](https://gitlab.com/pedgarrod), [Crowdin](https://crowdin.com/profile/breaking_pitt) - German - Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah) + - Katrin Leinweber - [GitLab](https://gitlab.com/katrinleinweber/), [Crowdin](https://crowdin.com/profile/katrinleinweber) - Greek - Proofreaders needed. - Hebrew -- cgit v1.2.1 From 104c8b890dbca25a0d08b2567d003f02953a0fc1 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Sat, 8 Dec 2018 14:12:50 +0000 Subject: Backport EE GroupSAML origin verification changes --- lib/gitlab/auth/omniauth_identity_linker_base.rb | 6 +++++- spec/controllers/omniauth_callbacks_controller_spec.rb | 2 +- spec/support/helpers/login_helpers.rb | 13 ++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb index 253445570f2..c620fc5d6bd 100644 --- a/lib/gitlab/auth/omniauth_identity_linker_base.rb +++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb @@ -12,7 +12,7 @@ module Gitlab end def link - save if identity.new_record? + save if unlinked? end def changed? @@ -35,6 +35,10 @@ module Gitlab @changed = identity.save end + def unlinked? + identity.new_record? + end + # rubocop: disable CodeReuse/ActiveRecord def identity @identity ||= current_user.identities diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 59463462e5a..f995867c5e1 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -170,7 +170,7 @@ describe OmniauthCallbacksController, type: :controller do before do stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config] }) - mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response) + mock_auth_hash_with_saml_xml('saml', 'my-uid', user.email, mock_saml_response) request.env["devise.mapping"] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] post :saml, params: { SAMLResponse: mock_saml_response } diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 87cfb6c04dc..c709f160bff 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -47,7 +47,7 @@ module LoginHelpers end def gitlab_sign_in_via(provider, user, uid, saml_response = nil) - mock_auth_hash(provider, uid, user.email, saml_response) + mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response) visit new_user_session_path click_link provider end @@ -87,7 +87,12 @@ module LoginHelpers click_link "oauth-login-#{provider}" end - def mock_auth_hash(provider, uid, email, saml_response = nil) + def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response) + response_object = { document: saml_xml(saml_response) } + mock_auth_hash(provider, uid, email, response_object: response_object) + end + + def mock_auth_hash(provider, uid, email, response_object: nil) # The mock_auth configuration allows you to set per-provider (or default) # authentication hashes to return during integration testing. OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ @@ -110,9 +115,7 @@ module LoginHelpers image: 'mock_user_thumbnail_url' } }, - response_object: { - document: saml_xml(saml_response) - } + response_object: response_object } }) Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] -- cgit v1.2.1 From 276147c1f6c54ae87941953ec62d0d674c12f462 Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Fri, 25 Jan 2019 15:44:50 +0800 Subject: Respond with 403 when non-member requests for private MRs --- lib/api/commits.rb | 2 ++ spec/requests/api/commits_spec.rb | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9d23daafe95..41cb3e17af8 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -318,6 +318,8 @@ module API use :pagination end get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do + authorize! :read_merge_request, user_project + commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 6b9bc6eda6a..c24e17fda3f 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1430,8 +1430,8 @@ describe API::Commits do end describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do - let!(:project) { create(:project, :repository, :private) } - let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } + let(:project) { create(:project, :repository, :private) } + let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } let(:commit) { merged_mr.merge_request_diff.commits.last } it 'returns the correct merge request' do @@ -1456,5 +1456,16 @@ describe API::Commits do expect(response).to have_gitlab_http_status(404) end + + context 'public project' do + let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let(:non_member) { create(:user) } + + it 'responds 403 when only members are allowed to read merge requests' do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", non_member) + + expect(response).to have_gitlab_http_status(403) + end + end end end -- cgit v1.2.1 From 65723a2cb60f46681a766f34102df700ef5b7573 Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Fri, 25 Jan 2019 17:22:48 +0800 Subject: Modify MergeRequestsFinder to allow filtering by commit --- app/controllers/projects/commit_controller.rb | 6 +++++- app/finders/merge_requests_finder.rb | 9 ++++++++- lib/api/commits.rb | 8 +++++++- spec/finders/merge_requests_finder_spec.rb | 26 +++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 32fc5140366..fbad56cf6b3 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -65,7 +65,11 @@ class Projects::CommitController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord def merge_requests - @merge_requests = @commit.merge_requests.map do |mr| + @merge_requests = MergeRequestsFinder.new( + current_user, + project_id: @project.id, + commit_sha: @commit.sha + ).execute.map do |mr| { iid: mr.iid, path: merge_request_path(mr), title: mr.title } end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index e190d5d90c9..bf01f05a9e9 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -36,13 +36,20 @@ class MergeRequestsFinder < IssuableFinder end def filter_items(_items) - items = by_source_branch(super) + items = by_commit(super) + items = by_source_branch(items) items = by_wip(items) by_target_branch(items) end private + def by_commit(items) + return items unless params[:commit_sha].presence + + items.by_commit_sha(params[:commit_sha]) + end + def source_branch @source_branch ||= params[:source_branch].presence end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 41cb3e17af8..be682982897 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -323,7 +323,13 @@ module API commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - present paginate(commit.merge_requests), with: Entities::MergeRequestBasic + commit_merge_requests = MergeRequestsFinder.new( + current_user, + project_id: user_project.id, + commit_sha: commit.sha + ).execute + + present paginate(commit_merge_requests), with: Entities::MergeRequestBasic end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 107da08a0a9..79f854cdb96 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -31,7 +31,7 @@ describe MergeRequestsFinder do p end end - let(:project4) { create_project_without_n_plus_1(group: subgroup) } + let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) } let(:project5) { create_project_without_n_plus_1(group: subgroup) } let(:project6) { create_project_without_n_plus_1(group: subgroup) } @@ -68,6 +68,15 @@ describe MergeRequestsFinder do expect(merge_requests.size).to eq(2) end + it 'filters by commit sha' do + merge_requests = described_class.new( + user, + commit_sha: merge_request5.merge_request_diff.last_commit_sha + ).execute + + expect(merge_requests).to contain_exactly(merge_request5) + end + context 'filtering by group' do it 'includes all merge requests when user has access' do params = { group_id: group.id } @@ -269,6 +278,21 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) end end + + context 'when project restricts merge requests' do + let(:non_member) { create(:user) } + let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let!(:merge_request) { create(:merge_request, source_project: project) } + + it "returns nothing to to non members" do + merge_requests = described_class.new( + non_member, + project_id: project.id + ).execute + + expect(merge_requests).to be_empty + end + end end describe '#row_count', :request_store do -- cgit v1.2.1 From 325527e6ca7635aeeea8e0beb7523c3892e21bf6 Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Mon, 28 Jan 2019 14:16:58 +0800 Subject: Add changelog for security fix --- changelogs/unreleased/security-commit-private-related-mr.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/security-commit-private-related-mr.yml diff --git a/changelogs/unreleased/security-commit-private-related-mr.yml b/changelogs/unreleased/security-commit-private-related-mr.yml new file mode 100644 index 00000000000..c4de200b0d8 --- /dev/null +++ b/changelogs/unreleased/security-commit-private-related-mr.yml @@ -0,0 +1,5 @@ +--- +title: Don't allow non-members to see private related MRs. +merge_request: +author: +type: security -- cgit v1.2.1 From d5c858cd4032b3bf37c6fbe47340ccea825503bc Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Fri, 8 Feb 2019 16:11:37 +0000 Subject: Prevent Releases links API to leak tag existance --- changelogs/unreleased/security-tags-oracle.yml | 5 +++++ lib/api/release/links.rb | 2 ++ spec/requests/api/release/links_spec.rb | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changelogs/unreleased/security-tags-oracle.yml diff --git a/changelogs/unreleased/security-tags-oracle.yml b/changelogs/unreleased/security-tags-oracle.yml new file mode 100644 index 00000000000..eb8ad6f646c --- /dev/null +++ b/changelogs/unreleased/security-tags-oracle.yml @@ -0,0 +1,5 @@ +--- +title: Prevent releases links API to leak tag existance +merge_request: +author: +type: security diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index e3072684ef7..5d1b40e3bff 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -8,6 +8,8 @@ module API RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(tag_name: API::NO_SLASH_URL_PART_REGEX) + before { authorize! :read_release, user_project } + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index ba948e37e2f..3a59052bb29 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -73,6 +73,22 @@ describe API::Release::Links do expect(response).to have_gitlab_http_status(:ok) end end + + context 'when project is public and the repository is private' do + let(:project) { create(:project, :repository, :public, :repository_private) } + + it_behaves_like '403 response' do + let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) } + end + + context 'when the release does not exists' do + let!(:release) { } + + it_behaves_like '403 response' do + let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) } + end + end + end end end -- cgit v1.2.1 From 5dc047dc72c08a64aaf4f4a0c9fe0fba2742b905 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Mon, 11 Feb 2019 18:51:53 +0800 Subject: Disable board policies when issues are disabled Board list policies are also included --- app/policies/project_policy.rb | 2 ++ .../unreleased/security-2798-fix-boards-policy.yml | 5 +++++ spec/policies/project_policy_spec.rb | 20 ++++++++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/security-2798-fix-boards-policy.yml diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index cadbc5ae009..a8270442ea9 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -299,6 +299,8 @@ class ProjectPolicy < BasePolicy rule { issues_disabled }.policy do prevent(*create_read_update_admin_destroy(:issue)) + prevent(*create_read_update_admin_destroy(:board)) + prevent(*create_read_update_admin_destroy(:list)) end rule { merge_requests_disabled | repository_disabled }.policy do diff --git a/changelogs/unreleased/security-2798-fix-boards-policy.yml b/changelogs/unreleased/security-2798-fix-boards-policy.yml new file mode 100644 index 00000000000..10e8ac3a787 --- /dev/null +++ b/changelogs/unreleased/security-2798-fix-boards-policy.yml @@ -0,0 +1,5 @@ +--- +title: Disable issue boards API when issues are disabled +merge_request: +author: +type: security diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 93a468f585b..f8d581ef38f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -130,22 +130,26 @@ describe ProjectPolicy do subject { described_class.new(owner, project) } context 'when the feature is disabled' do - it 'does not include the issues permissions' do + before do project.issues_enabled = false project.save! + end + it 'does not include the issues permissions' do expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue end - end - context 'when the feature is disabled and external tracker configured' do - it 'does not include the issues permissions' do - create(:jira_service, project: project) + it 'disables boards and lists permissions' do + expect_disallowed :read_board, :create_board, :update_board, :admin_board + expect_disallowed :read_list, :create_list, :update_list, :admin_list + end - project.issues_enabled = false - project.save! + context 'when external tracker configured' do + it 'does not include the issues permissions' do + create(:jira_service, project: project) - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + end end end end -- cgit v1.2.1 From e9b84f50e961ee7c3abfb8192de8f4fc778df041 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 11 Feb 2019 15:48:37 -0200 Subject: Migrate issuable states to integer patch 1 Patch 1 that migrates issues/merge requests states from integer to string. On this commit we are only adding the state_id column and syncing it with a backgroud migration. On Patch 2 the code to use the new integer column will be deployed and the old column will be removed. --- app/models/concerns/issuable.rb | 1 + app/models/concerns/issuable_states.rb | 37 +++++++++ app/models/merge_request.rb | 2 + .../20190211131150_add_state_id_to_issuables.rb | 37 +++++++++ db/schema.rb | 4 +- .../sync_issuables_state_id.rb | 29 +++++++ lib/gitlab/database/migration_helpers.rb | 2 +- spec/migrations/add_state_id_to_issuables_spec.rb | 89 ++++++++++++++++++++++ 8 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 app/models/concerns/issuable_states.rb create mode 100644 db/migrate/20190211131150_add_state_id_to_issuables.rb create mode 100644 lib/gitlab/background_migration/sync_issuables_state_id.rb create mode 100644 spec/migrations/add_state_id_to_issuables_spec.rb diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0a77fbeba08..7bf553a0221 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -23,6 +23,7 @@ module Issuable include Sortable include CreatedAtFilterable include UpdatedAtFilterable + include IssuableStates # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb new file mode 100644 index 00000000000..7feaf7e8aac --- /dev/null +++ b/app/models/concerns/issuable_states.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# == IssuableStates concern +# +# Defines statuses shared by issuables which are persisted on state column +# using the state machine. +# +# Used by EE::Epic, Issue and MergeRequest +# +module IssuableStates + extend ActiveSupport::Concern + + # Override this constant on model where different states are needed + # Check MergeRequest::AVAILABLE_STATES + AVAILABLE_STATES = { opened: 1, closed: 2 }.freeze + + included do + before_save :set_state_id + end + + class_methods do + def states + @states ||= OpenStruct.new(self::AVAILABLE_STATES) + end + end + + # The state:string column is being migrated to state_id:integer column + # This is a temporary hook to populate state_id column with new values + # and can be removed after the complete migration is done. + def set_state_id + return if state.nil? || state.empty? + + states_hash = self.class.states.to_h.with_indifferent_access + + self.state_id = states_hash[state] + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2035bffd829..67600383cf9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,6 +22,8 @@ class MergeRequest < ActiveRecord::Base self.reactive_cache_lifetime = 10.minutes SORTING_PREFERENCE_FIELD = :merge_requests_sort + MERGE_REQUEST_STATES = + AVAILABLE_STATES = AVAILABLE_STATES.merge(merged: 3, locked: 4).freeze ignore_column :locked_at, :ref_fetched, diff --git a/db/migrate/20190211131150_add_state_id_to_issuables.rb b/db/migrate/20190211131150_add_state_id_to_issuables.rb new file mode 100644 index 00000000000..af02aa84afd --- /dev/null +++ b/db/migrate/20190211131150_add_state_id_to_issuables.rb @@ -0,0 +1,37 @@ +class AddStateIdToIssuables < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + MIGRATION = 'SyncIssuablesStateId'.freeze + + # TODO - find out how many issues and merge requests in production + # to adapt the batch size and delay interval + # Keep in mind that the migration will be scheduled for issues and merge requests. + BATCH_SIZE = 5000 + DELAY_INTERVAL = 5.minutes.to_i + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + end + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + end + + def up + add_column :issues, :state_id, :integer, limit: 1 + add_column :merge_requests, :state_id, :integer, limit: 1 + + queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + remove_column :issues, :state_id + remove_column :merge_requests, :state_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 023eee5f33e..0fb31ce2ef2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190204115450) do +ActiveRecord::Schema.define(version: 20190211131150) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1046,6 +1046,7 @@ ActiveRecord::Schema.define(version: 20190204115450) do t.boolean "discussion_locked" t.datetime_with_timezone "closed_at" t.integer "closed_by_id" + t.integer "state_id", limit: 2 t.index ["author_id"], name: "index_issues_on_author_id", using: :btree t.index ["closed_by_id"], name: "index_issues_on_closed_by_id", using: :btree t.index ["confidential"], name: "index_issues_on_confidential", using: :btree @@ -1283,6 +1284,7 @@ ActiveRecord::Schema.define(version: 20190204115450) do t.string "rebase_commit_sha" t.boolean "squash", default: false, null: false t.boolean "allow_maintainer_to_push" + t.integer "state_id", limit: 2 t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree t.index ["author_id"], name: "index_merge_requests_on_author_id", using: :btree t.index ["created_at"], name: "index_merge_requests_on_created_at", using: :btree diff --git a/lib/gitlab/background_migration/sync_issuables_state_id.rb b/lib/gitlab/background_migration/sync_issuables_state_id.rb new file mode 100644 index 00000000000..1ac86b8acf2 --- /dev/null +++ b/lib/gitlab/background_migration/sync_issuables_state_id.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class SyncIssuablesStateId + def perform(start_id, end_id, model_class) + populate_new_state_id(start_id, end_id, model_class) + end + + def populate_new_state_id(start_id, end_id, model_class) + Rails.logger.info("#{model_class.model_name.human} - Populating state_id: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~SQL + UPDATE #{model_class.table_name} + SET state_id = + CASE state + WHEN 'opened' THEN 1 + WHEN 'closed' THEN 2 + WHEN 'merged' THEN 3 + WHEN 'locked' THEN 4 + END + WHERE state_id IS NULL + AND id BETWEEN #{start_id} AND #{end_id} + SQL + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 3abd0600e9d..20cbb9e096b 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1033,7 +1033,7 @@ into similar problems in the future (e.g. when new tables are created). # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for # the same time, which is not helpful in most cases where we wish to # spread the work over time. - BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id]) + BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id, model_class]) end end diff --git a/spec/migrations/add_state_id_to_issuables_spec.rb b/spec/migrations/add_state_id_to_issuables_spec.rb new file mode 100644 index 00000000000..4416f416c18 --- /dev/null +++ b/spec/migrations/add_state_id_to_issuables_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20190206144959_change_issuable_states_to_integer.rb') + +describe AddStateIdToIssuables, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:merge_requests) { table(:merge_requests) } + let(:issues) { table(:issues) } + let(:migration) { described_class.new } + + before do + @group = namespaces.create!(name: 'gitlab', path: 'gitlab') + @project = projects.create!(namespace_id: @group.id) + end + + describe '#up' do + context 'issues' do + it 'migrates state column to integer' do + opened_issue = issues.create!(description: 'first', state: 'opened') + closed_issue = issues.create!(description: 'second', state: 'closed') + nil_state_issue = issues.create!(description: 'third', state: nil) + + migrate! + + issues.reset_column_information + expect(opened_issue.reload.state).to eq(Issue.states.opened) + expect(closed_issue.reload.state).to eq(Issue.states.closed) + expect(nil_state_issue.reload.state).to eq(nil) + end + end + + context 'merge requests' do + it 'migrates state column to integer' do + opened_merge_request = merge_requests.create!(state: 'opened', target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') + closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') + merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') + locked_merge_request = merge_requests.create!(state: 'locked', target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') + nil_state_merge_request = merge_requests.create!(state: nil, target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') + + migrate! + + merge_requests.reset_column_information + expect(opened_merge_request.reload.state).to eq(MergeRequest.states.opened) + expect(closed_merge_request.reload.state).to eq(MergeRequest.states.closed) + expect(merged_merge_request.reload.state).to eq(MergeRequest.states.merged) + expect(locked_merge_request.reload.state).to eq(MergeRequest.states.locked) + expect(nil_state_merge_request.reload.state).to eq(nil) + end + end + end + + describe '#down' do + context 'issues' do + it 'migrates state column to string' do + opened_issue = issues.create!(description: 'first', state: 1) + closed_issue = issues.create!(description: 'second', state: 2) + nil_state_issue = issues.create!(description: 'third', state: nil) + + migration.down + + issues.reset_column_information + expect(opened_issue.reload.state).to eq('opened') + expect(closed_issue.reload.state).to eq('closed') + expect(nil_state_issue.reload.state).to eq(nil) + end + end + + context 'merge requests' do + it 'migrates state column to string' do + opened_merge_request = merge_requests.create!(state: 1, target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') + closed_merge_request = merge_requests.create!(state: 2, target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') + merged_merge_request = merge_requests.create!(state: 3, target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') + locked_merge_request = merge_requests.create!(state: 4, target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') + nil_state_merge_request = merge_requests.create!(state: nil, target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') + + migration.down + + merge_requests.reset_column_information + expect(opened_merge_request.reload.state).to eq('opened') + expect(closed_merge_request.reload.state).to eq('closed') + expect(merged_merge_request.reload.state).to eq('merged') + expect(locked_merge_request.reload.state).to eq('locked') + expect(nil_state_merge_request.reload.state).to eq(nil) + end + end + end +end -- cgit v1.2.1 From 0bba55438ceacde9836780e1a1e6e06e12122f6d Mon Sep 17 00:00:00 2001 From: Adi Ferdian Date: Tue, 12 Feb 2019 10:20:22 +0000 Subject: Add adiferd as Indonesia proofreader --- doc/development/i18n/proofreader.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index ac910e80a89..679913c1e2e 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -47,6 +47,7 @@ are very appreciative of the work done by translators and proofreaders! - Hungarian - Proofreaders needed. - Indonesian + - Adi Ferdian - [GitLab](https://gitlab.com/adiferd), [Crowdin](https://crowdin.com/profile/adiferd) - Ahmad Naufal Mukhtar - [GitLab](https://gitlab.com/anaufalm), [Crowdin](https://crowdin.com/profile/anaufalm) - Italian - Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo) -- cgit v1.2.1 From 362d56e65a0e23fcf4fd5bd4535d258c3659ffd5 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 12 Feb 2019 14:40:37 -0200 Subject: Schedule background migrations and specs --- .../20190211131150_add_state_id_to_issuables.rb | 10 +++- .../sync_issuables_state_id.rb | 10 ++-- lib/gitlab/database/migration_helpers.rb | 3 +- spec/migrations/add_state_id_to_issuables_spec.rb | 60 +++++----------------- 4 files changed, 27 insertions(+), 56 deletions(-) diff --git a/db/migrate/20190211131150_add_state_id_to_issuables.rb b/db/migrate/20190211131150_add_state_id_to_issuables.rb index af02aa84afd..b9d52fe63cd 100644 --- a/db/migrate/20190211131150_add_state_id_to_issuables.rb +++ b/db/migrate/20190211131150_add_state_id_to_issuables.rb @@ -1,5 +1,6 @@ class AddStateIdToIssuables < ActiveRecord::Migration[5.0] include Gitlab::Database::MigrationHelpers + #include AfterCommitQueue DOWNTIME = false MIGRATION = 'SyncIssuablesStateId'.freeze @@ -26,8 +27,13 @@ class AddStateIdToIssuables < ActiveRecord::Migration[5.0] add_column :issues, :state_id, :integer, limit: 1 add_column :merge_requests, :state_id, :integer, limit: 1 - queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) - queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + # Is this safe? + # Added to avoid an warning about jobs running inside transactions. + # Since we only add a column this should be ok + Sidekiq::Worker.skipping_transaction_check do + queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end end def down diff --git a/lib/gitlab/background_migration/sync_issuables_state_id.rb b/lib/gitlab/background_migration/sync_issuables_state_id.rb index 1ac86b8acf2..95734a7310e 100644 --- a/lib/gitlab/background_migration/sync_issuables_state_id.rb +++ b/lib/gitlab/background_migration/sync_issuables_state_id.rb @@ -4,15 +4,15 @@ module Gitlab module BackgroundMigration class SyncIssuablesStateId - def perform(start_id, end_id, model_class) - populate_new_state_id(start_id, end_id, model_class) + def perform(start_id, end_id, table_name) + populate_new_state_id(start_id, end_id, table_name) end - def populate_new_state_id(start_id, end_id, model_class) - Rails.logger.info("#{model_class.model_name.human} - Populating state_id: #{start_id} - #{end_id}") + def populate_new_state_id(start_id, end_id, table_name) + Rails.logger.info("#{table_name} - Populating state_id: #{start_id} - #{end_id}") ActiveRecord::Base.connection.execute <<~SQL - UPDATE #{model_class.table_name} + UPDATE #{table_name} SET state_id = CASE state WHEN 'opened' THEN 1 diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 20cbb9e096b..46b36d07c20 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1029,11 +1029,12 @@ into similar problems in the future (e.g. when new tables are created). model_class.each_batch(of: batch_size) do |relation, index| start_id, end_id = relation.pluck('MIN(id), MAX(id)').first + table_name = relation.base_class.table_name # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for # the same time, which is not helpful in most cases where we wish to # spread the work over time. - BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id, model_class]) + BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id, table_name]) end end diff --git a/spec/migrations/add_state_id_to_issuables_spec.rb b/spec/migrations/add_state_id_to_issuables_spec.rb index 4416f416c18..b0e285db1f3 100644 --- a/spec/migrations/add_state_id_to_issuables_spec.rb +++ b/spec/migrations/add_state_id_to_issuables_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -require Rails.root.join('db', 'migrate', '20190206144959_change_issuable_states_to_integer.rb') +require Rails.root.join('db', 'migrate', '20190211131150_add_state_id_to_issuables.rb') describe AddStateIdToIssuables, :migration do let(:namespaces) { table(:namespaces) } @@ -20,14 +20,15 @@ describe AddStateIdToIssuables, :migration do it 'migrates state column to integer' do opened_issue = issues.create!(description: 'first', state: 'opened') closed_issue = issues.create!(description: 'second', state: 'closed') + invalid_state_issue = issues.create!(description: 'fourth', state: 'not valid') nil_state_issue = issues.create!(description: 'third', state: nil) migrate! - issues.reset_column_information - expect(opened_issue.reload.state).to eq(Issue.states.opened) - expect(closed_issue.reload.state).to eq(Issue.states.closed) - expect(nil_state_issue.reload.state).to eq(nil) + expect(opened_issue.reload.state_id).to eq(Issue.states.opened) + expect(closed_issue.reload.state_id).to eq(Issue.states.closed) + expect(invalid_state_issue.reload.state_id).to be_nil + expect(nil_state_issue.reload.state_id).to be_nil end end @@ -37,52 +38,15 @@ describe AddStateIdToIssuables, :migration do closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') locked_merge_request = merge_requests.create!(state: 'locked', target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') - nil_state_merge_request = merge_requests.create!(state: nil, target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') + invalid_state_merge_request = merge_requests.create!(state: 'not valid', target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') migrate! - merge_requests.reset_column_information - expect(opened_merge_request.reload.state).to eq(MergeRequest.states.opened) - expect(closed_merge_request.reload.state).to eq(MergeRequest.states.closed) - expect(merged_merge_request.reload.state).to eq(MergeRequest.states.merged) - expect(locked_merge_request.reload.state).to eq(MergeRequest.states.locked) - expect(nil_state_merge_request.reload.state).to eq(nil) - end - end - end - - describe '#down' do - context 'issues' do - it 'migrates state column to string' do - opened_issue = issues.create!(description: 'first', state: 1) - closed_issue = issues.create!(description: 'second', state: 2) - nil_state_issue = issues.create!(description: 'third', state: nil) - - migration.down - - issues.reset_column_information - expect(opened_issue.reload.state).to eq('opened') - expect(closed_issue.reload.state).to eq('closed') - expect(nil_state_issue.reload.state).to eq(nil) - end - end - - context 'merge requests' do - it 'migrates state column to string' do - opened_merge_request = merge_requests.create!(state: 1, target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') - closed_merge_request = merge_requests.create!(state: 2, target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') - merged_merge_request = merge_requests.create!(state: 3, target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') - locked_merge_request = merge_requests.create!(state: 4, target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') - nil_state_merge_request = merge_requests.create!(state: nil, target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') - - migration.down - - merge_requests.reset_column_information - expect(opened_merge_request.reload.state).to eq('opened') - expect(closed_merge_request.reload.state).to eq('closed') - expect(merged_merge_request.reload.state).to eq('merged') - expect(locked_merge_request.reload.state).to eq('locked') - expect(nil_state_merge_request.reload.state).to eq(nil) + expect(opened_merge_request.reload.state_id).to eq(MergeRequest.states.opened) + expect(closed_merge_request.reload.state_id).to eq(MergeRequest.states.closed) + expect(merged_merge_request.reload.state_id).to eq(MergeRequest.states.merged) + expect(locked_merge_request.reload.state_id).to eq(MergeRequest.states.locked) + expect(invalid_state_merge_request.reload.state_id).to be_nil end end end -- cgit v1.2.1 From e2aa332504f6cd9eebaa30dfeb71edcea6f9495a Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 12 Feb 2019 16:39:56 -0200 Subject: Improve batch size --- app/models/concerns/issuable_states.rb | 12 ++++++------ app/models/merge_request.rb | 1 - db/migrate/20190211131150_add_state_id_to_issuables.rb | 10 ++++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb index 7feaf7e8aac..f9f5797065d 100644 --- a/app/models/concerns/issuable_states.rb +++ b/app/models/concerns/issuable_states.rb @@ -2,7 +2,7 @@ # == IssuableStates concern # -# Defines statuses shared by issuables which are persisted on state column +# Defines states shared by issuables which are persisted on state_id column # using the state machine. # # Used by EE::Epic, Issue and MergeRequest @@ -14,10 +14,6 @@ module IssuableStates # Check MergeRequest::AVAILABLE_STATES AVAILABLE_STATES = { opened: 1, closed: 2 }.freeze - included do - before_save :set_state_id - end - class_methods do def states @states ||= OpenStruct.new(self::AVAILABLE_STATES) @@ -26,7 +22,11 @@ module IssuableStates # The state:string column is being migrated to state_id:integer column # This is a temporary hook to populate state_id column with new values - # and can be removed after the complete migration is done. + # and can be removed after the state column is removed. + included do + before_save :set_state_id + end + def set_state_id return if state.nil? || state.empty? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 67600383cf9..ece31b359d1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,7 +22,6 @@ class MergeRequest < ActiveRecord::Base self.reactive_cache_lifetime = 10.minutes SORTING_PREFERENCE_FIELD = :merge_requests_sort - MERGE_REQUEST_STATES = AVAILABLE_STATES = AVAILABLE_STATES.merge(merged: 3, locked: 4).freeze ignore_column :locked_at, diff --git a/db/migrate/20190211131150_add_state_id_to_issuables.rb b/db/migrate/20190211131150_add_state_id_to_issuables.rb index b9d52fe63cd..d23c946cf88 100644 --- a/db/migrate/20190211131150_add_state_id_to_issuables.rb +++ b/db/migrate/20190211131150_add_state_id_to_issuables.rb @@ -5,10 +5,12 @@ class AddStateIdToIssuables < ActiveRecord::Migration[5.0] DOWNTIME = false MIGRATION = 'SyncIssuablesStateId'.freeze - # TODO - find out how many issues and merge requests in production - # to adapt the batch size and delay interval - # Keep in mind that the migration will be scheduled for issues and merge requests. - BATCH_SIZE = 5000 + # 2019-02-12 Gitlab.com issuable numbers + # issues count: 13587305 + # merge requests count: 18925274 + # Using this 50000 as batch size should take around 13 hours + # to migrate both issues and merge requests + BATCH_SIZE = 50000 DELAY_INTERVAL = 5.minutes.to_i class Issue < ActiveRecord::Base -- cgit v1.2.1 From a9291f15ea10e3cfc94282ffb4e0969e9d4175eb Mon Sep 17 00:00:00 2001 From: Mark Chao Date: Wed, 13 Feb 2019 16:23:58 +0800 Subject: Align spec with actual usage Currently we pass temp file path to FileMover --- spec/uploaders/file_mover_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index de29d0c943f..a28d7445b1c 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe FileMover do let(:filename) { 'banana_sample.gif' } - let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) } let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } let(:temp_description) do @@ -12,7 +11,7 @@ describe FileMover do let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) } let(:snippet) { create(:personal_snippet, description: temp_description) } - subject { described_class.new(file_path, snippet).execute } + subject { described_class.new(temp_file_path, snippet).execute } describe '#execute' do before do -- cgit v1.2.1 From 26f40aefb09b96538fa99f74d46542ad39bb1679 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 13 Feb 2019 17:31:14 -0200 Subject: Code improvements --- app/models/concerns/issuable.rb | 9 +++++++++ app/models/concerns/issuable_states.rb | 21 +-------------------- app/models/merge_request.rb | 5 ++++- .../20190211131150_add_state_id_to_issuables.rb | 8 ++++---- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 7bf553a0221..83bd8cc6478 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -132,6 +132,15 @@ module Issuable fuzzy_search(query, [:title]) end + # Available state values persisted in state_id column using state machine + # + # Override this on subclasses if different states are needed + # + # Check MergeRequest.available_states for example + def available_states + @available_states ||= { opened: 1, closed: 2 }.with_indifferent_access + end + # Searches for records with a matching title or description. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb index f9f5797065d..d7992f8e6db 100644 --- a/app/models/concerns/issuable_states.rb +++ b/app/models/concerns/issuable_states.rb @@ -1,25 +1,6 @@ -# frozen_string_literal: true - -# == IssuableStates concern -# -# Defines states shared by issuables which are persisted on state_id column -# using the state machine. -# -# Used by EE::Epic, Issue and MergeRequest -# module IssuableStates extend ActiveSupport::Concern - # Override this constant on model where different states are needed - # Check MergeRequest::AVAILABLE_STATES - AVAILABLE_STATES = { opened: 1, closed: 2 }.freeze - - class_methods do - def states - @states ||= OpenStruct.new(self::AVAILABLE_STATES) - end - end - # The state:string column is being migrated to state_id:integer column # This is a temporary hook to populate state_id column with new values # and can be removed after the state column is removed. @@ -30,7 +11,7 @@ module IssuableStates def set_state_id return if state.nil? || state.empty? - states_hash = self.class.states.to_h.with_indifferent_access + states_hash = self.class.available_states self.state_id = states_hash[state] end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ece31b359d1..6baf479d8d1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,7 +22,6 @@ class MergeRequest < ActiveRecord::Base self.reactive_cache_lifetime = 10.minutes SORTING_PREFERENCE_FIELD = :merge_requests_sort - AVAILABLE_STATES = AVAILABLE_STATES.merge(merged: 3, locked: 4).freeze ignore_column :locked_at, :ref_fetched, @@ -194,6 +193,10 @@ class MergeRequest < ActiveRecord::Base '!' end + def self.available_states + @states ||= super.merge(locked: 3, merged: 4) + end + def rebase_in_progress? strong_memoize(:rebase_in_progress) do # The source project can be deleted diff --git a/db/migrate/20190211131150_add_state_id_to_issuables.rb b/db/migrate/20190211131150_add_state_id_to_issuables.rb index d23c946cf88..440f577e1a3 100644 --- a/db/migrate/20190211131150_add_state_id_to_issuables.rb +++ b/db/migrate/20190211131150_add_state_id_to_issuables.rb @@ -8,9 +8,9 @@ class AddStateIdToIssuables < ActiveRecord::Migration[5.0] # 2019-02-12 Gitlab.com issuable numbers # issues count: 13587305 # merge requests count: 18925274 - # Using this 50000 as batch size should take around 13 hours + # Using this 25000 as batch size should take around 26 hours # to migrate both issues and merge requests - BATCH_SIZE = 50000 + BATCH_SIZE = 25000 DELAY_INTERVAL = 5.minutes.to_i class Issue < ActiveRecord::Base @@ -26,8 +26,8 @@ class AddStateIdToIssuables < ActiveRecord::Migration[5.0] end def up - add_column :issues, :state_id, :integer, limit: 1 - add_column :merge_requests, :state_id, :integer, limit: 1 + add_column :issues, :state_id, :integer, limit: 2 + add_column :merge_requests, :state_id, :integer, limit: 2 # Is this safe? # Added to avoid an warning about jobs running inside transactions. -- cgit v1.2.1 From 37741c59a4daf1b0d6d9f7a6a51337e9d8effb66 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 14 Feb 2019 11:48:20 -0200 Subject: Split background migration for issues and merge requests --- app/models/merge_request.rb | 2 +- .../20190211131150_add_state_id_to_issuables.rb | 30 ------------ ...90214112022_schedule_sync_issuables_state_id.rb | 43 ++++++++++++++++++ db/schema.rb | 2 +- .../sync_issuables_state_id.rb | 29 ------------ .../background_migration/sync_issues_state_id.rb | 23 ++++++++++ .../sync_merge_requests_state_id.rb | 25 ++++++++++ lib/gitlab/database/migration_helpers.rb | 3 +- spec/migrations/add_state_id_to_issuables_spec.rb | 53 ---------------------- .../schedule_sync_issuables_state_id_spec.rb | 53 ++++++++++++++++++++++ 10 files changed, 147 insertions(+), 116 deletions(-) create mode 100644 db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb delete mode 100644 lib/gitlab/background_migration/sync_issuables_state_id.rb create mode 100644 lib/gitlab/background_migration/sync_issues_state_id.rb create mode 100644 lib/gitlab/background_migration/sync_merge_requests_state_id.rb delete mode 100644 spec/migrations/add_state_id_to_issuables_spec.rb create mode 100644 spec/migrations/schedule_sync_issuables_state_id_spec.rb diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6baf479d8d1..0ec0789a24f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -194,7 +194,7 @@ class MergeRequest < ActiveRecord::Base end def self.available_states - @states ||= super.merge(locked: 3, merged: 4) + @states ||= super.merge(merged: 3, locked: 4) end def rebase_in_progress? diff --git a/db/migrate/20190211131150_add_state_id_to_issuables.rb b/db/migrate/20190211131150_add_state_id_to_issuables.rb index 440f577e1a3..cf3f7671a67 100644 --- a/db/migrate/20190211131150_add_state_id_to_issuables.rb +++ b/db/migrate/20190211131150_add_state_id_to_issuables.rb @@ -1,41 +1,11 @@ class AddStateIdToIssuables < ActiveRecord::Migration[5.0] include Gitlab::Database::MigrationHelpers - #include AfterCommitQueue DOWNTIME = false - MIGRATION = 'SyncIssuablesStateId'.freeze - - # 2019-02-12 Gitlab.com issuable numbers - # issues count: 13587305 - # merge requests count: 18925274 - # Using this 25000 as batch size should take around 26 hours - # to migrate both issues and merge requests - BATCH_SIZE = 25000 - DELAY_INTERVAL = 5.minutes.to_i - - class Issue < ActiveRecord::Base - include EachBatch - - self.table_name = 'issues' - end - - class MergeRequest < ActiveRecord::Base - include EachBatch - - self.table_name = 'merge_requests' - end def up add_column :issues, :state_id, :integer, limit: 2 add_column :merge_requests, :state_id, :integer, limit: 2 - - # Is this safe? - # Added to avoid an warning about jobs running inside transactions. - # Since we only add a column this should be ok - Sidekiq::Worker.skipping_transaction_check do - queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) - queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) - end end def down diff --git a/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb b/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb new file mode 100644 index 00000000000..d9b77d2f02c --- /dev/null +++ b/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ScheduleSyncIssuablesStateId < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # 2019-02-12 Gitlab.com issuable numbers + # issues count: 13587305 + # merge requests count: 18925274 + # Using this 25000 as batch size should take around 26 hours + # to migrate both issues and merge requests + BATCH_SIZE = 25000 + DELAY_INTERVAL = 5.minutes.to_i + ISSUE_MIGRATION = 'SyncIssuesStateId'.freeze + MERGE_REQUEST_MIGRATION = 'SyncMergeRequestsStateId'.freeze + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + end + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + end + + def up + Sidekiq::Worker.skipping_transaction_check do + queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), ISSUE_MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), MERGE_REQUEST_MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + end + + def down + # No op + end +end diff --git a/db/schema.rb b/db/schema.rb index 0fb31ce2ef2..4a347e7289b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190211131150) do +ActiveRecord::Schema.define(version: 20190214112022) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/gitlab/background_migration/sync_issuables_state_id.rb b/lib/gitlab/background_migration/sync_issuables_state_id.rb deleted file mode 100644 index 95734a7310e..00000000000 --- a/lib/gitlab/background_migration/sync_issuables_state_id.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true -# rubocop:disable Style/Documentation - -module Gitlab - module BackgroundMigration - class SyncIssuablesStateId - def perform(start_id, end_id, table_name) - populate_new_state_id(start_id, end_id, table_name) - end - - def populate_new_state_id(start_id, end_id, table_name) - Rails.logger.info("#{table_name} - Populating state_id: #{start_id} - #{end_id}") - - ActiveRecord::Base.connection.execute <<~SQL - UPDATE #{table_name} - SET state_id = - CASE state - WHEN 'opened' THEN 1 - WHEN 'closed' THEN 2 - WHEN 'merged' THEN 3 - WHEN 'locked' THEN 4 - END - WHERE state_id IS NULL - AND id BETWEEN #{start_id} AND #{end_id} - SQL - end - end - end -end diff --git a/lib/gitlab/background_migration/sync_issues_state_id.rb b/lib/gitlab/background_migration/sync_issues_state_id.rb new file mode 100644 index 00000000000..33b997c8533 --- /dev/null +++ b/lib/gitlab/background_migration/sync_issues_state_id.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class SyncIssuesStateId + def perform(start_id, end_id) + Rails.logger.info("Issues - Populating state_id: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~SQL + UPDATE issues + SET state_id = + CASE state + WHEN 'opened' THEN 1 + WHEN 'closed' THEN 2 + END + WHERE state_id IS NULL + AND id BETWEEN #{start_id} AND #{end_id} + SQL + end + end + end +end diff --git a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb new file mode 100644 index 00000000000..923ceaeec54 --- /dev/null +++ b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class SyncMergeRequestsStateId + def perform(start_id, end_id) + Rails.logger.info("Merge Requests - Populating state_id: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~SQL + UPDATE merge_requests + SET state_id = + CASE state + WHEN 'opened' THEN 1 + WHEN 'closed' THEN 2 + WHEN 'merged' THEN 3 + WHEN 'locked' THEN 4 + END + WHERE state_id IS NULL + AND id BETWEEN #{start_id} AND #{end_id} + SQL + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 46b36d07c20..3abd0600e9d 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1029,12 +1029,11 @@ into similar problems in the future (e.g. when new tables are created). model_class.each_batch(of: batch_size) do |relation, index| start_id, end_id = relation.pluck('MIN(id), MAX(id)').first - table_name = relation.base_class.table_name # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for # the same time, which is not helpful in most cases where we wish to # spread the work over time. - BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id, table_name]) + BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id]) end end diff --git a/spec/migrations/add_state_id_to_issuables_spec.rb b/spec/migrations/add_state_id_to_issuables_spec.rb deleted file mode 100644 index b0e285db1f3..00000000000 --- a/spec/migrations/add_state_id_to_issuables_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'migrate', '20190211131150_add_state_id_to_issuables.rb') - -describe AddStateIdToIssuables, :migration do - let(:namespaces) { table(:namespaces) } - let(:projects) { table(:projects) } - let(:merge_requests) { table(:merge_requests) } - let(:issues) { table(:issues) } - let(:migration) { described_class.new } - - before do - @group = namespaces.create!(name: 'gitlab', path: 'gitlab') - @project = projects.create!(namespace_id: @group.id) - end - - describe '#up' do - context 'issues' do - it 'migrates state column to integer' do - opened_issue = issues.create!(description: 'first', state: 'opened') - closed_issue = issues.create!(description: 'second', state: 'closed') - invalid_state_issue = issues.create!(description: 'fourth', state: 'not valid') - nil_state_issue = issues.create!(description: 'third', state: nil) - - migrate! - - expect(opened_issue.reload.state_id).to eq(Issue.states.opened) - expect(closed_issue.reload.state_id).to eq(Issue.states.closed) - expect(invalid_state_issue.reload.state_id).to be_nil - expect(nil_state_issue.reload.state_id).to be_nil - end - end - - context 'merge requests' do - it 'migrates state column to integer' do - opened_merge_request = merge_requests.create!(state: 'opened', target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') - closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') - merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') - locked_merge_request = merge_requests.create!(state: 'locked', target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') - invalid_state_merge_request = merge_requests.create!(state: 'not valid', target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') - - migrate! - - expect(opened_merge_request.reload.state_id).to eq(MergeRequest.states.opened) - expect(closed_merge_request.reload.state_id).to eq(MergeRequest.states.closed) - expect(merged_merge_request.reload.state_id).to eq(MergeRequest.states.merged) - expect(locked_merge_request.reload.state_id).to eq(MergeRequest.states.locked) - expect(invalid_state_merge_request.reload.state_id).to be_nil - end - end - end -end diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb new file mode 100644 index 00000000000..a926ee38387 --- /dev/null +++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190214112022_schedule_sync_issuables_state_id.rb') + +describe ScheduleSyncIssuablesStateId, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:merge_requests) { table(:merge_requests) } + let(:issues) { table(:issues) } + let(:migration) { described_class.new } + + before do + @group = namespaces.create!(name: 'gitlab', path: 'gitlab') + @project = projects.create!(namespace_id: @group.id) + end + + describe '#up' do + context 'issues' do + it 'migrates state column to integer' do + opened_issue = issues.create!(description: 'first', state: 'opened') + closed_issue = issues.create!(description: 'second', state: 'closed') + invalid_state_issue = issues.create!(description: 'fourth', state: 'not valid') + nil_state_issue = issues.create!(description: 'third', state: nil) + + migrate! + + expect(opened_issue.reload.state_id).to eq(Issue.available_states[:opened]) + expect(closed_issue.reload.state_id).to eq(Issue.available_states[:closed]) + expect(invalid_state_issue.reload.state_id).to be_nil + expect(nil_state_issue.reload.state_id).to be_nil + end + end + + context 'merge requests' do + it 'migrates state column to integer' do + opened_merge_request = merge_requests.create!(state: 'opened', target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') + closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') + merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') + locked_merge_request = merge_requests.create!(state: 'locked', target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') + invalid_state_merge_request = merge_requests.create!(state: 'not valid', target_project_id: @project.id, target_branch: 'feature5', source_branch: 'master') + + migrate! + + expect(opened_merge_request.reload.state_id).to eq(MergeRequest.available_states[:opened]) + expect(closed_merge_request.reload.state_id).to eq(MergeRequest.available_states[:closed]) + expect(merged_merge_request.reload.state_id).to eq(MergeRequest.available_states[:merged]) + expect(locked_merge_request.reload.state_id).to eq(MergeRequest.available_states[:locked]) + expect(invalid_state_merge_request.reload.state_id).to be_nil + end + end + end +end -- cgit v1.2.1 From 30ab6ee416783cd9481085f021603383eeb4f317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Mon, 14 Jan 2019 11:46:39 +0100 Subject: Check issue milestone availability Add project when creating milestone in specs We validate milestone is from the same project/parent group as issuable -> we need to set project in specs correctly Improve methods names and specs organization --- app/models/concerns/issuable.rb | 11 +++ app/models/merge_request.rb | 7 +- app/services/issuable_base_service.rb | 6 ++ app/services/issues/build_service.rb | 4 +- app/services/merge_requests/build_service.rb | 1 + .../unreleased/51971-milestones-visibility.yml | 5 ++ .../dashboard/milestones_controller_spec.rb | 2 +- spec/features/issues_spec.rb | 4 +- .../user_lists_merge_requests_spec.rb | 4 +- spec/models/concerns/issuable_spec.rb | 97 ++++++++++++++++++++-- spec/models/issue/metrics_spec.rb | 6 +- spec/models/milestone_spec.rb | 8 +- spec/requests/api/issues_spec.rb | 2 +- .../issuable/common_system_notes_service_spec.rb | 4 +- spec/services/issues/build_service_spec.rb | 76 ++++++++--------- spec/services/issues/update_service_spec.rb | 6 +- spec/services/merge_requests/build_service_spec.rb | 9 ++ .../services/merge_requests/update_service_spec.rb | 6 +- .../shared_examples/issuable_shared_examples.rb | 2 +- 19 files changed, 187 insertions(+), 73 deletions(-) create mode 100644 changelogs/unreleased/51971-milestones-visibility.yml diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0a77fbeba08..8918dc8f417 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -74,6 +74,7 @@ module Issuable validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } + validate :milestone_is_valid scope :authored, ->(user) { where(author_id: user) } scope :recent, -> { reorder(id: :desc) } @@ -117,6 +118,16 @@ module Issuable def has_multiple_assignees? assignees.count > 1 end + + def milestone_available? + project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group) + end + + private + + def milestone_is_valid + errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available? + end end class_methods do diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2035bffd829..a551e3e387d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -189,6 +189,9 @@ class MergeRequest < ActiveRecord::Base after_save :keep_around_commit + alias_attribute :project, :target_project + alias_attribute :project_id, :target_project_id + def self.reference_prefix '!' end @@ -837,10 +840,6 @@ class MergeRequest < ActiveRecord::Base target_project != source_project end - def project - target_project - end - # If the merge request closes any issues, save this information in the # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ef991eaf234..1e1f2fbd08e 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -387,4 +387,10 @@ class IssuableBaseService < BaseService def parent project end + + # we need to check this because milestone from milestone_id param is displayed on "new" page + # where private project milestone could leak without this check + def ensure_milestone_available(issuable) + issuable.milestone_id = nil unless issuable.milestone_available? + end end diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 52b45f1b2ce..77724e78972 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -6,7 +6,9 @@ module Issues def execute filter_resolve_discussion_params - @issue = project.issues.new(issue_params) + @issue = project.issues.new(issue_params).tap do |issue| + ensure_milestone_available(issue) + end end def issue_params_with_info_from_discussions diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 48419da98ad..109c964e577 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -19,6 +19,7 @@ module MergeRequests merge_request.target_project = find_target_project merge_request.target_branch = find_target_branch merge_request.can_be_created = projects_and_branches_valid? + ensure_milestone_available(merge_request) # compare branches only if branches are valid, otherwise # compare_branches may raise an error diff --git a/changelogs/unreleased/51971-milestones-visibility.yml b/changelogs/unreleased/51971-milestones-visibility.yml new file mode 100644 index 00000000000..818f0071e6c --- /dev/null +++ b/changelogs/unreleased/51971-milestones-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Check if desired milestone for an issue is available +merge_request: +author: +type: security diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 8b176e07bc8..ddf33ebad16 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -15,7 +15,7 @@ describe Dashboard::MilestonesController do ) end let(:issue) { create(:issue, project: project, milestone: project_milestone) } - let(:group_issue) { create(:issue, milestone: group_milestone) } + let(:group_issue) { create(:issue, milestone: group_milestone, project: create(:project, group: group)) } let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } let!(:group_label) { create(:group_label, group: group, title: 'Group Issue Label', issues: [group_issue]) } diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 406e80e91aa..9bc340ed4bb 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -233,8 +233,8 @@ describe 'Issues' do created_at: Time.now - (index * 60)) end end - let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') } - let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } + let(:newer_due_milestone) { create(:milestone, project: project, due_date: '2013-12-11') } + let(:later_due_milestone) { create(:milestone, project: project, due_date: '2013-12-12') } it 'sorts by newest' do visit project_issues_path(project, sort: sort_value_created_date) diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index ef7ae490b0f..c691011b9ca 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -13,7 +13,7 @@ describe 'Merge requests > User lists merge requests' do source_project: project, source_branch: 'fix', assignee: user, - milestone: create(:milestone, due_date: '2013-12-11'), + milestone: create(:milestone, project: project, due_date: '2013-12-11'), created_at: 1.minute.ago, updated_at: 1.minute.ago) create(:merge_request, @@ -21,7 +21,7 @@ describe 'Merge requests > User lists merge requests' do source_project: project, source_branch: 'markdown', assignee: user, - milestone: create(:milestone, due_date: '2013-12-12'), + milestone: create(:milestone, project: project, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) create(:merge_request, diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 41159348e04..72c6161424b 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,17 +32,56 @@ describe Issuable do end describe "Validation" do - subject { build(:issue) } + context 'general validations' do + subject { build(:issue) } - before do - allow(InternalId).to receive(:generate_next).and_return(nil) + before do + allow(InternalId).to receive(:generate_next).and_return(nil) + end + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:iid) } + it { is_expected.to validate_presence_of(:author) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } end - it { is_expected.to validate_presence_of(:project) } - it { is_expected.to validate_presence_of(:iid) } - it { is_expected.to validate_presence_of(:author) } - it { is_expected.to validate_presence_of(:title) } - it { is_expected.to validate_length_of(:title).is_at_most(255) } + describe 'milestone' do + let(:project) { create(:project) } + let(:milestone_id) { create(:milestone, project: project).id } + let(:params) do + { + title: 'something', + project: project, + author: build(:user), + milestone_id: milestone_id + } + end + + subject { issuable_class.new(params) } + + context 'with correct params' do + it { is_expected.to be_valid } + end + + context 'with empty string milestone' do + let(:milestone_id) { '' } + + it { is_expected.to be_valid } + end + + context 'with nil milestone id' do + let(:milestone_id) { nil } + + it { is_expected.to be_valid } + end + + context 'with a milestone id from another project' do + let(:milestone_id) { create(:milestone).id } + + it { is_expected.to be_invalid } + end + end end describe "Scope" do @@ -66,6 +105,48 @@ describe Issuable do end end + describe '#milestone_available?' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:issue) { create(:issue, project: project) } + + def build_issuable(milestone_id) + issuable_class.new(project: project, milestone_id: milestone_id) + end + + it 'returns true with a milestone from the issue project' do + milestone = create(:milestone, project: project) + + expect(build_issuable(milestone.id).milestone_available?).to be_truthy + end + + it 'returns true with a milestone from the issue project group' do + milestone = create(:milestone, group: group) + + expect(build_issuable(milestone.id).milestone_available?).to be_truthy + end + + it 'returns true with a milestone from the the parent of the issue project group', :nested_groups do + parent = create(:group) + group.update(parent: parent) + milestone = create(:milestone, group: parent) + + expect(build_issuable(milestone.id).milestone_available?).to be_truthy + end + + it 'returns false with a milestone from another project' do + milestone = create(:milestone) + + expect(build_issuable(milestone.id).milestone_available?).to be_falsey + end + + it 'returns false with a milestone from another group' do + milestone = create(:milestone, group: create(:group)) + + expect(build_issuable(milestone.id).milestone_available?).to be_falsey + end + end + describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") } let!(:searchable_issue2) { create(:issue, title: 'Aw') } diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb index 1bf0ecb98ad..b7291eebe64 100644 --- a/spec/models/issue/metrics_spec.rb +++ b/spec/models/issue/metrics_spec.rb @@ -9,7 +9,7 @@ describe Issue::Metrics do context "milestones" do it "records the first time an issue is associated with a milestone" do time = Time.now - Timecop.freeze(time) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) } metrics = subject.metrics expect(metrics).to be_present @@ -18,9 +18,9 @@ describe Issue::Metrics do it "does not record the second time an issue is associated with a milestone" do time = Time.now - Timecop.freeze(time) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) } Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) } - Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone, project: project)) } metrics = subject.metrics expect(metrics).to be_present diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index af7e3d3a6c9..77b7042424c 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -182,7 +182,7 @@ describe Milestone do describe '#total_items_count' do before do create :closed_issue, milestone: milestone, project: project - create :merge_request, milestone: milestone + create :merge_request, milestone: milestone, source_project: project end it 'returns total count of issues and merge requests assigned to milestone' do @@ -192,10 +192,10 @@ describe Milestone do describe '#can_be_closed?' do before do - milestone = create :milestone - create :closed_issue, milestone: milestone + milestone = create :milestone, project: project + create :closed_issue, milestone: milestone, project: project - create :issue + create :issue, project: project end it 'returns true if milestone active and all nested issues closed' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 04908378a24..46cd3ec88e1 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -49,7 +49,7 @@ describe API::Issues do create(:label, title: 'label', color: '#FFAABB', project: project) end let!(:label_link) { create(:label_link, label: label, target: issue) } - set(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } set(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index fa5d5ebac5c..0edc9016c96 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Issuable::CommonSystemNotesService do let(:user) { create(:user) } let(:project) { create(:project) } - let(:issuable) { create(:issue) } + let(:issuable) { create(:issue, project: project) } context 'on issuable update' do it_behaves_like 'system note creation', { title: 'New title' }, 'changed title' @@ -70,7 +70,7 @@ describe Issuable::CommonSystemNotesService do end context 'on issuable create' do - let(:issuable) { build(:issue) } + let(:issuable) { build(:issue, project: project) } subject { described_class.new(project, user).execute(issuable, old_labels: [], is_update: false) } diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 248e7d5a389..86e58fe06b9 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -8,29 +8,29 @@ describe Issues::BuildService do project.add_developer(user) end + def build_issue(issue_params = {}) + described_class.new(project, user, issue_params).execute + end + context 'for a single discussion' do describe '#execute' do let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) } let(:discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done").to_discussion } - let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) } - it 'references the noteable title in the issue title' do - issue = service.execute + subject { build_issue(merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) } - expect(issue.title).to include('Hello world') + it 'references the noteable title in the issue title' do + expect(subject.title).to include('Hello world') end it 'adds the note content to the description' do - issue = service.execute - - expect(issue.description).to include('Almost done') + expect(subject.description).to include('Almost done') end end end context 'for discussions in a merge request' do let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } - let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute } describe '#items_for_discussions' do it 'has an item for each discussion' do @@ -66,28 +66,30 @@ describe Issues::BuildService do end describe '#execute' do - it 'has the merge request reference in the title' do - expect(issue.title).to include(merge_request.title) - end + let(:base_params) { { merge_request_to_resolve_discussions_of: merge_request.iid } } - it 'has the reference of the merge request in the description' do - expect(issue.description).to include(merge_request.to_reference) + context 'without additional params' do + subject { build_issue(base_params) } + + it 'has the merge request reference in the title' do + expect(subject.title).to include(merge_request.title) + end + + it 'has the reference of the merge request in the description' do + expect(subject.description).to include(merge_request.to_reference) + end end - it 'does not assign title when a title was given' do - issue = described_class.new(project, user, - merge_request_to_resolve_discussions_of: merge_request, - title: 'What an issue').execute + it 'uses provided title if title param given' do + issue = build_issue(base_params.merge(title: 'What an issue')) expect(issue.title).to eq('What an issue') end - it 'does not assign description when a description was given' do - issue = described_class.new(project, user, - merge_request_to_resolve_discussions_of: merge_request, - description: 'Fix at your earliest conveignance').execute + it 'uses provided description if description param given' do + issue = build_issue(base_params.merge(description: 'Fix at your earliest convenience')) - expect(issue.description).to eq('Fix at your earliest conveignance') + expect(issue.description).to eq('Fix at your earliest convenience') end describe 'with multiple discussions' do @@ -96,20 +98,20 @@ describe Issues::BuildService do it 'mentions all the authors in the description' do authors = merge_request.resolvable_discussions.map(&:author) - expect(issue.description).to include(*authors.map(&:to_reference)) + expect(build_issue(base_params).description).to include(*authors.map(&:to_reference)) end it 'has a link for each unresolved discussion in the description' do notes = merge_request.resolvable_discussions.map(&:first_note) links = notes.map { |note| Gitlab::UrlBuilder.build(note) } - expect(issue.description).to include(*links) + expect(build_issue(base_params).description).to include(*links) end it 'mentions additional notes' do create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, in_reply_to: diff_note) - expect(issue.description).to include('(+2 comments)') + expect(build_issue(base_params).description).to include('(+2 comments)') end end end @@ -120,7 +122,7 @@ describe Issues::BuildService do describe '#execute' do it 'mentions the merge request in the description' do - issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute + issue = build_issue(merge_request_to_resolve_discussions_of: merge_request.iid) expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}") end @@ -128,20 +130,18 @@ describe Issues::BuildService do end describe '#execute' do - let(:milestone) { create(:milestone, project: project) } - it 'builds a new issues with given params' do - issue = described_class.new( - project, - user, - title: 'Issue #1', - description: 'Issue description', - milestone_id: milestone.id - ).execute - - expect(issue.title).to eq('Issue #1') - expect(issue.description).to eq('Issue description') + milestone = create(:milestone, project: project) + issue = build_issue(milestone_id: milestone.id) + expect(issue.milestone).to eq(milestone) end + + it 'sets milestone to nil if it is not available for the project' do + milestone = create(:milestone, project: create(:project)) + issue = build_issue(milestone_id: milestone.id) + + expect(issue.milestone).to be_nil + end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 931e47d3a77..f1684209729 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -356,7 +356,7 @@ describe Issues::UpdateService, :mailer do it_behaves_like 'system notes for milestones' it 'sends notifications for subscribers of changed milestone' do - issue.milestone = create(:milestone) + issue.milestone = create(:milestone, project: project) issue.save @@ -380,7 +380,7 @@ describe Issues::UpdateService, :mailer do end it 'marks todos as done' do - update_issue(milestone: create(:milestone)) + update_issue(milestone: create(:milestone, project: project)) expect(todo.reload.done?).to eq true end @@ -389,7 +389,7 @@ describe Issues::UpdateService, :mailer do it 'sends notifications for subscribers of changed milestone' do perform_enqueued_jobs do - update_issue(milestone: create(:milestone)) + update_issue(milestone: create(:milestone, project: project)) end should_email(subscriber) diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 536d0d345a4..057e8137a4e 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -229,6 +229,15 @@ describe MergeRequests::BuildService do end end end + + context 'when a milestone is from another project' do + let(:milestone) { create(:milestone, project: create(:project)) } + let(:milestone_id) { milestone.id } + + it 'sets milestone to nil' do + expect(merge_request.milestone).to be_nil + end + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 20580bf14b9..8e367db031c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -328,7 +328,7 @@ describe MergeRequests::UpdateService, :mailer do it_behaves_like 'system notes for milestones' it 'sends notifications for subscribers of changed milestone' do - merge_request.milestone = create(:milestone) + merge_request.milestone = create(:milestone, project: project) merge_request.save @@ -352,7 +352,7 @@ describe MergeRequests::UpdateService, :mailer do end it 'marks pending todos as done' do - update_merge_request({ milestone: create(:milestone) }) + update_merge_request({ milestone: create(:milestone, project: project) }) expect(pending_todo.reload).to be_done end @@ -361,7 +361,7 @@ describe MergeRequests::UpdateService, :mailer do it 'sends notifications for subscribers of changed milestone' do perform_enqueued_jobs do - update_merge_request(milestone: create(:milestone)) + update_merge_request(milestone: create(:milestone, project: project)) end should_email(subscriber) diff --git a/spec/support/shared_examples/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb index c3d40c5b231..d97b21f71cd 100644 --- a/spec/support/shared_examples/issuable_shared_examples.rb +++ b/spec/support/shared_examples/issuable_shared_examples.rb @@ -31,7 +31,7 @@ shared_examples 'system notes for milestones' do context 'project milestones' do it 'creates a system note' do expect do - update_issuable(milestone: create(:milestone)) + update_issuable(milestone: create(:milestone, project: project)) end.to change { Note.system.count }.by(1) end end -- cgit v1.2.1 From 8ad127083ef28cfea389ec00a6f7ebf39e15c431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Mon, 11 Feb 2019 11:31:30 +0100 Subject: Show only MRs visible to user on milestone detail --- app/controllers/concerns/milestone_actions.rb | 2 +- app/models/concerns/milestoneish.rb | 11 ++++- .../unreleased/security-2797-milestone-mrs.yml | 5 +++ spec/models/concerns/milestoneish_spec.rb | 47 +++++++++++++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/security-2797-milestone-mrs.yml diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index eccbe35577b..c0c0160a827 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -8,7 +8,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { - merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables + merge_requests: @milestone.sorted_merge_requests(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables show_project_name: true }) end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 055ffe04646..39372c4f68b 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -46,12 +46,19 @@ module Milestoneish end end + def merge_requests_visible_to_user(user) + memoize_per_user(user, :merge_requests_visible_to_user) do + MergeRequestsFinder.new(user, {}) + .execute.where(milestone_id: milestoneish_id) + end + end + def sorted_issues(user) issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority') end - def sorted_merge_requests - merge_requests.sort_by_attribute('label_priority') + def sorted_merge_requests(user) + merge_requests_visible_to_user(user).sort_by_attribute('label_priority') end def upcoming? diff --git a/changelogs/unreleased/security-2797-milestone-mrs.yml b/changelogs/unreleased/security-2797-milestone-mrs.yml new file mode 100644 index 00000000000..5bb104ec403 --- /dev/null +++ b/changelogs/unreleased/security-2797-milestone-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Show only merge requests visible to user on milestone detail page +merge_request: +author: +type: security diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 87bf731340f..4647eecbdef 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -48,7 +48,7 @@ describe Milestone, 'Milestoneish' do merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone) merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone) - merge_requests = milestone.sorted_merge_requests + merge_requests = milestone.sorted_merge_requests(member) expect(merge_requests.first).to eq(merge_request_2) expect(merge_requests.second).to eq(merge_request_1) @@ -56,6 +56,51 @@ describe Milestone, 'Milestoneish' do end end + describe '#merge_requests_visible_to_user' do + let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + context 'when project is private' do + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'does not return any merge request for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to be_empty + end + + it 'returns milestone merge requests for a member' do + merge_requests = milestone.merge_requests_visible_to_user(member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + + context 'when project is public' do + context 'when merge requests are available to anyone' do + it 'returns milestone merge requests for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + + context 'when merge requests are available to project members' do + before do + project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'does not return any merge request for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to be_empty + end + + it 'returns milestone merge requests for a member' do + merge_requests = milestone.merge_requests_visible_to_user(member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + end + end + describe '#closed_items_count' do it 'does not count confidential issues for non project members' do expect(milestone.closed_items_count(non_member)).to eq 2 -- cgit v1.2.1 From d4a5d8d07069acb6f068990633baaf56d20bc18b Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 14 Feb 2019 14:24:23 -0200 Subject: Add specs for issuable states sync --- app/models/concerns/issuable_states.rb | 1 + spec/models/concerns/issuable_states_spec.rb | 30 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 spec/models/concerns/issuable_states_spec.rb diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb index d7992f8e6db..a39a0ef77ed 100644 --- a/app/models/concerns/issuable_states.rb +++ b/app/models/concerns/issuable_states.rb @@ -4,6 +4,7 @@ module IssuableStates # The state:string column is being migrated to state_id:integer column # This is a temporary hook to populate state_id column with new values # and can be removed after the state column is removed. + # Check https://gitlab.com/gitlab-org/gitlab-ce/issues/51789 for more information included do before_save :set_state_id end diff --git a/spec/models/concerns/issuable_states_spec.rb b/spec/models/concerns/issuable_states_spec.rb new file mode 100644 index 00000000000..70450159cc0 --- /dev/null +++ b/spec/models/concerns/issuable_states_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# This spec checks if state_id column of issues and merge requests +# are being synced on every save. +# It can be removed in the next release. Check https://gitlab.com/gitlab-org/gitlab-ce/issues/51789 for more information. +describe IssuableStates do + [Issue, MergeRequest].each do |klass| + it "syncs state_id column when #{klass.model_name.human} gets created" do + klass.available_states.each do |state, state_id| + issuable = build(klass.model_name.param_key, state: state.to_s) + + issuable.save! + + expect(issuable.state_id).to eq(state_id) + end + end + + it "syncs state_id column when #{klass.model_name.human} gets updated" do + klass.available_states.each do |state, state_id| + issuable = create(klass.model_name.param_key, state: state.to_s) + + issuable.update(state: state) + + expect(issuable.state_id).to eq(state_id) + end + end + end +end -- cgit v1.2.1 From 2fd784a4c1cb991391399ac369bfb008db697380 Mon Sep 17 00:00:00 2001 From: Nathan Friend Date: Thu, 14 Feb 2019 13:26:17 -0400 Subject: Update border radius of form controls and remove space above page titles This commit makes changes to global CSS styles to bring the product closer to compliance with Pajamas (the GitLab design system). --- app/assets/stylesheets/framework/forms.scss | 2 +- app/assets/stylesheets/framework/page_title.scss | 1 - .../nfriend-css-updates-for-gitlab-design-system-compliance.yml | 5 +++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index cbf9ee24ec5..94fc684ca5f 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -128,7 +128,7 @@ label { .form-control { @include box-shadow(none); - border-radius: 2px; + border-radius: $border-radius-default; padding: $gl-vert-padding $gl-input-padding; &.input-short { diff --git a/app/assets/stylesheets/framework/page_title.scss b/app/assets/stylesheets/framework/page_title.scss index e8302953a63..ad6575761b5 100644 --- a/app/assets/stylesheets/framework/page_title.scss +++ b/app/assets/stylesheets/framework/page_title.scss @@ -2,7 +2,6 @@ @extend .d-flex; @extend .align-items-center; - padding-top: $gl-padding-top; border-bottom: 1px solid $border-color; .page-title { diff --git a/changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml b/changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml new file mode 100644 index 00000000000..8cde0958f7a --- /dev/null +++ b/changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml @@ -0,0 +1,5 @@ +--- +title: Update `border-radius` of form controls and remove extra space above page titles +merge_request: 24497 +author: +type: fixed -- cgit v1.2.1 From 9671a67a4ce58683ca0188ff9e75b1d5dfcc5dec Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 14 Feb 2019 16:33:26 -0200 Subject: Fix broken specs --- app/models/concerns/issuable_states.rb | 10 +++++++++- app/models/merge_request.rb | 2 +- db/migrate/20190211131150_add_state_id_to_issuables.rb | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb index a39a0ef77ed..12fa97a1469 100644 --- a/app/models/concerns/issuable_states.rb +++ b/app/models/concerns/issuable_states.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module IssuableStates extend ActiveSupport::Concern # The state:string column is being migrated to state_id:integer column # This is a temporary hook to populate state_id column with new values - # and can be removed after the state column is removed. + # and should be removed after the state column is removed. # Check https://gitlab.com/gitlab-org/gitlab-ce/issues/51789 for more information included do before_save :set_state_id @@ -12,6 +14,12 @@ module IssuableStates def set_state_id return if state.nil? || state.empty? + # Needed to prevent breaking some migration specs that + # rollback database to a point where state_id does not exist. + # We can use this guard clause for now since this file will should + # be removed in the next release. + return unless self.respond_to?(:state_id) + states_hash = self.class.available_states self.state_id = states_hash[state] diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0ec0789a24f..063433111cc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -194,7 +194,7 @@ class MergeRequest < ActiveRecord::Base end def self.available_states - @states ||= super.merge(merged: 3, locked: 4) + @available_states ||= super.merge(merged: 3, locked: 4) end def rebase_in_progress? diff --git a/db/migrate/20190211131150_add_state_id_to_issuables.rb b/db/migrate/20190211131150_add_state_id_to_issuables.rb index cf3f7671a67..c1173eb4249 100644 --- a/db/migrate/20190211131150_add_state_id_to_issuables.rb +++ b/db/migrate/20190211131150_add_state_id_to_issuables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddStateIdToIssuables < ActiveRecord::Migration[5.0] include Gitlab::Database::MigrationHelpers -- cgit v1.2.1 From 7b79f6ab0d342e335609a7f3eb6cb691d1f03111 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 14 Feb 2019 16:37:12 -0200 Subject: fix typo --- app/models/concerns/issuable_states.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb index 12fa97a1469..2ebd5013a4c 100644 --- a/app/models/concerns/issuable_states.rb +++ b/app/models/concerns/issuable_states.rb @@ -16,8 +16,8 @@ module IssuableStates # Needed to prevent breaking some migration specs that # rollback database to a point where state_id does not exist. - # We can use this guard clause for now since this file will should - # be removed in the next release. + # We can use this guard clause for now since this file will + # be removed on a future the next release. return unless self.respond_to?(:state_id) states_hash = self.class.available_states -- cgit v1.2.1 From bf99ce7bf8bf4bda75ca57f5db4d216f3585615a Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 15 Feb 2019 14:37:55 -0200 Subject: Fix specs --- spec/db/schema_spec.rb | 4 ++-- spec/lib/gitlab/import_export/safe_model_attributes.yml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 897b4411055..40c3a6d90d0 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -25,12 +25,12 @@ describe 'Database schema' do events: %w[target_id], forked_project_links: %w[forked_from_project_id], identities: %w[user_id], - issues: %w[last_edited_by_id], + issues: %w[last_edited_by_id state_id], keys: %w[user_id], label_links: %w[target_id], lfs_objects_projects: %w[lfs_object_id project_id], members: %w[source_id created_by_id], - merge_requests: %w[last_edited_by_id], + merge_requests: %w[last_edited_by_id state_id], namespaces: %w[owner_id parent_id], notes: %w[author_id commit_id noteable_id updated_by_id resolved_by_id discussion_id], notification_settings: %w[source_id], diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index baca8f6d542..9cfa29ca7d9 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -11,6 +11,7 @@ Issue: - branch_name - description - state +- state_id - iid - updated_by_id - confidential @@ -158,6 +159,7 @@ MergeRequest: - created_at - updated_at - state +- state_id - merge_status - target_project_id - iid -- cgit v1.2.1 From b1346db3a0b49b295a9702921285fdb30029563c Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 15 Feb 2019 17:11:41 -0200 Subject: Add Reschedulable module --- lib/gitlab/background_migration.rb | 2 + .../background_migration/concerns/reschedulable.rb | 50 ++++++++++++++++++++++ .../background_migration/delete_diff_files.rb | 37 +++------------- .../background_migration/delete_diff_files_spec.rb | 10 ++--- 4 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 lib/gitlab/background_migration/concerns/reschedulable.rb diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index 5251e0fadf9..b308e94bfa0 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +Dir[Rails.root.join("lib/gitlab/background_migration/concerns/*.rb")].each { |f| require f } + module Gitlab module BackgroundMigration def self.queue diff --git a/lib/gitlab/background_migration/concerns/reschedulable.rb b/lib/gitlab/background_migration/concerns/reschedulable.rb new file mode 100644 index 00000000000..fbf3d799743 --- /dev/null +++ b/lib/gitlab/background_migration/concerns/reschedulable.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module Reschedulable + extend ActiveSupport::Concern + + def reschedule_if_needed(args, &block) + if should_reschedule? + BackgroundMigrationWorker.perform_in(vacuum_wait_time, self.class.name.demodulize, args) + else + yield + end + end + + # Override this on base class if you need a different reschedule condition + def should_reschedule? + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end + + def wait_for_deadtuple_vacuum?(table_name) + return false unless Gitlab::Database.postgresql? + + dead_tuples_count_for(table_name) >= dead_tuples_threshold + end + + def dead_tuples_count_for(table_name) + dead_tuple = + execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ + "WHERE relname = '#{table_name}'")[0] + + dead_tuple&.fetch('n_dead_tup', 0).to_i + end + + def execute_statement(sql) + ActiveRecord::Base.connection.execute(sql) + end + + # Override in subclass if you need a different dead tuple threshold + def dead_tuples_threshold + @dead_tuples_threshold ||= 50_000 + end + + # Override in subclass if you need a different vacuum wait time + def vacuum_wait_time + @vacuum_wait_time ||= 5.minutes + end + end + end +end diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb index 664ead1af44..0a7cbd5c30f 100644 --- a/lib/gitlab/background_migration/delete_diff_files.rb +++ b/lib/gitlab/background_migration/delete_diff_files.rb @@ -4,6 +4,8 @@ module Gitlab module BackgroundMigration class DeleteDiffFiles + include Reschedulable + class MergeRequestDiff < ActiveRecord::Base self.table_name = 'merge_request_diffs' @@ -15,47 +17,24 @@ module Gitlab self.table_name = 'merge_request_diff_files' end - DEAD_TUPLES_THRESHOLD = 50_000 - VACUUM_WAIT_TIME = 5.minutes - def perform(ids) @ids = ids - # We should reschedule until deadtuples get in a desirable - # state (e.g. < 50_000). That may take more than one reschedule. - # - if should_wait_deadtuple_vacuum? - reschedule - return + reschedule_if_needed([ids]) do + prune_diff_files end - - prune_diff_files - end - - def should_wait_deadtuple_vacuum? - return false unless Gitlab::Database.postgresql? - - diff_files_dead_tuples_count >= DEAD_TUPLES_THRESHOLD end private - def reschedule - BackgroundMigrationWorker.perform_in(VACUUM_WAIT_TIME, self.class.name.demodulize, [@ids]) + def should_reschedule? + wait_for_deadtuple_vacuum?(MergeRequestDiffFile.table_name) end def diffs_collection MergeRequestDiff.where(id: @ids) end - def diff_files_dead_tuples_count - dead_tuple = - execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ - "WHERE relname = 'merge_request_diff_files'")[0] - - dead_tuple&.fetch('n_dead_tup', 0).to_i - end - def prune_diff_files removed = 0 updated = 0 @@ -69,10 +48,6 @@ module Gitlab "updated #{updated} merge_request_diffs rows") end - def execute_statement(sql) - ActiveRecord::Base.connection.execute(sql) - end - def log_info(message) Rails.logger.info("BackgroundMigration::DeleteDiffFiles - #{message}") end diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index 27281333348..690881ab59b 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -40,14 +40,14 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end - it 'reschedules itself when should_wait_deadtuple_vacuum' do + it 'reschedules itself when should wait for dead tuple vacuum' do merge_request = create(:merge_request, :merged) first_diff = merge_request.merge_request_diff second_diff = merge_request.create_merge_request_diff Sidekiq::Testing.fake! do worker = described_class.new - allow(worker).to receive(:should_wait_deadtuple_vacuum?) { true } + allow(worker).to receive(:wait_for_deadtuple_vacuum?) { true } worker.perform([first_diff.id, second_diff.id]) @@ -57,10 +57,10 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end - describe '#should_wait_deadtuple_vacuum?' do + describe '#wait_for_deadtuple_vacuum?' do it 'returns true when hitting merge_request_diff_files hits DEAD_TUPLES_THRESHOLD', :postgresql do worker = described_class.new - threshold_query_result = [{ "n_dead_tup" => described_class::DEAD_TUPLES_THRESHOLD.to_s }] + threshold_query_result = [{ "n_dead_tup" => 50_000 }] normal_query_result = [{ "n_dead_tup" => '3' }] allow(worker) @@ -68,7 +68,7 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch .with(/SELECT n_dead_tup */) .and_return(threshold_query_result, normal_query_result) - expect(worker.should_wait_deadtuple_vacuum?).to be(true) + expect(worker.wait_for_deadtuple_vacuum?('merge_request_diff_files')).to be(true) end end end -- cgit v1.2.1 From 75851599d1b7e8f0df7339ab3fcf5cd414deee97 Mon Sep 17 00:00:00 2001 From: Bertrand Jamin Date: Sun, 17 Feb 2019 10:14:25 +0000 Subject: Replace misinterpreted `|` caracter in a table --- doc/api/milestones.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 07e66f89443..fef27c146cc 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -91,7 +91,7 @@ Parameters: - `description` (optional) - The description of a milestone - `due_date` (optional) - The due date of the milestone - `start_date` (optional) - The start date of the milestone -- `state_event` (optional) - The state event of the milestone (close|activate) +- `state_event` (optional) - The state event of the milestone (close or activate) ## Delete project milestone -- cgit v1.2.1 From 618b87448e9167f39d8216d1100733cc0fbf020b Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 31 Jan 2019 17:21:35 +1300 Subject: Prevent leaking of private repo data through API default_branch, statistics and config_ci_path are now only exposed if the user has permissions to the repository. --- lib/api/entities.rb | 9 +++--- lib/api/environments.rb | 8 +++--- lib/api/projects.rb | 24 +++++++++------- spec/requests/api/projects_spec.rb | 59 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 20 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 27da2c2e5ed..46cd4841e2d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -156,7 +156,7 @@ module API class BasicProjectDetails < ProjectIdentity include ::API::ProjectsRelationBuilder - expose :default_branch + expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 expose :tag_list do |project| # project.tags.order(:name).pluck(:name) is the most suitable option @@ -261,7 +261,7 @@ module API expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds, as: :public_jobs - expose :ci_config_path + expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links, options) end @@ -270,8 +270,9 @@ module API expose :only_allow_merge_if_all_discussions_are_resolved expose :printing_merge_request_link_enabled expose :merge_method - - expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { + options[:statistics] && Ability.allowed?(options[:current_user], :download_code, project) + } # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 0278c6c54a5..5b0f3b914cb 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -22,7 +22,7 @@ module API get ':id/environments' do authorize! :read_environment, user_project - present paginate(user_project.environments), with: Entities::Environment + present paginate(user_project.environments), with: Entities::Environment, current_user: current_user end desc 'Creates a new environment' do @@ -40,7 +40,7 @@ module API environment = user_project.environments.create(declared_params) if environment.persisted? - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user else render_validation_error!(environment) end @@ -63,7 +63,7 @@ module API update_params = declared_params(include_missing: false).extract!(:name, :external_url) if environment.update(update_params) - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user else render_validation_error!(environment) end @@ -99,7 +99,7 @@ module API environment.stop_with_action!(current_user) status 200 - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6a93ef9f3ad..2325fc96a67 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -184,7 +184,8 @@ module API if project.saved? present project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) + user_can_admin_project: can?(current_user, :admin_project, project), + current_user: current_user else if project.errors[:limit_reached].present? error!(project.errors[:limit_reached], 403) @@ -217,7 +218,8 @@ module API if project.saved? present project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) + user_can_admin_project: can?(current_user, :admin_project, project), + current_user: current_user else render_validation_error!(project) end @@ -279,7 +281,8 @@ module API conflict!(forked_project.errors.messages) else present forked_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, forked_project) + user_can_admin_project: can?(current_user, :admin_project, forked_project), + current_user: current_user end end @@ -328,7 +331,8 @@ module API if result[:status] == :success present user_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, user_project) + user_can_admin_project: can?(current_user, :admin_project, user_project), + current_user: current_user else render_validation_error!(user_project) end @@ -342,7 +346,7 @@ module API ::Projects::UpdateService.new(user_project, current_user, archived: true).execute - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end desc 'Unarchive a project' do @@ -353,7 +357,7 @@ module API ::Projects::UpdateService.new(@project, current_user, archived: false).execute - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end desc 'Star a project' do @@ -366,7 +370,7 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end end @@ -378,7 +382,7 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user else not_modified! end @@ -414,7 +418,7 @@ module API result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project) if result - present user_project.reload, with: Entities::Project + present user_project.reload, with: Entities::Project, current_user: current_user else render_api_error!("Project already forked", 409) if user_project.forked? end @@ -520,7 +524,7 @@ module API result = ::Projects::TransferService.new(user_project, current_user).execute(namespace) if result - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user else render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400) end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cfa7a1a31a3..1c05be8de70 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -110,6 +110,7 @@ describe API::Projects do end let!(:public_project) { create(:project, :public, name: 'public_project') } + before do project project2 @@ -942,8 +943,16 @@ describe API::Projects do describe 'GET /projects/:id' do context 'when unauthenticated' do - it 'returns the public projects' do - public_project = create(:project, :public) + it 'does not return private projects' do + private_project = create(:project, :private) + + get api("/projects/#{private_project.id}") + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns public projects' do + public_project = create(:project, :repository, :public) get api("/projects/#{public_project.id}") @@ -951,8 +960,34 @@ describe API::Projects do expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) expect(json_response['default_branch']).to eq(public_project.default_branch) + expect(json_response['ci_config_path']).to eq(public_project.ci_config_path) expect(json_response.keys).not_to include('permissions') end + + context 'and the project has a private repository' do + let(:project) { create(:project, :repository, :public, :repository_private) } + let(:protected_attributes) { %w(default_branch ci_config_path) } + + it 'hides protected attributes of private repositories if user is not a member' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + protected_attributes.each do |attribute| + expect(json_response.keys).not_to include(attribute) + end + end + + it 'exposes protected attributes of private repositories if user is a member' do + project.add_developer(user) + + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + protected_attributes.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + end end context 'when authenticated' do @@ -1104,6 +1139,26 @@ describe API::Projects do expect(json_response).to include 'statistics' end + context "and the project has a private repository" do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it "does not include statistics if user is not a member" do + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'statistics' + end + + it "includes statistics if user is a member" do + project.add_developer(user) + + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'statistics' + end + end + it "includes import_error if user can admin project" do get api("/projects/#{project.id}", user) -- cgit v1.2.1 From 19cb1985c76ae756194a05f21af0d02caedb60c2 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 7 Feb 2019 12:04:09 +1300 Subject: Removing sensitive properties from ProjectType defaultBranch and ciConfigPath should only be available to users with the :download_code permission for the Project, as the respository might be private. When implementing the authorize check on these properties, it was found that our current Graphql::Authorize::Instrumentation class does not work with fields that resolve to subclasses of GraphQL::Schema::Scalar, like GraphQL::STRING_TYPE. After discussion with other Create Team members, it has been decided that because the GraphQL API is not GA, to remove these properties from ProjectType, and instead implement them as part of epic https://gitlab.com/groups/gitlab-org/-/epics/711 Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/55316 --- app/graphql/types/project_type.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index d25c8c8bd90..8a6ccb8c5c1 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -16,7 +16,6 @@ module Types field :description, GraphQL::STRING_TYPE, null: true - field :default_branch, GraphQL::STRING_TYPE, null: true field :tag_list, GraphQL::STRING_TYPE, null: true field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true @@ -59,7 +58,6 @@ module Types end field :import_status, GraphQL::STRING_TYPE, null: true - field :ci_config_path, GraphQL::STRING_TYPE, null: true field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true -- cgit v1.2.1 From 0ddba2d773cbbaaa6f0ed8d957f52d1ea792e611 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Mon, 18 Feb 2019 10:23:12 +0900 Subject: Fix author layouts in issuable meta line UIs on mobile Signed-off-by: Takuya Noguchi --- app/helpers/issuables_helper.rb | 2 +- .../unreleased/57829-issuable-meta-line-ui-broken-on-mobile.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/57829-issuable-meta-line-ui-broken-on-mobile.yml diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 1a471034972..7865d3b28a3 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -191,7 +191,7 @@ module IssuablesHelper output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline") - author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none") + author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none") if status = user_status(issuable.author) author_output << "#{status}".html_safe diff --git a/changelogs/unreleased/57829-issuable-meta-line-ui-broken-on-mobile.yml b/changelogs/unreleased/57829-issuable-meta-line-ui-broken-on-mobile.yml new file mode 100644 index 00000000000..b4b305e76d0 --- /dev/null +++ b/changelogs/unreleased/57829-issuable-meta-line-ui-broken-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Fix author layouts in issuable meta line UIs on mobile +merge_request: 25332 +author: Takuya Noguchi +type: fixed -- cgit v1.2.1 From a9886c7dd434d6936c455c9877f6c695cafebe0a Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 18 Feb 2019 11:51:44 -0300 Subject: Fix rubocop --- lib/gitlab/background_migration.rb | 2 +- .../background_migration/concerns/reschedulable.rb | 50 ------------------ .../background_migration/delete_diff_files.rb | 2 +- .../background_migration/helpers/reschedulable.rb | 59 ++++++++++++++++++++++ 4 files changed, 61 insertions(+), 52 deletions(-) delete mode 100644 lib/gitlab/background_migration/concerns/reschedulable.rb create mode 100644 lib/gitlab/background_migration/helpers/reschedulable.rb diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index b308e94bfa0..948488c844c 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -Dir[Rails.root.join("lib/gitlab/background_migration/concerns/*.rb")].each { |f| require f } +Dir[Rails.root.join("lib/gitlab/background_migration/helpers/*.rb")].each { |f| require f } module Gitlab module BackgroundMigration diff --git a/lib/gitlab/background_migration/concerns/reschedulable.rb b/lib/gitlab/background_migration/concerns/reschedulable.rb deleted file mode 100644 index fbf3d799743..00000000000 --- a/lib/gitlab/background_migration/concerns/reschedulable.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module BackgroundMigration - module Reschedulable - extend ActiveSupport::Concern - - def reschedule_if_needed(args, &block) - if should_reschedule? - BackgroundMigrationWorker.perform_in(vacuum_wait_time, self.class.name.demodulize, args) - else - yield - end - end - - # Override this on base class if you need a different reschedule condition - def should_reschedule? - raise NotImplementedError, "#{self.class} does not implement #{__method__}" - end - - def wait_for_deadtuple_vacuum?(table_name) - return false unless Gitlab::Database.postgresql? - - dead_tuples_count_for(table_name) >= dead_tuples_threshold - end - - def dead_tuples_count_for(table_name) - dead_tuple = - execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ - "WHERE relname = '#{table_name}'")[0] - - dead_tuple&.fetch('n_dead_tup', 0).to_i - end - - def execute_statement(sql) - ActiveRecord::Base.connection.execute(sql) - end - - # Override in subclass if you need a different dead tuple threshold - def dead_tuples_threshold - @dead_tuples_threshold ||= 50_000 - end - - # Override in subclass if you need a different vacuum wait time - def vacuum_wait_time - @vacuum_wait_time ||= 5.minutes - end - end - end -end diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb index 0a7cbd5c30f..11851c23ee3 100644 --- a/lib/gitlab/background_migration/delete_diff_files.rb +++ b/lib/gitlab/background_migration/delete_diff_files.rb @@ -4,7 +4,7 @@ module Gitlab module BackgroundMigration class DeleteDiffFiles - include Reschedulable + include Helpers::Reschedulable class MergeRequestDiff < ActiveRecord::Base self.table_name = 'merge_request_diffs' diff --git a/lib/gitlab/background_migration/helpers/reschedulable.rb b/lib/gitlab/background_migration/helpers/reschedulable.rb new file mode 100644 index 00000000000..7810f2627c8 --- /dev/null +++ b/lib/gitlab/background_migration/helpers/reschedulable.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module Helpers + # == Reschedulable helper + # + # Allows background migrations to be reschedule if a condition is not met. + # + # Check DeleteDiffFiles migration which reschedules itself if dead tuple count + # on DB is not acceptable. + # + module Reschedulable + extend ActiveSupport::Concern + + def reschedule_if_needed(args, &block) + if should_reschedule? + BackgroundMigrationWorker.perform_in(vacuum_wait_time, self.class.name.demodulize, args) + else + yield + end + end + + # Override this on base class if you need a different reschedule condition + def should_reschedule? + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end + + def wait_for_deadtuple_vacuum?(table_name) + return false unless Gitlab::Database.postgresql? + + dead_tuples_count_for(table_name) >= dead_tuples_threshold + end + + def dead_tuples_count_for(table_name) + dead_tuple = + execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ + "WHERE relname = '#{table_name}'")[0] + + dead_tuple&.fetch('n_dead_tup', 0).to_i + end + + def execute_statement(sql) + ActiveRecord::Base.connection.execute(sql) + end + + # Override in subclass if you need a different dead tuple threshold + def dead_tuples_threshold + @dead_tuples_threshold ||= 50_000 + end + + # Override in subclass if you need a different vacuum wait time + def vacuum_wait_time + @vacuum_wait_time ||= 5.minutes + end + end + end + end +end -- cgit v1.2.1 From d88b44caad35136bc42f245162c99e8bae0a5912 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 18 Feb 2019 12:00:14 -0300 Subject: Fix typo --- lib/gitlab/background_migration/helpers/reschedulable.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/background_migration/helpers/reschedulable.rb b/lib/gitlab/background_migration/helpers/reschedulable.rb index 7810f2627c8..3b653059858 100644 --- a/lib/gitlab/background_migration/helpers/reschedulable.rb +++ b/lib/gitlab/background_migration/helpers/reschedulable.rb @@ -5,9 +5,9 @@ module Gitlab module Helpers # == Reschedulable helper # - # Allows background migrations to be reschedule if a condition is not met. + # Allows background migrations to be reschedule itself if a condition is not met. # - # Check DeleteDiffFiles migration which reschedules itself if dead tuple count + # For example, check DeleteDiffFiles migration which is rescheduled if dead tuple count # on DB is not acceptable. # module Reschedulable -- cgit v1.2.1 From 04daa0b9701cd5b53c3c1fd6529aba05e4189114 Mon Sep 17 00:00:00 2001 From: Yauhen Kotau Date: Mon, 18 Feb 2019 20:08:36 +0300 Subject: Added YouTrack integration Fixes gitlab-org/gitlab-ce#42595 --- app/models/project.rb | 1 + app/models/project_services/youtrack_service.rb | 36 +++++++++++++++++++ app/models/service.rb | 1 + doc/integration/external-issue-tracker.md | 5 +-- doc/user/project/integrations/project_services.md | 1 + doc/user/project/integrations/youtrack.md | 28 +++++++++++++++ doc/user/project/issues/index.md | 2 +- lib/api/services.rb | 27 ++++++++++++++ spec/factories/projects.rb | 15 ++++++++ .../filter/external_issue_reference_filter_spec.rb | 22 ++++++++++++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/lib/gitlab/import_export/project.json | 20 +++++++++++ .../project_services/youtrack_service_spec.rb | 41 ++++++++++++++++++++++ 13 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 app/models/project_services/youtrack_service.rb create mode 100644 doc/user/project/integrations/youtrack.md create mode 100644 spec/models/project_services/youtrack_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index c72d3a3b725..ec6b9b495d4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -160,6 +160,7 @@ class Project < ActiveRecord::Base has_one :pushover_service has_one :jira_service has_one :redmine_service + has_one :youtrack_service has_one :custom_issue_tracker_service has_one :bugzilla_service has_one :gitlab_issue_tracker_service, inverse_of: :project diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb new file mode 100644 index 00000000000..ed8c0635124 --- /dev/null +++ b/app/models/project_services/youtrack_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class YoutrackService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1 + def self.reference_pattern(only_long: false) + if only_long + /(?\b[A-Z][A-Z0-9_]*-\d+)/ + else + /(?\b[A-Z][A-Z0-9_]*-\d+)|(#{Issue.reference_prefix}(?\d+))/ + end + end + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'YouTrack' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'YouTrack issue tracker' + end + end + + def self.to_param + 'youtrack' + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 3461e0bfe70..da523bfa426 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -266,6 +266,7 @@ class Service < ActiveRecord::Base prometheus pushover redmine + youtrack slack_slash_commands slack teamcity diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 075feaeead9..edd1af423ca 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,8 +1,8 @@ # External issue tracker GitLab has a great issue tracker but you can also use an external one such as -Jira, Redmine, or Bugzilla. Issue trackers are configurable per GitLab project and allow -you to do the following: +Jira, Redmine, YouTrack, or Bugzilla. Issue trackers are configurable per GitLab project +and allow you to do the following: - you can reference these external issues inside GitLab interface (merge requests, commits, comments) and they will be automatically converted @@ -20,6 +20,7 @@ To enable an external issue tracker you must configure the appropriate **Service Visit the links below for details: - [Redmine](../user/project/integrations/redmine.md) +- [YouTrack](../user/project/integrations/youtrack.md) - [Jira](../user/project/integrations/jira.md) - [Bugzilla](../user/project/integrations/bugzilla.md) - [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md) diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index cec9018b67f..e2f23827360 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -50,6 +50,7 @@ Click on the service links to see further configuration instructions and details | [Prometheus](prometheus.md) | Monitor the performance of your deployed apps | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | +| [YouTrack](youtrack.md) | YouTrack issue tracker | ## Services templates diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md new file mode 100644 index 00000000000..85c339d5fa9 --- /dev/null +++ b/doc/user/project/integrations/youtrack.md @@ -0,0 +1,28 @@ +# YouTrack Service + +1. To enable the YouTrack integration in a project, navigate to the +[Integrations page](project_services.md#accessing-the-project-services), click +the **YouTrack** service, and fill in the required details on the page as described +in the table below. + + | Field | Description | + | ----- | ----------- | + | `description` | A name for the issue tracker (to differentiate between instances, for example) | + | `project_url` | The URL to the project in YouTrack which is being linked to this GitLab project | + | `issues_url` | The URL to the issue in YouTrack project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | + | `new_issue_url` | This is the URL to create a new issue in YouTrack for the project linked to this GitLab project. **This is currently not being used and will be removed in a future release.** | + + Once you have configured and enabled YouTrack you'll see the YouTrack link on the GitLab project pages that takes you to the appropriate YouTrack project. + +1. To disable the internal issue tracking system in a project, navigate to the General page, expand [Permissions](../settings/index.md#sharing-and-permissions), and slide the Issues switch invalid. + + ![Issue configuration](img/issue_configuration.png) + +## Referencing issues in YouTrack + +Issues in YouTrack can be referenced as `-` where `` +starts with a capital letter which is then followed by capital letters, numbers +or underscores, and `` is a number (example `API_32-143`). + +`` part is included into issue_id and links can point any YouTrack +project (`issues_url` + issue_id) diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 5a3ac9c175b..907a305fe23 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -155,7 +155,7 @@ For further details, see [Importing issues from CSV](csv_import.md) Alternatively to GitLab's built-in Issue Tracker, you can also use an [external tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, -or Bugzilla. +YouTrack, or Bugzilla. ### Issue API diff --git a/lib/api/services.rb b/lib/api/services.rb index 163c7505a65..fcaec06061b 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -592,6 +592,32 @@ module API desc: 'The description of the tracker' } ], + 'youtrack' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], 'slack' => [ CHAT_NOTIFICATION_SETTINGS, CHAT_NOTIFICATION_FLAGS, @@ -665,6 +691,7 @@ module API PrometheusService, PushoverService, RedmineService, + YoutrackService, SlackService, MattermostService, MicrosoftTeamsService, diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f7ef34d773b..0efb15337ed 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -313,6 +313,21 @@ FactoryBot.define do end end + factory :youtrack_project, parent: :project do + has_external_issue_tracker true + + after :create do |project| + project.create_youtrack_service( + active: true, + properties: { + 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack', + 'issues_url' => 'http://youtrack/issues/:id', + 'new_issue_url' => 'http://youtrack/newIssue' + } + ) + end + end + factory :jira_project, parent: :project do has_external_issue_tracker true jira_service diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a0270d93d50..e415bbcfc92 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -121,6 +121,28 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do end end + context "youtrack project" do + let(:project) { create(:youtrack_project) } + + before do + project.update!(issues_enabled: false) + end + + context "with right markdown" do + let(:issue) { ExternalIssue.new("YT-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with a single-letter prefix" do + let(:issue) { ExternalIssue.new("T-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + end + context "jira project" do let(:project) { create(:jira_project) } let(:reference) { issue.to_reference } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c15b360b563..46d78b92b21 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -232,6 +232,7 @@ project: - pushover_service - jira_service - redmine_service +- youtrack_service - custom_issue_tracker_service - bugzilla_service - gitlab_issue_tracker_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 1327f414498..773651dd226 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6629,6 +6629,26 @@ ], "deploy_keys": [], "services": [ + { + "id": 101, + "title": "YouTrack", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.327Z", + "updated_at": "2016-06-14T15:01:51.327Z", + "active": false, + "properties": {}, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "job_events": true, + "type": "YoutrackService", + "category": "issue_tracker", + "default": false, + "wiki_page_events": true + }, { "id": 100, "title": "JetBrains TeamCity CI", diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb new file mode 100644 index 00000000000..26591b09d0d --- /dev/null +++ b/spec/models/project_services/youtrack_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe YoutrackService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it { is_expected.to validate_presence_of(:new_issue_url) } + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + it_behaves_like 'issue tracker service URL attribute', :new_issue_url + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + it { is_expected.not_to validate_presence_of(:new_issue_url) } + end + end + + describe '.reference_pattern' do + it_behaves_like 'allows project key on reference pattern' + + it 'does allow project prefix on the reference' do + expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YK-123') + end + end +end -- cgit v1.2.1 From fdfa6f1e4d03c295111a3ca95b05b0b189af20c3 Mon Sep 17 00:00:00 2001 From: Yauhen Kotau Date: Mon, 18 Feb 2019 20:50:48 +0300 Subject: Fix failed test --- spec/models/project_services/youtrack_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb index 26591b09d0d..b1e74a6f877 100644 --- a/spec/models/project_services/youtrack_service_spec.rb +++ b/spec/models/project_services/youtrack_service_spec.rb @@ -35,7 +35,7 @@ describe YoutrackService do it_behaves_like 'allows project key on reference pattern' it 'does allow project prefix on the reference' do - expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YK-123') + expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YT-123') end end end -- cgit v1.2.1 From cc7a44c8e130a8f3f753ba0ed32e45b0a6b0e6f7 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 18 Feb 2019 17:43:16 -0300 Subject: Make migrations reschedulable --- .../background_migration/delete_diff_files.rb | 2 +- .../background_migration/helpers/reschedulable.rb | 8 ++-- .../background_migration/sync_issues_state_id.rb | 30 ++++++++++----- .../sync_merge_requests_state_id.rb | 34 ++++++++++------ .../schedule_sync_issuables_state_id_spec.rb | 45 ++++++++++++++++++++++ 5 files changed, 92 insertions(+), 27 deletions(-) diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb index 11851c23ee3..4cb068eb71a 100644 --- a/lib/gitlab/background_migration/delete_diff_files.rb +++ b/lib/gitlab/background_migration/delete_diff_files.rb @@ -27,7 +27,7 @@ module Gitlab private - def should_reschedule? + def need_reschedule? wait_for_deadtuple_vacuum?(MergeRequestDiffFile.table_name) end diff --git a/lib/gitlab/background_migration/helpers/reschedulable.rb b/lib/gitlab/background_migration/helpers/reschedulable.rb index 3b653059858..6087181609e 100644 --- a/lib/gitlab/background_migration/helpers/reschedulable.rb +++ b/lib/gitlab/background_migration/helpers/reschedulable.rb @@ -14,7 +14,7 @@ module Gitlab extend ActiveSupport::Concern def reschedule_if_needed(args, &block) - if should_reschedule? + if need_reschedule? BackgroundMigrationWorker.perform_in(vacuum_wait_time, self.class.name.demodulize, args) else yield @@ -22,9 +22,9 @@ module Gitlab end # Override this on base class if you need a different reschedule condition - def should_reschedule? - raise NotImplementedError, "#{self.class} does not implement #{__method__}" - end + # def need_reschedule? + # raise NotImplementedError, "#{self.class} does not implement #{__method__}" + # end def wait_for_deadtuple_vacuum?(table_name) return false unless Gitlab::Database.postgresql? diff --git a/lib/gitlab/background_migration/sync_issues_state_id.rb b/lib/gitlab/background_migration/sync_issues_state_id.rb index 33b997c8533..f95c433c64c 100644 --- a/lib/gitlab/background_migration/sync_issues_state_id.rb +++ b/lib/gitlab/background_migration/sync_issues_state_id.rb @@ -4,19 +4,29 @@ module Gitlab module BackgroundMigration class SyncIssuesStateId + include Helpers::Reschedulable + def perform(start_id, end_id) Rails.logger.info("Issues - Populating state_id: #{start_id} - #{end_id}") - ActiveRecord::Base.connection.execute <<~SQL - UPDATE issues - SET state_id = - CASE state - WHEN 'opened' THEN 1 - WHEN 'closed' THEN 2 - END - WHERE state_id IS NULL - AND id BETWEEN #{start_id} AND #{end_id} - SQL + reschedule_if_needed([start_id, end_id]) do + ActiveRecord::Base.connection.execute <<~SQL + UPDATE issues + SET state_id = + CASE state + WHEN 'opened' THEN 1 + WHEN 'closed' THEN 2 + END + WHERE state_id IS NULL + AND id BETWEEN #{start_id} AND #{end_id} + SQL + end + end + + private + + def need_reschedule? + wait_for_deadtuple_vacuum?('issues') end end end diff --git a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb index 923ceaeec54..5d92553f577 100644 --- a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb +++ b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb @@ -4,21 +4,31 @@ module Gitlab module BackgroundMigration class SyncMergeRequestsStateId + include Helpers::Reschedulable + def perform(start_id, end_id) Rails.logger.info("Merge Requests - Populating state_id: #{start_id} - #{end_id}") - ActiveRecord::Base.connection.execute <<~SQL - UPDATE merge_requests - SET state_id = - CASE state - WHEN 'opened' THEN 1 - WHEN 'closed' THEN 2 - WHEN 'merged' THEN 3 - WHEN 'locked' THEN 4 - END - WHERE state_id IS NULL - AND id BETWEEN #{start_id} AND #{end_id} - SQL + reschedule_if_needed([start_id, end_id]) do + ActiveRecord::Base.connection.execute <<~SQL + UPDATE merge_requests + SET state_id = + CASE state + WHEN 'opened' THEN 1 + WHEN 'closed' THEN 2 + WHEN 'merged' THEN 3 + WHEN 'locked' THEN 4 + END + WHERE state_id IS NULL + AND id BETWEEN #{start_id} AND #{end_id} + SQL + end + end + + private + + def need_reschedule? + wait_for_deadtuple_vacuum?('issues') end end end diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb index a926ee38387..a746fb68c30 100644 --- a/spec/migrations/schedule_sync_issuables_state_id_spec.rb +++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb @@ -15,7 +15,40 @@ describe ScheduleSyncIssuablesStateId, :migration do @project = projects.create!(namespace_id: @group.id) end + shared_examples 'rescheduling migrations' do + before do + Sidekiq::Worker.clear_all + end + + it 'reschedules migrations when should wait for dead tuple vacuum' do + worker = worker_class.new + + Sidekiq::Testing.fake! do + allow(worker).to receive(:wait_for_deadtuple_vacuum?) { true } + + worker.perform(resource_1.id, resource_2.id) + + expect(worker_class.name.demodulize).to be_scheduled_delayed_migration(5.minutes, resource_1.id, resource_2.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + end + end + describe '#up' do + # it 'correctly schedules diff file deletion workers' do + # Sidekiq::Testing.fake! do + # Timecop.freeze do + # described_class.new.perform + + # expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, [1, 4, 5]) + + # expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, [6]) + + # expect(BackgroundMigrationWorker.jobs.size).to eq(2) + # end + # end + # end + context 'issues' do it 'migrates state column to integer' do opened_issue = issues.create!(description: 'first', state: 'opened') @@ -30,6 +63,12 @@ describe ScheduleSyncIssuablesStateId, :migration do expect(invalid_state_issue.reload.state_id).to be_nil expect(nil_state_issue.reload.state_id).to be_nil end + + it_behaves_like 'rescheduling migrations' do + let(:worker_class) { Gitlab::BackgroundMigration::SyncIssuesStateId } + let(:resource_1) { issues.create!(description: 'first', state: 'opened') } + let(:resource_2) { issues.create!(description: 'second', state: 'closed') } + end end context 'merge requests' do @@ -48,6 +87,12 @@ describe ScheduleSyncIssuablesStateId, :migration do expect(locked_merge_request.reload.state_id).to eq(MergeRequest.available_states[:locked]) expect(invalid_state_merge_request.reload.state_id).to be_nil end + + it_behaves_like 'rescheduling migrations' do + let(:worker_class) { Gitlab::BackgroundMigration::SyncMergeRequestsStateId } + let(:resource_1) { merge_requests.create!(state: 'opened', target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') } + let(:resource_2) { merge_requests.create!(state: 'closed', target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') } + end end end end -- cgit v1.2.1 From 9d046c8704c0e7df18d2f9e380e987d22b9a0b2e Mon Sep 17 00:00:00 2001 From: Mark Chao Date: Mon, 18 Feb 2019 11:51:56 +0800 Subject: Fix git clone revealing private repo's presence Ensure redirection to path with .git suffix regardless whether project exists or not. --- changelogs/unreleased/security-50334.yml | 5 + config/routes/git_http.rb | 2 +- lib/constraints/project_url_constrainer.rb | 3 +- .../constraints/project_url_constrainer_spec.rb | 4 + spec/requests/git_http_spec.rb | 134 +++++++++++---------- 5 files changed, 82 insertions(+), 66 deletions(-) create mode 100644 changelogs/unreleased/security-50334.yml diff --git a/changelogs/unreleased/security-50334.yml b/changelogs/unreleased/security-50334.yml new file mode 100644 index 00000000000..828ef82b517 --- /dev/null +++ b/changelogs/unreleased/security-50334.yml @@ -0,0 +1,5 @@ +--- +title: Fix git clone revealing private repo's presence +merge_request: +author: +type: security diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index ec5c68f81df..a959d40881b 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -40,7 +40,7 @@ scope(path: '*namespace_id/:project_id', # /info/refs?service=git-receive-pack, but nothing else. # git_http_handshake = lambda do |request| - ::Constraints::ProjectUrlConstrainer.new.matches?(request) && + ::Constraints::ProjectUrlConstrainer.new.matches?(request, existence_check: false) && (request.query_string.blank? || request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)) end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index eadfbf7bc01..d41490d2ebd 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -2,12 +2,13 @@ module Constraints class ProjectUrlConstrainer - def matches?(request) + def matches?(request, existence_check: true) namespace_path = request.params[:namespace_id] project_path = request.params[:project_id] || request.params[:id] full_path = [namespace_path, project_path].join('/') return false unless ProjectPathValidator.valid_path?(full_path) + return true unless existence_check # We intentionally allow SELECT(*) here so result of this query can be used # as cache for further Project.find_by_full_path calls within request diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index c96e7ab8495..3496b01ebcc 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do let(:request) { build_request('foo', 'bar') } it { expect(subject.matches?(request)).to be_falsey } + + context 'existence_check is false' do + it { expect(subject.matches?(request, existence_check: false)).to be_truthy } + end end context "project id ending with .git" do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 5b625fd47be..bfa178f5cae 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -104,6 +104,70 @@ describe 'Git HTTP requests' do end end + shared_examples_for 'project path without .git suffix' do + context "GET info/refs" do + let(:path) { "/#{project_path}/info/refs" } + + context "when no params are added" do + before do + get path + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs") + end + end + + context "when the upload-pack service is requested" do + let(:params) { { service: 'git-upload-pack' } } + + before do + get path, params: params + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the receive-pack service is requested" do + let(:params) { { service: 'git-receive-pack' } } + + before do + get path, params: params + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the params are anything else" do + let(:params) { { service: 'git-implode-pack' } } + + before do + get path, params: params + end + + it "redirects to the sign-in page" do + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "POST git-upload-pack" do + it "fails to find a route" do + expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError) + end + end + + context "POST git-receive-pack" do + it "fails to find a route" do + expect { push_post(project_path) }.to raise_error(ActionController::RoutingError) + end + end + end + describe "User with no identities" do let(:user) { create(:user) } @@ -143,6 +207,10 @@ describe 'Git HTTP requests' do expect(response).to have_gitlab_http_status(:unprocessable_entity) end end + + it_behaves_like 'project path without .git suffix' do + let(:project_path) { "#{user.namespace.path}/project.git-project" } + end end end @@ -706,70 +774,8 @@ describe 'Git HTTP requests' do end end - context "when the project path doesn't end in .git" do - let(:project) { create(:project, :repository, :public, path: 'project.git-project') } - - context "GET info/refs" do - let(:path) { "/#{project.full_path}/info/refs" } - - context "when no params are added" do - before do - get path - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs") - end - end - - context "when the upload-pack service is requested" do - let(:params) { { service: 'git-upload-pack' } } - - before do - get path, params: params - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}") - end - end - - context "when the receive-pack service is requested" do - let(:params) { { service: 'git-receive-pack' } } - - before do - get path, params: params - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}") - end - end - - context "when the params are anything else" do - let(:params) { { service: 'git-implode-pack' } } - - before do - get path, params: params - end - - it "redirects to the sign-in page" do - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context "POST git-upload-pack" do - it "fails to find a route" do - expect { clone_post(project.full_path) }.to raise_error(ActionController::RoutingError) - end - end - - context "POST git-receive-pack" do - it "fails to find a route" do - expect { push_post(project.full_path) }.to raise_error(ActionController::RoutingError) - end - end + it_behaves_like 'project path without .git suffix' do + let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path } end context "retrieving an info/refs file" do -- cgit v1.2.1 From fc8c1a77d36003795586fe076243b6eb90db6f03 Mon Sep 17 00:00:00 2001 From: Tiger Date: Wed, 13 Feb 2019 11:11:28 +1100 Subject: Validate session key when authorizing with GCP to create a cluster It was previously possible to link a GCP account to another user's GitLab account by having them visit the callback URL, as there was no check that they were the initiator of the request. We now reject the callback unless the state parameter matches the one added to the initiating user's session. --- .../google_api/authorizations_controller.rb | 32 ++++++++---- .../security-kubernetes-google-login-csrf.yml | 5 ++ .../google_api/authorizations_controller_spec.rb | 60 +++++++++++++++------- 3 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 changelogs/unreleased/security-kubernetes-google-login-csrf.yml diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb index dd9f5af61b3..ed0995e7ffd 100644 --- a/app/controllers/google_api/authorizations_controller.rb +++ b/app/controllers/google_api/authorizations_controller.rb @@ -2,6 +2,10 @@ module GoogleApi class AuthorizationsController < ApplicationController + include Gitlab::Utils::StrongMemoize + + before_action :validate_session_key! + def callback token, expires_at = GoogleApi::CloudPlatform::Client .new(nil, callback_google_api_auth_url) @@ -11,21 +15,27 @@ module GoogleApi session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = expires_at.to_s - state_redirect_uri = redirect_uri_from_session_key(params[:state]) - - if state_redirect_uri - redirect_to state_redirect_uri - else - redirect_to root_path - end + redirect_to redirect_uri_from_session end private - def redirect_uri_from_session_key(state) - key = GoogleApi::CloudPlatform::Client - .session_key_for_redirect_uri(params[:state]) - session[key] if key + def validate_session_key! + access_denied! unless redirect_uri_from_session.present? + end + + def redirect_uri_from_session + strong_memoize(:redirect_uri_from_session) do + if params[:state].present? + session[session_key_for_redirect_uri(params[:state])] + else + nil + end + end + end + + def session_key_for_redirect_uri(state) + GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(state) end end end diff --git a/changelogs/unreleased/security-kubernetes-google-login-csrf.yml b/changelogs/unreleased/security-kubernetes-google-login-csrf.yml new file mode 100644 index 00000000000..2f87100a8dd --- /dev/null +++ b/changelogs/unreleased/security-kubernetes-google-login-csrf.yml @@ -0,0 +1,5 @@ +--- +title: Validate session key when authorizing with GCP to create a cluster +merge_request: +author: +type: security diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb index 1e8e82da4f3..d9ba85cf56a 100644 --- a/spec/controllers/google_api/authorizations_controller_spec.rb +++ b/spec/controllers/google_api/authorizations_controller_spec.rb @@ -6,7 +6,7 @@ describe GoogleApi::AuthorizationsController do let(:token) { 'token' } let(:expires_at) { 1.hour.since.strftime('%s') } - subject { get :callback, params: { code: 'xxx', state: @state } } + subject { get :callback, params: { code: 'xxx', state: state } } before do sign_in(user) @@ -15,35 +15,57 @@ describe GoogleApi::AuthorizationsController do .to receive(:get_token).and_return([token, expires_at]) end - it 'sets token and expires_at in session' do - subject + shared_examples_for 'access denied' do + it 'returns a 404' do + subject - expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]) - .to eq(token) - expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]) - .to eq(expires_at) + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil + expect(response).to have_http_status(:not_found) + end end - context 'when redirect uri key is stored in state' do - set(:project) { create(:project) } - let(:redirect_uri) { project_clusters_url(project).to_s } + context 'session key is present' do + let(:session_key) { 'session-key' } + let(:redirect_uri) { 'example.com' } before do - @state = GoogleApi::CloudPlatform::Client - .new_session_key_for_redirect_uri do |key| - session[key] = redirect_uri + session[GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(session_key)] = redirect_uri + end + + context 'session key matches state param' do + let(:state) { session_key } + + it 'sets token and expires_at in session' do + subject + + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]) + .to eq(token) + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]) + .to eq(expires_at) + end + + it 'redirects to the URL stored in state param' do + expect(subject).to redirect_to(redirect_uri) end end - it 'redirects to the URL stored in state param' do - expect(subject).to redirect_to(redirect_uri) + context 'session key does not match state param' do + let(:state) { 'bad-key' } + + it_behaves_like 'access denied' end - end - context 'when redirection url is not stored in state' do - it 'redirects to root_path' do - expect(subject).to redirect_to(root_path) + context 'state param is blank' do + let(:state) { '' } + + it_behaves_like 'access denied' end end + + context 'state param is present, but session key is blank' do + let(:state) { 'session-key' } + + it_behaves_like 'access denied' + end end end -- cgit v1.2.1 From 52155d8cf8374e9184c2ae834cab761b7520db93 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 19 Feb 2019 11:33:30 -0300 Subject: Add more specs and code improvements --- ...90214112022_schedule_sync_issuables_state_id.rb | 31 ++++++++----- .../background_migration/delete_diff_files.rb | 4 +- .../background_migration/helpers/reschedulable.rb | 45 +++++++++--------- .../background_migration/sync_issues_state_id.rb | 6 +-- .../sync_merge_requests_state_id.rb | 6 +-- .../schedule_sync_issuables_state_id_spec.rb | 53 +++++++++++++++------- 6 files changed, 89 insertions(+), 56 deletions(-) diff --git a/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb b/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb index d9b77d2f02c..898bad043ac 100644 --- a/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb +++ b/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb @@ -1,22 +1,22 @@ -# frozen_string_literal: true - -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - class ScheduleSyncIssuablesStateId < ActiveRecord::Migration[5.0] + # This migration schedules the sync of state_id for issues and merge requests + # which are converting the state column from string to integer. + # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/51789 + include Gitlab::Database::MigrationHelpers DOWNTIME = false - # 2019-02-12 Gitlab.com issuable numbers + # 2019-02-12 gitlab.com issuable numbers # issues count: 13587305 # merge requests count: 18925274 - # Using this 25000 as batch size should take around 26 hours + # + # Using 25000 as batch size should take around 26 hours # to migrate both issues and merge requests BATCH_SIZE = 25000 DELAY_INTERVAL = 5.minutes.to_i - ISSUE_MIGRATION = 'SyncIssuesStateId'.freeze - MERGE_REQUEST_MIGRATION = 'SyncMergeRequestsStateId'.freeze + ISSUES_MIGRATION = 'SyncIssuesStateId'.freeze + MERGE_REQUESTS_MIGRATION = 'SyncMergeRequestsStateId'.freeze class Issue < ActiveRecord::Base include EachBatch @@ -32,8 +32,17 @@ class ScheduleSyncIssuablesStateId < ActiveRecord::Migration[5.0] def up Sidekiq::Worker.skipping_transaction_check do - queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), ISSUE_MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) - queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), MERGE_REQUEST_MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), + ISSUES_MIGRATION, + DELAY_INTERVAL, + batch_size: BATCH_SIZE + ) + + queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), + MERGE_REQUESTS_MIGRATION, + DELAY_INTERVAL, + batch_size: BATCH_SIZE + ) end end diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb index 4cb068eb71a..0066faa23dd 100644 --- a/lib/gitlab/background_migration/delete_diff_files.rb +++ b/lib/gitlab/background_migration/delete_diff_files.rb @@ -20,14 +20,14 @@ module Gitlab def perform(ids) @ids = ids - reschedule_if_needed([ids]) do + reschedule_if_needed(ids) do prune_diff_files end end private - def need_reschedule? + def should_reschedule? wait_for_deadtuple_vacuum?(MergeRequestDiffFile.table_name) end diff --git a/lib/gitlab/background_migration/helpers/reschedulable.rb b/lib/gitlab/background_migration/helpers/reschedulable.rb index 6087181609e..60ab62cd973 100644 --- a/lib/gitlab/background_migration/helpers/reschedulable.rb +++ b/lib/gitlab/background_migration/helpers/reschedulable.rb @@ -5,7 +5,8 @@ module Gitlab module Helpers # == Reschedulable helper # - # Allows background migrations to be reschedule itself if a condition is not met. + # Allows background migrations to be rescheduled if a condition is met, + # the condition should be overridden in classes in #should_reschedule? method. # # For example, check DeleteDiffFiles migration which is rescheduled if dead tuple count # on DB is not acceptable. @@ -13,18 +14,34 @@ module Gitlab module Reschedulable extend ActiveSupport::Concern - def reschedule_if_needed(args, &block) - if need_reschedule? - BackgroundMigrationWorker.perform_in(vacuum_wait_time, self.class.name.demodulize, args) + # Use this method to perform the background migration and it will be rescheduled + # if #should_reschedule? returns true. + def reschedule_if_needed(*args, &block) + if should_reschedule? + BackgroundMigrationWorker.perform_in(wait_time, self.class.name.demodulize, args) else yield end end # Override this on base class if you need a different reschedule condition - # def need_reschedule? - # raise NotImplementedError, "#{self.class} does not implement #{__method__}" - # end + def should_reschedule? + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end + + # Override in subclass if a different dead tuple threshold + def dead_tuples_threshold + @dead_tuples_threshold ||= 50_000 + end + + # Override in subclass if a different wait time + def wait_time + @wait_time ||= 5.minutes + end + + def execute_statement(sql) + ActiveRecord::Base.connection.execute(sql) + end def wait_for_deadtuple_vacuum?(table_name) return false unless Gitlab::Database.postgresql? @@ -39,20 +56,6 @@ module Gitlab dead_tuple&.fetch('n_dead_tup', 0).to_i end - - def execute_statement(sql) - ActiveRecord::Base.connection.execute(sql) - end - - # Override in subclass if you need a different dead tuple threshold - def dead_tuples_threshold - @dead_tuples_threshold ||= 50_000 - end - - # Override in subclass if you need a different vacuum wait time - def vacuum_wait_time - @vacuum_wait_time ||= 5.minutes - end end end end diff --git a/lib/gitlab/background_migration/sync_issues_state_id.rb b/lib/gitlab/background_migration/sync_issues_state_id.rb index f95c433c64c..50841c3fe4d 100644 --- a/lib/gitlab/background_migration/sync_issues_state_id.rb +++ b/lib/gitlab/background_migration/sync_issues_state_id.rb @@ -9,8 +9,8 @@ module Gitlab def perform(start_id, end_id) Rails.logger.info("Issues - Populating state_id: #{start_id} - #{end_id}") - reschedule_if_needed([start_id, end_id]) do - ActiveRecord::Base.connection.execute <<~SQL + reschedule_if_needed(start_id, end_id) do + execute_statement <<~SQL UPDATE issues SET state_id = CASE state @@ -25,7 +25,7 @@ module Gitlab private - def need_reschedule? + def should_reschedule? wait_for_deadtuple_vacuum?('issues') end end diff --git a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb index 5d92553f577..4cf8e993c52 100644 --- a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb +++ b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb @@ -9,8 +9,8 @@ module Gitlab def perform(start_id, end_id) Rails.logger.info("Merge Requests - Populating state_id: #{start_id} - #{end_id}") - reschedule_if_needed([start_id, end_id]) do - ActiveRecord::Base.connection.execute <<~SQL + reschedule_if_needed(start_id, end_id) do + execute_statement <<~SQL UPDATE merge_requests SET state_id = CASE state @@ -27,7 +27,7 @@ module Gitlab private - def need_reschedule? + def should_reschedule? wait_for_deadtuple_vacuum?('issues') end end diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb index a746fb68c30..6c4c31ae554 100644 --- a/spec/migrations/schedule_sync_issuables_state_id_spec.rb +++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb @@ -15,6 +15,25 @@ describe ScheduleSyncIssuablesStateId, :migration do @project = projects.create!(namespace_id: @group.id) end + shared_examples 'scheduling migrations' do + before do + Sidekiq::Worker.clear_all + stub_const("#{described_class.name}::BATCH_SIZE", 2) + end + + it 'correctly schedules issuable sync background migration' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(migration).to be_scheduled_delayed_migration(5.minutes, resource_1.id, resource_2.id) + expect(migration).to be_scheduled_delayed_migration(10.minutes, resource_3.id, resource_4.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end + shared_examples 'rescheduling migrations' do before do Sidekiq::Worker.clear_all @@ -35,20 +54,6 @@ describe ScheduleSyncIssuablesStateId, :migration do end describe '#up' do - # it 'correctly schedules diff file deletion workers' do - # Sidekiq::Testing.fake! do - # Timecop.freeze do - # described_class.new.perform - - # expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, [1, 4, 5]) - - # expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, [6]) - - # expect(BackgroundMigrationWorker.jobs.size).to eq(2) - # end - # end - # end - context 'issues' do it 'migrates state column to integer' do opened_issue = issues.create!(description: 'first', state: 'opened') @@ -64,10 +69,18 @@ describe ScheduleSyncIssuablesStateId, :migration do expect(nil_state_issue.reload.state_id).to be_nil end + it_behaves_like 'scheduling migrations' do + let(:migration) { described_class::ISSUES_MIGRATION } + let!(:resource_1) { issues.create!(description: 'first', state: 'opened') } + let!(:resource_2) { issues.create!(description: 'second', state: 'closed') } + let!(:resource_3) { issues.create!(description: 'third', state: 'closed') } + let!(:resource_4) { issues.create!(description: 'fourth', state: 'closed') } + end + it_behaves_like 'rescheduling migrations' do let(:worker_class) { Gitlab::BackgroundMigration::SyncIssuesStateId } - let(:resource_1) { issues.create!(description: 'first', state: 'opened') } - let(:resource_2) { issues.create!(description: 'second', state: 'closed') } + let!(:resource_1) { issues.create!(description: 'first', state: 'opened') } + let!(:resource_2) { issues.create!(description: 'second', state: 'closed') } end end @@ -88,6 +101,14 @@ describe ScheduleSyncIssuablesStateId, :migration do expect(invalid_state_merge_request.reload.state_id).to be_nil end + it_behaves_like 'scheduling migrations' do + let(:migration) { described_class::MERGE_REQUESTS_MIGRATION } + let!(:resource_1) { merge_requests.create!(state: 'opened', target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') } + let!(:resource_2) { merge_requests.create!(state: 'closed', target_project_id: @project.id, target_branch: 'feature2', source_branch: 'master') } + let!(:resource_3) { merge_requests.create!(state: 'merged', target_project_id: @project.id, target_branch: 'feature3', source_branch: 'master') } + let!(:resource_4) { merge_requests.create!(state: 'locked', target_project_id: @project.id, target_branch: 'feature4', source_branch: 'master') } + end + it_behaves_like 'rescheduling migrations' do let(:worker_class) { Gitlab::BackgroundMigration::SyncMergeRequestsStateId } let(:resource_1) { merge_requests.create!(state: 'opened', target_project_id: @project.id, target_branch: 'feature1', source_branch: 'master') } -- cgit v1.2.1 From e5d491d15bd1f7a0679ca18a7bd36459392ae629 Mon Sep 17 00:00:00 2001 From: Paul B Date: Mon, 31 Dec 2018 14:13:07 +0100 Subject: tests: adding a failing unit test --- spec/lib/gitlab/ci/yaml_processor_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 91139d421f5..8d9ae2ff22a 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -602,6 +602,15 @@ module Gitlab end end + describe "Include" do + it "does not return any error with a valid configuration" do + config = YAML.dump({ include: "/local.gitlab-ci.yml" }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.not_to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + describe "When" do %w(on_success on_failure always).each do |when_state| it "returns #{when_state} when defined" do -- cgit v1.2.1 From 30791a36c57140f56cb5d2f14a7f79ff36c1afa9 Mon Sep 17 00:00:00 2001 From: Paul B Date: Wed, 2 Jan 2019 10:35:48 +0100 Subject: ci(config): validate 'include' keyword During `.gitlab-ci.yml` configuration file validation there are two cases: 1- either we are validating a file within a project context (a project and a sha is given in option parameters) 2- either we are validating a file with no project context (as in the api POST /ci/lint) In the first case, all `include:` keywords are replaced by the content of the included CI cnfiguration files. In the second case, the `include:` configuration is kept untouched. The current problem arises in the second case when one wants to validate a generic gitlab-ci configuration file with an `include:` keyword from the API. The following error will be raised by the API: `"jobs:include config should be a hash"`. This commit fixes the behavior by validating the `include:` keyword if its present to not parse it as a job definition. Fixes #55863 / #52822 --- lib/gitlab/ci/config/entry/global.rb | 3 +++ lib/gitlab/ci/config/entry/include.rb | 23 +++++++++++++++++++++++ lib/gitlab/ci/config/entry/includes.rb | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 lib/gitlab/ci/config/entry/include.rb create mode 100644 lib/gitlab/ci/config/entry/includes.rb diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index 09ecb5fdb99..2b5a59c078e 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -17,6 +17,9 @@ module Gitlab entry :image, Entry::Image, description: 'Docker image that will be used to execute jobs.' + entry :include, Entry::Includes, + description: 'List of external YAML files to include.' + entry :services, Entry::Services, description: 'Docker images that will be linked to the container.' diff --git a/lib/gitlab/ci/config/entry/include.rb b/lib/gitlab/ci/config/entry/include.rb new file mode 100644 index 00000000000..f2f3dd84eda --- /dev/null +++ b/lib/gitlab/ci/config/entry/include.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a single include. + # + class Include < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_KEYS = %i[local file remote template].freeze + + validations do + validates :config, hash_or_string: true + validates :config, allowed_keys: ALLOWED_KEYS + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/includes.rb b/lib/gitlab/ci/config/entry/includes.rb new file mode 100644 index 00000000000..82b2b1ccf4b --- /dev/null +++ b/lib/gitlab/ci/config/entry/includes.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a list of include. + # + class Includes < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, type: Array + end + + def self.aspects + super.append -> do + @config = Array.wrap(@config) + + @config.each_with_index do |config, i| + @entries[i] = ::Gitlab::Config::Entry::Factory.new(Entry::Include) + .value(config || {}) + .create! + end + end + end + end + end + end + end +end -- cgit v1.2.1 From d3aa9feffd725f8fd7e1826c56c0e26d6708ac38 Mon Sep 17 00:00:00 2001 From: Paul B Date: Wed, 2 Jan 2019 10:57:31 +0100 Subject: tests(yaml_processor): complete test scenarios on 'include' keyword --- spec/lib/gitlab/ci/config/entry/global_spec.rb | 6 +- spec/lib/gitlab/ci/yaml_processor_spec.rb | 80 ++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 7651f594a4c..e23efff18d5 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Global do expect(described_class.nodes.keys) .to match_array(%i[before_script image services after_script variables stages - types cache]) + types cache include]) end end end @@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -189,7 +189,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 8d9ae2ff22a..29638ef47c5 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -603,11 +603,81 @@ module Gitlab end describe "Include" do - it "does not return any error with a valid configuration" do - config = YAML.dump({ include: "/local.gitlab-ci.yml" }) - expect do - Gitlab::Ci::YamlProcessor.new(config) - end.not_to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + let(:opts) { {} } + + let(:config) do + { + include: include_content, + rspec: { script: "test" } + } + end + + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } + + context "when validating a ci config file with no project context" do + context "when an array is provided" do + let(:include_content) { ["/local.gitlab-ci.yml"] } + + it "does not return any error" do + expect { subject }.not_to raise_error + end + end + + context "when an array of wrong keyed object is provided" do + let(:include_content) { [{ yolo: "/local.gitlab-ci.yml" }] } + + it "returns a validation error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + + context "when an array of mixed typed objects is provided" do + let(:include_content) do + [ + 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml', + '/templates/.after-script-template.yml', + { template: 'Auto-DevOps.gitlab-ci.yml' } + ] + end + + it "does not return any error" do + expect { subject }.not_to raise_error + end + end + + context "when the include type is incorrect" do + let(:include_content) { { name: "/local.gitlab-ci.yml" } } + + it "returns an invalid configuration error" do + expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + end + end + + context "when validating a ci config file within a project" do + let(:include_content) { "/local.gitlab-ci.yml" } + let(:project) { create(:project, :repository) } + let(:opts) { { project: project, sha: project.commit.sha } } + + context "when the included internal file is present" do + before do + expect(project.repository).to receive(:blob_data_at) + .and_return(YAML.dump({ job1: { script: 'hello' } })) + end + + it "does not return an error" do + expect { subject }.not_to raise_error + end + end + + context "when the included internal file is not present" do + it "returns an error with missing file details" do + expect { subject }.to raise_error( + Gitlab::Ci::YamlProcessor::ValidationError, + "Local file `#{include_content}` does not exist!" + ) + end + end end end -- cgit v1.2.1 From 3a2f42925fab6ff398bd85447fbb3bd5ff42cae9 Mon Sep 17 00:00:00 2001 From: Paul B Date: Wed, 2 Jan 2019 16:39:52 +0100 Subject: changelog: add unrelease patch notes --- changelogs/unreleased/include-ci-yaml.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/include-ci-yaml.yml diff --git a/changelogs/unreleased/include-ci-yaml.yml b/changelogs/unreleased/include-ci-yaml.yml new file mode 100644 index 00000000000..5909950ef0b --- /dev/null +++ b/changelogs/unreleased/include-ci-yaml.yml @@ -0,0 +1,5 @@ +--- +title: Validate 'include' keywords in gitlab-ci.yml configuration files. +merge_request: 24098 +author: Paul Bonaud +type: fixed -- cgit v1.2.1 From 7bd066a1fa51018211e26ca0c5624aecbc364a66 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 19 Feb 2019 14:00:53 -0300 Subject: Address review comments --- ...90214112022_schedule_sync_issuables_state_id.rb | 28 +++++----- lib/gitlab/background_migration.rb | 2 - .../background_migration/delete_diff_files.rb | 37 ++++++++++--- .../background_migration/helpers/reschedulable.rb | 62 ---------------------- lib/gitlab/background_migration/reschedulable.rb | 60 +++++++++++++++++++++ .../background_migration/sync_issues_state_id.rb | 2 +- .../sync_merge_requests_state_id.rb | 2 +- .../background_migration/delete_diff_files_spec.rb | 10 ++-- 8 files changed, 113 insertions(+), 90 deletions(-) delete mode 100644 lib/gitlab/background_migration/helpers/reschedulable.rb create mode 100644 lib/gitlab/background_migration/reschedulable.rb diff --git a/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb b/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb index 898bad043ac..2167f19e022 100644 --- a/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb +++ b/db/post_migrate/20190214112022_schedule_sync_issuables_state_id.rb @@ -18,6 +18,8 @@ class ScheduleSyncIssuablesStateId < ActiveRecord::Migration[5.0] ISSUES_MIGRATION = 'SyncIssuesStateId'.freeze MERGE_REQUESTS_MIGRATION = 'SyncMergeRequestsStateId'.freeze + disable_ddl_transaction! + class Issue < ActiveRecord::Base include EachBatch @@ -31,19 +33,19 @@ class ScheduleSyncIssuablesStateId < ActiveRecord::Migration[5.0] end def up - Sidekiq::Worker.skipping_transaction_check do - queue_background_migration_jobs_by_range_at_intervals(Issue.where(state_id: nil), - ISSUES_MIGRATION, - DELAY_INTERVAL, - batch_size: BATCH_SIZE - ) - - queue_background_migration_jobs_by_range_at_intervals(MergeRequest.where(state_id: nil), - MERGE_REQUESTS_MIGRATION, - DELAY_INTERVAL, - batch_size: BATCH_SIZE - ) - end + queue_background_migration_jobs_by_range_at_intervals( + Issue.where(state_id: nil), + ISSUES_MIGRATION, + DELAY_INTERVAL, + batch_size: BATCH_SIZE + ) + + queue_background_migration_jobs_by_range_at_intervals( + MergeRequest.where(state_id: nil), + MERGE_REQUESTS_MIGRATION, + DELAY_INTERVAL, + batch_size: BATCH_SIZE + ) end def down diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index 948488c844c..5251e0fadf9 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -Dir[Rails.root.join("lib/gitlab/background_migration/helpers/*.rb")].each { |f| require f } - module Gitlab module BackgroundMigration def self.queue diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb index 0066faa23dd..664ead1af44 100644 --- a/lib/gitlab/background_migration/delete_diff_files.rb +++ b/lib/gitlab/background_migration/delete_diff_files.rb @@ -4,8 +4,6 @@ module Gitlab module BackgroundMigration class DeleteDiffFiles - include Helpers::Reschedulable - class MergeRequestDiff < ActiveRecord::Base self.table_name = 'merge_request_diffs' @@ -17,24 +15,47 @@ module Gitlab self.table_name = 'merge_request_diff_files' end + DEAD_TUPLES_THRESHOLD = 50_000 + VACUUM_WAIT_TIME = 5.minutes + def perform(ids) @ids = ids - reschedule_if_needed(ids) do - prune_diff_files + # We should reschedule until deadtuples get in a desirable + # state (e.g. < 50_000). That may take more than one reschedule. + # + if should_wait_deadtuple_vacuum? + reschedule + return end + + prune_diff_files + end + + def should_wait_deadtuple_vacuum? + return false unless Gitlab::Database.postgresql? + + diff_files_dead_tuples_count >= DEAD_TUPLES_THRESHOLD end private - def should_reschedule? - wait_for_deadtuple_vacuum?(MergeRequestDiffFile.table_name) + def reschedule + BackgroundMigrationWorker.perform_in(VACUUM_WAIT_TIME, self.class.name.demodulize, [@ids]) end def diffs_collection MergeRequestDiff.where(id: @ids) end + def diff_files_dead_tuples_count + dead_tuple = + execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ + "WHERE relname = 'merge_request_diff_files'")[0] + + dead_tuple&.fetch('n_dead_tup', 0).to_i + end + def prune_diff_files removed = 0 updated = 0 @@ -48,6 +69,10 @@ module Gitlab "updated #{updated} merge_request_diffs rows") end + def execute_statement(sql) + ActiveRecord::Base.connection.execute(sql) + end + def log_info(message) Rails.logger.info("BackgroundMigration::DeleteDiffFiles - #{message}") end diff --git a/lib/gitlab/background_migration/helpers/reschedulable.rb b/lib/gitlab/background_migration/helpers/reschedulable.rb deleted file mode 100644 index 60ab62cd973..00000000000 --- a/lib/gitlab/background_migration/helpers/reschedulable.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module BackgroundMigration - module Helpers - # == Reschedulable helper - # - # Allows background migrations to be rescheduled if a condition is met, - # the condition should be overridden in classes in #should_reschedule? method. - # - # For example, check DeleteDiffFiles migration which is rescheduled if dead tuple count - # on DB is not acceptable. - # - module Reschedulable - extend ActiveSupport::Concern - - # Use this method to perform the background migration and it will be rescheduled - # if #should_reschedule? returns true. - def reschedule_if_needed(*args, &block) - if should_reschedule? - BackgroundMigrationWorker.perform_in(wait_time, self.class.name.demodulize, args) - else - yield - end - end - - # Override this on base class if you need a different reschedule condition - def should_reschedule? - raise NotImplementedError, "#{self.class} does not implement #{__method__}" - end - - # Override in subclass if a different dead tuple threshold - def dead_tuples_threshold - @dead_tuples_threshold ||= 50_000 - end - - # Override in subclass if a different wait time - def wait_time - @wait_time ||= 5.minutes - end - - def execute_statement(sql) - ActiveRecord::Base.connection.execute(sql) - end - - def wait_for_deadtuple_vacuum?(table_name) - return false unless Gitlab::Database.postgresql? - - dead_tuples_count_for(table_name) >= dead_tuples_threshold - end - - def dead_tuples_count_for(table_name) - dead_tuple = - execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ - "WHERE relname = '#{table_name}'")[0] - - dead_tuple&.fetch('n_dead_tup', 0).to_i - end - end - end - end -end diff --git a/lib/gitlab/background_migration/reschedulable.rb b/lib/gitlab/background_migration/reschedulable.rb new file mode 100644 index 00000000000..3d6781c07c0 --- /dev/null +++ b/lib/gitlab/background_migration/reschedulable.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # == Reschedulable helper + # + # Allows background migrations to be rescheduled if a condition is met, + # the condition should be overridden in classes in #should_reschedule? method. + # + # For example, check DeleteDiffFiles migration which is rescheduled if dead tuple count + # on DB is not acceptable. + # + module Reschedulable + extend ActiveSupport::Concern + + # Use this method to perform the background migration and it will be rescheduled + # if #should_reschedule? returns true. + def reschedule_if_needed(*args, &block) + if should_reschedule? + BackgroundMigrationWorker.perform_in(wait_time, self.class.name.demodulize, args) + else + yield + end + end + + # Override this on base class if you need a different reschedule condition + def should_reschedule? + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end + + # Override in subclass if a different dead tuple threshold + def dead_tuples_threshold + @dead_tuples_threshold ||= 50_000 + end + + # Override in subclass if a different wait time + def wait_time + @wait_time ||= 5.minutes + end + + def execute_statement(sql) + ActiveRecord::Base.connection.execute(sql) + end + + def wait_for_deadtuple_vacuum?(table_name) + return false unless Gitlab::Database.postgresql? + + dead_tuples_count_for(table_name) >= dead_tuples_threshold + end + + def dead_tuples_count_for(table_name) + dead_tuple = + execute_statement("SELECT n_dead_tup FROM pg_stat_all_tables "\ + "WHERE relname = '#{table_name}'")[0] + + dead_tuple&.fetch('n_dead_tup', 0).to_i + end + end + end +end diff --git a/lib/gitlab/background_migration/sync_issues_state_id.rb b/lib/gitlab/background_migration/sync_issues_state_id.rb index 50841c3fe4d..053d154cef8 100644 --- a/lib/gitlab/background_migration/sync_issues_state_id.rb +++ b/lib/gitlab/background_migration/sync_issues_state_id.rb @@ -4,7 +4,7 @@ module Gitlab module BackgroundMigration class SyncIssuesStateId - include Helpers::Reschedulable + include Reschedulable def perform(start_id, end_id) Rails.logger.info("Issues - Populating state_id: #{start_id} - #{end_id}") diff --git a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb index 4cf8e993c52..a94529aaf5c 100644 --- a/lib/gitlab/background_migration/sync_merge_requests_state_id.rb +++ b/lib/gitlab/background_migration/sync_merge_requests_state_id.rb @@ -4,7 +4,7 @@ module Gitlab module BackgroundMigration class SyncMergeRequestsStateId - include Helpers::Reschedulable + include Reschedulable def perform(start_id, end_id) Rails.logger.info("Merge Requests - Populating state_id: #{start_id} - #{end_id}") diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index 690881ab59b..27281333348 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -40,14 +40,14 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end - it 'reschedules itself when should wait for dead tuple vacuum' do + it 'reschedules itself when should_wait_deadtuple_vacuum' do merge_request = create(:merge_request, :merged) first_diff = merge_request.merge_request_diff second_diff = merge_request.create_merge_request_diff Sidekiq::Testing.fake! do worker = described_class.new - allow(worker).to receive(:wait_for_deadtuple_vacuum?) { true } + allow(worker).to receive(:should_wait_deadtuple_vacuum?) { true } worker.perform([first_diff.id, second_diff.id]) @@ -57,10 +57,10 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end - describe '#wait_for_deadtuple_vacuum?' do + describe '#should_wait_deadtuple_vacuum?' do it 'returns true when hitting merge_request_diff_files hits DEAD_TUPLES_THRESHOLD', :postgresql do worker = described_class.new - threshold_query_result = [{ "n_dead_tup" => 50_000 }] + threshold_query_result = [{ "n_dead_tup" => described_class::DEAD_TUPLES_THRESHOLD.to_s }] normal_query_result = [{ "n_dead_tup" => '3' }] allow(worker) @@ -68,7 +68,7 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch .with(/SELECT n_dead_tup */) .and_return(threshold_query_result, normal_query_result) - expect(worker.wait_for_deadtuple_vacuum?('merge_request_diff_files')).to be(true) + expect(worker.should_wait_deadtuple_vacuum?).to be(true) end end end -- cgit v1.2.1 From 54a5d513e5f068c53fad3b2dac04998f5e9afd88 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Tue, 19 Feb 2019 13:27:53 -0700 Subject: Fixing CI icon mismatch MR list, and related MRs and branches were using a deprecated helper. Created a new icon haml file to help move these forward. --- app/views/ci/status/_icon.html.haml | 12 +++ .../projects/issues/_merge_requests.html.haml | 2 +- .../projects/issues/_related_branches.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- ...ch-on-merge-requests-page-and-the-mr-itself.yml | 5 ++ spec/views/ci/status/_icon.html.haml_spec.rb | 88 ++++++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 app/views/ci/status/_icon.html.haml create mode 100644 changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml create mode 100644 spec/views/ci/status/_icon.html.haml_spec.rb diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml new file mode 100644 index 00000000000..bfdf9c2ec9f --- /dev/null +++ b/app/views/ci/status/_icon.html.haml @@ -0,0 +1,12 @@ +- status = local_assigns.fetch(:status) +- size = local_assigns.fetch(:size, 16) +- link = local_assigns.fetch(:link, true) +- title = local_assigns.fetch(:title, nil) +- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} #{'has-tooltip' if title.present?}" + +- if link && status.has_details? + = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do + = sprite_icon(status.icon, size: size) +- else + %span{ class: css_classes, title: title, data: { html: title.present? } } + = sprite_icon(status.icon, size: size) \ No newline at end of file diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 310e339ac8d..ab3d7907ad8 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -27,7 +27,7 @@ = merge_request.to_reference %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 - if merge_request.can_read_pipeline? - = render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom') + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true - elsif has_any_head_pipeline = icon('blank fw') diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index ffdd96870ef..f8d1d64100a 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -8,7 +8,7 @@ - pipeline = @project.pipeline_for(branch, target.sha) if target - if can?(current_user, :read_pipeline, pipeline) %span.related-branch-ci-status - = render_pipeline_status(pipeline) + = render 'ci/status/icon', status: pipeline.detailed_status(current_user), link: true %span.related-branch-info %strong = link_to branch, project_compare_path(@project, from: @project.default_branch, to: branch), class: "ref-name" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index ac29cd8f679..4111e823701 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -48,7 +48,7 @@ CLOSED - if can?(current_user, :read_pipeline, merge_request.head_pipeline) %li.issuable-pipeline-status.d-none.d-sm-inline-block - = render_pipeline_status(merge_request.head_pipeline) + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true - if merge_request.open? && merge_request.broken? %li.issuable-pipeline-broken.d-none.d-sm-inline-block = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do diff --git a/changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml b/changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml new file mode 100644 index 00000000000..64ab76a2b05 --- /dev/null +++ b/changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml @@ -0,0 +1,5 @@ +--- +title: Fix pipeline status icon mismatch +merge_request: 25407 +author: +type: fixed diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb new file mode 100644 index 00000000000..43806446164 --- /dev/null +++ b/spec/views/ci/status/_icon.html.haml_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe 'ci/status/_icon' do + let(:user) { create(:user) } + let(:project) { create(:project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when rendering status for build' do + let(:build) do + create(:ci_build, :success, pipeline: pipeline) + end + + context 'when user has ability to see details' do + before do + project.add_developer(user) + end + + it 'has link to build details page' do + details_path = project_job_path(project, build) + + render_status(build) + + expect(rendered).to have_link(href: details_path) + end + end + + context 'when user do not have ability to see build details' do + before do + render_status(build) + end + + it 'contains build status text' do + expect(rendered).to have_css('.ci-status-icon.ci-status-icon-success') + end + + it 'does not contain links' do + expect(rendered).not_to have_link + end + end + end + + context 'when rendering status for external job' do + context 'when user has ability to see commit status details' do + before do + project.add_developer(user) + end + + context 'status has external target url' do + before do + external_job = create(:generic_commit_status, + status: :running, + pipeline: pipeline, + target_url: 'http://gitlab.com') + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_css('.ci-status-icon.ci-status-icon-running') + end + + it 'has link to external status page' do + expect(rendered).to have_link(href: 'http://gitlab.com') + end + end + + context 'status do not have external target url' do + before do + external_job = create(:generic_commit_status, status: :canceled) + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_css('.ci-status-icon.ci-status-icon-canceled') + end + + it 'has link to external status page' do + expect(rendered).not_to have_link + end + end + end + end + + def render_status(resource) + render 'ci/status/icon', status: resource.detailed_status(user) + end +end -- cgit v1.2.1 From 2e182f337ed2cdfcca26dad1e7a482bc81a2000e Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Thu, 14 Feb 2019 01:20:52 +0900 Subject: Fix alignment of dropdown icon on issuable on mobile Signed-off-by: Takuya Noguchi --- app/views/shared/issuable/_sort_dropdown.html.haml | 2 +- ...misalignment-on-issues-list-on-mobile-screen.yml | 5 +++++ .../issues/filtered_search/visual_tokens_spec.rb | 21 --------------------- 3 files changed, 6 insertions(+), 22 deletions(-) create mode 100644 changelogs/unreleased/57582-dropdown-icon-misalignment-on-issues-list-on-mobile-screen.yml diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml index b6ea9185b10..967f31c8325 100644 --- a/app/views/shared/issuable/_sort_dropdown.html.haml +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -5,7 +5,7 @@ .dropdown.inline.prepend-left-10.issue-sort-dropdown .btn-group{ role: 'group' } .btn-group{ role: 'group' } - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' } + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' } = sort_title = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort diff --git a/changelogs/unreleased/57582-dropdown-icon-misalignment-on-issues-list-on-mobile-screen.yml b/changelogs/unreleased/57582-dropdown-icon-misalignment-on-issues-list-on-mobile-screen.yml new file mode 100644 index 00000000000..5681309cb9e --- /dev/null +++ b/changelogs/unreleased/57582-dropdown-icon-misalignment-on-issues-list-on-mobile-screen.yml @@ -0,0 +1,5 @@ +--- +title: Fix alignment of dropdown icon on issuable on mobile +merge_request: 25205 +author: Takuya Noguchi +type: fixed diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index a4c34ce85f0..9fd661d80ae 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -59,13 +59,6 @@ describe 'Visual tokens', :js do expect(page).to have_css('#js-dropdown-author', visible: false) end - it 'ends editing mode when scroll container is clicked' do - find('.scroll-container').click - - expect_filtered_search_input_empty - expect(page).to have_css('#js-dropdown-author', visible: false) - end - describe 'selecting different author from dropdown' do before do filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click @@ -109,13 +102,6 @@ describe 'Visual tokens', :js do expect(page).to have_css('#js-dropdown-assignee', visible: false) end - it 'ends editing mode when scroll container is clicked' do - find('.scroll-container').click - - expect_filtered_search_input_empty - expect(page).to have_css('#js-dropdown-assignee', visible: false) - end - describe 'selecting static option from dropdown' do before do find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click @@ -167,13 +153,6 @@ describe 'Visual tokens', :js do expect_filtered_search_input_empty expect(page).to have_css('#js-dropdown-milestone', visible: false) end - - it 'ends editing mode when scroll container is clicked' do - find('.scroll-container').click - - expect_filtered_search_input_empty - expect(page).to have_css('#js-dropdown-milestone', visible: false) - end end describe 'editing label token' do -- cgit v1.2.1 From 0b5255f7dd176dc093f25dd106932c588099ce4e Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Wed, 20 Feb 2019 05:28:34 +0000 Subject: Fix anchors in CE HA docs --- doc/administration/high_availability/redis.md | 2 +- doc/administration/high_availability/redis_source.md | 4 ++-- doc/university/high-availability/aws/README.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index bf5d064d79d..57429bf7e6c 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -363,7 +363,7 @@ Now that the Redis servers are all set up, let's configure the Sentinel servers. If you are not sure if your Redis servers are working and replicating -correctly, please read the [Troubleshooting Replication](#troubleshooting-replication) +correctly, please read the [Troubleshooting Replication](#troubleshooting-redis-replication) and fix it before proceeding with Sentinel setup. You must have at least `3` Redis Sentinel servers, and they need to diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md index 14e2784c419..8ecf100d483 100644 --- a/doc/administration/high_availability/redis_source.md +++ b/doc/administration/high_availability/redis_source.md @@ -46,7 +46,7 @@ valuable information for the general setup. Assuming that the Redis master instance IP is `10.0.0.1`: -1. [Install Redis](../../install/installation.md#6-redis) +1. [Install Redis](../../install/installation.md#7-redis) 1. Edit `/etc/redis/redis.conf`: ```conf @@ -72,7 +72,7 @@ Assuming that the Redis master instance IP is `10.0.0.1`: Assuming that the Redis slave instance IP is `10.0.0.2`: -1. [Install Redis](../../install/installation.md#6-redis) +1. [Install Redis](../../install/installation.md#7-redis) 1. Edit `/etc/redis/redis.conf`: ```conf diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md index b4ba5b7a24e..83c83c9db65 100644 --- a/doc/university/high-availability/aws/README.md +++ b/doc/university/high-availability/aws/README.md @@ -3,7 +3,7 @@ comments: false --- > **Note**: We **do not** recommend using the AWS Elastic File System (EFS), as it can result -in [significantly degraded performance](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/administration/high_availability/nfs.md#aws-elastic-file-system). +in [significantly degraded performance](../../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs). # High Availability on AWS @@ -182,7 +182,7 @@ Another option is to build a simple NFS server using a vanilla Linux server back by AWS Elastic Block Storage (EBS). > **Note:** GitLab does not recommend using AWS Elastic File System (EFS). See - details in [High Availability NFS documentation](../../../administration/high_availability/nfs.md#aws-elastic-file-system) + details in [High Availability NFS documentation](../../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) *** -- cgit v1.2.1 From 38f3c2e18684f94f75ab52e5039203e8321ed7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Wed, 13 Feb 2019 11:50:24 +0100 Subject: Remove link after issue move when no permissions Don't show new issue link after move when a user does not have permissions to display the new issue --- app/mailers/emails/issues.rb | 1 + app/views/notify/issue_moved_email.html.haml | 11 +++-- app/views/notify/issue_moved_email.text.erb | 4 ++ changelogs/unreleased/security-2799-emails.yml | 5 +++ spec/mailers/notify_spec.rb | 56 ++++++++++++++++++++------ 5 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/security-2799-emails.yml diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 654ae211310..d2e334fb856 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -74,6 +74,7 @@ module Emails @new_issue = new_issue @new_project = new_issue.project + @can_access_project = recipient.can?(:read_project, @new_project) mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason)) end diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml index 472c31e9a5e..b766cb1a523 100644 --- a/app/views/notify/issue_moved_email.html.haml +++ b/app/views/notify/issue_moved_email.html.haml @@ -1,6 +1,9 @@ %p Issue was moved to another project. -%p - New issue: - = link_to project_issue_url(@new_project, @new_issue) do - = @new_issue.title +- if @can_access_project + %p + New issue: + = link_to project_issue_url(@new_project, @new_issue) do + = @new_issue.title +- else + You don't have access to the project. diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb index 66ede43635b..985e689aa9d 100644 --- a/app/views/notify/issue_moved_email.text.erb +++ b/app/views/notify/issue_moved_email.text.erb @@ -1,4 +1,8 @@ Issue was moved to another project. +<% if @can_access_project %> New issue location: <%= project_issue_url(@new_project, @new_issue) %> +<% else %> +You don't have access to the project. +<% end %> diff --git a/changelogs/unreleased/security-2799-emails.yml b/changelogs/unreleased/security-2799-emails.yml new file mode 100644 index 00000000000..dbf1207810e --- /dev/null +++ b/changelogs/unreleased/security-2799-emails.yml @@ -0,0 +1,5 @@ +--- +title: Don't show new issue link after move when a user does not have permissions +merge_request: +author: +type: security diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 4f578c48d5b..418f707a130 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -194,23 +194,53 @@ describe Notify do let(:new_issue) { create(:issue) } subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) } - it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do - let(:model) { issue } - end - it_behaves_like 'it should show Gmail Actions View Issue link' - it_behaves_like 'an unsubscribeable thread' + context 'when a user has permissions to access the new issue' do + before do + new_issue.project.add_developer(recipient) + end + + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' + + it 'contains description about action taken' do + is_expected.to have_body_text 'Issue was moved to another project' + end + + it 'has the correct subject and body' do + new_issue_url = project_issue_path(new_issue.project, new_issue) - it 'contains description about action taken' do - is_expected.to have_body_text 'Issue was moved to another project' + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(new_issue_url) + is_expected.to have_body_text(project_issue_path(project, issue)) + end + end + + it 'contains the issue title' do + is_expected.to have_body_text new_issue.title + end end - it 'has the correct subject and body' do - new_issue_url = project_issue_path(new_issue.project, new_issue) + context 'when a user does not permissions to access the new issue' do + it 'has the correct subject and body' do + new_issue_url = project_issue_path(new_issue.project, new_issue) - aggregate_failures do - is_expected.to have_referable_subject(issue, reply: true) - is_expected.to have_body_text(new_issue_url) - is_expected.to have_body_text(project_issue_path(project, issue)) + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.not_to have_body_text(new_issue_url) + is_expected.to have_body_text(project_issue_path(project, issue)) + end + end + + it 'does not contain the issue title' do + is_expected.not_to have_body_text new_issue.title + end + + it 'contains information about missing permissions' do + is_expected.to have_body_text "You don't have access to the project." end end end -- cgit v1.2.1 From 211c4e5985bf40afe7cf2391c76a6cfde153fb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Tue, 12 Feb 2019 13:29:47 +0100 Subject: Change policy regarding group visibility --- app/controllers/projects/group_links_controller.rb | 5 +-- .../projects/group_links/create_service.rb | 10 ++++-- ...rnal-groups-as-members-to-your-project-idor.yml | 6 ++++ lib/api/projects.rb | 15 ++++----- .../groups/shared_projects_controller_spec.rb | 2 ++ .../projects/group_links_controller_spec.rb | 37 ++++++++++++++++++++++ .../features/projects/members/invite_group_spec.rb | 2 ++ .../settings/user_manages_group_links_spec.rb | 1 + spec/requests/api/projects_spec.rb | 12 +++++++ .../projects/group_links/create_service_spec.rb | 8 +++++ 10 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 7c713c19762..bc942ba9288 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -13,9 +13,10 @@ class Projects::GroupLinksController < Projects::ApplicationController group = Group.find(params[:link_group_id]) if params[:link_group_id].present? if group - return render_404 unless can?(current_user, :read_group, group) + result = Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) + return render_404 if result[:http_status] == 404 - Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) + flash[:alert] = result[:message] if result[:http_status] == 409 else flash[:alert] = 'Please select a group.' end diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb index 1392775f805..e3d5bea0852 100644 --- a/app/services/projects/group_links/create_service.rb +++ b/app/services/projects/group_links/create_service.rb @@ -4,13 +4,19 @@ module Projects module GroupLinks class CreateService < BaseService def execute(group) - return false unless group + return error('Not Found', 404) unless group && can?(current_user, :read_namespace, group) - project.project_group_links.create( + link = project.project_group_links.new( group: group, group_access: params[:link_group_access], expires_at: params[:expires_at] ) + + if link.save + success(link: link) + else + error(link.errors.full_messages.to_sentence, 409) + end end end end diff --git a/changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml b/changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml new file mode 100644 index 00000000000..27ad151cd06 --- /dev/null +++ b/changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml @@ -0,0 +1,6 @@ +--- +title: Remove the possibility to share a project with a group that a user is not a member + of +merge_request: +author: +type: security diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6a93ef9f3ad..a7b4dc06832 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -436,27 +436,24 @@ module API end params do requires :group_id, type: Integer, desc: 'The ID of a group' - requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' + requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level' optional :expires_at, type: Date, desc: 'Share expiration date' end post ":id/share" do authorize! :admin_project, user_project group = Group.find_by_id(params[:group_id]) - unless group && can?(current_user, :read_group, group) - not_found!('Group') - end - unless user_project.allowed_to_share_with_group? break render_api_error!("The project sharing with group is disabled", 400) end - link = user_project.project_group_links.new(declared_params(include_missing: false)) + result = ::Projects::GroupLinks::CreateService.new(user_project, current_user, declared_params(include_missing: false)) + .execute(group) - if link.save - present link, with: Entities::ProjectGroupLink + if result[:status] == :success + present result[:link], with: Entities::ProjectGroupLink else - render_api_error!(link.errors.full_messages.first, 409) + render_api_error!(result[:message], result[:http_status]) end end diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb index dab7700cf64..b0c20fb5a90 100644 --- a/spec/controllers/groups/shared_projects_controller_spec.rb +++ b/spec/controllers/groups/shared_projects_controller_spec.rb @@ -6,6 +6,8 @@ describe Groups::SharedProjectsController do end def share_project(project) + group.add_developer(user) + Projects::GroupLinks::CreateService.new( project, user, diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 675eeff8d12..ce021b2f085 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -65,8 +65,24 @@ describe Projects::GroupLinksController do end end + context 'when user does not have access to the public group' do + let(:group) { create(:group, :public) } + + include_context 'link project to group' + + it 'renders 404' do + expect(response.status).to eq 404 + end + + it 'does not share project with that group' do + expect(group.shared_projects).not_to include project + end + end + context 'when project group id equal link group id' do before do + group2.add_developer(user) + post(:create, params: { namespace_id: project.namespace, project_id: project, @@ -102,5 +118,26 @@ describe Projects::GroupLinksController do expect(flash[:alert]).to eq('Please select a group.') end end + + context 'when link is not persisted in the database' do + before do + allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) + .and_return({ status: :error, http_status: 409, message: 'error' }) + + post(:create, params: { + namespace_id: project.namespace, + project_id: project, + link_group_id: group.id, + link_group_access: ProjectGroupLink.default_access + }) + end + + it 'redirects to project group links page' do + expect(response).to redirect_to( + project_project_members_path(project) + ) + expect(flash[:alert]).to eq('error') + end + end end end diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index fceead0b45e..b2d2dba55f1 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -27,6 +27,7 @@ describe 'Project > Members > Invite group', :js do before do project.add_maintainer(maintainer) + group_to_share_with.add_guest(maintainer) sign_in(maintainer) end @@ -112,6 +113,7 @@ describe 'Project > Members > Invite group', :js do before do project.add_maintainer(maintainer) + group.add_guest(maintainer) sign_in(maintainer) visit project_settings_members_path(project) diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb index 676659b90c3..e5a58c44e41 100644 --- a/spec/features/projects/settings/user_manages_group_links_spec.rb +++ b/spec/features/projects/settings/user_manages_group_links_spec.rb @@ -10,6 +10,7 @@ describe 'Projects > Settings > User manages group links' do before do project.add_maintainer(user) + group_market.add_guest(user) sign_in(user) share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cfa7a1a31a3..feff53ebc25 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1484,6 +1484,9 @@ describe API::Projects do describe "POST /projects/:id/share" do let(:group) { create(:group) } + before do + group.add_developer(user) + end it "shares project with group" do expires_at = 10.days.from_now.to_date @@ -1534,6 +1537,15 @@ describe API::Projects do expect(response).to have_gitlab_http_status(400) expect(json_response['error']).to eq 'group_access does not have a valid value' end + + it "returns a 409 error when link is not saved" do + allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) + .and_return({ status: :error, http_status: 409, message: 'error' }) + + post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } + + expect(response).to have_gitlab_http_status(409) + end end describe 'DELETE /projects/:id/share/:group_id' do diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb index ffb270d277e..68fd82b4cbe 100644 --- a/spec/services/projects/group_links/create_service_spec.rb +++ b/spec/services/projects/group_links/create_service_spec.rb @@ -12,6 +12,10 @@ describe Projects::GroupLinks::CreateService, '#execute' do end let(:subject) { described_class.new(project, user, opts) } + before do + group.add_developer(user) + end + it 'adds group to project' do expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1) end @@ -19,4 +23,8 @@ describe Projects::GroupLinks::CreateService, '#execute' do it 'returns false if group is blank' do expect { subject.execute(nil) }.not_to change { project.project_group_links.count } end + + it 'returns error if user is not allowed to share with a group' do + expect { subject.execute(create :group) }.not_to change { project.project_group_links.count } + end end -- cgit v1.2.1 From 5011d83cb14b5ffae0060b3e7309a24eca80a1b2 Mon Sep 17 00:00:00 2001 From: Yauhen Kotau Date: Wed, 20 Feb 2019 19:00:55 +0300 Subject: Lowercase letters support and additional tests for YouTrack integration service --- app/models/project_services/youtrack_service.rb | 4 ++-- .../projects/services/user_activates_issue_tracker_spec.rb | 1 + .../banzai/filter/external_issue_reference_filter_spec.rb | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index ed8c0635124..ad9d26f3950 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -8,9 +8,9 @@ class YoutrackService < IssueTrackerService # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1 def self.reference_pattern(only_long: false) if only_long - /(?\b[A-Z][A-Z0-9_]*-\d+)/ + /(?\b[A-Z][A-Za-z0-9_]*-\d+)/ else - /(?\b[A-Z][A-Z0-9_]*-\d+)|(#{Issue.reference_prefix}(?\d+))/ + /(?\b[A-Z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?\d+))/ end end diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb index 7cd5b12802b..4a71eea1add 100644 --- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb @@ -87,6 +87,7 @@ describe 'User activates issue tracker', :js do end it_behaves_like 'external issue tracker activation', tracker: 'Redmine' + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack' it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker' end diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index e415bbcfc92..43222ddb5e2 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -135,6 +135,20 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do it_behaves_like "external issue tracker" end + context "with underscores in the prefix" do + let(:issue) { ExternalIssue.new("PRJ_1-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with lowercase letters in the prefix" do + let(:issue) { ExternalIssue.new("YTkPrj-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + context "with a single-letter prefix" do let(:issue) { ExternalIssue.new("T-123", project) } let(:reference) { issue.to_reference } -- cgit v1.2.1 From dd6fbbc44d2807b93d609d304db2c4da229297dc Mon Sep 17 00:00:00 2001 From: Yauhen Kotau Date: Wed, 20 Feb 2019 20:54:47 +0300 Subject: API documentation, changelog and additional tests for YouTrack integration service --- changelogs/unreleased/add-youtrack-integration.yml | 5 +++ doc/api/services.md | 36 ++++++++++++++++++++++ spec/models/project_spec.rb | 1 + 3 files changed, 42 insertions(+) create mode 100644 changelogs/unreleased/add-youtrack-integration.yml diff --git a/changelogs/unreleased/add-youtrack-integration.yml b/changelogs/unreleased/add-youtrack-integration.yml new file mode 100644 index 00000000000..f500e625145 --- /dev/null +++ b/changelogs/unreleased/add-youtrack-integration.yml @@ -0,0 +1,5 @@ +--- +title: Add YouTrack integration service +merge_request: 25361 +author: Yauhen Kotau @bessorion +type: added diff --git a/doc/api/services.md b/doc/api/services.md index 5d5aa3e5b3e..7efd86dafd6 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -1101,3 +1101,39 @@ GET /projects/:id/services/mock-ci ``` [11435]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11435 + +## YouTrack + +YouTrack issue tracker + +### Create/Edit YouTrack service + +Set YouTrack service for a project. + +``` +PUT /projects/:id/services/youtrack +``` + +Parameters: + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issues_url` | string | true | Issue url | +| `project_url` | string | true | Project url | +| `description` | string | false | Description | + +### Delete YouTrack Service + +Delete YouTrack service for a project. + +``` +DELETE /projects/:id/services/youtrack +``` + +### Get YouTrack Service Settings + +Get YouTrack service settings for a project. + +``` +GET /projects/:id/services/youtrack +``` diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1f9088c2e6b..75fb30554b3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -50,6 +50,7 @@ describe Project do it { is_expected.to have_one(:teamcity_service) } it { is_expected.to have_one(:jira_service) } it { is_expected.to have_one(:redmine_service) } + it { is_expected.to have_one(:youtrack_service) } it { is_expected.to have_one(:custom_issue_tracker_service) } it { is_expected.to have_one(:bugzilla_service) } it { is_expected.to have_one(:gitlab_issue_tracker_service) } -- cgit v1.2.1 From 52c910eeca47f140246d003fbb6b4748d1be8bb8 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Wed, 20 Feb 2019 12:20:09 -0700 Subject: Remove deprecated ci status helper function After changing all places that used the function, we can now remove it. --- app/helpers/ci_status_helper.rb | 6 ------ app/views/ci/status/_icon.html.haml | 2 +- spec/views/ci/status/_icon.html.haml_spec.rb | 1 + 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 923a06a0512..dfeeecf1228 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -123,12 +123,6 @@ module CiStatusHelper icon_size: 24) end - def render_pipeline_status(pipeline, tooltip_placement: 'left') - project = pipeline.project - path = project_pipeline_path(project, pipeline) - render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement) - end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index bfdf9c2ec9f..29ce9cd1afd 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -9,4 +9,4 @@ = sprite_icon(status.icon, size: size) - else %span{ class: css_classes, title: title, data: { html: title.present? } } - = sprite_icon(status.icon, size: size) \ No newline at end of file + = sprite_icon(status.icon, size: size) diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb index 43806446164..626159fc512 100644 --- a/spec/views/ci/status/_icon.html.haml_spec.rb +++ b/spec/views/ci/status/_icon.html.haml_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'spec_helper' describe 'ci/status/_icon' do -- cgit v1.2.1 From b11d018bd59a3c94086dbeb11f38db81e441a1e2 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 7 Feb 2019 12:46:41 +1300 Subject: Add changelog entry --- changelogs/unreleased/security-protect-private-repo-information.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/security-protect-private-repo-information.yml diff --git a/changelogs/unreleased/security-protect-private-repo-information.yml b/changelogs/unreleased/security-protect-private-repo-information.yml new file mode 100644 index 00000000000..8b1a528206d --- /dev/null +++ b/changelogs/unreleased/security-protect-private-repo-information.yml @@ -0,0 +1,5 @@ +--- +title: Fix leaking private repository information in API +merge_request: +author: +type: security -- cgit v1.2.1 From d24ea868ab5a3644884cb02f6faa2b8daf48b1d2 Mon Sep 17 00:00:00 2001 From: Diana Stanley Date: Wed, 20 Feb 2019 15:09:27 -0800 Subject: Capture due date when importing milestones from Github --- .../unreleased/57612-github-importer-ignores-milestone-due_date.yml | 5 +++++ lib/gitlab/github_import/importer/milestones_importer.rb | 1 + spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 changelogs/unreleased/57612-github-importer-ignores-milestone-due_date.yml diff --git a/changelogs/unreleased/57612-github-importer-ignores-milestone-due_date.yml b/changelogs/unreleased/57612-github-importer-ignores-milestone-due_date.yml new file mode 100644 index 00000000000..0d5cd057ade --- /dev/null +++ b/changelogs/unreleased/57612-github-importer-ignores-milestone-due_date.yml @@ -0,0 +1,5 @@ +--- +title: Capture due date when importing milestones from Github +merge_request: 25182 +author: dstanley +type: changed diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb index 87cf2c8b598..8a5b3db9e08 100644 --- a/lib/gitlab/github_import/importer/milestones_importer.rb +++ b/lib/gitlab/github_import/importer/milestones_importer.rb @@ -42,6 +42,7 @@ module Gitlab description: milestone.description, project_id: project.id, state: state_for(milestone), + due_date: milestone.due_on.to_date, created_at: milestone.created_at, updated_at: milestone.updated_at } diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb index b1cac3b6e46..471f499dd19 100644 --- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis let(:project) { create(:project, import_source: 'foo/bar') } let(:client) { double(:client) } let(:importer) { described_class.new(project, client) } + let(:due_on) { Time.new(2017, 2, 1, 12, 00) } let(:created_at) { Time.new(2017, 1, 1, 12, 00) } let(:updated_at) { Time.new(2017, 1, 1, 12, 15) } @@ -14,6 +15,7 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis title: '1.0', description: 'The first release', state: 'open', + due_on: due_on, created_at: created_at, updated_at: updated_at ) @@ -98,6 +100,10 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis expect(milestone_hash[:state]).to eq(:active) end + it 'includes the due date' do + expect(milestone_hash[:due_date]).to eq(due_on.to_date) + end + it 'includes the created timestamp' do expect(milestone_hash[:created_at]).to eq(created_at) end -- cgit v1.2.1 From 85f3a26dc163610fa69b66532934f067aa645934 Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Thu, 21 Feb 2019 06:18:28 +0000 Subject: Add punctuation. --- doc/administration/high_availability/redis_source.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md index 8ecf100d483..be6b547372a 100644 --- a/doc/administration/high_availability/redis_source.md +++ b/doc/administration/high_availability/redis_source.md @@ -46,7 +46,7 @@ valuable information for the general setup. Assuming that the Redis master instance IP is `10.0.0.1`: -1. [Install Redis](../../install/installation.md#7-redis) +1. [Install Redis](../../install/installation.md#7-redis). 1. Edit `/etc/redis/redis.conf`: ```conf @@ -72,7 +72,7 @@ Assuming that the Redis master instance IP is `10.0.0.1`: Assuming that the Redis slave instance IP is `10.0.0.2`: -1. [Install Redis](../../install/installation.md#7-redis) +1. [Install Redis](../../install/installation.md#7-redis). 1. Edit `/etc/redis/redis.conf`: ```conf -- cgit v1.2.1 From d72b1cd0b5b01d6fec6b93d9dfe84f8302083072 Mon Sep 17 00:00:00 2001 From: Mark Chao Date: Wed, 13 Feb 2019 16:24:26 +0800 Subject: Check snippet attached file to be moved is within designated directory Previously one could move any temp/ sub folder around. --- app/uploaders/file_mover.rb | 8 ++++++++ changelogs/unreleased/security-56348.yml | 5 +++++ spec/controllers/snippets_controller_spec.rb | 4 ++++ spec/support/helpers/file_mover_helpers.rb | 12 +++++++++++ spec/uploaders/file_mover_spec.rb | 30 ++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 changelogs/unreleased/security-56348.yml create mode 100644 spec/support/helpers/file_mover_helpers.rb diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb index a7f8615e9ba..236b7ed2b3d 100644 --- a/app/uploaders/file_mover.rb +++ b/app/uploaders/file_mover.rb @@ -11,6 +11,8 @@ class FileMover end def execute + return unless valid? + move if update_markdown @@ -21,6 +23,12 @@ class FileMover private + def valid? + Pathname.new(temp_file_path).realpath.to_path.start_with?( + (Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path + ) + end + def move FileUtils.mkdir_p(File.dirname(file_path)) FileUtils.move(temp_file_path, file_path) diff --git a/changelogs/unreleased/security-56348.yml b/changelogs/unreleased/security-56348.yml new file mode 100644 index 00000000000..a289e4e9077 --- /dev/null +++ b/changelogs/unreleased/security-56348.yml @@ -0,0 +1,5 @@ +--- +title: Check snippet attached file to be moved is within designated directory +merge_request: +author: +type: security diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 5c6858dc7b2..77a94f26d8c 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -205,6 +205,8 @@ describe SnippetsController do end context 'when the snippet description contains a file' do + include FileMoverHelpers + let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:description) do @@ -215,6 +217,8 @@ describe SnippetsController do before do allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:move) + stub_file_mover(text_file) + stub_file_mover(picture_file) end subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) } diff --git a/spec/support/helpers/file_mover_helpers.rb b/spec/support/helpers/file_mover_helpers.rb new file mode 100644 index 00000000000..1ba7cc03354 --- /dev/null +++ b/spec/support/helpers/file_mover_helpers.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module FileMoverHelpers + def stub_file_mover(file_path, stub_real_path: nil) + file_name = File.basename(file_path) + allow(Pathname).to receive(:new).and_call_original + + expect_next_instance_of(Pathname, a_string_including(file_name)) do |pathname| + allow(pathname).to receive(:realpath) { stub_real_path || pathname.cleanpath } + end + end +end diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index a28d7445b1c..e474a714b10 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe FileMover do + include FileMoverHelpers + let(:filename) { 'banana_sample.gif' } let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } @@ -19,6 +21,8 @@ describe FileMover do expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path)) allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true) allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10) + + stub_file_mover(temp_file_path) end context 'when move and field update successful' do @@ -65,4 +69,30 @@ describe FileMover do end end end + + context 'security' do + context 'when relative path is involved' do + let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') } + + it 'does not trigger move if path is outside designated directory' do + stub_file_mover('uploads/-/system/another_subdir_of_temp') + expect(FileUtils).not_to receive(:move) + + subject + + expect(snippet.reload.description).to eq(temp_description) + end + end + + context 'when symlink is involved' do + it 'does not trigger move if path is outside designated directory' do + stub_file_mover(temp_file_path, stub_real_path: Pathname('/etc')) + expect(FileUtils).not_to receive(:move) + + subject + + expect(snippet.reload.description).to eq(temp_description) + end + end + end end -- cgit v1.2.1 From af16fd687e2e5b15a63e6e51d76847512ae8ee72 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Wed, 13 Feb 2019 09:46:59 +1300 Subject: Do not allow local urls in Kubernetes form Use existing `public_url` validation to block various local urls. Note that this validation will allow local urls if the "Allow requests to the local network from hooks and services" admin setting is enabled. Block KubeClient from using local addresses It will also respect `allow_local_requests_from_hooks_and_services` so if that is enabled KubeClinet will allow local addresses --- app/models/clusters/platforms/kubernetes.rb | 2 +- .../unreleased/security-kubernetes-local-ssrf.yml | 5 ++++ lib/gitlab/kubernetes/kube_client.rb | 8 ++++++ spec/lib/gitlab/kubernetes/kube_client_spec.rb | 30 ++++++++++++++++++++++ spec/models/clusters/platforms/kubernetes_spec.rb | 16 ++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/security-kubernetes-local-ssrf.yml diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 46d0898014e..814fc591408 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -41,7 +41,7 @@ module Clusters validate :no_namespace, unless: :allow_user_defined_namespace? # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) - validates :api_url, url: true, presence: true + validates :api_url, public_url: true, presence: true validates :token, presence: true validates :ca_cert, certificate: true, allow_blank: true, if: :ca_cert_changed? diff --git a/changelogs/unreleased/security-kubernetes-local-ssrf.yml b/changelogs/unreleased/security-kubernetes-local-ssrf.yml new file mode 100644 index 00000000000..7a2ad092339 --- /dev/null +++ b/changelogs/unreleased/security-kubernetes-local-ssrf.yml @@ -0,0 +1,5 @@ +--- +title: Block local URLs for Kubernetes integration +merge_request: +author: +type: security diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 624c2c67551..de14df56555 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -82,6 +82,8 @@ module Gitlab def initialize(api_prefix, **kubeclient_options) @api_prefix = api_prefix @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0) + + validate_url! end def create_or_update_cluster_role_binding(resource) @@ -118,6 +120,12 @@ module Gitlab private + def validate_url! + return if Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services? + + Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false) + end + def cluster_role_binding_exists?(resource) get_cluster_role_binding(resource.metadata.name) rescue ::Kubeclient::ResourceNotFoundError diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 02364e92149..978e64c4407 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do end end + describe '#initialize' do + shared_examples 'local address' do + it 'blocks local addresses' do + expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_hooks_and_services: true) + end + + it 'allows local addresses' do + expect { client }.not_to raise_error + end + end + end + + context 'localhost address' do + let(:api_url) { 'http://localhost:22' } + + it_behaves_like 'local address' + end + + context 'private network address' do + let(:api_url) { 'http://192.168.1.2:3003' } + + it_behaves_like 'local address' + end + end + describe '#core_client' do subject { client.core_client } diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 4068d98d8f7..3b32ca8df05 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -98,6 +98,22 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { expect(kubernetes.save).to be_truthy } end + + context 'when api_url is localhost' do + let(:api_url) { 'http://localhost:22' } + + it { expect(kubernetes.save).to be_falsey } + + context 'Application settings allows local requests' do + before do + allow(ApplicationSetting) + .to receive(:current) + .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true)) + end + + it { expect(kubernetes.save).to be_truthy } + end + end end context 'when validates token' do -- cgit v1.2.1 From d5e38b00cffacf2f0599c99a1bb1515b6f56aa9b Mon Sep 17 00:00:00 2001 From: Kotau Yauhen Date: Thu, 21 Feb 2019 10:39:44 +0000 Subject: Remove new_issue_url field from YouTrack integration service --- app/models/project_services/youtrack_service.rb | 18 +++-- doc/user/project/integrations/youtrack.md | 1 - lib/api/services.rb | 6 -- spec/factories/projects.rb | 3 +- .../services/user_activates_issue_tracker_spec.rb | 1 - .../services/user_activates_youtrack_spec.rb | 89 ++++++++++++++++++++++ .../project_services/youtrack_service_spec.rb | 3 - 7 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 spec/features/projects/services/user_activates_youtrack_spec.rb diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index ad9d26f3950..bf4a4166045 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class YoutrackService < IssueTrackerService - validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? + validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + prop_accessor :title, :description, :project_url, :issues_url # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1 def self.reference_pattern(only_long: false) @@ -15,11 +15,7 @@ class YoutrackService < IssueTrackerService end def title - if self.properties && self.properties['title'].present? - self.properties['title'] - else - 'YouTrack' - end + 'YouTrack' end def description @@ -33,4 +29,12 @@ class YoutrackService < IssueTrackerService def self.to_param 'youtrack' end + + def fields + [ + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url', required: true }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true } + ] + end end diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index 85c339d5fa9..3e9295dfcbd 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -10,7 +10,6 @@ in the table below. | `description` | A name for the issue tracker (to differentiate between instances, for example) | | `project_url` | The URL to the project in YouTrack which is being linked to this GitLab project | | `issues_url` | The URL to the issue in YouTrack project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | - | `new_issue_url` | This is the URL to create a new issue in YouTrack for the project linked to this GitLab project. **This is currently not being used and will be removed in a future release.** | Once you have configured and enabled YouTrack you'll see the YouTrack link on the GitLab project pages that takes you to the appropriate YouTrack project. diff --git a/lib/api/services.rb b/lib/api/services.rb index fcaec06061b..7967d4fe5e4 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -593,12 +593,6 @@ module API } ], 'youtrack' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'The new issue URL' - }, { required: true, name: :project_url, diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 0efb15337ed..30d3b22d868 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -321,8 +321,7 @@ FactoryBot.define do active: true, properties: { 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack', - 'issues_url' => 'http://youtrack/issues/:id', - 'new_issue_url' => 'http://youtrack/newIssue' + 'issues_url' => 'http://youtrack/issues/:id' } ) end diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb index 4a71eea1add..7cd5b12802b 100644 --- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb @@ -87,7 +87,6 @@ describe 'User activates issue tracker', :js do end it_behaves_like 'external issue tracker activation', tracker: 'Redmine' - it_behaves_like 'external issue tracker activation', tracker: 'YouTrack' it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker' end diff --git a/spec/features/projects/services/user_activates_youtrack_spec.rb b/spec/features/projects/services/user_activates_youtrack_spec.rb new file mode 100644 index 00000000000..bb6a030c1cf --- /dev/null +++ b/spec/features/projects/services/user_activates_youtrack_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'User activates issue tracker', :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:url) { 'http://tracker.example.com' } + + def fill_form(active = true) + check 'Active' if active + + fill_in 'service_project_url', with: url + fill_in 'service_issues_url', with: "#{url}/:id" + end + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_settings_integrations_path(project) + end + + shared_examples 'external issue tracker activation' do |tracker:| + describe 'user sets and activates the Service' do + context 'when the connection test succeeds' do + before do + stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + end + + it 'activates the service' do + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'shows the link in the menu' do + page.within('.nav-sidebar') do + expect(page).to have_link(tracker, href: url) + end + end + end + + context 'when the connection test fails' do + it 'activates the service' do + stub_request(:head, url).to_raise(HTTParty::Error) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + + expect(find('.flash-container-page')).to have_content 'Test failed.' + expect(find('.flash-container-page')).to have_content 'Save anyway' + + find('.flash-alert .flash-action').click + wait_for_requests + + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + end + end + + describe 'user sets the service but keeps it disabled' do + before do + click_link(tracker) + fill_form(false) + click_button('Save changes') + end + + it 'saves but does not activate the service' do + expect(page).to have_content("#{tracker} settings saved, but not activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'does not show the external tracker link in the menu' do + page.within('.nav-sidebar') do + expect(page).not_to have_link(tracker, href: url) + end + end + end + end + + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack' +end diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb index b1e74a6f877..9524b526a46 100644 --- a/spec/models/project_services/youtrack_service_spec.rb +++ b/spec/models/project_services/youtrack_service_spec.rb @@ -14,10 +14,8 @@ describe YoutrackService do it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } - it { is_expected.to validate_presence_of(:new_issue_url) } it_behaves_like 'issue tracker service URL attribute', :project_url it_behaves_like 'issue tracker service URL attribute', :issues_url - it_behaves_like 'issue tracker service URL attribute', :new_issue_url end context 'when service is inactive' do @@ -27,7 +25,6 @@ describe YoutrackService do it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } - it { is_expected.not_to validate_presence_of(:new_issue_url) } end end -- cgit v1.2.1 From 445709345f8a451b7882c81166063b8948688e0f Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Fri, 15 Feb 2019 17:11:16 -0200 Subject: Stop linking to unrecognized package sources --- .../blob/viewers/_dependency_manager.html.haml | 5 --- lib/gitlab/dependency_linker/base_linker.rb | 14 ++++++++ .../dependency_linker/composer_json_linker.rb | 4 +-- lib/gitlab/dependency_linker/gemfile_linker.rb | 30 +++++++++++++--- lib/gitlab/dependency_linker/gemspec_linker.rb | 2 +- lib/gitlab/dependency_linker/method_linker.rb | 10 ++++-- lib/gitlab/dependency_linker/package.rb | 19 ++++++++++ .../dependency_linker/package_json_linker.rb | 21 ++++------- lib/gitlab/dependency_linker/parser/gemfile.rb | 40 +++++++++++++++++++++ lib/gitlab/dependency_linker/podfile_linker.rb | 11 +++++- lib/gitlab/dependency_linker/podspec_linker.rb | 2 +- spec/features/projects/blobs/blob_show_spec.rb | 5 +-- .../dependency_linker/composer_json_linker_spec.rb | 4 +-- .../dependency_linker/gemfile_linker_spec.rb | 9 +++-- .../dependency_linker/gemspec_linker_spec.rb | 4 +-- .../dependency_linker/package_json_linker_spec.rb | 18 +++++++--- .../dependency_linker/parser/gemfile_spec.rb | 42 ++++++++++++++++++++++ .../dependency_linker/podfile_linker_spec.rb | 5 ++- .../dependency_linker/podspec_linker_spec.rb | 4 +-- 19 files changed, 198 insertions(+), 51 deletions(-) create mode 100644 lib/gitlab/dependency_linker/package.rb create mode 100644 lib/gitlab/dependency_linker/parser/gemfile.rb create mode 100644 spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml index 87aa7c1dbf8..5970d41fdab 100644 --- a/app/views/projects/blob/viewers/_dependency_manager.html.haml +++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml @@ -3,9 +3,4 @@ This project manages its dependencies using %strong= viewer.manager_name - - if viewer.package_name - and defines a #{viewer.package_type} named - %strong< - = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer' - = link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer' diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index ac2efe598b4..6b44d5b1518 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -4,6 +4,7 @@ module Gitlab module DependencyLinker class BaseLinker URL_REGEX = %r{https?://[^'" ]+}.freeze + GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze class_attribute :file_type @@ -29,6 +30,19 @@ module Gitlab highlighted_lines.join.html_safe end + def external_url(name, external_ref) + return if external_ref =~ GIT_INVALID_URL_REGEX + + case external_ref + when /\A#{URL_REGEX}\z/ + external_ref + when /\A#{REPO_REGEX}\z/ + github_url(external_ref) + else + package_url(name) + end + end + private def link_dependencies diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb index 22d2bead891..4b8862b31ee 100644 --- a/lib/gitlab/dependency_linker/composer_json_linker.rb +++ b/lib/gitlab/dependency_linker/composer_json_linker.rb @@ -8,8 +8,8 @@ module Gitlab private def link_packages - link_packages_at_key("require", &method(:package_url)) - link_packages_at_key("require-dev", &method(:package_url)) + link_packages_at_key("require") + link_packages_at_key("require-dev") end def package_url(name) diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb index 8ab219c4962..c6e02248b0a 100644 --- a/lib/gitlab/dependency_linker/gemfile_linker.rb +++ b/lib/gitlab/dependency_linker/gemfile_linker.rb @@ -3,8 +3,14 @@ module Gitlab module DependencyLinker class GemfileLinker < MethodLinker + class_attribute :package_keyword + + self.package_keyword = :gem self.file_type = :gemfile + GITHUB_REGEX = /(github:|:github\s*=>)\s*['"](?[^'"]+)['"]/.freeze + GIT_REGEX = /(git:|:git\s*=>)\s*['"](?#{URL_REGEX})['"]/.freeze + private def link_dependencies @@ -14,21 +20,35 @@ module Gitlab def link_urls # Link `github: "user/repo"` to https://github.com/user/repo - link_regex(/(github:|:github\s*=>)\s*['"](?[^'"]+)['"]/, &method(:github_url)) + link_regex(GITHUB_REGEX, &method(:github_url)) # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo - link_regex(/(git:|:git\s*=>)\s*['"](?#{URL_REGEX})['"]/, &:itself) + link_regex(GIT_REGEX, &:itself) # Link `source "https://rubygems.org"` to https://rubygems.org link_method_call('source', URL_REGEX, &:itself) end def link_packages - # Link `gem "package_name"` to https://rubygems.org/gems/package_name - link_method_call('gem') do |name| - "https://rubygems.org/gems/#{name}" + packages = parse_packages + + return if packages.blank? + + packages.each do |package| + link_method_call('gem', package.name) do + external_url(package.name, package.external_ref) + end end end + + def package_url(name) + "https://rubygems.org/gems/#{name}" + end + + def parse_packages + parser = Gitlab::DependencyLinker::Parser::Gemfile.new(plain_text) + parser.parse(keyword: self.class.package_keyword) + end end end end diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb index b924ea86d89..94c2b375cf9 100644 --- a/lib/gitlab/dependency_linker/gemspec_linker.rb +++ b/lib/gitlab/dependency_linker/gemspec_linker.rb @@ -11,7 +11,7 @@ module Gitlab link_method_call('homepage', URL_REGEX, &:itself) link_method_call('license', &method(:license_url)) - link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) do |name| + link_method_call(%w[add_dependency add_runtime_dependency add_development_dependency]) do |name| "https://rubygems.org/gems/#{name}" end end diff --git a/lib/gitlab/dependency_linker/method_linker.rb b/lib/gitlab/dependency_linker/method_linker.rb index d4d85bb3390..33899a931c6 100644 --- a/lib/gitlab/dependency_linker/method_linker.rb +++ b/lib/gitlab/dependency_linker/method_linker.rb @@ -23,18 +23,22 @@ module Gitlab # link_method_call('name') # # Will link `package` in `self.name = "package"` def link_method_call(method_name, value = nil, &url_proc) + regex = method_call_regex(method_name, value) + + link_regex(regex, &url_proc) + end + + def method_call_regex(method_name, value = nil) method_name = regexp_for_value(method_name) value = regexp_for_value(value) - regex = %r{ + %r{ #{method_name} # Method name \s* # Whitespace [(=]? # Opening brace or equals sign \s* # Whitespace ['"](?#{value})['"] # Package name in quotes }x - - link_regex(regex, &url_proc) end end end diff --git a/lib/gitlab/dependency_linker/package.rb b/lib/gitlab/dependency_linker/package.rb new file mode 100644 index 00000000000..8a509bbd562 --- /dev/null +++ b/lib/gitlab/dependency_linker/package.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module DependencyLinker + class Package + attr_reader :name, :git_ref, :github_ref + + def initialize(name, git_ref, github_ref) + @name = name + @git_ref = git_ref + @github_ref = github_ref + end + + def external_ref + @git_ref || @github_ref + end + end + end +end diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb index 578e25f806a..6857f2a4fa2 100644 --- a/lib/gitlab/dependency_linker/package_json_linker.rb +++ b/lib/gitlab/dependency_linker/package_json_linker.rb @@ -8,7 +8,6 @@ module Gitlab private def link_dependencies - link_json('name', json["name"], &method(:package_url)) link_json('license', &method(:license_url)) link_json(%w[homepage url], URL_REGEX, &:itself) @@ -16,25 +15,19 @@ module Gitlab end def link_packages - link_packages_at_key("dependencies", &method(:package_url)) - link_packages_at_key("devDependencies", &method(:package_url)) + link_packages_at_key("dependencies") + link_packages_at_key("devDependencies") end - def link_packages_at_key(key, &url_proc) + def link_packages_at_key(key) dependencies = json[key] return unless dependencies dependencies.each do |name, version| - link_json(name, version, link: :key, &url_proc) - - link_json(name) do |value| - case value - when /\A#{URL_REGEX}\z/ - value - when /\A#{REPO_REGEX}\z/ - github_url(value) - end - end + external_url = external_url(name, version) + + link_json(name, version, link: :key) { external_url } + link_json(name) { external_url } end end diff --git a/lib/gitlab/dependency_linker/parser/gemfile.rb b/lib/gitlab/dependency_linker/parser/gemfile.rb new file mode 100644 index 00000000000..7f755375cea --- /dev/null +++ b/lib/gitlab/dependency_linker/parser/gemfile.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module DependencyLinker + module Parser + class Gemfile < MethodLinker + GIT_REGEX = Gitlab::DependencyLinker::GemfileLinker::GIT_REGEX + GITHUB_REGEX = Gitlab::DependencyLinker::GemfileLinker::GITHUB_REGEX + + def initialize(plain_text) + @plain_text = plain_text + end + + # Returns a list of Gitlab::DependencyLinker::Package + # + # keyword - The package definition keyword, e.g. `:gem` for + # Gemfile parsing, `:pod` for Podfile. + def parse(keyword:) + plain_lines.each_with_object([]) do |line, packages| + name = fetch(line, method_call_regex(keyword)) + + next unless name + + git_ref = fetch(line, GIT_REGEX) + github_ref = fetch(line, GITHUB_REGEX) + + packages << Gitlab::DependencyLinker::Package.new(name, git_ref, github_ref) + end + end + + private + + def fetch(line, regex, group: :name) + match = line.match(regex) + match[group] if match + end + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb index def9b04cca9..a20d285da79 100644 --- a/lib/gitlab/dependency_linker/podfile_linker.rb +++ b/lib/gitlab/dependency_linker/podfile_linker.rb @@ -5,12 +5,21 @@ module Gitlab class PodfileLinker < GemfileLinker include Cocoapods + self.package_keyword = :pod self.file_type = :podfile private def link_packages - link_method_call('pod', &method(:package_url)) + packages = parse_packages + + return unless packages + + packages.each do |package| + link_method_call('pod', package.name) do + external_url(package.name, package.external_ref) + end + end end end end diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb index 6b1758c5a43..14abd3999c4 100644 --- a/lib/gitlab/dependency_linker/podspec_linker.rb +++ b/lib/gitlab/dependency_linker/podspec_linker.rb @@ -19,7 +19,7 @@ module Gitlab link_method_call('license', &method(:license_url)) link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url)) - link_method_call(%w[name dependency], &method(:package_url)) + link_method_call('dependency', &method(:package_url)) end end end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 3edcc7ac2cd..a7aa63018fd 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -548,10 +548,7 @@ describe 'File blob', :js do it 'displays an auxiliary viewer' do aggregate_failures do # shows names of dependency manager and package - expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') - - # shows a link to the gem - expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord') + expect(page).to have_content('This project manages its dependencies using RubyGems.') # shows a learn more link expect(page).to have_link('Learn more', href: 'https://rubygems.org/') diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb index 4d222564fd0..0ebd8994636 100644 --- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do %{#{name}} end - it 'links the module name' do - expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) + it 'does not link the module name' do + expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) end it 'links the homepage' do diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb index a97803b119e..f00f6b47b94 100644 --- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do end it 'links dependencies' do - expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails')) expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer')) - expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders')) - expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets')) expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for')) end + it 'links to external dependencies' do + expect(subject).to include(link('rails', 'https://github.com/rails/rails')) + expect(subject).to include(link('responders', 'https://github.com/rails/responders')) + expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets')) + end + it 'links GitHub repos' do expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails')) expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders')) diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb index 24ad7d12f4c..6c6a5d70576 100644 --- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do %{#{name}} end - it 'links the gem name' do - expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) + it 'does not link the gem name' do + expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) end it 'links the license' do diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb index 1e8b72afb7b..9050127af7f 100644 --- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do "express": "4.2.x", "bigpipe": "bigpipe/pagelet", "plates": "https://github.com/flatiron/plates/tarball/master", - "karma": "^1.4.1" + "karma": "^1.4.1", + "random": "git+https://EdOverflow@github.com/example/example.git" }, "devDependencies": { "vows": "^0.7.0", @@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do %{#{name}} end - it 'links the module name' do - expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) + it 'does not link the module name' do + expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name')) end it 'links the homepage' do @@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) expect(subject).to include(link('async', 'https://npmjs.com/package/async')) expect(subject).to include(link('express', 'https://npmjs.com/package/express')) - expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe')) - expect(subject).to include(link('plates', 'https://npmjs.com/package/plates')) expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) end + it 'links dependencies to URL detected on value' do + expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet')) + expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master')) + end + + it 'does not link to NPM when invalid git URL' do + expect(subject).not_to include(link('random', 'https://npmjs.com/package/random')) + end + it 'links GitHub repos' do expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) end diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb new file mode 100644 index 00000000000..f81dbcf62da --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::Parser::Gemfile do + describe '#parse' do + let(:file_content) do + <<-CONTENT.strip_heredoc + source 'https://rubygems.org' + + gem "rails", '4.2.6', github: "rails/rails" + gem 'rails-deprecated_sanitizer', '~> 1.0.3' + gem 'responders', '~> 2.0', :github => 'rails/responders' + gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets' + gem 'default_value_for', '~> 3.0.0' + CONTENT + end + + subject { described_class.new(file_content).parse(keyword: 'gem') } + + def fetch_package(name) + subject.find { |package| package.name == name } + end + + it 'returns parsed packages' do + expect(subject.size).to eq(5) + expect(subject).to all(be_a(Gitlab::DependencyLinker::Package)) + end + + it 'packages respond to name and external_ref accordingly' do + expect(fetch_package('rails')).to have_attributes(name: 'rails', + github_ref: 'rails/rails', + git_ref: nil) + + expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets', + github_ref: nil, + git_ref: 'https://gitlab.example.com/gems/sprockets') + + expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for', + github_ref: nil, + git_ref: nil) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb index cdfd7ad9826..8f1b523653e 100644 --- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do it 'links packages' do expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) - expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) + end + + it 'links external packages' do + expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git')) end it 'links Git repos' do diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb index ed60ab45955..bacec830103 100644 --- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do %{#{name}} end - it 'links the gem name' do - expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) + it 'does not link the pod name' do + expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) end it 'links the license' do -- cgit v1.2.1 From f6754010abf46fc65faade15054eef9343bb5622 Mon Sep 17 00:00:00 2001 From: Kotau Yauhen Date: Fri, 22 Feb 2019 08:57:27 +0000 Subject: Update external issue tracker services test, remove `prop_accessor :title` from YouTrack integration service --- app/models/project_services/youtrack_service.rb | 2 +- .../services/user_activates_issue_tracker_spec.rb | 35 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index bf4a4166045..957be685aea 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -3,7 +3,7 @@ class YoutrackService < IssueTrackerService validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? - prop_accessor :title, :description, :project_url, :issues_url + prop_accessor :description, :project_url, :issues_url # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1 def self.reference_pattern(only_long: false) diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb index 7cd5b12802b..74b9a2b20cd 100644 --- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb @@ -6,11 +6,17 @@ describe 'User activates issue tracker', :js do let(:url) { 'http://tracker.example.com' } - def fill_form(active = true) + def fill_short_form(active = true) check 'Active' if active fill_in 'service_project_url', with: url fill_in 'service_issues_url', with: "#{url}/:id" + end + + def fill_full_form(active = true) + fill_short_form(active) + check 'Active' if active + fill_in 'service_new_issue_url', with: url end @@ -21,14 +27,20 @@ describe 'User activates issue tracker', :js do visit project_settings_integrations_path(project) end - shared_examples 'external issue tracker activation' do |tracker:| + shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false| describe 'user sets and activates the Service' do context 'when the connection test succeeds' do before do stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) click_link(tracker) - fill_form + + if skip_new_issue_url + fill_short_form + else + fill_full_form + end + click_button('Test settings and save changes') wait_for_requests end @@ -50,7 +62,13 @@ describe 'User activates issue tracker', :js do stub_request(:head, url).to_raise(HTTParty::Error) click_link(tracker) - fill_form + + if skip_new_issue_url + fill_short_form + else + fill_full_form + end + click_button('Test settings and save changes') wait_for_requests @@ -69,7 +87,13 @@ describe 'User activates issue tracker', :js do describe 'user sets the service but keeps it disabled' do before do click_link(tracker) - fill_form(false) + + if skip_new_issue_url + fill_short_form(false) + else + fill_full_form(false) + end + click_button('Save changes') end @@ -87,6 +111,7 @@ describe 'User activates issue tracker', :js do end it_behaves_like 'external issue tracker activation', tracker: 'Redmine' + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack', skip_new_issue_url: true it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker' end -- cgit v1.2.1 From 9b2befb65b5ff6a6a00333366981eccf791f98f3 Mon Sep 17 00:00:00 2001 From: James Fargher Date: Thu, 10 Jan 2019 08:44:06 +1300 Subject: Use auto-build-image for build stage --- ...se-kaniko-to-build-containers-in-autodevops.yml | 5 +++ lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml | 52 ++-------------------- 2 files changed, 8 insertions(+), 49 deletions(-) create mode 100644 changelogs/unreleased/50313-use-kaniko-to-build-containers-in-autodevops.yml diff --git a/changelogs/unreleased/50313-use-kaniko-to-build-containers-in-autodevops.yml b/changelogs/unreleased/50313-use-kaniko-to-build-containers-in-autodevops.yml new file mode 100644 index 00000000000..0188df7fce7 --- /dev/null +++ b/changelogs/unreleased/50313-use-kaniko-to-build-containers-in-autodevops.yml @@ -0,0 +1,5 @@ +--- +title: Use auto-build-image for build job in Auto-DevOps.gitlab-ci.yml +merge_request: 24279 +author: +type: changed diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 7c1182d4293..7a9a11f1e33 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -73,12 +73,11 @@ stages: build: stage: build - image: docker:stable-git + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable" services: - - docker:stable-dind + - docker:stable-dind script: - - setup_docker - - build + - /build/build.sh only: - branches @@ -490,7 +489,6 @@ rollout 100%: export DATABASE_URL=${DATABASE_URL-$auto_database_url} export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG export CI_APPLICATION_TAG=$CI_COMMIT_SHA - export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID} export TILLER_NAMESPACE=$KUBE_NAMESPACE # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') @@ -849,50 +847,6 @@ rollout 100%: fi } - function build() { - registry_login - - if [[ -f Dockerfile ]]; then - echo "Building Dockerfile-based application..." - docker build \ - --build-arg HTTP_PROXY="$HTTP_PROXY" \ - --build-arg http_proxy="$http_proxy" \ - --build-arg HTTPS_PROXY="$HTTPS_PROXY" \ - --build-arg https_proxy="$https_proxy" \ - --build-arg FTP_PROXY="$FTP_PROXY" \ - --build-arg ftp_proxy="$ftp_proxy" \ - --build-arg NO_PROXY="$NO_PROXY" \ - --build-arg no_proxy="$no_proxy" \ - -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . - else - echo "Building Heroku-based application using gliderlabs/herokuish docker image..." - docker run -i \ - -e BUILDPACK_URL \ - -e HTTP_PROXY \ - -e http_proxy \ - -e HTTPS_PROXY \ - -e https_proxy \ - -e FTP_PROXY \ - -e ftp_proxy \ - -e NO_PROXY \ - -e no_proxy \ - --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build - docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" - docker rm "$CI_CONTAINER_NAME" >/dev/null - echo "" - - echo "Configuring $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG docker image..." - docker create --expose 5000 --env PORT=5000 --name="$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" /bin/herokuish procfile start web - docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" - docker rm "$CI_CONTAINER_NAME" >/dev/null - echo "" - fi - - echo "Pushing to GitLab Container Registry..." - docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" - echo "" - } - function initialize_tiller() { echo "Checking Tiller..." -- cgit v1.2.1 From f09399fc52d649bc4e145fc37ae309586846cc25 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Mon, 25 Feb 2019 17:16:02 +1300 Subject: Turn on backtrace for sidekiq in development This enables easier debugging in GDK --- config/initializers/sidekiq.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index be4183f39be..69a60a65e77 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -11,6 +11,10 @@ queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE # Default is to retry 25 times with exponential backoff. That's too much. Sidekiq.default_worker_options = { retry: 3 } +if Rails.env.development? + Sidekiq.default_worker_options[:backtrace] = true +end + enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' Sidekiq.configure_server do |config| -- cgit v1.2.1 From 2949479c67bf1058ad919452cfea6cadd52d4c41 Mon Sep 17 00:00:00 2001 From: Elias Werberich Date: Mon, 25 Feb 2019 08:57:41 +0100 Subject: Remove timezone conversion of issue due dates for issue board cards --- app/assets/javascripts/boards/components/issue_due_date.vue | 2 +- changelogs/unreleased/56251-fix-issue-board-weekday-shift.yml | 5 +++++ spec/javascripts/boards/components/issue_due_date_spec.js | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/56251-fix-issue-board-weekday-shift.yml diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue index 9c4c6632976..9bc66978198 100644 --- a/app/assets/javascripts/boards/components/issue_due_date.vue +++ b/app/assets/javascripts/boards/components/issue_due_date.vue @@ -53,7 +53,7 @@ export default { } else if (timeDifference === -1) { return __('Yesterday'); } else if (timeDifference > 0 && timeDifference < 7) { - return dateFormat(issueDueDate, 'dddd', true); + return dateFormat(issueDueDate, 'dddd'); } return standardDateFormat; diff --git a/changelogs/unreleased/56251-fix-issue-board-weekday-shift.yml b/changelogs/unreleased/56251-fix-issue-board-weekday-shift.yml new file mode 100644 index 00000000000..bedc488ebd4 --- /dev/null +++ b/changelogs/unreleased/56251-fix-issue-board-weekday-shift.yml @@ -0,0 +1,5 @@ +--- +title: "Fix weekday shift in issue board cards for UTC+X timezones by removing local timezone to UTC conversion" +merge_request: 25512 +author: Elias Werberich +type: fixed diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js index 054cf8c5b7d..68e26b68f04 100644 --- a/spec/javascripts/boards/components/issue_due_date_spec.js +++ b/spec/javascripts/boards/components/issue_due_date_spec.js @@ -43,7 +43,7 @@ describe('Issue Due Date component', () => { date.setDate(date.getDate() + 5); vm = createComponent(date); - expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true)); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd')); }); it('should render month and day for other dates', () => { @@ -53,7 +53,7 @@ describe('Issue Due Date component', () => { const isDueInCurrentYear = today.getFullYear() === date.getFullYear(); const format = isDueInCurrentYear ? 'mmm d' : 'mmm d, yyyy'; - expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, format, true)); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, format)); }); it('should contain the correct `.text-danger` css class for overdue issue', () => { -- cgit v1.2.1 From c0009b088fd7fe463679b530e56ed162824df071 Mon Sep 17 00:00:00 2001 From: Yauhen Kotau Date: Mon, 25 Feb 2019 14:37:22 +0300 Subject: Update YouTrack integration service documentation --- doc/user/project/integrations/youtrack.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index 3e9295dfcbd..1186a75f8bb 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -1,5 +1,9 @@ # YouTrack Service +JetBrains YouTrack is a web-based issue tracking and project management platform. +Please refer official [documentation]([YouTrack](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html)) for details about YouTrack itself. + + 1. To enable the YouTrack integration in a project, navigate to the [Integrations page](project_services.md#accessing-the-project-services), click the **YouTrack** service, and fill in the required details on the page as described @@ -20,8 +24,8 @@ in the table below. ## Referencing issues in YouTrack Issues in YouTrack can be referenced as `-` where `` -starts with a capital letter which is then followed by capital letters, numbers -or underscores, and `` is a number (example `API_32-143`). +starts with a capital letter which is then followed by capital or lower case +letters, numbers or underscores, and `` is a number (example `Api_32-143`). `` part is included into issue_id and links can point any YouTrack project (`issues_url` + issue_id) -- cgit v1.2.1 From 1d388205d52f129bda6890edf3f3fa95d4d63b7c Mon Sep 17 00:00:00 2001 From: Yauhen Kotau Date: Mon, 25 Feb 2019 14:56:10 +0300 Subject: Fix docs-link test --- doc/user/project/integrations/youtrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index 1186a75f8bb..2ab14a8db2c 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -1,7 +1,7 @@ # YouTrack Service JetBrains YouTrack is a web-based issue tracking and project management platform. -Please refer official [documentation]([YouTrack](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html)) for details about YouTrack itself. +Please refer official [documentation](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html) for details about YouTrack itself. 1. To enable the YouTrack integration in a project, navigate to the -- cgit v1.2.1 From c6bc91387765fa156dcbac15ee451dda34c1a897 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 25 Feb 2019 10:22:13 -0300 Subject: Raise not implemented error on BaseLinker for package_url --- lib/gitlab/dependency_linker/base_linker.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 6b44d5b1518..ffad00fa7d7 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -45,6 +45,10 @@ module Gitlab private + def package_url(_name) + raise NotImplementedError + end + def link_dependencies raise NotImplementedError end -- cgit v1.2.1 From ddef2f1fbd3af7e91cb568adc8258043e05f15fc Mon Sep 17 00:00:00 2001 From: Sanad Liaquat Date: Mon, 25 Feb 2019 18:37:27 +0500 Subject: Update GitHub Import test Removes the unnecessary :orchestrated tag and updates a few selectors. Also wait_for_success for import before proceeding. The test is currently placed in quarantine because of a bug. --- .../javascripts/notes/components/noteable_note.vue | 2 +- qa/qa/page/base.rb | 8 +++--- qa/qa/page/group/show.rb | 2 +- qa/qa/page/project/import/github.rb | 21 +++++++++++++--- qa/qa/page/project/issue/show.rb | 12 ++++++++- qa/qa/page/project/pipeline/show.rb | 2 +- qa/qa/page/project/settings/deploy_keys.rb | 4 +-- qa/qa/resource/project_imported_from_github.rb | 2 +- .../1_manage/project/import_github_repo_spec.rb | 29 ++++++++++++++-------- qa/qa/support/page/logging.rb | 13 +++++++--- qa/spec/page/logging_spec.rb | 15 +++++++---- 11 files changed, 75 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 04e74a43acc..5fa0ab3de98 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -219,7 +219,7 @@ export default { :class="classNameBindings" :data-award-url="note.toggle_award_path" :data-note-id="note.id" - class="note note-wrapper" + class="note note-wrapper qa-noteable-note-item" >
Date: Thu, 31 Jan 2019 16:32:44 -0200 Subject: Support merge to ref for merge-commit and squash Adds the ground work for writing into the merge ref refs/merge-requests/:iid/merge the merge result between source and target branches of a MR, without further side-effects such as mailing, MR updates and target branch changes. --- GITALY_SERVER_VERSION | 2 +- Gemfile | 3 +- Gemfile.lock | 4 +- app/models/merge_request.rb | 14 ++ app/models/project.rb | 8 + app/models/repository.rb | 6 + app/services/merge_requests/merge_base_service.rb | 53 ++++++ app/services/merge_requests/merge_service.rb | 39 +---- .../merge_requests/merge_to_ref_service.rb | 61 +++++++ .../osw-create-and-store-merge-ref-for-mrs.yml | 5 + lib/gitlab/git/repository.rb | 6 + lib/gitlab/gitaly_client/operation_service.rb | 19 +++ spec/lib/gitlab/git/repository_spec.rb | 31 ++++ spec/models/repository_spec.rb | 23 +++ .../merge_requests/merge_to_ref_service_spec.rb | 180 +++++++++++++++++++++ 15 files changed, 413 insertions(+), 41 deletions(-) create mode 100644 app/services/merge_requests/merge_base_service.rb create mode 100644 app/services/merge_requests/merge_to_ref_service.rb create mode 100644 changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml create mode 100644 spec/services/merge_requests/merge_to_ref_service_spec.rb diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 39893559155..3500250a4b0 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.20.0 +1.21.0 diff --git a/Gemfile b/Gemfile index 67ce615c01e..28666199c0f 100644 --- a/Gemfile +++ b/Gemfile @@ -419,7 +419,8 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.12.0', require: 'gitaly' + gem 'grpc', '~> 1.15.0' gem 'google-protobuf', '~> 3.6' diff --git a/Gemfile.lock b/Gemfile.lock index 841f85dc7e5..59e152c27fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.10.0) + gitaly-proto (1.12.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -1017,7 +1017,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.10.0) + gitaly-proto (~> 1.12.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.6.5) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 75fca96ce0a..1468ae1c34a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -764,6 +764,16 @@ class MergeRequest < ActiveRecord::Base true end + def mergeable_to_ref? + return false if merged? + return false if broken? + + # Given the `merge_ref_path` will have the same + # state the `target_branch` would have. Ideally + # we need to check if it can be merged to it. + project.repository.can_be_merged?(diff_head_sha, target_branch) + end + def ff_merge_possible? project.repository.ancestor?(target_branch_sha, diff_head_sha) end @@ -1077,6 +1087,10 @@ class MergeRequest < ActiveRecord::Base "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" end + def merge_ref_path + "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge" + end + def in_locked_state begin lock_mr diff --git a/app/models/project.rb b/app/models/project.rb index 83f8d004a46..889572e693a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1925,6 +1925,14 @@ class Project < ActiveRecord::Base persisted? && path_changed? end + def human_merge_method + if merge_method == :ff + 'Fast-forward' + else + merge_method.to_s.humanize + end + end + def merge_method if self.merge_requests_ff_only_enabled :ff diff --git a/app/models/repository.rb b/app/models/repository.rb index ed55a6e572b..cd761a29618 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -854,6 +854,12 @@ class Repository end end + def merge_to_ref(user, source_sha, merge_request, target_ref, message) + branch = merge_request.target_branch + + raw.merge_to_ref(user, source_sha, branch, target_ref, message) + end + def ff_merge(user, source, target_branch, merge_request: nil) their_commit_id = commit(source)&.id raise 'Invalid merge source' if their_commit_id.nil? diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb new file mode 100644 index 00000000000..4c5a0d96957 --- /dev/null +++ b/app/services/merge_requests/merge_base_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module MergeRequests + class MergeBaseService < MergeRequests::BaseService + include Gitlab::Utils::StrongMemoize + + MergeError = Class.new(StandardError) + + attr_reader :merge_request + + # Overridden in EE. + def hooks_validation_pass?(_merge_request) + true + end + + # Overridden in EE. + def hooks_validation_error(_merge_request) + end + + def source + if merge_request.squash + squash_sha! + else + merge_request.diff_head_sha + end + end + + private + + def handle_merge_error(*args) + # No-op + end + + def commit_message + params[:commit_message] || + merge_request.default_merge_commit_message + end + + def squash_sha! + strong_memoize(:squash_sha) do + params[:merge_request] = merge_request + squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute + + case squash_result[:status] + when :success + squash_result[:squash_sha] + when :error + raise ::MergeRequests::MergeService::MergeError, squash_result[:message] + end + end + end + end +end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 449997bcf07..b1d01955d85 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -7,13 +7,7 @@ module MergeRequests # mark merge request as merged and execute all hooks and notifications # Executed when you do merge via GitLab UI # - class MergeService < MergeRequests::BaseService - include Gitlab::Utils::StrongMemoize - - MergeError = Class.new(StandardError) - - attr_reader :merge_request, :source - + class MergeService < MergeRequests::MergeBaseService delegate :merge_jid, :state, to: :@merge_request def execute(merge_request) @@ -38,19 +32,6 @@ module MergeRequests handle_merge_error(log_message: e.message, save_message_on_model: true) end - def source - if merge_request.squash - squash_sha! - else - merge_request.diff_head_sha - end - end - - # Overridden in EE. - def hooks_validation_pass?(_merge_request) - true - end - private def error_check! @@ -79,24 +60,8 @@ module MergeRequests merge_request.update!(merge_commit_sha: commit_id) end - def squash_sha! - strong_memoize(:squash_sha) do - params[:merge_request] = merge_request - squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute - - case squash_result[:status] - when :success - squash_result[:squash_sha] - when :error - raise ::MergeRequests::MergeService::MergeError, squash_result[:message] - end - end - end - def try_merge - message = params[:commit_message] || merge_request.default_merge_commit_message - - repository.merge(current_user, source, merge_request, message) + repository.merge(current_user, source, merge_request, commit_message) rescue Gitlab::Git::PreReceiveError => e handle_merge_error(log_message: e.message) raise MergeError, 'Something went wrong during merge pre-receive hook' diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb new file mode 100644 index 00000000000..c4a40044143 --- /dev/null +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module MergeRequests + # Performs the merge between source SHA and the target branch. Instead + # of writing the result to the MR target branch, it targets the `target_ref`. + # + # Ideally this should leave the `target_ref` state with the same state the + # target branch would have if we used the regular `MergeService`, but without + # every side-effect that comes with it (MR updates, mails, source branch + # deletion, etc). This service should be kept idempotent (i.e. can + # be executed regardless of the `target_ref` current state). + # + class MergeToRefService < MergeRequests::MergeBaseService + def execute(merge_request) + @merge_request = merge_request + + error_check! + + commit_id = commit + + raise MergeError, 'Conflicts detected during merge' unless commit_id + + success(commit_id: commit_id) + rescue MergeError => error + error(error.message) + end + + private + + def error_check! + error = + if !merge_method_supported? + "#{project.human_merge_method} to #{target_ref} is currently not supported." + elsif !hooks_validation_pass?(merge_request) + hooks_validation_error(merge_request) + elsif @merge_request.should_be_rebased? + 'Fast-forward merge is not possible. Please update your source branch.' + elsif !@merge_request.mergeable_to_ref? + "Merge request is not mergeable to #{target_ref}" + elsif !source + 'No source for merge' + end + + raise MergeError, error if error + end + + def target_ref + merge_request.merge_ref_path + end + + def commit + repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message) + rescue Gitlab::Git::PreReceiveError => error + raise MergeError, error.message + end + + def merge_method_supported? + [:merge, :rebase_merge].include?(project.merge_method) + end + end +end diff --git a/changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml b/changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml new file mode 100644 index 00000000000..012b547a630 --- /dev/null +++ b/changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Support merge ref writing (without merging to target branch) +merge_request: 24692 +author: +type: added diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 593a3676519..aea132a3dd9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -556,6 +556,12 @@ module Gitlab tags.find { |tag| tag.name == name } end + def merge_to_ref(user, source_sha, branch, target_ref, message) + wrapped_gitaly_errors do + gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message) + end + end + def merge(user, source_sha, target_branch, message, &block) wrapped_gitaly_errors do gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 22d2d149e65..d172c798da2 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -100,6 +100,25 @@ module Gitlab end end + def user_merge_to_ref(user, source_sha, branch, target_ref, message) + request = Gitaly::UserMergeToRefRequest.new( + repository: @gitaly_repo, + source_sha: source_sha, + branch: encode_binary(branch), + target_ref: encode_binary(target_ref), + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + message: message + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_merge_to_ref, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::PreReceiveError, pre_receive_error + end + + response.commit_id + end + def user_merge_branch(user, source_sha, target_branch, message) request_enum = QueueEnumerator.new response_enum = GitalyClient.call( diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8a9e78ba3c3..b3a728c139e 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1704,6 +1704,37 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#merge_to_ref' do + let(:repository) { mutable_repository } + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:right_branch) { 'test-master' } + let(:target_ref) { 'refs/merge-requests/999/merge' } + + before do + repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + end + + def merge_to_ref + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + end + + it 'generates a commit in the target_ref' do + expect(repository.ref_exists?(target_ref)).to be(false) + + commit_sha = merge_to_ref + ref_head = repository.commit(target_ref) + + expect(commit_sha).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(commit_sha) + end + + it 'does not change the right branch HEAD' do + expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + end + end + describe '#merge' do let(:repository) { mutable_repository } let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f78760bf567..17201d8b90a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1373,6 +1373,29 @@ describe Repository do end end + describe '#merge_to_ref' do + let(:merge_request) do + create(:merge_request, source_branch: 'feature', + target_branch: 'master', + source_project: project) + end + + it 'writes merge of source and target to MR merge_ref_path' do + merge_commit_id = repository.merge_to_ref(user, + merge_request.diff_head_sha, + merge_request, + merge_request.merge_ref_path, + 'Custom message') + + merge_commit = repository.commit(merge_commit_id) + + expect(merge_commit.message).to eq('Custom message') + expect(merge_commit.author_name).to eq(user.name) + expect(merge_commit.author_email).to eq(user.commit_email) + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end + end + describe '#ff_merge' do before do repository.add_branch(user, 'ff-target', 'feature~5') diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb new file mode 100644 index 00000000000..435a863cbd4 --- /dev/null +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::MergeToRefService do + set(:user) { create(:user) } + let(:merge_request) { create(:merge_request, :simple) } + let(:project) { merge_request.project } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + let(:service) do + described_class.new(project, user, + commit_message: 'Awesome message', + 'should_remove_source_branch' => true) + end + + def process_merge_to_ref + perform_enqueued_jobs do + service.execute(merge_request) + end + end + + it 'writes commit to merge ref' do + repository = project.repository + target_ref = merge_request.merge_ref_path + + expect(repository.ref_exists?(target_ref)).to be(false) + + result = service.execute(merge_request) + + ref_head = repository.commit(target_ref) + + expect(result[:status]).to eq(:success) + expect(result[:commit_id]).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(result[:commit_id]) + end + + it 'does not send any mail' do + expect { process_merge_to_ref }.not_to change { ActionMailer::Base.deliveries.count } + end + + it 'does not change the MR state' do + expect { process_merge_to_ref }.not_to change { merge_request.state } + end + + it 'does not create notes' do + expect { process_merge_to_ref }.not_to change { merge_request.notes.count } + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + + process_merge_to_ref + end + + it 'returns an error when the failing to process the merge' do + allow(project.repository).to receive(:merge_to_ref).and_return(nil) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Conflicts detected during merge') + end + + context 'commit history comparison with regular MergeService' do + let(:merge_ref_service) do + described_class.new(project, user, {}) + end + + let(:merge_service) do + MergeRequests::MergeService.new(project, user, {}) + end + + shared_examples_for 'MergeService for target ref' do + it 'target_ref has the same state of target branch' do + repo = merge_request.target_project.repository + + process_merge_to_ref + merge_service.execute(merge_request) + + ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3) + target_branch_commits = repo.commits(merge_request.target_branch, limit: 3) + + ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit| + expect(ref_commit.parents).to eq(target_branch_commit.parents) + end + end + end + + context 'when merge commit' do + it_behaves_like 'MergeService for target ref' + end + + context 'when merge commit with squash' do + before do + merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature') + end + + it_behaves_like 'MergeService for target ref' + end + end + + context 'merge pre-condition checks' do + before do + merge_request.project.update!(merge_method: merge_method) + end + + context 'when semi-linear merge method' do + let(:merge_method) { :rebase_merge } + + it 'return error when MR should be able to fast-forward' do + allow(merge_request).to receive(:should_be_rebased?) { true } + + error_message = 'Fast-forward merge is not possible. Please update your source branch.' + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + + context 'when fast-forward merge method' do + let(:merge_method) { :ff } + + it 'returns error' do + error_message = "Fast-forward to #{merge_request.merge_ref_path} is currently not supported." + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + + context 'when MR is not mergeable to ref' do + let(:merge_method) { :merge } + + it 'returns error' do + allow(merge_request).to receive(:mergeable_to_ref?) { false } + + error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}" + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + end + + context 'does not close related todos' do + let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:project) { merge_request.project } + let!(:todo) do + create(:todo, :assigned, + project: project, + author: user, + user: user, + target: merge_request) + end + + before do + allow(service).to receive(:execute_hooks) + + perform_enqueued_jobs do + service.execute(merge_request) + todo.reload + end + end + + it { expect(todo).not_to be_done } + end + end +end -- cgit v1.2.1 From 105212ce49007ffc3489c2039e55056d8df8fa95 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 11 Feb 2019 13:14:11 -0200 Subject: Check authorization in merge services Move authorization checks to merge services instead relying solely on external checks. --- app/services/merge_requests/merge_base_service.rb | 4 ++ app/services/merge_requests/merge_service.rb | 21 ++++++++--- .../merge_requests/merge_to_ref_service.rb | 17 +++++++-- spec/services/merge_requests/merge_service_spec.rb | 12 ++++++ .../merge_requests/merge_to_ref_service_spec.rb | 44 ++++++++++++++-------- 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb index 4c5a0d96957..61049f394aa 100644 --- a/app/services/merge_requests/merge_base_service.rb +++ b/app/services/merge_requests/merge_base_service.rb @@ -27,6 +27,10 @@ module MergeRequests private + def raise_error(message) + raise MergeError, message + end + def handle_merge_error(*args) # No-op end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index b1d01955d85..f5d66589196 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -18,7 +18,7 @@ module MergeRequests @merge_request = merge_request - error_check! + validate! merge_request.in_locked_state do if commit @@ -34,6 +34,17 @@ module MergeRequests private + def validate! + authorization_check! + error_check! + end + + def authorization_check! + unless @merge_request.can_be_merged_by?(current_user) + raise_error('You are not allowed to merge this merge request') + end + end + def error_check! error = if @merge_request.should_be_rebased? @@ -44,7 +55,7 @@ module MergeRequests 'No source for merge' end - raise MergeError, error if error + raise_error(error) if error end def commit @@ -54,7 +65,7 @@ module MergeRequests if commit_id log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}") else - raise MergeError, 'Conflicts detected during merge' + raise_error('Conflicts detected during merge') end merge_request.update!(merge_commit_sha: commit_id) @@ -64,10 +75,10 @@ module MergeRequests repository.merge(current_user, source, merge_request, commit_message) rescue Gitlab::Git::PreReceiveError => e handle_merge_error(log_message: e.message) - raise MergeError, 'Something went wrong during merge pre-receive hook' + raise_error('Something went wrong during merge pre-receive hook') rescue => e handle_merge_error(log_message: e.message) - raise MergeError, 'Something went wrong during merge' + raise_error('Something went wrong during merge') ensure merge_request.update!(in_progress_merge_commit_sha: nil) end diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index c4a40044143..6b99c4fc483 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -14,11 +14,11 @@ module MergeRequests def execute(merge_request) @merge_request = merge_request - error_check! + validate! commit_id = commit - raise MergeError, 'Conflicts detected during merge' unless commit_id + raise_error('Conflicts detected during merge') unless commit_id success(commit_id: commit_id) rescue MergeError => error @@ -27,6 +27,11 @@ module MergeRequests private + def validate! + authorization_check! + error_check! + end + def error_check! error = if !merge_method_supported? @@ -41,7 +46,13 @@ module MergeRequests 'No source for merge' end - raise MergeError, error if error + raise_error(error) if error + end + + def authorization_check! + unless Ability.allowed?(current_user, :admin_merge_request, project) + raise_error("You are not allowed to merge to this ref") + end end def target_ref diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 04a62aa454d..ede79b87bcc 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -224,6 +224,18 @@ describe MergeRequests::MergeService do expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end + it 'logs and saves error if user is not authorized' do + unauthorized_user = create(:user) + project.add_reporter(unauthorized_user) + + service = described_class.new(project, unauthorized_user) + + service.execute(merge_request) + + expect(merge_request.merge_error) + .to eq('You are not allowed to merge this merge request') + end + it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 435a863cbd4..696f1b83157 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -3,6 +3,22 @@ require 'spec_helper' describe MergeRequests::MergeToRefService do + shared_examples_for 'MergeService for target ref' do + it 'target_ref has the same state of target branch' do + repo = merge_request.target_project.repository + + process_merge_to_ref + merge_service.execute(merge_request) + + ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3) + target_branch_commits = repo.commits(merge_request.target_branch, limit: 3) + + ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit| + expect(ref_commit.parents).to eq(target_branch_commit.parents) + end + end + end + set(:user) { create(:user) } let(:merge_request) { create(:merge_request, :simple) } let(:project) { merge_request.project } @@ -76,22 +92,6 @@ describe MergeRequests::MergeToRefService do MergeRequests::MergeService.new(project, user, {}) end - shared_examples_for 'MergeService for target ref' do - it 'target_ref has the same state of target branch' do - repo = merge_request.target_project.repository - - process_merge_to_ref - merge_service.execute(merge_request) - - ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3) - target_branch_commits = repo.commits(merge_request.target_branch, limit: 3) - - ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit| - expect(ref_commit.parents).to eq(target_branch_commit.parents) - end - end - end - context 'when merge commit' do it_behaves_like 'MergeService for target ref' end @@ -176,5 +176,17 @@ describe MergeRequests::MergeToRefService do it { expect(todo).not_to be_done } end + + it 'returns error when user has no authorization to admin the merge request' do + unauthorized_user = create(:user) + project.add_reporter(unauthorized_user) + + service = described_class.new(project, unauthorized_user) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('You are not allowed to merge to this ref') + end end end -- cgit v1.2.1 From 4e16edbe0a26d95ea94ad61ebaadac4f7463352b Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 12 Feb 2019 08:22:44 -0200 Subject: Add feature-flag support Returns error in MergeToRefService when merge_to_tmp_merge_ref_path ff is disabled. --- app/services/merge_requests/merge_to_ref_service.rb | 4 +++- spec/services/merge_requests/merge_to_ref_service_spec.rb | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index 6b99c4fc483..ed62c3424bd 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -34,7 +34,9 @@ module MergeRequests def error_check! error = - if !merge_method_supported? + if Feature.disabled?(:merge_to_tmp_merge_ref_path, project) + 'Feature is not enabled' + elsif !merge_method_supported? "#{project.human_merge_method} to #{target_ref} is currently not supported." elsif !hooks_validation_pass?(merge_request) hooks_validation_error(merge_request) diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 696f1b83157..96f2fde7117 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -74,6 +74,15 @@ describe MergeRequests::MergeToRefService do process_merge_to_ref end + it 'returns error when feature is disabled' do + stub_feature_flags(merge_to_tmp_merge_ref_path: false) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Feature is not enabled') + end + it 'returns an error when the failing to process the merge' do allow(project.repository).to receive(:merge_to_ref).and_return(nil) -- cgit v1.2.1 From 7a8a5fe541c617ef62aa7afac90b60440d9fddcc Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 12 Feb 2019 19:21:14 -0200 Subject: Extend error checking to be overwritten in EE --- app/services/merge_requests/merge_base_service.rb | 4 ++++ app/services/merge_requests/merge_service.rb | 2 ++ app/services/merge_requests/merge_to_ref_service.rb | 2 ++ 3 files changed, 8 insertions(+) diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb index 61049f394aa..9bf421466c7 100644 --- a/app/services/merge_requests/merge_base_service.rb +++ b/app/services/merge_requests/merge_base_service.rb @@ -8,6 +8,10 @@ module MergeRequests attr_reader :merge_request + # Overridden in EE. + def error_check! + end + # Overridden in EE. def hooks_validation_pass?(_merge_request) true diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index f5d66589196..8241e408ce5 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -46,6 +46,8 @@ module MergeRequests end def error_check! + super + error = if @merge_request.should_be_rebased? 'Only fast-forward merge is allowed for your project. Please update your source branch' diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index ed62c3424bd..586652ae44e 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -33,6 +33,8 @@ module MergeRequests end def error_check! + super + error = if Feature.disabled?(:merge_to_tmp_merge_ref_path, project) 'Feature is not enabled' -- cgit v1.2.1 From 16011886be00247d98c49e1727867767085e4398 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 12 Feb 2019 19:57:57 -0200 Subject: Move error check to pvt --- app/services/merge_requests/merge_base_service.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb index 9bf421466c7..095bdca5472 100644 --- a/app/services/merge_requests/merge_base_service.rb +++ b/app/services/merge_requests/merge_base_service.rb @@ -8,10 +8,6 @@ module MergeRequests attr_reader :merge_request - # Overridden in EE. - def error_check! - end - # Overridden in EE. def hooks_validation_pass?(_merge_request) true @@ -19,6 +15,7 @@ module MergeRequests # Overridden in EE. def hooks_validation_error(_merge_request) + # No-op end def source @@ -31,6 +28,11 @@ module MergeRequests private + # Overridden in EE. + def error_check! + # No-op + end + def raise_error(message) raise MergeError, message end -- cgit v1.2.1 From b81e7c52d3e82e0c053a39fac9c4fe21abcf6103 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Mon, 25 Feb 2019 21:36:28 +0800 Subject: Enable `:read_list` when `:read_group` is enabled --- app/policies/group_policy.rb | 1 + changelogs/unreleased/58149-fix-read-list-board-policy.yml | 6 ++++++ spec/policies/group_policy_spec.rb | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/58149-fix-read-list-board-policy.yml diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index c25766a5af8..298769c0eb8 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -54,6 +54,7 @@ class GroupPolicy < BasePolicy rule { has_projects }.policy do enable :read_group + enable :read_list enable :read_label end diff --git a/changelogs/unreleased/58149-fix-read-list-board-policy.yml b/changelogs/unreleased/58149-fix-read-list-board-policy.yml new file mode 100644 index 00000000000..964813f4c9a --- /dev/null +++ b/changelogs/unreleased/58149-fix-read-list-board-policy.yml @@ -0,0 +1,6 @@ +--- +title: Fix error when viewing group issue boards when user doesn't have explicit group + permissions +merge_request: 25524 +author: +type: fixed diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index be1804c5ce0..af6d6f084a9 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -83,7 +83,7 @@ describe GroupPolicy do end it do - expect_allowed(:read_group, :read_label) + expect_allowed(:read_group, :read_list, :read_label) end context 'in subgroups', :nested_groups do @@ -91,7 +91,7 @@ describe GroupPolicy do let(:project) { create(:project, namespace: subgroup) } it do - expect_allowed(:read_group, :read_label) + expect_allowed(:read_group, :read_list, :read_label) end end end -- cgit v1.2.1 From 7e83acb8a2f7fe4a0c0acd6769114e0593c677bb Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 22 Feb 2019 11:31:02 -0300 Subject: Prevent disclosing project milestone titles Prevent unauthorized users having access to milestone titles through autocomplete endpoint. --- .../projects/autocomplete_sources_controller.rb | 2 ++ changelogs/unreleased/security-issue_54789_2.yml | 5 ++++ .../autocomplete_sources_controller_spec.rb | 31 ++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 changelogs/unreleased/security-issue_54789_2.yml diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 9c130af8394..0e3f13045ce 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Projects::AutocompleteSourcesController < Projects::ApplicationController + before_action :authorize_read_milestone!, only: :milestones + def members render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target) end diff --git a/changelogs/unreleased/security-issue_54789_2.yml b/changelogs/unreleased/security-issue_54789_2.yml new file mode 100644 index 00000000000..8ecb72a2ae3 --- /dev/null +++ b/changelogs/unreleased/security-issue_54789_2.yml @@ -0,0 +1,5 @@ +--- +title: Do not disclose milestone titles for unauthorized users +merge_request: +author: +type: security diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb index 4bc72042710..a9a058e7e17 100644 --- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb +++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb @@ -35,4 +35,35 @@ describe Projects::AutocompleteSourcesController do avatar_url: user.avatar_url) end end + + describe 'GET milestones' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + let!(:project_milestone) { create(:milestone, project: project) } + let!(:group_milestone) { create(:milestone, group: group) } + + before do + sign_in(user) + end + + it 'lists milestones' do + group.add_owner(user) + + get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path } + + milestone_titles = json_response.map { |milestone| milestone["title"] } + expect(milestone_titles).to match_array([project_milestone.title, group_milestone.title]) + end + + context 'when user cannot read project issues and merge requests' do + it 'renders 404' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path } + + expect(response).to have_gitlab_http_status(404) + end + end + end end -- cgit v1.2.1 From fa882a674a389fc41a155f5ba72e757a7ea02d7a Mon Sep 17 00:00:00 2001 From: Andrew Fontaine Date: Mon, 11 Feb 2019 10:25:10 -0500 Subject: Sort Environments in Table by Last Updated Ensure folders push to the top, if both have no last update, sort by name. The sorting algorithm should sort in the following priorities: 1. folders first, 2. last updated descending, 3. by name ascending, the sorting algorithm must: 1. Sort by name ascending, 2. Reverse (sort by name descending), 3. Sort by last deployment ascending, 4. Reverse (last deployment descending, name ascending), 5. Put folders first. It is done this way, as `underscore`'s sort API is very basic: simple comparisons, sorting by ascending only. --- .../environments/components/environments_table.vue | 36 +++- ...eated-even-when-a-job-isn-t-run-when-manual.yml | 5 + .../environments/environment_table_spec.js | 220 +++++++++++++++++++++ 3 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/46750-ci-empty-environment-is-created-even-when-a-job-isn-t-run-when-manual.yml diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index e2c304de00a..eef141a07ba 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -3,6 +3,7 @@ * Render environments table. */ import { GlLoadingIcon } from '@gitlab/ui'; +import _ from 'underscore'; import environmentItem from './environment_item.vue'; export default { @@ -24,6 +25,15 @@ export default { default: false, }, }, + computed: { + sortedEnvironments() { + return this.sortEnvironments(this.environments).map(env => + this.shouldRenderFolderContent(env) + ? { ...env, children: this.sortEnvironments(env.children) } + : env, + ); + }, + }, methods: { folderUrl(model) { return `${window.location.pathname}/folders/${model.folderName}`; @@ -31,6 +41,30 @@ export default { shouldRenderFolderContent(env) { return env.isFolder && env.isOpen && env.children && env.children.length > 0; }, + sortEnvironments(environments) { + /* + * The sorting algorithm should sort in the following priorities: + * + * 1. folders first, + * 2. last updated descending, + * 3. by name ascending, + * + * the sorting algorithm must: + * + * 1. Sort by name ascending, + * 2. Reverse (sort by name descending), + * 3. Sort by last deployment ascending, + * 4. Reverse (last deployment descending, name ascending), + * 5. Put folders first. + */ + return _.chain(environments) + .sortBy(env => (env.isFolder ? env.folderName : env.name)) + .reverse() + .sortBy(env => (env.last_deployment ? env.last_deployment.created_at : '0000')) + .reverse() + .sortBy(env => (env.isFolder ? -1 : 1)) + .value(); + }, }, }; @@ -53,7 +87,7 @@ export default { {{ s__('Environments|Updated') }}
-