From 5915b219e3a6ff70fbaad3ed938428fddfc5997b Mon Sep 17 00:00:00 2001 From: ernstvn Date: Mon, 22 May 2017 13:10:36 -0700 Subject: [PATCH 01/26] Make sketch dynamic and link back to production architecture --- doc/development/architecture.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index b36fd52603b..da40033a788 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -12,7 +12,7 @@ Both EE and CE require some add-on components called gitlab-shell and Gitaly. Th You can imagine GitLab as a physical office. -**The repositories** are the goods GitLab handling. +**The repositories** are the goods GitLab handles. They can be stored in a warehouse. This can be either a hard disk, or something more complex, such as a NFS filesystem; @@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell ### Components -![GitLab Diagram Overview](gitlab_architecture_diagram.png) + _[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_ @@ -66,7 +66,9 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. -Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files) +Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files). + +You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/). ### Installation Folder Summary From 93b22677ac4b76d7e5fd995627f48f782c1078d3 Mon Sep 17 00:00:00 2001 From: Ernst van Nierop Date: Mon, 22 May 2017 21:33:34 +0000 Subject: [PATCH 02/26] Per discussion --- doc/development/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index da40033a788..acd5e3c2093 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -66,7 +66,7 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. -Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files). +Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files). You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/). From 5a31cbcb33d93c8a9ece77b713f0c31cdfd6ee6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien?= Date: Sat, 3 Jun 2017 06:53:10 +0000 Subject: [PATCH 03/26] Fix typo on namespace --- doc/install/kubernetes/gitlab_runner_chart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 305b4593c73..b8bc0795f2e 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi run the following: ```bash -helm install --namepace --name gitlab-runner -f gitlab/gitlab-runner +helm install --namespace --name gitlab-runner -f gitlab/gitlab-runner ``` - `` is the Kubernetes namespace where you want to install the GitLab Runner. @@ -153,7 +153,7 @@ helm install --namepace --name gitlab-runner -f Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade` ```bash -helm upgrade --namepace -f gitlab/gitlab-runner +helm upgrade --namespace -f gitlab/gitlab-runner ``` Where: From a048e4a4717f71ecf1a1780d55639b029be5d71e Mon Sep 17 00:00:00 2001 From: Gustav Ernberg Date: Mon, 29 May 2017 22:18:09 +0200 Subject: [PATCH 04/26] Fixed style on unsubscribe page Removed unnecassary logic Added missing 'the' Fix case Fixed specs --- app/views/sent_notifications/unsubscribe.html.haml | 10 ++++------ changelogs/unreleased/issue-23254.yml | 4 ++++ spec/features/unsubscribe_links_spec.rb | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/issue-23254.yml diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml index 9ce6a1aeef5..de52fd00157 100644 --- a/app/views/sent_notifications/unsubscribe.html.haml +++ b/app/views/sent_notifications/unsubscribe.html.haml @@ -1,16 +1,14 @@ - noteable = @sent_notification.noteable -- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false) +- noteable_type = @sent_notification.noteable_type.titleize.downcase - noteable_text = %(#{noteable.title} (#{noteable.to_reference})) - -- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace - +- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace %h3.page-title - Unsubscribe from #{noteable_type} #{noteable_text} + Unsubscribe from #{noteable_type} %p = succeed '?' do - Are you sure you want to unsubscribe from #{noteable_type} + Are you sure you want to unsubscribe from the #{noteable_type}: = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable]) %p diff --git a/changelogs/unreleased/issue-23254.yml b/changelogs/unreleased/issue-23254.yml new file mode 100644 index 00000000000..568a7a41c30 --- /dev/null +++ b/changelogs/unreleased/issue-23254.yml @@ -0,0 +1,4 @@ +--- +title: Fixed style on unsubscribe page +merge_request: +author: Gustav Ernberg diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb index a23c4ca2b92..8509551ce4a 100644 --- a/spec/features/unsubscribe_links_spec.rb +++ b/spec/features/unsubscribe_links_spec.rb @@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do visit body_link expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last) - expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference}))) - expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?)) + expect(page).to have_text(%(Unsubscribe from issue)) + expect(page).to have_text(%(Are you sure you want to unsubscribe from the issue: #{issue.title} (#{issue.to_reference})?)) expect(issue.subscribed?(recipient, project)).to be_truthy click_link 'Unsubscribe' From 46e9161b3286c4a66f4db791945bf7e845911cb1 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 3 Jun 2017 23:11:53 -0700 Subject: [PATCH 05/26] Fix typo in user activity debug log message --- app/services/users/activity_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb index facf21a7f5c..ab532a1fdcf 100644 --- a/app/services/users/activity_service.rb +++ b/app/services/users/activity_service.rb @@ -16,7 +16,7 @@ module Users def record_activity Gitlab::UserActivities.record(@author.id) - Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}") + Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})") end end end From 144f0e0d12c287e034d9f86326c86f2b1bad55ef Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 5 Jun 2017 06:26:34 +0300 Subject: [PATCH 06/26] Backport CI codeclimate example doc change from EE Signed-off-by: Dmitriy Zaporozhets --- doc/ci/examples/code_climate.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index bd53f80ce14..c6b9e2065cf 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -1,11 +1,11 @@ # Analyze project code quality with Code Climate CLI -This example shows how to run [Code Climate CLI][cli] on your code by using\ +This example shows how to run [Code Climate CLI][cli] on your code by using GitLab CI and Docker. -First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor). +First, you need GitLab Runner with [docker-in-docker executor][dind]. -Once you setup the Runner add new job to `.gitlab-ci.yml`: +Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`: ```yaml codeclimate: @@ -25,4 +25,10 @@ codeclimate: This will create a `codeclimate` job in your CI pipeline and will allow you to download and analyze the report artifact in JSON format. +For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically +extracted and shown right in the merge request widget. [Learn more on code quality +diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md). + [cli]: https://github.com/codeclimate/codeclimate +[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor +[ee]: https://about.gitlab.com/gitlab-ee/ From e341c4d5f528c04f5ef4cb5df10a5ca7b54d8f9e Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Mon, 5 Jun 2017 03:29:19 +0000 Subject: [PATCH 07/26] Update gitlab_chart.md --- doc/install/kubernetes/gitlab_chart.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index b4ffd57afbb..d2442a4fbde 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -207,7 +207,9 @@ its class in an annotation. >**Note:** The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that. Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure -to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md) +to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md). +>**Note:** +If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size). #### Preserving Source IPs From 91bf43873d7ae0c139cc8442597197fef3e0c109 Mon Sep 17 00:00:00 2001 From: Andrew Featherstone Date: Mon, 5 Jun 2017 09:48:26 +0000 Subject: [PATCH 08/26] Corrected spelling mistake in permissions.md "it total" -> "in total". --- doc/user/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/permissions.md b/doc/user/permissions.md index b0145b0a759..3fda47b9e34 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -126,7 +126,7 @@ which visibility level you select on project settings. ## GitLab CI GitLab CI permissions rely on the role the user has in GitLab. There are four -permission levels it total: +permission levels in total: - admin - master From 30f4f6b1fb8890e106883a84454121a56c9723e0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 5 Jun 2017 14:10:25 +0300 Subject: [PATCH 09/26] Fix link to ee code quality doc Signed-off-by: Dmitriy Zaporozhets --- doc/ci/examples/code_climate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index c6b9e2065cf..a047e809788 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -27,7 +27,7 @@ download and analyze the report artifact in JSON format. For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically extracted and shown right in the merge request widget. [Learn more on code quality -diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md). +diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md). [cli]: https://github.com/codeclimate/codeclimate [dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor From b2800ee0c7c0ca47bb11b21b6b32134ae6f4594e Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 2 Jun 2017 17:28:54 +0100 Subject: [PATCH 10/26] Add a Rake task to aid in rotating otp_key_base --- .../unreleased/29690-rotate-otp-key-base.yml | 4 + doc/raketasks/user_management.md | 79 +++++++++++++++++ lib/gitlab/otp_key_rotator.rb | 87 +++++++++++++++++++ lib/tasks/gitlab/two_factor.rake | 16 ++++ spec/lib/gitlab/otp_key_rotator_spec.rb | 70 +++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 changelogs/unreleased/29690-rotate-otp-key-base.yml create mode 100644 lib/gitlab/otp_key_rotator.rb create mode 100644 spec/lib/gitlab/otp_key_rotator_spec.rb diff --git a/changelogs/unreleased/29690-rotate-otp-key-base.yml b/changelogs/unreleased/29690-rotate-otp-key-base.yml new file mode 100644 index 00000000000..94d73a24758 --- /dev/null +++ b/changelogs/unreleased/29690-rotate-otp-key-base.yml @@ -0,0 +1,4 @@ +--- +title: Add a Rake task to aid in rotating otp_key_base +merge_request: 11881 +author: diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 044b104f5c2..3ae46019daf 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production ``` +## Rotate Two-factor Authentication (2FA) encryption key + +GitLab stores the secret data enabling 2FA to work in an encrypted database +column. The encryption key for this data is known as `otp_key_base`, and is +stored in `config/secrets.yml`. + + +If that file is leaked, but the individual 2FA secrets have not, it's possible +to re-encrypt those secrets with a new encryption key. This allows you to change +the leaked key without forcing all users to change their 2FA details. + +First, look up the old key. This is in the `config/secrets.yml` file, but +**make sure you're working with the production section**. The line you're +interested in will look like this: + +```yaml +production: + otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +``` + +Next, generate a new secret: + +``` +# omnibus-gitlab +sudo gitlab-rake secret + +# installation from source +bundle exec rake secret RAILS_ENV=production +``` + +Now you need to stop the GitLab server, back up the existing secrets file and +update the database: + +``` +# omnibus-gitlab +sudo gitlab-ctl stop +sudo cp config/secrets.yml config/secrets.yml.bak +sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key= new_key= + +# installation from source +sudo /etc/init.d/gitlab stop +cp config/secrets.yml config/secrets.yml.bak +bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key= new_key= RAILS_ENV=production +``` + +The `` value can be read from `config/secrets.yml`; `` was +generated earlier. The **encrypted** values for the user 2FA secrets will be +written to the specified `filename` - you can use this to rollback in case of +error. + +Finally, change `config/secrets.yml` to set `otp_key_base` to `` and +restart. Again, make sure you're operating in the **production** section. + +``` +# omnibus-gitlab +sudo gitlab-ctl start + +# installation from source +sudo /etc/init.d/gitlab start +``` + +If there are any problems (perhaps using the wrong value for `old_key`), you can +restore your backup of `config/secrets.yml` and rollback the changes: + +``` +# omnibus-gitlab +sudo gitlab-ctl stop +sudo gitlab-rake gitlab:two_factor:rotate_key:rollback filename=backup.csv +sudo cp config/secrets.yml.bak config/secrets.yml +sudo gitlab-ctl start + +# installation from source +sudo /etc/init.d/gitlab start +bundle exec rake gitlab:two_factor:rotate_key:rollback filename=backup.csv RAILS_ENV=production +cp config/secrets.yml.bak config/secrets.yml +sudo /etc/init.d/gitlab start + +``` + ## Clear authentication tokens for all users. Important! Data loss! Clear authentication tokens for all users in the GitLab database. This diff --git a/lib/gitlab/otp_key_rotator.rb b/lib/gitlab/otp_key_rotator.rb new file mode 100644 index 00000000000..0d541935bc6 --- /dev/null +++ b/lib/gitlab/otp_key_rotator.rb @@ -0,0 +1,87 @@ +module Gitlab + # The +otp_key_base+ param is used to encrypt the User#otp_secret attribute. + # + # When +otp_key_base+ is changed, it invalidates the current encrypted values + # of User#otp_secret. This class can be used to decrypt all the values with + # the old key, encrypt them with the new key, and and update the database + # with the new values. + # + # For persistence between runs, a CSV file is used with the following columns: + # + # user_id, old_value, new_value + # + # Only the encrypted values are stored in this file. + # + # As users may have their 2FA settings changed at any time, this is only + # guaranteed to be safe if run offline. + class OtpKeyRotator + HEADERS = %w[user_id old_value new_value].freeze + + attr_reader :filename + + # Create a new rotator. +filename+ is used to store values by +calculate!+, + # and to update the database with new and old values in +apply!+ and + # +rollback!+, respectively. + def initialize(filename) + @filename = filename + end + + def rotate!(old_key:, new_key:) + old_key ||= Gitlab::Application.secrets.otp_key_base + + raise ArgumentError.new("Old key is the same as the new key") if old_key == new_key + raise ArgumentError.new("New key is too short! Must be 256 bits") if new_key.size < 64 + + write_csv do |csv| + ActiveRecord::Base.transaction do + User.with_two_factor.in_batches do |relation| + rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt) + rows.each do |row| + user = %i[id ciphertext iv salt].zip(row).to_h + new_value = reencrypt(user, old_key, new_key) + + User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value) + csv << [user[:id], user[:ciphertext], new_value] + end + end + end + end + end + + def rollback! + ActiveRecord::Base.transaction do + CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row| + User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value']) + end + end + end + + private + + attr_reader :old_key, :new_key + + def otp_secret_settings + @otp_secret_settings ||= User.encrypted_attributes[:otp_secret] + end + + def reencrypt(user, old_key, new_key) + original = user[:ciphertext].unpack("m").join + opts = { + iv: user[:iv].unpack("m").join, + salt: user[:salt].unpack("m").join, + algorithm: otp_secret_settings[:algorithm], + insecure_mode: otp_secret_settings[:insecure_mode] + } + + decrypted = Encryptor.decrypt(original, opts.merge(key: old_key)) + encrypted = Encryptor.encrypt(decrypted, opts.merge(key: new_key)) + [encrypted].pack("m") + end + + def write_csv(&blk) + File.open(filename, "w") do |file| + yield CSV.new(file, headers: HEADERS, write_headers: false) + end + end + end +end diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake index fc0ccc726ed..7728c485e8d 100644 --- a/lib/tasks/gitlab/two_factor.rake +++ b/lib/tasks/gitlab/two_factor.rake @@ -19,5 +19,21 @@ namespace :gitlab do puts "There are currently no users with 2FA enabled.".color(:yellow) end end + + namespace :rotate_key do + def rotator + @rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename']) + end + + desc "Encrypt user OTP secrets with a new encryption key" + task apply: :environment do |t, args| + rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key']) + end + + desc "Rollback to secrets encrypted with the old encryption key" + task rollback: :environment do + rotator.rollback! + end + end end end diff --git a/spec/lib/gitlab/otp_key_rotator_spec.rb b/spec/lib/gitlab/otp_key_rotator_spec.rb new file mode 100644 index 00000000000..6e6e9ce29ac --- /dev/null +++ b/spec/lib/gitlab/otp_key_rotator_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe Gitlab::OtpKeyRotator do + let(:file) { Tempfile.new("otp-key-rotator-test") } + let(:filename) { file.path } + let(:old_key) { Gitlab::Application.secrets.otp_key_base } + let(:new_key) { "00" * 32 } + let!(:users) { create_list(:user, 5, :two_factor) } + + after do + file.close + file.unlink + end + + def data + CSV.read(filename) + end + + def build_row(user, applied = false) + [user.id.to_s, encrypt_otp(user, old_key), encrypt_otp(user, new_key)] + end + + def encrypt_otp(user, key) + opts = { + value: user.otp_secret, + iv: user.encrypted_otp_secret_iv.unpack("m").join, + salt: user.encrypted_otp_secret_salt.unpack("m").join, + algorithm: 'aes-256-cbc', + insecure_mode: true, + key: key + } + [Encryptor.encrypt(opts)].pack("m") + end + + subject(:rotator) { described_class.new(filename) } + + describe '#rotate!' do + subject(:rotation) { rotator.rotate!(old_key: old_key, new_key: new_key) } + + it 'stores the calculated values in a spreadsheet' do + rotation + + expect(data).to match_array(users.map {|u| build_row(u) }) + end + + context 'new key is too short' do + let(:new_key) { "00" * 31 } + + it { expect { rotation }.to raise_error(ArgumentError) } + end + + context 'new key is the same as the old key' do + let(:new_key) { old_key } + + it { expect { rotation }.to raise_error(ArgumentError) } + end + end + + describe '#rollback!' do + it 'updates rows to the old value' do + file.puts("#{users[0].id},old,new") + file.close + + rotator.rollback! + + expect(users[0].reload.encrypted_otp_secret).to eq('old') + expect(users[1].reload.encrypted_otp_secret).not_to eq('old') + end + end +end From 0f0af7af335d5ed10c424df7934ff6a397d05989 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 6 Jun 2017 02:38:34 +0000 Subject: [PATCH 11/26] change headings to improve SEO --- doc/user/project/issues/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 9598cb801be..fe87e6f9495 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -1,4 +1,4 @@ -# GitLab Issues Documentation +# Issues documentation The GitLab Issue Tracker is an advanced and complete tool for tracking the evolution of a new idea or the process @@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like: Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md). -## New Issue +## New issue Read through the [documentation on creating issues](create_new_issue.md). ## Closing issues -Read through the distinct ways to [close issues](closing_issues.md) on GitLab. +Learn distinct ways to [close issues](closing_issues.md) in GitLab. ## Create a merge request from an issue @@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests. -### GitLab Issue Board +### Issue Board The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to enhance your workflow by organizing and prioritizing issues in GitLab. From c4878a762fe9cc90022277e5f559fa0c169e09e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Bechmann=20Yamamoto-S=C3=B8rensen?= Date: Tue, 6 Jun 2017 16:55:24 +0900 Subject: [PATCH 12/26] Update explanation of job-level variable override to fit example --- doc/ci/variables/README.md | 2 +- doc/ci/yaml/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 56ff245f9f9..23c93c59604 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -120,7 +120,7 @@ The YAML-defined variables are also set to all created tune them. Variables can be defined at a global level, but also at a job level. To turn off -global defined variables in your job, define an empty array: +global defined variables in your job, define an empty hash: ```yaml job_name: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 2c9aa437932..dd4b589d37c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -434,7 +434,7 @@ but allows you to define job-specific variables. When the `variables` keyword is used on a job level, it overrides the global YAML job variables and predefined ones. To turn off global defined variables -in your job, define an empty array: +in your job, define an empty hash: ```yaml job_name: From e245d7eebe747378f4158b30634ab0da4df59117 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 6 Jun 2017 08:28:39 +0000 Subject: [PATCH 13/26] Resolve "When changing project visibility setting, change other dropdowns automatically" --- app/assets/javascripts/project_new.js | 63 +++++++++++++++++-- .../stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/projects.scss | 14 +++++ app/helpers/projects_helper.rb | 10 ++- app/helpers/visibility_level_helper.rb | 4 +- app/views/projects/edit.html.haml | 6 +- ...g-change-other-dropdowns-automatically.yml | 4 ++ .../settings/visibility_settings_spec.rb | 4 +- spec/helpers/projects_helper_spec.rb | 4 +- spec/helpers/visibility_level_helper_spec.rb | 2 +- 10 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 04b381fe0e0..c0f757269cb 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,11 +1,17 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ +function highlightChanges($elm) { + $elm.addClass('highlight-changes'); + setTimeout(() => $elm.removeClass('highlight-changes'), 10); +} + (function() { this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = this.toggleSettings.bind(this); this.$selects = $('.features select'); this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); $('.project-edit-container').on('ajax:before', (function(_this) { return function() { @@ -26,6 +32,42 @@ if (!visibilityContainer) return; const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); + } + $options.slice(2).disable(); + } + + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); + } + } + }); + + projectVisibility = newProjectVisibility; + } + }); }; ProjectNew.prototype.toggleSettings = function() { @@ -56,8 +98,10 @@ ProjectNew.prototype.toggleRepoVisibility = function () { var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") .nextAll() @@ -71,29 +115,40 @@ var $this = $(this); var repoSelectVal = parseInt($this.val(), 10); - $this.find('option').show(); + $this.find('option').enable(); - if (selectedVal < repoSelectVal) { - $this.val(selectedVal); + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); } - $this.find("option[value='" + selectedVal + "']").nextAll().hide(); + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); }); if (selectedVal) { this.$repoSelects.removeClass('disabled'); + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } if (containerRegistry) { containerRegistry.style.display = ''; } } else { this.$repoSelects.addClass('disabled'); + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } if (containerRegistry) { containerRegistry.style.display = 'none'; containerRegistryCheckbox.checked = false; } } + + prevSelectedVal = selectedVal; }.bind(this)); }; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b3a86b92d93..4114a050d9a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc; $divergence-graph-separator-bg: #ccc; $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; +$highlight-changes-color: rgb(235, 255, 232); /* diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 4719bf826dc..a2f781a6a6e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -29,6 +29,20 @@ & > .form-group { padding-left: 0; } + + select option[disabled] { + display: none; + } + } + + select { + background: transparent; + transition: background 2s ease-out; + + &.highlight-changes { + background: $highlight-changes-color; + transition: none; + } } .help-block { diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7b0584c42a2..f74e61c9481 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -138,11 +138,15 @@ module ProjectsHelper if @project.private? level = @project.project_feature.send(field) - options.delete('Everyone with access') - highest_available_option = options.values.max if level == ProjectFeature::ENABLED + disabled_option = ProjectFeature::ENABLED + highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end - options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) + options = options_for_select( + options, + selected: highest_available_option || @project.project_feature.public_send(field), + disabled: disabled_option + ) content_tag( :select, diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index b4aaf498068..50757b01538 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -31,9 +31,9 @@ module VisibilityLevelHelper when Gitlab::VisibilityLevel::PRIVATE "Project access must be granted explicitly to each user." when Gitlab::VisibilityLevel::INTERNAL - "The project can be cloned by any logged in user." + "The project can be accessed by any logged in user." when Gitlab::VisibilityLevel::PUBLIC - "The project can be cloned without any authentication." + "The project can be accessed without any authentication." end end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f5549d7f4cd..c3dab68cea5 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -42,7 +42,7 @@ .col-md-9 .label-light = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to "(?)", help_page_path("public_access/public_access") + = link_to icon('question-circle'), help_page_path("public_access/public_access") %span.help-block .col-md-3.visibility-select-container = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) @@ -92,14 +92,14 @@ .form-group = render 'shared/allow_request_access', form: f - if Gitlab.config.lfs.enabled && current_user.admin? - .row + .row.js-lfs-enabled .col-md-9 = f.label :lfs_enabled, 'LFS', class: 'label-light' %span.help-block Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' } - if Gitlab.config.registry.enabled diff --git a/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml b/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml new file mode 100644 index 00000000000..dbd8a538d51 --- /dev/null +++ b/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml @@ -0,0 +1,4 @@ +--- +title: Automatically adjust project settings to match changes in project visibility +merge_request: 11831 +author: diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index cef315ac9cd..fac4506bdf6 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do visibility_select_container = find('.js-visibility-select') expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s - expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.' + expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.' end scenario 'project visibility description updates on change' do @@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do expect(visibility_select_container).not_to have_select '.visibility-select' expect(visibility_select_container).to have_content 'Public' - expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.' + expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.' end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 54c5ba57bdf..a695621b87a 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -257,7 +257,7 @@ describe ProjectsHelper do result = helper.project_feature_access_select(:issues_access_level) expect(result).to include("Disabled") expect(result).to include("Only team members") - expect(result).not_to include("Everyone with access") + expect(result).to have_selector('option[disabled]', text: "Everyone with access") end end @@ -272,7 +272,7 @@ describe ProjectsHelper do expect(result).to include("Disabled") expect(result).to include("Only team members") - expect(result).not_to include("Everyone with access") + expect(result).to have_selector('option[disabled]', text: "Everyone with access") expect(result).to have_selector('option[selected]', text: "Only team members") end end diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index 8942b00b128..ad19cf9263d 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -37,7 +37,7 @@ describe VisibilityLevelHelper do it "describes public projects" do expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC)) - .to eq "The project can be cloned without any authentication." + .to eq "The project can be accessed without any authentication." end end From 33fad62d8e13921c765f446fe4d03b2b13f24404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 6 Jun 2017 11:53:06 +0200 Subject: [PATCH 14/26] Fix Projects API spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/requests/api/projects_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 5c13cea69fb..c0ecb4d2aaa 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1480,7 +1480,7 @@ describe API::Projects do expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['namespace']['id']).to eq(user2.namespace.id) expect(json_response['forked_from_project']['id']).to eq(project.id) - expect(json_response['import_status']).to eq('started') + expect(json_response['import_status']).to eq('scheduled') expect(json_response).to include("import_error") end @@ -1493,7 +1493,7 @@ describe API::Projects do expect(json_response['owner']['id']).to eq(admin.id) expect(json_response['namespace']['id']).to eq(admin.namespace.id) expect(json_response['forked_from_project']['id']).to eq(project.id) - expect(json_response['import_status']).to eq('started') + expect(json_response['import_status']).to eq('scheduled') expect(json_response).to include("import_error") end From a17bfd368f8ca158b0155862d6fb72cf252977dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien?= Date: Tue, 6 Jun 2017 10:23:29 +0000 Subject: [PATCH 15/26] Resolve "API: Environment info missed" --- doc/api/README.md | 1 + doc/api/{enviroments.md => environments.md} | 0 2 files changed, 1 insertion(+) rename doc/api/{enviroments.md => environments.md} (100%) diff --git a/doc/api/README.md b/doc/api/README.md index 45579ccac4e..44e345b1cf6 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -15,6 +15,7 @@ following locations: - [Commits](commits.md) - [Deployments](deployments.md) - [Deploy Keys](deploy_keys.md) +- [Environments](environments.md) - [Gitignores templates](templates/gitignores.md) - [GitLab CI Config templates](templates/gitlab_ci_ymls.md) - [Groups](groups.md) diff --git a/doc/api/enviroments.md b/doc/api/environments.md similarity index 100% rename from doc/api/enviroments.md rename to doc/api/environments.md From 39710abc621c54b821d6b969b512034dc487d5c5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 6 Jun 2017 12:45:55 +0200 Subject: [PATCH 16/26] Remove references to old settings location --- doc/user/project/img/project_settings_list.png | Bin 5919 -> 0 bytes .../integrations/img/accessing_integrations.png | Bin 8941 -> 0 bytes doc/user/project/integrations/index.md | 8 +++----- .../project/integrations/project_services.md | 15 +++++---------- doc/user/project/integrations/webhooks.md | 7 ++----- doc/user/project/pipelines/settings.md | 9 ++------- doc/user/project/protected_branches.md | 7 ++----- 7 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 doc/user/project/img/project_settings_list.png delete mode 100644 doc/user/project/integrations/img/accessing_integrations.png diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png deleted file mode 100644 index 0bb761b45c9ef08b354fc3c40772f6aadfdd3514..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5919 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 PSP=R;#@dew#EAa_opU5x diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 99093ebaed5..e384ed57de9 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -1,10 +1,8 @@ # Project integrations -You can find the available integrations under the **Integrations** page by -navigating to the cog icon in the upper right corner of your project. You need -to have at least [master permission][permissions] on the project. - -![Accessing the integrations](img/accessing_integrations.png) +You can find the available integrations under your project's +**Settings ➔ Integrations** page. You need to have at least +[master permission][permissions] on the project. ## Project services diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index 31baea507d7..51989ccaaea 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -6,18 +6,13 @@ functionality to GitLab. ## Accessing the project services -You can find the available services under the **Integrations** page in your -project's settings. +You can find the available services under your project's +**Settings ➔ Integrations** page. -1. Navigate to the cog icon in the upper right corner of your project. You need - to have at least [master permission][permissions] on the project. +There are more than 20 services to integrate with. Click on the one that you +want to configure. - ![Accessing the services](img/accessing_integrations.png) - -1. There are more than 20 services to integrate with. Click on the one that you - want to configure. - - ![Project services list](img/project_services.png) + ![Project services list](img/project_services.png) Below, you will find a list of the currently supported ones accompanied with comprehensive documentation. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index d0bb1cd11a8..0517ed3ec18 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -14,11 +14,8 @@ to the webhook URL. Webhooks can be used to update an external issue tracker, trigger CI jobs, update a backup mirror, or even deploy to your production server. -Navigate to the webhooks page by going to the **Integrations** page from your -project's settings which can be found under the wheel icon in the upper right -corner. - -![Accessing the integrations](img/accessing_integrations.png) +Navigate to the webhooks page by going to your project's +**Settings ➔ Integrations**. ## Webhook endpoint tips diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 1b42c43cf8f..1d2eba4f74b 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,12 +1,7 @@ # Pipelines settings -To reach the pipelines settings: - -1. Navigate to your project and click the cog icon in the upper right corner. - - ![Project settings menu](../img/project_settings_list.png) - -1. Select **Pipelines** from the menu. +To reach the pipelines settings navigate to your project's +**Settings ➔ CI/CD Pipelines**. The following settings can be configured per project. diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index f7a686d2ccf..7650020b37e 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -27,11 +27,8 @@ See the [Changelog](#changelog) section for changes over time. To protect a branch, you need to have at least Master permission level. Note that the `master` branch is protected by default. -1. Navigate to the main page of the project. -1. In the upper right corner, click the settings wheel and select **Protected branches**. - - ![Project settings list](img/project_settings_list.png) - +1. Navigate to your project's **Settings ➔ Repository** +1. Scroll to find the **Protected branches** section. 1. From the **Branch** dropdown menu, select the branch you want to protect and click **Protect**. In the screenshot below, we chose the `develop` branch. From 8df7bcf532c9f0407fcefa12d205cb9d160fe5f4 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 19 May 2017 09:07:38 -0500 Subject: [PATCH 17/26] Allow numeric pages domain Previously, `PagesDomain` would not allow a domain such as 123.example.com. With this change, this is now allowed, because it is a perfectly valid domain. --- app/models/pages_domain.rb | 4 +- .../unreleased/allow_numeric_pages_domain.yml | 4 ++ spec/models/pages_domain_spec.rb | 47 +++++++------------ 3 files changed, 24 insertions(+), 31 deletions(-) create mode 100644 changelogs/unreleased/allow_numeric_pages_domain.yml diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index f2f2fc1e32a..5d798247863 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -1,7 +1,7 @@ class PagesDomain < ActiveRecord::Base belongs_to :project - validates :domain, hostname: true + validates :domain, hostname: { allow_numeric_hostname: true } validates :domain, uniqueness: { case_sensitive: false } validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true @@ -98,7 +98,7 @@ class PagesDomain < ActiveRecord::Base def validate_pages_domain return unless domain - if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase) + if domain.downcase.ends_with?(Settings.pages.host.downcase) self.errors.add(:domain, "*.#{Settings.pages.host} is restricted") end end diff --git a/changelogs/unreleased/allow_numeric_pages_domain.yml b/changelogs/unreleased/allow_numeric_pages_domain.yml new file mode 100644 index 00000000000..10d9f26f88d --- /dev/null +++ b/changelogs/unreleased/allow_numeric_pages_domain.yml @@ -0,0 +1,4 @@ +--- +title: Allow numeric pages domain +merge_request: 11550 +author: diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index c6c45d78990..f9d060d4e0e 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -6,7 +6,7 @@ describe PagesDomain, models: true do end describe 'validate domain' do - subject { build(:pages_domain, domain: domain) } + subject(:pages_domain) { build(:pages_domain, domain: domain) } context 'is unique' do let(:domain) { 'my.domain.com' } @@ -14,36 +14,25 @@ describe PagesDomain, models: true do it { is_expected.to validate_uniqueness_of(:domain) } end - context 'valid domain' do - let(:domain) { 'my.domain.com' } + { + 'my.domain.com' => true, + '123.456.789' => true, + '0x12345.com' => true, + '0123123' => true, + '_foo.com' => false, + 'reserved.com' => false, + 'a.reserved.com' => false, + nil => false + }.each do |value, validity| + context "domain #{value.inspect} validity" do + before do + allow(Settings.pages).to receive(:host).and_return('reserved.com') + end - it { is_expected.to be_valid } - end + let(:domain) { value } - context 'valid hexadecimal-looking domain' do - let(:domain) { '0x12345.com'} - - it { is_expected.to be_valid } - end - - context 'no domain' do - let(:domain) { nil } - - it { is_expected.not_to be_valid } - end - - context 'invalid domain' do - let(:domain) { '0123123' } - - it { is_expected.not_to be_valid } - end - - context 'domain from .example.com' do - let(:domain) { 'my.domain.com' } - - before { allow(Settings.pages).to receive(:host).and_return('domain.com') } - - it { is_expected.not_to be_valid } + it { expect(pages_domain.valid?).to eq(validity) } + end end end From 0a04eab86ebca30534615b27e8d418b1749b5169 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 6 Jun 2017 11:26:23 +0100 Subject: [PATCH 18/26] Update GitLab Pages to v0.4.3 --- GITLAB_PAGES_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 2b7c5ae0184..17b2ccd9bf9 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.4.2 +0.4.3 From ad3e180ed3d99494414cb1b367f6b4e40ec28b87 Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Mon, 29 May 2017 13:49:17 +0800 Subject: [PATCH 19/26] Introduce an Events API * Meld the following disparate endpoints: * `/projects/:id/events` * `/events` * `/users/:id/events` + Add result filtering to the above endpoints: * action * target_type * before and after dates --- app/finders/events_finder.rb | 62 ++++ app/models/event.rb | 32 ++ ...vity-feed-to-be-accessible-through-api.yml | 4 + doc/api/README.md | 1 + doc/api/events.md | 347 ++++++++++++++++++ doc/api/projects.md | 138 +------ doc/api/users.md | 141 +------ lib/api/api.rb | 1 + lib/api/events.rb | 86 +++++ lib/api/projects.rb | 10 - lib/api/users.rb | 21 -- spec/finders/events_finder_spec.rb | 44 +++ spec/requests/api/events_spec.rb | 133 +++++++ spec/requests/api/projects_spec.rb | 58 --- spec/requests/api/users_spec.rb | 77 ---- 15 files changed, 712 insertions(+), 443 deletions(-) create mode 100644 app/finders/events_finder.rb create mode 100644 changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml create mode 100644 doc/api/events.md create mode 100644 lib/api/events.rb create mode 100644 spec/finders/events_finder_spec.rb create mode 100644 spec/requests/api/events_spec.rb diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb new file mode 100644 index 00000000000..b0450ddc1fd --- /dev/null +++ b/app/finders/events_finder.rb @@ -0,0 +1,62 @@ +class EventsFinder + attr_reader :source, :params, :current_user + + # Used to filter Events + # + # Arguments: + # source - which user or project to looks for events on + # current_user - only return events for projects visible to this user + # params: + # action: string + # target_type: string + # before: datetime + # after: datetime + # + def initialize(params = {}) + @source = params.delete(:source) + @current_user = params.delete(:current_user) + @params = params + end + + def execute + events = source.events + + events = by_current_user_access(events) + events = by_action(events) + events = by_target_type(events) + events = by_created_at_before(events) + events = by_created_at_after(events) + + events + end + + private + + def by_current_user_access(events) + events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project) + end + + def by_action(events) + return events unless Event::ACTIONS[params[:action]] + + events.where(action: Event::ACTIONS[params[:action]]) + end + + def by_target_type(events) + return events unless Event::TARGET_TYPES[params[:target_type]] + + events.where(target_type: Event::TARGET_TYPES[params[:target_type]]) + end + + def by_created_at_before(events) + return events unless params[:before] + + events.where('events.created_at < ?', params[:before].beginning_of_day) + end + + def by_created_at_after(events) + return events unless params[:after] + + events.where('events.created_at > ?', params[:after].end_of_day) + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 46e89388bc1..d6d39473774 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -14,6 +14,30 @@ class Event < ActiveRecord::Base DESTROYED = 10 EXPIRED = 11 # User left project due to expiry + ACTIONS = HashWithIndifferentAccess.new( + created: CREATED, + updated: UPDATED, + closed: CLOSED, + reopened: REOPENED, + pushed: PUSHED, + commented: COMMENTED, + merged: MERGED, + joined: JOINED, + left: LEFT, + destroyed: DESTROYED, + expired: EXPIRED + ).freeze + + TARGET_TYPES = HashWithIndifferentAccess.new( + issue: Issue, + milestone: Milestone, + merge_request: MergeRequest, + note: Note, + project: Project, + snippet: Snippet, + user: User + ).freeze + RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true @@ -55,6 +79,14 @@ class Event < ActiveRecord::Base def limit_recent(limit = 20, offset = nil) recent.limit(limit).offset(offset) end + + def actions + ACTIONS.keys + end + + def target_types + TARGET_TYPES.keys + end end def visible_to_user?(user = nil) diff --git a/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml new file mode 100644 index 00000000000..9c17c3b949c --- /dev/null +++ b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml @@ -0,0 +1,4 @@ +--- +title: Introduce an Events API +merge_request: 11755 +author: diff --git a/doc/api/README.md b/doc/api/README.md index 44e345b1cf6..e1d4009dedc 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -16,6 +16,7 @@ following locations: - [Deployments](deployments.md) - [Deploy Keys](deploy_keys.md) - [Environments](environments.md) +- [Events](events.md) - [Gitignores templates](templates/gitignores.md) - [GitLab CI Config templates](templates/gitlab_ci_ymls.md) - [Groups](groups.md) diff --git a/doc/api/events.md b/doc/api/events.md new file mode 100644 index 00000000000..ef57287264d --- /dev/null +++ b/doc/api/events.md @@ -0,0 +1,347 @@ +# Events + +## Filter parameters + +### Action Types + +Available action types for the `action` parameter are: + +- `created` +- `updated` +- `closed` +- `reopened` +- `pushed` +- `commented` +- `merged` +- `joined` +- `left` +- `destroyed` +- `expired` + +Note that these options are downcased. + +### Target Types + +Available target types for the `target_type` parameter are: + +- `issue` +- `milestone` +- `merge_request` +- `note` +- `project` +- `snippet` +- `user` + +Note that these options are downcased. + +### Date formatting + +Dates for the `before` and `after` parameters should be supplied in the following format: + +``` +YYYY-MM-DD +``` + +## List currently authenticated user's events + +>**Note:** This endpoint was introduced in GitLab 9.3. + +Get a list of events for the authenticated user. + +``` +GET /events +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `action` | string | no | Include only events of a particular [action type][action-types] | +| `target_type` | string | no | Include only events of a particular [target type][target-types] | +| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] | +| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] | +| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` | + +Example request: + +``` +curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01 +``` + +Example response: + +```json +[ + { + "title":null, + "project_id":1, + "action_name":"opened", + "target_id":160, + "target_type":"Issue", + "author_id":25, + "data":null, + "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.", + "created_at":"2017-02-09T10:43:19.667Z", + "author":{ + "name":"User 3", + "username":"user3", + "id":25, + "state":"active", + "avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon", + "web_url":"https://gitlab.example.com/user3" + }, + "author_username":"user3" + }, + { + "title":null, + "project_id":1, + "action_name":"opened", + "target_id":159, + "target_type":"Issue", + "author_id":21, + "data":null, + "target_title":"Nostrum enim non et sed optio illo deleniti non.", + "created_at":"2017-02-09T10:43:19.426Z", + "author":{ + "name":"Test User", + "username":"ted", + "id":21, + "state":"active", + "avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon", + "web_url":"https://gitlab.example.com/ted" + }, + "author_username":"ted" + } +] +``` + +### Get user contribution events + +>**Note:** Documentation was formerly located in the [Users API pages][users-api]. + +Get the contribution events for the specified user, sorted from newest to oldest. + +``` +GET /users/:id/events +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `action` | string | no | Include only events of a particular [action type][action-types] | +| `target_type` | string | no | Include only events of a particular [target type][target-types] | +| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] | +| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] | +| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events +``` + +Example response: + +```json +[ + { + "title": null, + "project_id": 15, + "action_name": "closed", + "target_id": 830, + "target_type": "Issue", + "author_id": 1, + "data": null, + "target_title": "Public project search field", + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/root" + }, + "author_username": "root" + }, + { + "title": null, + "project_id": 15, + "action_name": "opened", + "target_id": null, + "target_type": null, + "author_id": 1, + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/root" + }, + "author_username": "john", + "data": { + "before": "50d4420237a9de7be1304607147aec22e4a14af7", + "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "ref": "refs/heads/master", + "user_id": 1, + "user_name": "Dmitriy Zaporozhets", + "repository": { + "name": "gitlabhq", + "url": "git@dev.gitlab.org:gitlab/gitlabhq.git", + "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.", + "homepage": "https://dev.gitlab.org/gitlab/gitlabhq" + }, + "commits": [ + { + "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "message": "Add simple search to projects in public area", + "timestamp": "2013-05-13T18:18:08+00:00", + "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } + ], + "total_commits_count": 1 + }, + "target_title": null + }, + { + "title": null, + "project_id": 15, + "action_name": "closed", + "target_id": 840, + "target_type": "Issue", + "author_id": 1, + "data": null, + "target_title": "Finish & merge Code search PR", + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/root" + }, + "author_username": "root" + }, + { + "title": null, + "project_id": 15, + "action_name": "commented on", + "target_id": 1312, + "target_type": "Note", + "author_id": 1, + "data": null, + "target_title": null, + "created_at": "2015-12-04T10:33:58.089Z", + "note": { + "id": 1312, + "body": "What an awesome day!", + "attachment": null, + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/root" + }, + "created_at": "2015-12-04T10:33:56.698Z", + "system": false, + "noteable_id": 377, + "noteable_type": "Issue" + }, + "author": { + "name": "Dmitriy Zaporozhets", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", + "web_url": "http://localhost:3000/root" + }, + "author_username": "root" + } +] +``` + +## List a Project's visible events + +>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api]. + +Get a list of visible events for a particular project. + +``` +GET /:project_id/events +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `action` | string | no | Include only events of a particular [action type][action-types] | +| `target_type` | string | no | Include only events of a particular [target type][target-types] | +| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] | +| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] | +| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` | + +Example request: + +``` +curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01 +``` + +Example response: + +```json +[ + { + "title":null, + "project_id":1, + "action_name":"opened", + "target_id":160, + "target_type":"Issue", + "author_id":25, + "data":null, + "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.", + "created_at":"2017-02-09T10:43:19.667Z", + "author":{ + "name":"User 3", + "username":"user3", + "id":25, + "state":"active", + "avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon", + "web_url":"https://gitlab.example.com/user3" + }, + "author_username":"user3" + }, + { + "title":null, + "project_id":1, + "action_name":"opened", + "target_id":159, + "target_type":"Issue", + "author_id":21, + "data":null, + "target_title":"Nostrum enim non et sed optio illo deleniti non.", + "created_at":"2017-02-09T10:43:19.426Z", + "author":{ + "name":"Test User", + "username":"ted", + "id":21, + "state":"active", + "avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon", + "web_url":"https://gitlab.example.com/ted" + }, + "author_username":"ted" + } +] +``` + +[target-types]: #target-types "Target Type parameter" +[action-types]: #action-types "Action Type parameter" +[date-formatting]: #date-formatting "Date Formatting guidance" +[projects-api]: projects.md "Projects API pages" +[users-api]: users.md "Users API pages" diff --git a/doc/api/projects.md b/doc/api/projects.md index 70cad8a6025..0debdcfae89 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -310,143 +310,7 @@ GET /projects/:id/users ### Get project events -Get the events for the specified project sorted from newest to oldest. This -endpoint can be accessed without authentication if the project is publicly -accessible. - -``` -GET /projects/:id/events -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | - -```json -[ - { - "title": null, - "project_id": 15, - "action_name": "closed", - "target_id": 830, - "target_type": "Issue", - "author_id": 1, - "data": null, - "target_title": "Public project search field", - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "root" - }, - { - "title": null, - "project_id": 15, - "action_name": "opened", - "target_id": null, - "target_type": null, - "author_id": 1, - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "john", - "data": { - "before": "50d4420237a9de7be1304607147aec22e4a14af7", - "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "ref": "refs/heads/master", - "user_id": 1, - "user_name": "Dmitriy Zaporozhets", - "repository": { - "name": "gitlabhq", - "url": "git@dev.gitlab.org:gitlab/gitlabhq.git", - "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.", - "homepage": "https://dev.gitlab.org/gitlab/gitlabhq" - }, - "commits": [ - { - "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "message": "Add simple search to projects in public area", - "timestamp": "2013-05-13T18:18:08+00:00", - "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "author": { - "name": "Dmitriy Zaporozhets", - "email": "dmitriy.zaporozhets@gmail.com" - } - } - ], - "total_commits_count": 1 - }, - "target_title": null - }, - { - "title": null, - "project_id": 15, - "action_name": "closed", - "target_id": 840, - "target_type": "Issue", - "author_id": 1, - "data": null, - "target_title": "Finish & merge Code search PR", - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "root" - }, - { - "title": null, - "project_id": 15, - "action_name": "commented on", - "target_id": 1312, - "target_type": "Note", - "author_id": 1, - "data": null, - "target_title": null, - "created_at": "2015-12-04T10:33:58.089Z", - "note": { - "id": 1312, - "body": "What an awesome day!", - "attachment": null, - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "created_at": "2015-12-04T10:33:56.698Z", - "system": false, - "noteable_id": 377, - "noteable_type": "Issue" - }, - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "root" - } -] -``` +Please refer to the [Events API documentation](events.md#list-a-projects-visible-events) ### Create project diff --git a/doc/api/users.md b/doc/api/users.md index 7e118dcf4a9..f4167ba2605 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or ### Get user contribution events -Get the contribution events for the specified user, sorted from newest to oldest. +Please refer to the [Events API documentation](events.md#get-user-contribution-events) -``` -GET /users/:id/events -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | - -```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events -``` - -Example response: - -```json -[ - { - "title": null, - "project_id": 15, - "action_name": "closed", - "target_id": 830, - "target_type": "Issue", - "author_id": 1, - "data": null, - "target_title": "Public project search field", - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "root" - }, - { - "title": null, - "project_id": 15, - "action_name": "opened", - "target_id": null, - "target_type": null, - "author_id": 1, - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "john", - "data": { - "before": "50d4420237a9de7be1304607147aec22e4a14af7", - "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "ref": "refs/heads/master", - "user_id": 1, - "user_name": "Dmitriy Zaporozhets", - "repository": { - "name": "gitlabhq", - "url": "git@dev.gitlab.org:gitlab/gitlabhq.git", - "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.", - "homepage": "https://dev.gitlab.org/gitlab/gitlabhq" - }, - "commits": [ - { - "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "message": "Add simple search to projects in public area", - "timestamp": "2013-05-13T18:18:08+00:00", - "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "author": { - "name": "Dmitriy Zaporozhets", - "email": "dmitriy.zaporozhets@gmail.com" - } - } - ], - "total_commits_count": 1 - }, - "target_title": null - }, - { - "title": null, - "project_id": 15, - "action_name": "closed", - "target_id": 840, - "target_type": "Issue", - "author_id": 1, - "data": null, - "target_title": "Finish & merge Code search PR", - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "root" - }, - { - "title": null, - "project_id": 15, - "action_name": "commented on", - "target_id": 1312, - "target_type": "Note", - "author_id": 1, - "data": null, - "target_title": null, - "created_at": "2015-12-04T10:33:58.089Z", - "note": { - "id": 1312, - "body": "What an awesome day!", - "attachment": null, - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "created_at": "2015-12-04T10:33:56.698Z", - "system": false, - "noteable_id": 377, - "noteable_type": "Issue" - }, - "author": { - "name": "Dmitriy Zaporozhets", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/root" - }, - "author_username": "root" - } -] -``` ## Get all impersonation tokens of a user diff --git a/lib/api/api.rb b/lib/api/api.rb index 7ae2f3cad40..88f91c07194 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -94,6 +94,7 @@ module API mount ::API::DeployKeys mount ::API::Deployments mount ::API::Environments + mount ::API::Events mount ::API::Features mount ::API::Files mount ::API::Groups diff --git a/lib/api/events.rb b/lib/api/events.rb new file mode 100644 index 00000000000..ed5df268ae3 --- /dev/null +++ b/lib/api/events.rb @@ -0,0 +1,86 @@ +module API + class Events < Grape::API + include PaginationParams + + helpers do + params :event_filter_params do + optional :action, type: String, values: Event.actions, desc: 'Event action to filter on' + optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on' + optional :before, type: Date, desc: 'Include only events created before this date' + optional :after, type: Date, desc: 'Include only events created after this date' + end + + params :sort_params do + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return events sorted in ascending and descending order' + end + + def present_events(events) + events = events.reorder(created_at: params[:sort]) + + present paginate(events), with: Entities::Event + end + end + + resource :events do + desc "List currently authenticated user's events" do + detail 'This feature was introduced in GitLab 9.3.' + success Entities::Event + end + params do + use :pagination + use :event_filter_params + use :sort_params + end + get do + authenticate! + + events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target) + + present_events(events) + end + end + + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + resource :users do + desc 'Get the contribution events of a specified user' do + detail 'This feature was introduced in GitLab 8.13.' + success Entities::Event + end + params do + use :pagination + use :event_filter_params + use :sort_params + end + get ':id/events' do + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target) + + present_events(events) + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc "List a Project's visible events" do + success Entities::Event + end + params do + use :pagination + use :event_filter_params + use :sort_params + end + get ":id/events" do + events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target) + + present_events(events) + end + end + end +end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index deac3934d57..56046742e08 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -167,16 +167,6 @@ module API user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics] end - desc 'Get events for a single project' do - success Entities::Event - end - params do - use :pagination - end - get ":id/events" do - present paginate(user_project.events.recent), with: Entities::Event - end - desc 'Fork new project for the current user or provided namespace.' do success Entities::Project end diff --git a/lib/api/users.rb b/lib/api/users.rb index e8694e90cf2..3f87a403a09 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -328,27 +328,6 @@ module API end end - desc 'Get the contribution events of a specified user' do - detail 'This feature was introduced in GitLab 8.13.' - success Entities::Event - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/events' do - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - events = user.events. - merge(ProjectsFinder.new(current_user: current_user).execute). - references(:project). - with_associations. - recent - - present paginate(events), with: Entities::Event - end - params do requires :user_id, type: Integer, desc: 'The ID of the user' end diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb new file mode 100644 index 00000000000..30a2bd14f10 --- /dev/null +++ b/spec/finders/events_finder_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe EventsFinder do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) } + let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) } + let(:closed_issue) { create(:closed_issue, project: project1, author: user) } + let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) } + let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) } + let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) } + let(:closed_issue2) { create(:closed_issue, project: project1, author: user) } + let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) } + let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) } + let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) } + + context 'when targeting a user' do + it 'returns events between specified dates filtered on action and type' do + events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute + + expect(events).to eq([opened_merge_request_event]) + end + + it 'does not return events the current_user does not have access to' do + events = described_class.new(source: user, current_user: other_user).execute + + expect(events).not_to include(opened_merge_request_event) + end + end + + context 'when targeting a project' do + it 'returns project events between specified dates filtered on action and type' do + events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute + + expect(events).to eq([closed_issue_event]) + end + + it 'does not return events the current_user does not have access to' do + events = described_class.new(source: project2, current_user: other_user).execute + + expect(events).to be_empty + end + end +end diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb new file mode 100644 index 00000000000..51e72c39a30 --- /dev/null +++ b/spec/requests/api/events_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +describe API::Events, api: true do + include ApiHelpers + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:other_user) { create(:user, username: 'otheruser') } + let(:private_project) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) } + let(:closed_issue) { create(:closed_issue, project: private_project, author: user) } + let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) } + + describe 'GET /events' do + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/events') + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns users events' do + get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + end + end + + describe 'GET /users/:id/events' do + context "as a user that cannot see the event's project" do + it 'returns no events' do + get api("/users/#{user.id}/events", other_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_empty + end + end + + context "as a user that can see the event's project" do + it 'returns the events' do + get api("/users/#{user.id}/events", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + + context 'when there are multiple events from different projects' do + let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } + + before do + second_note.project.add_user(user, :developer) + + [second_note].each do |note| + EventCreateService.new.leave_note(note, user) + end + end + + it 'returns events in the correct order (from newest to oldest)' do + get api("/users/#{user.id}/events", user) + + comment_events = json_response.select { |e| e['action_name'] == 'commented on' } + close_events = json_response.select { |e| e['action_name'] == 'closed' } + + expect(comment_events[0]['target_id']).to eq(second_note.id) + expect(close_events[0]['target_id']).to eq(closed_issue.id) + end + + it 'accepts filter parameters' do + get api("/users/#{user.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user) + + expect(json_response.size).to eq(1) + expect(json_response[0]['target_id']).to eq(closed_issue.id) + end + end + end + + it 'returns a 404 error if not found' do + get api('/users/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end + + describe 'GET /projects/:id/events' do + context 'when unauthenticated ' do + it 'returns 404 for private project' do + get api("/projects/#{private_project.id}/events") + + expect(response).to have_http_status(404) + end + + it 'returns 200 status for a public project' do + public_project = create(:empty_project, :public) + + get api("/projects/#{public_project.id}/events") + + expect(response).to have_http_status(200) + end + end + + context 'when not permitted to read' do + it 'returns 404' do + get api("/projects/#{private_project.id}/events", non_member) + + expect(response).to have_http_status(404) + end + end + + context 'when authenticated' do + it 'returns project events' do + get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + + it 'returns 404 if project does not exist' do + get api("/projects/1234/events", user) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c0ecb4d2aaa..86c57204971 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -762,64 +762,6 @@ describe API::Projects do end end - describe 'GET /projects/:id/events' do - shared_examples_for 'project events response' do - it 'returns the project events' do - member = create(:user) - create(:project_member, :developer, user: member, project: project) - note = create(:note_on_issue, note: 'What an awesome day!', project: project) - EventCreateService.new.leave_note(note, note.author) - - get api("/projects/#{project.id}/events", current_user) - - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - - first_event = json_response.first - expect(first_event['action_name']).to eq('commented on') - expect(first_event['note']['body']).to eq('What an awesome day!') - - last_event = json_response.last - - expect(last_event['action_name']).to eq('joined') - expect(last_event['project_id'].to_i).to eq(project.id) - expect(last_event['author_username']).to eq(member.username) - expect(last_event['author']['name']).to eq(member.name) - end - end - - context 'when unauthenticated' do - it_behaves_like 'project events response' do - let(:project) { create(:empty_project, :public) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - context 'valid request' do - it_behaves_like 'project events response' do - let(:current_user) { user } - end - end - - it 'returns a 404 error if not found' do - get api('/projects/42/events', user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - - get api("/projects/#{project.id}/events", other_user) - - expect(response).to have_http_status(404) - end - end - end - describe 'GET /projects/:id/users' do shared_examples_for 'project users response' do it 'returns the project users' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 1c33b8f9502..4efc3e1a1e2 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1130,83 +1130,6 @@ describe API::Users do end end - describe 'GET /users/:id/events' do - let(:user) { create(:user) } - let(:project) { create(:empty_project) } - let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) } - - before do - project.add_user(user, :developer) - EventCreateService.new.leave_note(note, user) - end - - context "as a user than cannot see the event's project" do - it 'returns no events' do - other_user = create(:user) - - get api("/users/#{user.id}/events", other_user) - - expect(response).to have_http_status(200) - expect(json_response).to be_empty - end - end - - context "as a user than can see the event's project" do - context 'joined event' do - it 'returns the "joined" event' do - get api("/users/#{user.id}/events", user) - - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - - comment_event = json_response.find { |e| e['action_name'] == 'commented on' } - - expect(comment_event['project_id'].to_i).to eq(project.id) - expect(comment_event['author_username']).to eq(user.username) - expect(comment_event['note']['id']).to eq(note.id) - expect(comment_event['note']['body']).to eq('What an awesome day!') - - joined_event = json_response.find { |e| e['action_name'] == 'joined' } - - expect(joined_event['project_id'].to_i).to eq(project.id) - expect(joined_event['author_username']).to eq(user.username) - expect(joined_event['author']['name']).to eq(user.name) - end - end - - context 'when there are multiple events from different projects' do - let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } - let(:third_note) { create(:note_on_issue, project: project) } - - before do - second_note.project.add_user(user, :developer) - - [second_note, third_note].each do |note| - EventCreateService.new.leave_note(note, user) - end - end - - it 'returns events in the correct order (from newest to oldest)' do - get api("/users/#{user.id}/events", user) - - comment_events = json_response.select { |e| e['action_name'] == 'commented on' } - - expect(comment_events[0]['target_id']).to eq(third_note.id) - expect(comment_events[1]['target_id']).to eq(second_note.id) - expect(comment_events[2]['target_id']).to eq(note.id) - end - end - end - - it 'returns a 404 error if not found' do - get api('/users/42/events', user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - end - context "user activities", :redis do let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) } let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) } From 3c15f0eae757817b4d852be6b62abd3d73186d35 Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Fri, 2 Jun 2017 11:41:47 +0800 Subject: [PATCH 20/26] Accept a username for User-level Events API --- doc/api/events.md | 2 +- lib/api/events.rb | 4 ++-- spec/requests/api/events_spec.rb | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/api/events.md b/doc/api/events.md index ef57287264d..e7829c9f479 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -129,7 +129,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | +| `id` | integer | yes | The ID or Username of the user | | `action` | string | no | Include only events of a particular [action type][action-types] | | `target_type` | string | no | Include only events of a particular [target type][target-types] | | `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] | diff --git a/lib/api/events.rb b/lib/api/events.rb index ed5df268ae3..dabdf579119 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -42,7 +42,7 @@ module API end params do - requires :id, type: Integer, desc: 'The ID of the user' + requires :id, type: String, desc: 'The ID or Username of the user' end resource :users do desc 'Get the contribution events of a specified user' do @@ -55,7 +55,7 @@ module API use :sort_params end get ':id/events' do - user = User.find_by(id: params[:id]) + user = find_user(params[:id]) not_found!('User') unless user events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target) diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 51e72c39a30..a19870a95e8 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -41,6 +41,15 @@ describe API::Events, api: true do end context "as a user that can see the event's project" do + it 'accepts a username' do + get api("/users/#{user.username}/events", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + it 'returns the events' do get api("/users/#{user.id}/events", user) From 7a9c9a259e590e16ecc8c1fe82f5c3e808850006 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 6 Jun 2017 12:58:29 +0000 Subject: [PATCH 21/26] Responsive environment tables --- .../components/environment_actions.vue | 2 +- .../components/environment_item.vue | 69 ++++++----- .../components/environment_monitoring.vue | 2 +- .../components/environment_rollback.vue | 2 +- .../components/environment_stop.vue | 2 +- .../environment_terminal_button.vue | 2 +- .../components/environments_table.vue | 109 ++++++++---------- .../vue_shared/components/commit.js | 6 +- app/assets/stylesheets/framework.scss | 1 + .../framework/responsive-tables.scss | 86 ++++++++++++++ .../stylesheets/pages/environments.scss | 72 +++++++----- .../environments/environments_spec.rb | 4 +- .../commit/pipelines/pipelines_spec.js | 4 +- .../environments/environment_spec.js | 2 +- .../environments/environment_table_spec.js | 2 +- 15 files changed, 237 insertions(+), 128 deletions(-) create mode 100644 app/assets/stylesheets/framework/responsive-tables.scss diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index a2448520a5f..41d5453f1b2 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -70,7 +70,7 @@ export default { -