summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2018-05-14 17:20:47 +0900
committerShinya Maeda <shinya@gitlab.com>2018-05-14 17:20:47 +0900
commit1e76f289fb3f6e125594d2ec0169ecde25e4e335 (patch)
tree3087903b6f5aab8c74f3f0d76b2908cd5dc169e3
parenta74184eb5e692ef77fe3be28b1f4a40549c8fcff (diff)
parent40683268b2b5ad807194387d8345a30195e178c4 (diff)
downloadgitlab-ce-1e76f289fb3f6e125594d2ec0169ecde25e4e335.tar.gz
Merge branch 'master' into per-project-pipeline-iid
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.rails5.lock120
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue2
-rw-r--r--app/assets/javascripts/ide/stores/utils.js1
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue95
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue96
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue8
-rw-r--r--app/assets/javascripts/user_callout.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue14
-rw-r--r--app/assets/stylesheets/framework/terms.scss9
-rw-r--r--app/controllers/application_controller.rb9
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/users/terms_controller.rb4
-rw-r--r--app/helpers/users_helper.rb19
-rw-r--r--app/models/ci/runner.rb8
-rw-r--r--app/models/concerns/routable.rb4
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/user.rb15
-rw-r--r--app/policies/global_policy.rb19
-rw-r--r--app/views/groups/group_members/index.html.haml26
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/layouts/terms.html.haml4
-rw-r--r--app/views/shared/members/_filter_2fa_dropdown.html.haml11
-rw-r--r--app/views/shared/members/_member.html.haml4
-rw-r--r--changelogs/unreleased/40725-move-mr-external-link-to-right.yml5
-rw-r--r--changelogs/unreleased/45715-remove-modal-retry.yml5
-rw-r--r--changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml5
-rw-r--r--changelogs/unreleased/bvl-restrict-api-git-for-terms.yml6
-rw-r--r--changelogs/unreleased/docs-42067-document-runner-registration-api.yml5
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter.yml5
-rw-r--r--changelogs/unreleased/fix-registry-created-at-tooltip.yml5
-rw-r--r--changelogs/unreleased/zj-wiki-find-file-opt-out.yml5
-rw-r--r--config/initializers/6_validations.rb11
-rw-r--r--doc/api/runners.md83
-rw-r--r--doc/api/services.md6
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md2
-rw-r--r--features/project/merge_requests/references.feature31
-rw-r--r--features/steps/shared/authentication.rb12
-rw-r--r--features/steps/shared/issuable.rb61
-rw-r--r--features/steps/shared/note.rb4
-rw-r--r--features/steps/shared/project.rb23
-rw-r--r--lib/api/api_guard.rb12
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb33
-rw-r--r--lib/gitlab/build_access.rb12
-rw-r--r--lib/gitlab/git/repository.rb5
-rw-r--r--lib/gitlab/git/wiki.rb3
-rw-r--r--lib/gitlab/git_access.rb12
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb11
-rw-r--r--lib/gitlab/multi_collection_paginator.rb2
-rw-r--r--lib/gitlab/repo_path.rb19
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb24
-rw-r--r--spec/features/groups/members/filter_members_spec.rb54
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb5
-rw-r--r--spec/features/users/login_spec.rb102
-rw-r--r--spec/features/users/terms_spec.rb18
-rw-r--r--spec/initializers/6_validations_spec.rb20
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js26
-rw-r--r--spec/javascripts/pipelines/async_button_spec.js62
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js33
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb34
-rw-r--r--spec/lib/gitlab/build_access_spec.rb23
-rw-r--r--spec/lib/gitlab/git_access_spec.rb94
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb29
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb19
-rw-r--r--spec/models/ci/runner_spec.rb26
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb9
-rw-r--r--spec/models/user_spec.rb27
-rw-r--r--spec/policies/global_policy_spec.rb90
-rw-r--r--spec/requests/api/helpers_spec.rb18
-rw-r--r--spec/requests/git_http_spec.rb53
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb69
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml54
78 files changed, 1263 insertions, 638 deletions
diff --git a/Gemfile b/Gemfile
index 886925e0674..78af9ee4d05 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,7 @@ end
gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
-gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
+gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 ---
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 3056b97ccd5..fc6dfd040c2 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -4,43 +4,43 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.4)
- actioncable (5.0.6)
- actionpack (= 5.0.6)
+ actioncable (5.0.7)
+ actionpack (= 5.0.7)
nio4r (>= 1.2, < 3.0)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.6)
- actionpack (= 5.0.6)
- actionview (= 5.0.6)
- activejob (= 5.0.6)
+ actionmailer (5.0.7)
+ actionpack (= 5.0.7)
+ actionview (= 5.0.7)
+ activejob (= 5.0.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.0.6)
- actionview (= 5.0.6)
- activesupport (= 5.0.6)
+ actionpack (5.0.7)
+ actionview (= 5.0.7)
+ activesupport (= 5.0.7)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.6)
- activesupport (= 5.0.6)
+ actionview (5.0.7)
+ activesupport (= 5.0.7)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.0.6)
- activesupport (= 5.0.6)
+ activejob (5.0.7)
+ activesupport (= 5.0.7)
globalid (>= 0.3.6)
- activemodel (5.0.6)
- activesupport (= 5.0.6)
- activerecord (5.0.6)
- activemodel (= 5.0.6)
- activesupport (= 5.0.6)
+ activemodel (5.0.7)
+ activesupport (= 5.0.7)
+ activerecord (5.0.7)
+ activemodel (= 5.0.7)
+ activesupport (= 5.0.7)
arel (~> 7.0)
activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6)
- activesupport (5.0.6)
+ activesupport (5.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (~> 0.7)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0)
@@ -62,13 +62,13 @@ GEM
asciidoctor (1.5.6.1)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.2.0)
+ asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.4.0)
- atomic (1.1.100)
+ atomic (1.1.99)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
@@ -144,12 +144,10 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.3)
+ crass (1.0.4)
creole (0.5.0)
css_parser (1.6.0)
addressable
- d3_rails (3.5.17)
- railties (>= 3.1.0)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.3)
@@ -292,7 +290,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.97.0)
+ gitaly-proto (0.99.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -335,9 +333,8 @@ GEM
activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gon (6.1.0)
+ gon (6.2.0)
actionpack (>= 3.0)
- json
multi_json
request_store (>= 1.0)
google-api-client (0.19.8)
@@ -367,8 +364,8 @@ GEM
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
- grape-entity (0.6.1)
- activesupport (>= 5.0.0)
+ grape-entity (0.7.1)
+ activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-route-helpers (2.1.0)
activesupport
@@ -420,7 +417,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (0.9.5)
+ i18n (1.0.1)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
influxdb (0.5.3)
@@ -515,7 +512,7 @@ GEM
net-ldap (0.16.1)
net-ssh (4.2.0)
netrc (0.11.0)
- nio4r (2.2.0)
+ nio4r (2.3.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
numerizer (0.1.1)
@@ -545,9 +542,9 @@ GEM
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
- omniauth-github (1.1.2)
- omniauth (~> 1.0)
- omniauth-oauth2 (~> 1.1)
+ omniauth-github (1.3.0)
+ omniauth (~> 1.5)
+ omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.3)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
@@ -633,7 +630,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.1)
+ prometheus-client-mmap (0.9.2)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@@ -644,7 +641,7 @@ GEM
pry (>= 0.10.4)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
- rack (2.0.4)
+ rack (2.0.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -662,17 +659,17 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (5.0.6)
- actioncable (= 5.0.6)
- actionmailer (= 5.0.6)
- actionpack (= 5.0.6)
- actionview (= 5.0.6)
- activejob (= 5.0.6)
- activemodel (= 5.0.6)
- activerecord (= 5.0.6)
- activesupport (= 5.0.6)
+ rails (5.0.7)
+ actioncable (= 5.0.7)
+ actionmailer (= 5.0.7)
+ actionpack (= 5.0.7)
+ actionview (= 5.0.7)
+ activejob (= 5.0.7)
+ activemodel (= 5.0.7)
+ activerecord (= 5.0.7)
+ activesupport (= 5.0.7)
bundler (>= 1.3.0)
- railties (= 5.0.6)
+ railties (= 5.0.7)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -683,21 +680,21 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
- rails-html-sanitizer (1.0.3)
- loofah (~> 2.0)
+ rails-html-sanitizer (1.0.4)
+ loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.1)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
- railties (5.0.6)
- actionpack (= 5.0.6)
- activesupport (= 5.0.6)
+ railties (5.0.7)
+ actionpack (= 5.0.7)
+ activesupport (= 5.0.7)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
raindrops (0.19.0)
- rake (12.3.0)
+ rake (12.3.1)
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@@ -1001,7 +998,7 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.2.0)
+ asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -1027,12 +1024,11 @@ DEPENDENCIES
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
- d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5)
device_detector
- devise (~> 4.2)
+ devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
@@ -1063,7 +1059,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.97.0)
+ gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1071,12 +1067,12 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- gon (~> 6.1.0)
+ gon (~> 6.2)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
- grape-entity (~> 0.6.0)
+ grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.11.0)
@@ -1117,7 +1113,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
- omniauth-github (~> 1.1.1)
+ omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0)
@@ -1136,14 +1132,14 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.1)
+ prometheus-client-mmap (~> 0.9.2)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 5.0.6)
+ rails (= 5.0.7)
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1)
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index ff7e546fb9c..f8678b602ac 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -140,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
- if (this.viewer === viewerTypes.mr) {
+ if (this.viewer === viewerTypes.mr && this.file.mrChange) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index bc79ff4a542..e0b9766fbee 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -44,6 +44,7 @@ export const dataStructure = () => ({
size: 0,
parentPath: null,
lastOpenedAt: 0,
+ mrChange: null,
});
export const decorateData = entity => {
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
deleted file mode 100644
index 0cdffbde05b..00000000000
--- a/app/assets/javascripts/pipelines/components/async_button.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
- /* eslint-disable no-alert */
-
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
- },
- components: {
- loadingIcon,
- icon,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- icon: {
- type: String,
- required: true,
- },
- cssClass: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- computed: {
- buttonClass() {
- return `btn ${this.cssClass}`;
- },
- },
- created() {
- // We're using eventHub to listen to the modal here instead of
- // using props because it would would make the parent components
- // much more complex to keep track of the loading state of each button
- eventHub.$on('postAction', this.setLoading);
- },
- beforeDestroy() {
- eventHub.$off('postAction', this.setLoading);
- },
- methods: {
- onClick() {
- eventHub.$emit('openConfirmationModal', {
- pipelineId: this.pipelineId,
- endpoint: this.endpoint,
- type: this.type,
- });
- },
- setLoading(endpoint) {
- if (endpoint === this.endpoint) {
- this.isLoading = true;
- }
- },
- },
- };
-</script>
-
-<template>
- <button
- v-tooltip
- type="button"
- @click="onClick"
- :class="buttonClass"
- :title="title"
- :aria-label="title"
- data-container="body"
- data-placement="top"
- :disabled="isLoading">
- <icon
- :name="icon"
- />
- <loading-icon v-if="isLoading" />
- </button>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 714aed1333e..41986b827cd 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,7 +1,7 @@
<script>
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+ import Modal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
- import pipelinesTableRowComponent from './pipelines_table_row.vue';
+ import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub';
/**
@@ -11,8 +11,8 @@
*/
export default {
components: {
- pipelinesTableRowComponent,
- DeprecatedModal,
+ PipelinesTableRowComponent,
+ Modal,
},
props: {
pipelines: {
@@ -37,30 +37,18 @@
return {
pipelineId: '',
endpoint: '',
- type: '',
};
},
computed: {
modalTitle() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false) :
- sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false);
+ return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
+ pipelineId: `${this.pipelineId}`,
+ }, false);
},
modalText() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false) :
- sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false);
- },
- primaryButtonLabel() {
- return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
+ return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
+ pipelineId: `<strong>#${this.pipelineId}</strong>`,
+ }, false);
},
},
created() {
@@ -73,7 +61,6 @@
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
- this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
@@ -120,20 +107,16 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
- <deprecated-modal
+
+ <modal
id="confirmation-modal"
- :title="modalTitle"
- :text="modalText"
- kind="danger"
- :primary-button-label="primaryButtonLabel"
+ :header-title-text="modalTitle"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('Pipeline|Stop pipeline')"
@submit="onSubmit"
>
- <template
- slot="body"
- slot-scope="props"
- >
- <p v-html="props.text"></p>
- </template>
- </deprecated-modal>
+ <span v-html="modalText"></span>
+ </modal>
+
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 4cbd67e0372..498a97851fa 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -1,13 +1,14 @@
<script>
- /* eslint-disable no-param-reassign */
- import asyncButtonComponent from './async_button.vue';
- import pipelinesActionsComponent from './pipelines_actions.vue';
- import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
- import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
- import pipelineStage from './stage.vue';
- import pipelineUrl from './pipeline_url.vue';
- import pipelinesTimeago from './time_ago.vue';
- import commitComponent from '../../vue_shared/components/commit.vue';
+ import eventHub from '../event_hub';
+ import PipelinesActionsComponent from './pipelines_actions.vue';
+ import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+ import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
+ import PipelineStage from './stage.vue';
+ import PipelineUrl from './pipeline_url.vue';
+ import PipelinesTimeago from './time_ago.vue';
+ import CommitComponent from '../../vue_shared/components/commit.vue';
+ import LoadingButton from '../../vue_shared/components/loading_button.vue';
+ import Icon from '../../vue_shared/components/icon.vue';
/**
* Pipeline table row.
@@ -16,14 +17,15 @@
*/
export default {
components: {
- asyncButtonComponent,
- pipelinesActionsComponent,
- pipelinesArtifactsComponent,
- commitComponent,
- pipelineStage,
- pipelineUrl,
- ciBadge,
- pipelinesTimeago,
+ PipelinesActionsComponent,
+ PipelinesArtifactsComponent,
+ CommitComponent,
+ PipelineStage,
+ PipelineUrl,
+ CiBadge,
+ PipelinesTimeago,
+ LoadingButton,
+ Icon,
},
props: {
pipeline: {
@@ -44,6 +46,12 @@
required: true,
},
},
+ data() {
+ return {
+ isRetrying: false,
+ isCancelling: false,
+ };
+ },
computed: {
/**
* If provided, returns the commit tag.
@@ -119,8 +127,10 @@
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
+ // eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
+ // eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
@@ -216,6 +226,21 @@
return this.viewType === 'child';
},
},
+
+ methods: {
+ handleCancelClick() {
+ this.isCancelling = true;
+
+ eventHub.$emit('openConfirmationModal', {
+ pipelineId: this.pipeline.id,
+ endpoint: this.pipeline.cancel_path,
+ });
+ },
+ handleRetryClick() {
+ this.isRetrying = true;
+ eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+ },
+ },
};
</script>
<template>
@@ -287,7 +312,8 @@
<div
v-if="displayPipelineActions"
- class="table-section section-20 table-button-footer pipeline-actions">
+ class="table-section section-20 table-button-footer pipeline-actions"
+ >
<div class="btn-group table-action-buttons">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
@@ -300,29 +326,27 @@
:artifacts="pipeline.details.artifacts"
/>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.retryable"
- :endpoint="pipeline.retry_path"
- css-class="js-pipelines-retry-button btn-default btn-retry"
- title="Retry"
- icon="repeat"
- :pipeline-id="pipeline.id"
- data-toggle="modal"
- data-target="#confirmation-modal"
- type="retry"
- />
+ @click="handleRetryClick"
+ container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ :loading="isRetrying"
+ :disabled="isRetrying"
+ >
+ <icon name="repeat" />
+ </loading-button>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.cancelable"
- :endpoint="pipeline.cancel_path"
- css-class="js-pipelines-cancel-button btn-remove"
- title="Stop"
- icon="close"
- :pipeline-id="pipeline.id"
+ @click="handleCancelClick"
data-toggle="modal"
data-target="#confirmation-modal"
- type="stop"
- />
+ container-class="js-pipelines-cancel-button btn btn-remove"
+ :loading="isCancelling"
+ :disabled="isCancelling"
+ >
+ <icon name="close" />
+ </loading-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 6d87f75ae8e..de0faf181e5 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -53,10 +53,12 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
},
destroyed() {
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index ee4eb3581f3..a2227b2f554 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -111,7 +111,13 @@
</td>
<td>
- {{ timeFormated(item.createdAt) }}
+ <span
+ v-tooltip
+ :title="tooltipTitle(item.createdAt)"
+ data-placement="bottom"
+ >
+ {{ timeFormated(item.createdAt) }}
+ </span>
</td>
<td class="content">
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index 97d5cf96bcb..96dfff77859 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -15,7 +15,7 @@ export default class UserCallout {
init() {
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $('.js-close-callout').on('click', e => this.dismissCallout(e));
+ this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
@@ -23,12 +23,15 @@ export default class UserCallout {
const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) {
- Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') });
+ Cookies.set(this.cookieName, 'true', {
+ expires: 365,
+ path: this.userCalloutBody.data('projectPath'),
+ });
} else {
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
- if ($currentTarget.hasClass('close')) {
+ if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) {
this.userCalloutBody.remove();
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 7bef2e97349..1fea231c816 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -109,12 +109,12 @@ export default {
rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-url"
>
+ {{ deployment.external_url_formatted }}
<i
class="fa fa-external-link"
aria-hidden="true"
>
</i>
- {{ deployment.external_url_formatted }}
</a>
</template>
<span
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index e832d94d32f..88c13a1f340 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -70,12 +70,14 @@
/>
</transition>
<transition name="fade">
- <span
- v-if="label"
- class="js-loading-button-label"
- >
- {{ label }}
- </span>
+ <slot>
+ <span
+ v-if="label"
+ class="js-loading-button-label"
+ >
+ {{ label }}
+ </span>
+ </slot>
</transition>
</button>
</template>
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
index 16293d32dfa..744fd0ff796 100644
--- a/app/assets/stylesheets/framework/terms.scss
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -17,6 +17,7 @@
display: flex;
align-items: center;
justify-content: space-between;
+ line-height: $line-height-base;
.title {
display: flex;
@@ -33,10 +34,14 @@
.navbar-collapse {
padding-right: 0;
+
+ .navbar-nav {
+ margin: 0;
+ }
}
- .nav li a {
- color: $theme-gray-700;
+ .nav li {
+ float: none;
}
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2caffec66ac..2843d70c645 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
- before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms },
- unless: :peek_request?
+ before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
before_action :check_password_expiration
before_action :ldap_security_check
@@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base
def peek_request?
request.path.start_with?('/-/peek')
end
+
+ def should_enforce_terms?
+ return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
+
+ !(peek_request? || devise_controller?)
+ end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 134b0dfc0db..ef3eba80154 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override
def index
+ can_manage_members = can?(current_user, :admin_group_member, @group)
+
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = GroupMembersFinder.new(@group).execute
- @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.non_invite unless can_manage_members
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort_by_attribute(@sort)
+
+ if can_manage_members && params[:two_factor].present?
+ @members = @members.filter_by_2fa(params[:two_factor])
+ end
+
@members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user))
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index 95c5c3432d5..ab685b9106e 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -3,6 +3,10 @@ module Users
include InternalRedirect
skip_before_action :enforce_terms!
+ skip_before_action :check_password_expiration
+ skip_before_action :check_two_factor_requirement
+ skip_before_action :require_email
+
before_action :terms
layout 'terms'
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index e803cd3a8d8..ce9373f5883 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -42,22 +42,11 @@ module UsersHelper
items << :sign_out if current_user
- # TODO: Remove these conditions when the permissions are prevented in
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849
- terms_not_enforced = !Gitlab::CurrentSettings
- .current_application_settings
- .enforce_terms?
- required_terms_accepted = terms_not_enforced || current_user.terms_accepted?
+ return items if current_user&.required_terms_not_accepted?
- items << :help if required_terms_accepted
-
- if can?(current_user, :read_user, current_user) && required_terms_accepted
- items << :profile
- end
-
- if can?(current_user, :update_user, current_user) && required_terms_accepted
- items << :settings
- end
+ items << :help
+ items << :profile if can?(current_user, :read_user, current_user)
+ items << :settings if can?(current_user, :update_user, current_user)
items
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ed8b30dae49..bda69f85a78 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -108,7 +108,13 @@ module Ci
end
def assign_to(project, current_user = nil)
- self.is_shared = false if shared?
+ if shared?
+ self.is_shared = false if shared?
+ self.runner_type = :project_type
+ elsif group_type?
+ raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
+ end
+
self.save
project.runner_projects.create(runner_id: self.id)
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 915ad6959be..0176a12a131 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -4,7 +4,9 @@ module Routable
extend ActiveSupport::Concern
included do
- has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # Remove `inverse_of: source` when upgraded to rails 5.2
+ # See https://github.com/rails/rails/pull/28808
+ has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :route, presence: true
diff --git a/app/models/member.rb b/app/models/member.rb
index eac4a22a03f..68572f2e33a 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -96,6 +96,17 @@ class Member < ActiveRecord::Base
joins(:user).merge(User.search(query))
end
+ def filter_by_2fa(value)
+ case value
+ when 'enabled'
+ left_join_users.merge(User.with_two_factor_indistinct)
+ when 'disabled'
+ left_join_users.merge(User.without_two_factor)
+ else
+ all
+ end
+ end
+
def sort_by_attribute(method)
case method.to_s
when 'access_level_asc' then reorder(access_level: :asc)
diff --git a/app/models/user.rb b/app/models/user.rb
index a9cfd39f604..dfef065f094 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -237,14 +237,18 @@ class User < ActiveRecord::Base
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
- def self.with_two_factor
+ def self.with_two_factor_indistinct
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
+ .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true)
+ end
+
+ def self.with_two_factor
+ with_two_factor_indistinct.distinct(arel_table[:id])
end
def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NULL AND otp_required_for_login = ?", false)
+ .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false)
end
#
@@ -1193,6 +1197,11 @@ class User < ActiveRecord::Base
accepted_term_id.present?
end
+ def required_terms_not_accepted?
+ Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
+ !terms_accepted?
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 64e550d19d0..1cf5515d9d7 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -1,22 +1,24 @@
class GlobalPolicy < BasePolicy
desc "User is blocked"
with_options scope: :user, score: 0
- condition(:blocked) { @user.blocked? }
+ condition(:blocked) { @user&.blocked? }
desc "User is an internal user"
with_options scope: :user, score: 0
- condition(:internal) { @user.internal? }
+ condition(:internal) { @user&.internal? }
desc "User's access has been locked"
with_options scope: :user, score: 0
- condition(:access_locked) { @user.access_locked? }
+ condition(:access_locked) { @user&.access_locked? }
- condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+ condition(:can_create_fork, scope: :user) { @user && @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+
+ condition(:required_terms_not_accepted, scope: :user, score: 0) do
+ @user&.required_terms_not_accepted?
+ end
rule { anonymous }.policy do
prevent :log_in
- prevent :access_api
- prevent :access_git
prevent :receive_notifications
prevent :use_quick_actions
prevent :create_group
@@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy
prevent :use_quick_actions
end
+ rule { required_terms_not_accepted }.policy do
+ prevent :access_api
+ prevent :access_git
+ end
+
rule { can_create_group }.policy do
enable :create_group
end
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ad9d5562ded..c8addc49117 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,10 +1,11 @@
- page_title "Members"
+- can_manage_members = can?(current_user, :admin_group_member, @group)
.project-members-page.prepend-top-default
%h4
Members
%hr
- - if can?(current_user, :admin_group_member, @group)
+ - if can_manage_members
.project-members-new.append-bottom-default
%p.clearfix
Add new member to
@@ -13,20 +14,23 @@
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
- .append-bottom-default.clearfix
+ .clearfix
%h5.member.existing-title
Existing members
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- = render 'shared/members/sort_dropdown'
.panel.panel-default
- .panel-heading
- Members with access to
- %strong= @group.name
+ .panel-heading.flex-project-members-panel
+ %span.flex-project-title
+ Members with access to
+ %strong= @group.name
%span.badge= @members.total_count
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ - if can_manage_members
+ = render 'shared/members/filter_2fa_dropdown'
+ = render 'shared/members/sort_dropdown'
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index bbfbea4ac7a..662db18cf86 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,7 +8,7 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn' do
+ = link_to safe_params.merge(rss_url_options), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index a30d6e2688c..87f4151f241 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -20,10 +20,10 @@
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
- %span.logo-text.hidden-xs.prepend-left-8
+ %span.logo-text.prepend-left-8
= logo_text
- if header_link?(:user_dropdown)
- .navbar-collapse.collapse
+ .navbar-collapse
%ul.nav.navbar-nav
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml
new file mode 100644
index 00000000000..95c35c56b3c
--- /dev/null
+++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml
@@ -0,0 +1,11 @@
+- filter = params[:two_factor] || 'everyone'
+- filter_options = { 'everyone' => 'Everyone', 'enabled' => 'Enabled', 'disabled' => 'Disabled' }
+.dropdown.inline.member-filter-2fa-dropdown
+ = dropdown_toggle('2FA: ' + filter_options[filter], { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Filter by two-factor authentication
+ - filter_options.each do |value, title|
+ %li
+ = link_to filter_group_project_member_path(two_factor: value), class: ("is-active" if filter == value) do
+ = title
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 1c139827acf..1961ad6d616 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -20,6 +20,10 @@
%label.label.label-danger
%strong Blocked
+ - if user.two_factor_enabled?
+ %label.label.label-info
+ 2FA
+
- if source.instance_of?(Group) && source != @group
&middot;
= link_to source.full_name, source, class: "member-group-link"
diff --git a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
new file mode 100644
index 00000000000..e3ebeb5eb61
--- /dev/null
+++ b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
@@ -0,0 +1,5 @@
+---
+title: Moves MR widget external link icon to the right
+merge_request: 18828
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml
new file mode 100644
index 00000000000..04f2ff5142e
--- /dev/null
+++ b/changelogs/unreleased/45715-remove-modal-retry.yml
@@ -0,0 +1,5 @@
+---
+title: Remove modalbox confirmation when retrying a pipeline
+merge_request: 18879
+author:
+type: changed
diff --git a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml
new file mode 100644
index 00000000000..8a7c549e356
--- /dev/null
+++ b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml
@@ -0,0 +1,5 @@
+---
+title: 46210 Display logo and user dropdown on mobile for terms page and fix styling
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
new file mode 100644
index 00000000000..c0ba984bfdc
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog'
+merge_request: 18794
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml
new file mode 100644
index 00000000000..49cd04b065b
--- /dev/null
+++ b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml
@@ -0,0 +1,6 @@
+---
+title: Block access to the API & git for users that did not accept enforced Terms
+ of Service
+merge_request: 18816
+author:
+type: other
diff --git a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
new file mode 100644
index 00000000000..6b507174044
--- /dev/null
+++ b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expand documentation for Runners API
+merge_request: 16484
+author:
+type: other
diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml
new file mode 100644
index 00000000000..82d501d6604
--- /dev/null
+++ b/changelogs/unreleased/dz-add-2fa-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Add 2FA filter to the group members page
+merge_request: 18483
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-registry-created-at-tooltip.yml b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
new file mode 100644
index 00000000000..911b3b10fd4
--- /dev/null
+++ b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add missing tooltip to creation date on container registry overview'
+merge_request: 18767
+author: Lars Greiss
+type: fixed
diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
new file mode 100644
index 00000000000..5af53c56017
--- /dev/null
+++ b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Finding a wiki page is done by Gitaly by default
+merge_request:
+author:
+type: other
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index d92cdb97766..89aabe530fe 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -26,17 +26,6 @@ def validate_storages_config
Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
- if repository_storage.is_a?(String)
- raise "#{name} is not a valid storage, because it has no `path` key. " \
- "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \
- "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \
- "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
- end
-
- if !repository_storage.is_a?(Gitlab::GitalyClient::StorageSettings) || repository_storage.legacy_disk_path.nil?
- storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
- end
-
%w(failure_count_threshold failure_reset_time storage_timeout).each do |setting|
# Falling back to the defaults is fine!
next if repository_storage[setting].nil?
diff --git a/doc/api/runners.md b/doc/api/runners.md
index f384ac57bfe..3ca07ce9795 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -411,3 +411,86 @@ DELETE /projects/:id/runners/:runner_id
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9"
```
+
+## Register a new Runner
+
+Register a new Runner for the instance.
+
+```
+POST /runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Registration token ([Read how to obtain a token](../ci/runners/README.md)) |
+| `description`| string | no | Runner's description|
+| `info` | hash | no | Runner's metadata |
+| `active` | boolean| no | Whether the Runner is active |
+| `locked` | boolean| no | Whether the Runner should be locked for current project |
+| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs |
+| `tag_list` | Array[String] | no | List of Runner's tags |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
+
+```
+curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=ipzXrMhuyyJPifUt6ANz" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 201 | Runner was created |
+
+Example response:
+
+```json
+{
+ "id": "12345",
+ "token": "6337ff461c94fd3fa32ba3b1ff4125"
+}
+```
+
+## Delete a registered Runner
+
+Deletes a registed Runner.
+
+```
+DELETE /runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Runner's authentication token |
+
+```
+curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=ebb6fc00521627750c8bb750f2490e"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 204 | Runner was deleted |
+
+## Verify authentication for a registered Runner
+
+Validates authentication credentials for a registered Runner.
+
+```
+POST /runners/verify
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Runner's authentication token |
+
+```
+curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=ebb6fc00521627750c8bb750f2490e"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Credentials are valid |
+| 403 | Credentials are invalid |
diff --git a/doc/api/services.md b/doc/api/services.md
index 92f12acbc73..ec632125325 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -968,7 +968,7 @@ Group Chat Software
Set Microsoft Teams service for a project.
```
-PUT /projects/:id/services/microsoft_teams
+PUT /projects/:id/services/microsoft-teams
```
Parameters:
@@ -982,7 +982,7 @@ Parameters:
Delete Microsoft Teams service for a project.
```
-DELETE /projects/:id/services/microsoft_teams
+DELETE /projects/:id/services/microsoft-teams
```
### Get Microsoft Teams service settings
@@ -990,7 +990,7 @@ DELETE /projects/:id/services/microsoft_teams
Get Microsoft Teams service settings for a project.
```
-GET /projects/:id/services/microsoft_teams
+GET /projects/:id/services/microsoft-teams
```
## Mattermost notifications
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 9c5258c2cdf..98af87455ec 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -129,8 +129,8 @@ You may see a temporary error message `SchedulerPredicates failed due to Persist
Add the GitLab Helm repository and initialize Helm:
```bash
-helm repo add gitlab https://charts.gitlab.io
helm init
+helm repo add gitlab https://charts.gitlab.io
```
Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature
deleted file mode 100644
index 571612261a9..00000000000
--- a/features/project/merge_requests/references.feature
+++ /dev/null
@@ -1,31 +0,0 @@
-@project_merge_requests
-Feature: Project Merge Requests References
- Background:
- Given I sign in as "John Doe"
- And public project "Community"
- And "John Doe" owns public project "Community"
- And project "Community" has "Community fix" open merge request
- And I logout
- And I sign in as "Mary Jane"
- And private project "Enterprise"
- And "Mary Jane" owns private project "Enterprise"
- And project "Enterprise" has "Enterprise issue" open issue
- And project "Enterprise" has "Enterprise fix" open merge request
- And I visit issue page "Enterprise issue"
- And I leave a comment referencing issue "Community fix"
- And I visit merge request page "Enterprise fix"
- And I leave a comment referencing issue "Community fix"
- And I logout
-
- @javascript
- Scenario: Viewing the public issue as a "John Doe"
- Given I sign in as "John Doe"
- When I visit issue page "Community fix"
- Then I should see no notes at all
-
- @javascript
- Scenario: Viewing the public issue as "Mary Jane"
- Given I sign in as "Mary Jane"
- When I visit issue page "Community fix"
- And I should see a note linking to "Enterprise fix" merge request
- And I should see a note linking to "Enterprise issue" issue
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
index 97fac595d8e..27dd391b83d 100644
--- a/features/steps/shared/authentication.rb
+++ b/features/steps/shared/authentication.rb
@@ -22,22 +22,10 @@ module SharedAuthentication
sign_in(@user)
end
- step 'I sign in as "John Doe"' do
- gitlab_sign_in(user_exists("John Doe"))
- end
-
- step 'I sign in as "Mary Jane"' do
- gitlab_sign_in(user_exists("Mary Jane"))
- end
-
step 'I should be redirected to sign in page' do
expect(current_path).to eq new_user_session_path
end
- step "I logout" do
- gitlab_sign_out
- end
-
step "I logout directly" do
gitlab_sign_out
end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index 8d9cd3db9d9..cc6fd48935f 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -5,29 +5,6 @@ module SharedIssuable
find('.js-issuable-edit', visible: true).click
end
- step 'project "Community" has "Community fix" open merge request' do
- create_issuable_for_project(
- project_name: 'Community',
- type: :merge_request,
- title: 'Community fix'
- )
- end
-
- step 'project "Enterprise" has "Enterprise issue" open issue' do
- create_issuable_for_project(
- project_name: 'Enterprise',
- title: 'Enterprise issue'
- )
- end
-
- step 'project "Enterprise" has "Enterprise fix" open merge request' do
- create_issuable_for_project(
- project_name: 'Enterprise',
- type: :merge_request,
- title: 'Enterprise fix'
- )
- end
-
step 'I leave a comment referencing issue "Community issue"' do
leave_reference_comment(
issuable: Issue.find_by(title: 'Community issue'),
@@ -35,44 +12,6 @@ module SharedIssuable
)
end
- step 'I leave a comment referencing issue "Community fix"' do
- leave_reference_comment(
- issuable: MergeRequest.find_by(title: 'Community fix'),
- from_project_name: 'Enterprise'
- )
- end
-
- step 'I visit issue page "Enterprise issue"' do
- issue = Issue.find_by(title: 'Enterprise issue')
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit merge request page "Enterprise fix"' do
- mr = MergeRequest.find_by(title: 'Enterprise fix')
- visit project_merge_request_path(mr.target_project, mr)
- end
-
- step 'I visit issue page "Community fix"' do
- mr = MergeRequest.find_by(title: 'Community fix')
- visit project_merge_request_path(mr.target_project, mr)
- end
-
- step 'I should see a note linking to "Enterprise fix" merge request' do
- visible_note(
- issuable: MergeRequest.find_by(title: 'Enterprise fix'),
- from_project_name: 'Community',
- user_name: 'Mary Jane'
- )
- end
-
- step 'I should see a note linking to "Enterprise issue" issue' do
- visible_note(
- issuable: Issue.find_by(title: 'Enterprise issue'),
- from_project_name: 'Community',
- user_name: 'Mary Jane'
- )
- end
-
step 'I click link "Edit" for the merge request' do
edit_issuable
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index bf1b88c60d7..4a6dee3c7b8 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -18,8 +18,4 @@ module SharedNote
expect(find('.js-md-preview')).to have_content('Nothing to preview.')
end
end
-
- step 'I should see no notes at all' do
- expect(page).not_to have_css('.note')
- end
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index a1945cf5f3d..dbfb90fcc48 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -42,10 +42,6 @@ module SharedProject
# Visibility level
# ----------------------------------------
- step 'private project "Enterprise"' do
- create(:project, :private, :repository, name: 'Enterprise')
- end
-
step 'I should see project "Enterprise"' do
expect(page).to have_content "Enterprise"
end
@@ -70,10 +66,6 @@ module SharedProject
end
end
- step 'public project "Community"' do
- create(:project, :public, :repository, name: 'Community')
- end
-
step 'I should see project "Community"' do
expect(page).to have_content "Community"
end
@@ -89,13 +81,6 @@ module SharedProject
)
end
- step '"Mary Jane" owns private project "Enterprise"' do
- user_owns_project(
- user_name: 'Mary Jane',
- project_name: 'Enterprise'
- )
- end
-
step '"John Doe" owns internal project "Internal"' do
user_owns_project(
user_name: 'John Doe',
@@ -104,14 +89,6 @@ module SharedProject
)
end
- step '"John Doe" owns public project "Community"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Community',
- visibility: :public
- )
- end
-
step 'public empty project "Empty Public Project"' do
create :project_empty_repo, :public, name: "Empty Public Project"
end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index c2113551207..c17089759de 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -45,7 +45,9 @@ module API
user = find_user_from_sources
return unless user
- forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ unless api_access_allowed?(user)
+ forbidden!(api_access_denied_message(user))
+ end
user
end
@@ -72,6 +74,14 @@ module API
end
end
end
+
+ def api_access_allowed?(user)
+ Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ end
+
+ def api_access_denied_message(user)
+ Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
+ end
end
module ClassMethods
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
new file mode 100644
index 00000000000..af310aa12fc
--- /dev/null
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Auth
+ class UserAccessDeniedReason
+ def initialize(user)
+ @user = user
+ end
+
+ def rejection_message
+ case rejection_type
+ when :internal
+ 'This action cannot be performed by internal users'
+ when :terms_not_accepted
+ 'You must accept the Terms of Service in order to perform this action. '\
+ 'Please access GitLab from a web browser to accept these terms.'
+ else
+ 'Your account has been blocked.'
+ end
+ end
+
+ private
+
+ def rejection_type
+ if @user.internal?
+ :internal
+ elsif @user.required_terms_not_accepted?
+ :terms_not_accepted
+ else
+ :blocked
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/build_access.rb b/lib/gitlab/build_access.rb
new file mode 100644
index 00000000000..08a8f846ca5
--- /dev/null
+++ b/lib/gitlab/build_access.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ class BuildAccess < UserAccess
+ attr_accessor :user, :project
+
+ # This bypasses the `can?(:access_git)`-check we normally do in `UserAccess`
+ # for CI. That way if a user was able to trigger a pipeline, then the
+ # build is allowed to clone the project.
+ def can_access_git?
+ true
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 5d47f8b2075..29a3a35812c 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -579,11 +579,6 @@ module Gitlab
count_commits(from: from, to: to, **options)
end
- # Counts the amount of commits between `from` and `to`.
- def count_commits_between(from, to, options = {})
- count_commits(from: from, to: to, **options)
- end
-
# old_rev and new_rev are commit ID's
# the result of this method is an array of Gitlab::Git::RawDiffChange
def raw_changes_between(old_rev, new_rev)
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 84a26fe4a6f..d75a5f15c29 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -67,7 +67,8 @@ module Gitlab
end
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_find_page,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0d1ee73ca1a..db7c29be94b 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,8 +2,6 @@
# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
- include Gitlab::Utils::StrongMemoize
-
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
@@ -17,7 +15,6 @@ module Gitlab
deploy_key_upload: 'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.',
- account_blocked: 'Your account has been blocked.',
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
@@ -108,8 +105,11 @@ module Gitlab
end
def check_active_user!
- if user && !user_access.allowed?
- raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
+ return unless user
+
+ unless user_access.allowed?
+ message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
+ raise UnauthorizedError, message
end
end
@@ -340,6 +340,8 @@ module Gitlab
def user_access
@user_access ||= if ci?
CiAccess.new
+ elsif user && request_from_ci_build?
+ BuildAccess.new(user, project: project)
else
UserAccess.new(user, project: project)
end
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 8668caf0c55..9a576e463e3 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -5,6 +5,14 @@ module Gitlab
# directly.
class StorageSettings
DirectPathAccessError = Class.new(StandardError)
+ InvalidConfigurationError = Class.new(StandardError)
+
+ INVALID_STORAGE_MESSAGE = <<~MSG.freeze
+ Storage is invalid because it has no `path` key.
+
+ For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.
+ If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.
+ MSG
# This class will give easily recognizable NoMethodErrors
Deprecated = Class.new
@@ -12,7 +20,8 @@ module Gitlab
attr_reader :legacy_disk_path
def initialize(storage)
- raise "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+ raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+ raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path')
# Support a nil 'path' field because some of the circuit breaker tests use it.
@legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb
index 43921a8c1c0..fd5de73c526 100644
--- a/lib/gitlab/multi_collection_paginator.rb
+++ b/lib/gitlab/multi_collection_paginator.rb
@@ -5,7 +5,7 @@ module Gitlab
def initialize(*collections, per_page: nil)
raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2
- @per_page = per_page || Kaminari.config.default_per_page
+ @per_page = (per_page || Kaminari.config.default_per_page).to_i
@first_collection, @second_collection = collections
end
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 1fa2a19b0af..4888184403c 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -4,7 +4,8 @@ module Gitlab
def self.parse(repo_path)
wiki = false
- project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false)
+ project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '')
+
project, was_redirected = find_project(project_path)
if project_path.end_with?('.wiki') && project.nil?
@@ -17,22 +18,6 @@ module Gitlab
[project, wiki, redirected_path]
end
- def self.strip_storage_path(repo_path, fail_on_not_found: true)
- result = repo_path
-
- storage = Gitlab.config.repositories.storages.values.find do |params|
- repo_path.start_with?(params.legacy_disk_path)
- end
-
- if storage
- result = result.sub(storage.legacy_disk_path, '')
- elsif fail_on_not_found
- raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
- end
-
- result.sub(%r{\A/*}, '')
- end
-
def self.find_project(project_path)
project = Project.find_by_full_path(project_path, follow_redirects: true)
was_redirected = project && project.full_path.casecmp(project_path) != 0
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 2281cb420d9..a08fcea27a5 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -490,43 +490,43 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
id: job.id
end
- context 'when job has a trace artifact' do
+ context "when job has a trace artifact" do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq job.job_artifacts_trace.open.read
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq(job.job_artifacts_trace.open.read)
end
end
- context 'when job has a trace file' do
+ context "when job has a trace file" do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
- it 'send a trace file' do
+ it "send a trace file" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq 'BUILD TRACE'
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq("BUILD TRACE")
end
end
- context 'when job has a trace in database' do
+ context "when job has a trace in database" do
let(:job) { create(:ci_build, pipeline: pipeline) }
before do
- job.update_column(:trace, 'Sample trace')
+ job.update_column(:trace, "Sample trace")
end
- it 'send a trace file' do
+ it "send a trace file" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq 'Sample trace'
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq("Sample trace")
end
end
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
new file mode 100644
index 00000000000..5ddb5894624
--- /dev/null
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Filter members' do
+ let(:user) { create(:user) }
+ let(:user_with_2fa) { create(:user, :two_factor_via_otp) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_owner(user)
+ group.add_master(user_with_2fa)
+
+ sign_in(user)
+ end
+
+ scenario 'shows all members' do
+ visit_members_list
+
+ expect(first_member).to include(user.name)
+ expect(second_member).to include(user_with_2fa.name)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone')
+ end
+
+ scenario 'shows only 2FA members' do
+ visit_members_list(two_factor: 'enabled')
+
+ expect(first_member).to include(user_with_2fa.name)
+ expect(members_list.size).to eq(1)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled')
+ end
+
+ scenario 'shows only non 2FA members' do
+ visit_members_list(two_factor: 'disabled')
+
+ expect(first_member).to include(user.name)
+ expect(members_list.size).to eq(1)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Disabled')
+ end
+
+ def visit_members_list(options = {})
+ visit group_group_members_path(group.to_param, options)
+ end
+
+ def members_list
+ page.all('ul.content-list > li')
+ end
+
+ def first_member
+ members_list.first.text
+ end
+
+ def second_member
+ members_list.last.text
+ end
+end
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index 8af4b157cd8..9613e22bf24 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -10,6 +10,7 @@ describe "Internal references", :js do
let(:public_project_user) { public_project.owner }
let(:public_project) { create(:project, :public, :repository) }
let(:public_project_issue) { create(:issue, project: public_project) }
+ let(:public_project_merge_request) { create(:merge_request, source_project: public_project) }
context "when referencing to open issue" do
context "from private project" do
@@ -77,4 +78,63 @@ describe "Internal references", :js do
end
end
end
+
+ context "when referencing to open merge request" do
+ context "from private project" do
+ context "from issue" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_issue_path(private_project, private_project_issue))
+
+ add_note("##{public_project_merge_request.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it { expect(page).not_to have_css(".note") }
+ end
+ end
+
+ context "from merge request" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_merge_request_path(private_project, private_project_merge_request))
+
+ add_note("##{public_project_merge_request.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it "doesn't show any references" do
+ page.within(".merge-request-details") do
+ expect(page).not_to have_content("#merge-requests .merge-requests-title")
+ end
+ end
+ end
+
+ context "when user has access to private project" do
+ before do
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it "shows references" do
+ expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
+ .and have_content(private_project_user.name)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 90e28483c6c..9c165b17704 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -125,7 +125,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
- find('.js-primary-button').click
+ find('.js-modal-primary-action').click
wait_for_requests
end
@@ -156,7 +156,6 @@ describe 'Pipelines', :js do
context 'when retrying' do
before do
find('.js-pipelines-retry-button').click
- find('.js-primary-button').click
wait_for_requests
end
@@ -256,7 +255,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
- find('.js-primary-button').click
+ find('.js-modal-primary-action').click
end
it 'indicates that pipeline was canceled' do
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 94a2b289e64..6f968a2c590 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -437,5 +437,107 @@ feature 'Login' do
expect(current_path).to eq(root_path)
end
+
+ context 'when 2FA is required for the user' do
+ before do
+ group = create(:group, require_two_factor_authentication: true)
+ group.add_developer(user)
+ end
+
+ context 'when the user did not enable 2FA' do
+ it 'asks to set 2FA before asking to accept the terms' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(profile_two_factor_auth_path)
+
+ fill_in 'pin_code', with: user.reload.current_otp
+
+ click_button 'Register with two-factor app'
+ click_link 'Proceed'
+
+ expect(current_path).to eq(profile_account_path)
+ end
+ end
+
+ context 'when the user already enabled 2FA' do
+ before do
+ user.update!(otp_required_for_login: true,
+ otp_secret: User.generate_otp_secret(32))
+ end
+
+ it 'asks the user to accept the terms' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ fill_in 'user_otp_attempt', with: user.reload.current_otp
+ click_button 'Verify code'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(root_path)
+ end
+ end
+ end
+
+ context 'when the users password is expired' do
+ before do
+ user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
+ end
+
+ it 'asks the user to accept the terms before setting a new password' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(new_profile_password_path)
+
+ fill_in 'user_current_password', with: '12345678'
+ fill_in 'user_password', with: 'new password'
+ fill_in 'user_password_confirmation', with: 'new password'
+ click_button 'Set new password'
+
+ expect(page).to have_content('Password successfully changed')
+ end
+ end
+
+ context 'when the user does not have an email configured' do
+ let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
+
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
+ end
+
+ it 'asks the user to accept the terms before setting an email' do
+ gitlab_sign_in_via('saml', user, 'my-uid')
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(profile_path)
+
+ fill_in 'Email', with: 'hello@world.com'
+
+ click_button 'Update profile settings'
+
+ expect(page).to have_content('Profile was successfully updated')
+ end
+ end
end
end
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index bf6b5fa3d6a..f9469adbfe3 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -81,4 +81,22 @@ describe 'Users > Terms' do
expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed")
end
end
+
+ context 'when the terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'signing out', :js do
+ it 'allows the user to sign out without a response' do
+ visit terms_path
+
+ find('.header-user-dropdown-toggle').click
+ click_link('Sign out')
+
+ expect(page).to have_content('Sign in')
+ expect(page).to have_content('Register')
+ end
+ end
+ end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 1dc307ea922..8d9dc092547 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -42,26 +42,6 @@ describe '6_validations' do
expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
end
end
-
- context 'with incomplete settings' do
- before do
- mock_storages('foo' => {})
- end
-
- it 'throws an error suggesting the user to update its settings' do
- expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
- end
- end
-
- context 'with deprecated settings structure' do
- before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c')
- end
-
- it 'throws an error suggesting the user to update its settings' do
- expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n")
- end
- end
end
describe 'validate_storages_paths' do
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 360b6d4dc15..ff500acd849 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -24,7 +24,7 @@ describe('RepoEditor', () => {
f.active = true;
f.tempFile = true;
vm.$store.state.openFiles.push(f);
- vm.$store.state.entries[f.path] = f;
+ Vue.set(vm.$store.state.entries, f.path, f);
vm.monaco = true;
vm.$mount();
@@ -215,6 +215,30 @@ describe('RepoEditor', () => {
expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
});
+ it('attaches model to merge request editor', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = true;
+ spyOn(vm.editor, 'attachMergeRequestModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('does not attach model to merge request editor when not a MR change', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = false;
+ spyOn(vm.editor, 'attachMergeRequestModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model);
+ });
+
it('adds callback methods', () => {
spyOn(vm.editor, 'onPositionChange').and.callThrough();
diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js
deleted file mode 100644
index e0ea3649646..00000000000
--- a/spec/javascripts/pipelines/async_button_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import Vue from 'vue';
-import asyncButtonComp from '~/pipelines/components/async_button.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipelines Async Button', () => {
- let component;
- let AsyncButtonComponent;
-
- beforeEach(() => {
- AsyncButtonComponent = Vue.extend(asyncButtonComp);
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'repeat',
- cssClass: 'bar',
- pipelineId: 123,
- type: 'explode',
- },
- }).$mount();
- });
-
- it('should render a button', () => {
- expect(component.$el.tagName).toEqual('BUTTON');
- });
-
- it('should render svg icon', () => {
- expect(component.$el.querySelector('svg')).not.toBeNull();
- });
-
- it('should render the provided title', () => {
- expect(component.$el.getAttribute('data-original-title')).toContain('Foo');
- expect(component.$el.getAttribute('aria-label')).toContain('Foo');
- });
-
- it('should render the provided cssClass', () => {
- expect(component.$el.getAttribute('class')).toContain('bar');
- });
-
- describe('With confirm dialog', () => {
- it('should call the service when confimation is positive', () => {
- eventHub.$on('openConfirmationModal', (data) => {
- expect(data.pipelineId).toEqual(123);
- expect(data.type).toEqual('explode');
- });
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'fa fa-foo',
- cssClass: 'bar',
- pipelineId: 123,
- type: 'explode',
- },
- }).$mount();
-
- component.$el.click();
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index de744739e42..05ca4cb9044 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
+import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
@@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => {
describe('actions column', () => {
beforeEach(() => {
- component = buildComponent(pipeline);
+ const withActions = Object.assign({}, pipeline);
+ withActions.flags.cancelable = true;
+ withActions.flags.retryable = true;
+ withActions.cancel_path = '/cancel';
+ withActions.retry_path = '/retry';
+
+ component = buildComponent(withActions);
});
it('should render the provided actions', () => {
- expect(
- component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length,
- ).toEqual(pipeline.details.manual_actions.length);
+ expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull();
+ expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull();
+ });
+
+ it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
+ eventHub.$on('retryPipeline', (endpoint) => {
+ expect(endpoint).toEqual('/retry');
+ });
+
+ component.$el.querySelector('.js-pipelines-retry-button').click();
+ expect(component.isRetrying).toEqual(true);
+ });
+
+ it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
+ eventHub.$on('openConfirmationModal', (data) => {
+ expect(data.endpoint).toEqual('/cancel');
+ expect(data.pipelineId).toEqual(pipeline.id);
+ });
+
+ component.$el.querySelector('.js-pipelines-cancel-button').click();
+ expect(component.isCancelling).toEqual(true);
});
});
});
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
new file mode 100644
index 00000000000..fa209bed74e
--- /dev/null
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UserAccessDeniedReason do
+ include TermsHelper
+ let(:user) { build(:user) }
+
+ let(:reason) { described_class.new(user) }
+
+ describe '#rejection_message' do
+ subject { reason.rejection_message }
+
+ context 'when a user is blocked' do
+ before do
+ user.block!
+ end
+
+ it { is_expected.to match /blocked/ }
+ end
+
+ context 'a user did not accept the enforced terms' do
+ before do
+ enforce_terms
+ end
+
+ it { is_expected.to match /You must accept the Terms of Service/ }
+ end
+
+ context 'when the user is internal' do
+ let(:user) { User.ghost }
+
+ it { is_expected.to match /This action cannot be performed by internal users/ }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/build_access_spec.rb b/spec/lib/gitlab/build_access_spec.rb
new file mode 100644
index 00000000000..08f50bf4fac
--- /dev/null
+++ b/spec/lib/gitlab/build_access_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::BuildAccess do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '#can_do_action' do
+ subject { described_class.new(user, project: project).can_do_action?(:download_code) }
+
+ context 'when the user can do an action on the project but cannot access git' do
+ before do
+ user.block!
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when the user cannot do an action on the project' do
+ it { is_expected.to be(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 6c625596605..317a932d5a6 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::GitAccess do
- set(:user) { create(:user) }
+ include TermsHelper
+
+ let(:user) { create(:user) }
let(:actor) { user }
let(:project) { create(:project, :repository) }
@@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do
end
end
+ context 'terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ shared_examples 'access after accepting terms' do
+ let(:actions) do
+ [-> { pull_access_check },
+ -> { push_access_check }]
+ end
+
+ it 'blocks access when the user did not accept terms', :aggregate_failures do
+ actions.each do |action|
+ expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/)
+ end
+ end
+
+ it 'allows access when the user accepted the terms', :aggregate_failures do
+ accept_terms(user)
+
+ actions.each do |action|
+ expect { action.call }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'as an anonymous user to a public project' do
+ let(:actor) { nil }
+ let(:project) { create(:project, :public, :repository) }
+
+ it { expect { pull_access_check }.not_to raise_error }
+ end
+
+ describe 'as a guest to a public project' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'access after accepting terms' do
+ let(:actions) { [-> { pull_access_check }] }
+ end
+ end
+
+ describe 'as a reporter to the project' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'access after accepting terms' do
+ let(:actions) { [-> { pull_access_check }] }
+ end
+ end
+
+ describe 'as a developer of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'as a master of the project' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'as an owner of the project' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'when a ci build clones the project' do
+ let(:protocol) { 'http' }
+ let(:authentication_abilities) { [:build_download_code] }
+ let(:auth_result_type) { :build }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it "doesn't block http pull" do
+ aggregate_failures do
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+ end
+
private
def raise_unauthorized(message)
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
new file mode 100644
index 00000000000..c89913ec8e9
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::StorageSettings do
+ describe "#initialize" do
+ context 'when the storage contains no path' do
+ it 'raises an error' do
+ expect do
+ described_class.new("foo" => {})
+ end.to raise_error(described_class::InvalidConfigurationError)
+ end
+ end
+
+ context "when the argument isn't a hash" do
+ it 'raises an error' do
+ expect do
+ described_class.new("test")
+ end.to raise_error("expected a Hash, got a String")
+ end
+ end
+
+ context 'when the storage is valid' do
+ it 'raises no error' do
+ expect do
+ described_class.new("path" => Rails.root)
+ end.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index f030f371372..13940713dfc 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do
end
end
- describe '.strip_storage_path' do
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return({
- 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'),
- 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar')
- })
- end
-
- it 'strips the storage path' do
- expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git')
- end
-
- it 'raises NotFoundError if no storage matches the path' do
- expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error(
- described_class::NotFoundError
- )
- end
- end
-
describe '.find_project' do
let(:project) { create(:project) }
let(:redirect) { project.route.create_redirect('foo/bar/baz') }
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index eb59ba7cbe9..e2b212f4f4c 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -200,15 +200,29 @@ describe Ci::Runner do
describe '#assign_to' do
let!(:project) { FactoryBot.create(:project) }
- let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) }
- before do
- shared_runner.assign_to(project)
+ subject { runner.assign_to(project) }
+
+ context 'with shared_runner' do
+ let!(:runner) { FactoryBot.create(:ci_runner, :shared) }
+
+ it 'transitions shared runner to project runner and assigns project' do
+ subject
+ expect(runner).to be_specific
+ expect(runner).to be_project_type
+ expect(runner.projects).to eq([project])
+ expect(runner.only_for?(project)).to be_truthy
+ end
end
- it { expect(shared_runner).to be_specific }
- it { expect(shared_runner.projects).to eq([project]) }
- it { expect(shared_runner.only_for?(project)).to be_truthy }
+ context 'with group runner' do
+ let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) }
+
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(ArgumentError, 'Transitioning a group runner to a project runner is not supported')
+ end
+ end
end
describe '.online' do
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 5f2c723d483..3ef59457c5f 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -55,13 +55,9 @@ describe Clusters::Applications::Runner do
context 'without a runner' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster) }
+ let(:cluster) { create(:cluster, projects: [project]) }
let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) }
- before do
- cluster.projects << project
- end
-
it 'creates a runner' do
expect do
subject
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index add481b8096..ab7f89f9bf4 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -239,17 +239,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_nil }
end
- context 'when kubernetes responds with valid pods' do
+ context 'when kubernetes responds with valid pods and deployments' do
before do
stub_kubeclient_pods
+ stub_kubeclient_deployments
end
- it { is_expected.to eq(pods: [kube_pod]) }
+ it { is_expected.to include(pods: [kube_pod]) }
end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_pods(status: 500)
+ stub_kubeclient_deployments(status: 500)
end
it { expect { subject }.to raise_error(Kubeclient::HttpError) }
@@ -258,9 +260,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_pods(status: 404)
+ stub_kubeclient_deployments(status: 404)
end
- it { is_expected.to eq(pods: []) }
+ it { is_expected.to include(pods: []) }
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3f2eb58f009..ad094b3ed48 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe User do
include ProjectForksHelper
+ include TermsHelper
describe 'modules' do
subject { described_class }
@@ -2728,4 +2729,30 @@ describe User do
.to change { RedirectRoute.where(path: 'foo').count }.by(-1)
end
end
+
+ describe '#required_terms_not_accepted?' do
+ let(:user) { build(:user) }
+ subject { user.required_terms_not_accepted? }
+
+ context "when terms are not enforced" do
+ it { is_expected.to be_falsy }
+ end
+
+ context "when terms are enforced and accepted by the user" do
+ before do
+ enforce_terms
+ accept_terms(user)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context "when terms are enforced but the user has not accepted" do
+ before do
+ enforce_terms
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index ec26810e371..873673b50ef 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -90,4 +90,94 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:update_custom_attribute) }
end
end
+
+ shared_examples 'access allowed when terms accepted' do |ability|
+ it { is_expected.not_to be_allowed(ability) }
+
+ it "allows #{ability} when the user accepted the terms" do
+ accept_terms(current_user)
+
+ is_expected.to be_allowed(ability)
+ end
+ end
+
+ describe 'API access' do
+ context 'regular user' do
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'regular user' do
+ it_behaves_like 'access allowed when terms accepted', :access_api
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'access allowed when terms accepted', :access_api
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+ end
+ end
+
+ describe 'git access' do
+ describe 'regular user' do
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ describe 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ describe 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'regular user' do
+ it_behaves_like 'access allowed when terms accepted', :access_git
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'access allowed when terms accepted', :access_git
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+ end
+ end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 837389451e8..d3ab44c0d7e 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -6,6 +6,7 @@ describe API::Helpers do
include API::APIGuard::HelperMethods
include described_class
include SentryHelper
+ include TermsHelper
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -163,6 +164,23 @@ describe API::Helpers do
expect { current_user }.to raise_error /403/
end
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ end
+
+ it 'returns a 403 when a user has not accepted the terms' do
+ expect { current_user }.to raise_error /You must accept the Terms of Service/
+ end
+
+ it 'sets the current user when the user accepted the terms' do
+ accept_terms(user)
+
+ expect(current_user).to eq(user)
+ end
+ end
+
it "sets current_user" do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 494db30e8e0..2514dab1714 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,6 +1,7 @@
require "spec_helper"
describe 'Git HTTP requests' do
+ include TermsHelper
include GitHttpHelpers
include WorkhorseHelpers
include UserActivitiesHelpers
@@ -824,4 +825,56 @@ describe 'Git HTTP requests' do
end
end
end
+
+ context 'when terms are enforced' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:path) { "#{project.full_path}.git" }
+ let(:env) { { user: user.username, password: user.password } }
+
+ before do
+ project.add_master(user)
+ enforce_terms
+ end
+
+ it 'blocks git access when the user did not accept terms', :aggregate_failures do
+ clone_get(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ download(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ upload(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when the user accepted the terms' do
+ before do
+ accept_terms(user)
+ end
+
+ it 'allows clones' do
+ clone_get(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+
+ context 'from CI' do
+ let(:build) { create(:ci_build, :running) }
+ let(:env) { { user: 'gitlab-ci-token', password: build.token } }
+
+ before do
+ build.update!(user: user, project: project)
+ end
+
+ it_behaves_like 'pulls are allowed'
+ end
+ end
end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index e46b61b6461..683a64504a1 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -9,8 +9,13 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
+ def kube_deployments_response
+ kube_response(kube_deployments_body)
+ end
+
def stub_kubeclient_discover(api_url)
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
@@ -20,6 +25,13 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_deployments(response = nil)
+ stub_kubeclient_discover(service.api_url)
+ deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments"
+
+ WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
+ end
+
def stub_kubeclient_get_secrets(api_url, **options)
WebMock.stub_request(:get, api_url + '/api/v1/secrets')
.to_return(kube_response(kube_v1_secrets_body(options)))
@@ -53,6 +65,18 @@ module KubernetesHelpers
"kind" => "APIResourceList",
"resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
+ ]
+ }
+ end
+
+ def kube_v1beta1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
]
}
@@ -65,14 +89,25 @@ module KubernetesHelpers
}
end
+ def kube_deployments_body
+ {
+ "kind" => "DeploymentList",
+ "items" => [kube_deployment]
+ }
+ end
+
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
- def kube_pod(name: "kube-pod", app: "valid-pod-label")
+ def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil)
{
"metadata" => {
"name" => name,
+ "generate_name" => "generated-name-with-suffix",
"creationTimestamp" => "2016-11-25T19:55:19Z",
- "labels" => { "app" => app }
+ "labels" => {
+ "app" => app,
+ "track" => track
+ }
},
"spec" => {
"containers" => [
@@ -80,7 +115,27 @@ module KubernetesHelpers
{ "name" => "container-1" }
]
},
- "status" => { "phase" => "Running" }
+ "status" => { "phase" => status }
+ }
+ end
+
+ def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
+ {
+ "metadata" => {
+ "name" => name,
+ "generation" => 4,
+ "labels" => {
+ "app" => app,
+ "track" => track
+ }.compact
+ },
+ "spec" => { "replicas" => 3 },
+ "status" => {
+ "observedGeneration" => 4,
+ "replicas" => 3,
+ "updatedReplicas" => 3,
+ "availableReplicas" => 3
+ }
}
end
@@ -101,4 +156,12 @@ module KubernetesHelpers
terminal
end
end
+
+ def kube_deployment_rollout_status
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments(kube_deployment)
+ end
+
+ def empty_deployment_rollout_status
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments()
+ end
end
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 020031af3cb..a00c6e89a1d 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -12,8 +12,10 @@
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
# level, or manually added below.
#
-# If you want to deploy to staging first, or enable canary deploys,
-# uncomment the relevant jobs in the pipeline below.
+# Continuous deployment to production is enabled by default.
+# If you want to deploy to staging first, or enable incremental rollouts,
+# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables.
+# If you want to use canary deployments, uncomment the canary job.
#
# If Auto DevOps fails to detect the proper buildpack, or if you want to
# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
@@ -88,14 +90,6 @@ codequality:
artifacts:
paths: [codeclimate.json]
-license_management:
- image: registry.gitlab.com/gitlab-org/security-products/license-management:latest
- allow_failure: true
- script:
- - license_management
- artifacts:
- paths: [gl-license-report.json]
-
performance:
stage: performance
image: docker:stable
@@ -223,8 +217,8 @@ stop_review:
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
# If you prefer to automatically deploy to staging and
-# only manually promote to production, enable this job by removing the dot (.),
-# and uncomment the `when: manual` line in the `production` job.
+# only manually promote to production, enable this job by setting
+# STAGING_ENABLED.
staging:
stage: staging
@@ -245,13 +239,9 @@ staging:
kubernetes: active
variables:
- $STAGING_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_ENABLED
# Canaries are disabled by default, but if you want them,
-# and know what the downsides are, enable this job by removing the dot (.),
-# and uncomment the `when: manual` line in the `production` job.
+# and know what the downsides are, enable this job by removing the dot (.).
.canary:
stage: canary
@@ -272,11 +262,6 @@ staging:
- master
kubernetes: active
-# This job continuously deploys to production on every push to `master`.
-# To make this a manual process, either because you're enabling `staging`
-# or `canary` deploys, or you simply want more control over when you deploy
-# to production, uncomment the `when: manual` line in the `production` job.
-
.production: &production_template
stage: production
script:
@@ -310,6 +295,7 @@ production:
production_manual:
<<: *production_template
when: manual
+ allow_failure: false
only:
refs:
- master
@@ -345,6 +331,7 @@ rollout 10%:
<<: *rollout_template
variables:
ROLLOUT_PERCENTAGE: 10
+ when: manual
only:
refs:
- master
@@ -379,6 +366,7 @@ rollout 50%:
rollout 100%:
<<: *production_template
when: manual
+ allow_failure: false
only:
refs:
- master
@@ -428,14 +416,6 @@ rollout 100%:
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
}
- function license_management() {
- if echo $GITLAB_FEATURES |grep license_management > /dev/null ; then
- /run.sh .
- else
- echo "License management is not available in your subscription"
- fi
- }
-
function sast() {
case "$CI_SERVER_VERSION" in
*-ee)
@@ -562,12 +542,14 @@ rollout 100%:
replicas=$(get_replicas "$track" "$percentage")
- helm upgrade --reuse-values \
- --wait \
- --set replicaCount="$replicas" \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
+ if [[ -n "$(helm ls -q "^$name$")" ]]; then
+ helm upgrade --reuse-values \
+ --wait \
+ --set replicaCount="$replicas" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+ fi
}
function install_dependencies() {