Commit 1e76f289 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into per-project-pipeline-iid

parents a74184eb 40683268
...@@ -6,7 +6,7 @@ end ...@@ -6,7 +6,7 @@ end
gem_versions = {} gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2' 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['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' gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 --- # --- The end of special code for migrating to Rails 5.0 ---
......
...@@ -4,43 +4,43 @@ GEM ...@@ -4,43 +4,43 @@ GEM
RedCloth (4.3.2) RedCloth (4.3.2)
abstract_type (0.0.7) abstract_type (0.0.7)
ace-rails-ap (4.1.4) ace-rails-ap (4.1.4)
actioncable (5.0.6) actioncable (5.0.7)
actionpack (= 5.0.6) actionpack (= 5.0.7)
nio4r (>= 1.2, < 3.0) nio4r (>= 1.2, < 3.0)
websocket-driver (~> 0.6.1) websocket-driver (~> 0.6.1)
actionmailer (5.0.6) actionmailer (5.0.7)
actionpack (= 5.0.6) actionpack (= 5.0.7)
actionview (= 5.0.6) actionview (= 5.0.7)
activejob (= 5.0.6) activejob (= 5.0.7)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.0.6) actionpack (5.0.7)
actionview (= 5.0.6) actionview (= 5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
rack (~> 2.0) rack (~> 2.0)
rack-test (~> 0.6.3) rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.0.6) actionview (5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.0.6) activejob (5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.0.6) activemodel (5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
activerecord (5.0.6) activerecord (5.0.7)
activemodel (= 5.0.6) activemodel (= 5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
arel (~> 7.0) arel (~> 7.0)
activerecord_sane_schema_dumper (1.0) activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6) rails (>= 5, < 6)
activesupport (5.0.6) activesupport (5.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0) acts-as-taggable-on (5.0.0)
...@@ -62,13 +62,13 @@ GEM ...@@ -62,13 +62,13 @@ GEM
asciidoctor (1.5.6.1) asciidoctor (1.5.6.1)
asciidoctor-plantuml (0.0.8) asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5) asciidoctor (~> 1.5)
asset_sync (2.2.0) asset_sync (2.4.0)
activemodel (>= 4.1.0) activemodel (>= 4.1.0)
fog-core fog-core
mime-types (>= 2.99) mime-types (>= 2.99)
unf unf
ast (2.4.0) ast (2.4.0)
atomic (1.1.100) atomic (1.1.99)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.1) attr_required (1.0.1)
...@@ -144,12 +144,10 @@ GEM ...@@ -144,12 +144,10 @@ GEM
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.3) crass (1.0.4)
creole (0.5.0) creole (0.5.0)
css_parser (1.6.0) css_parser (1.6.0)
addressable addressable
d3_rails (3.5.17)
railties (>= 3.1.0)
daemons (1.2.6) daemons (1.2.6)
database_cleaner (1.5.3) database_cleaner (1.5.3)
debug_inspector (0.0.3) debug_inspector (0.0.3)
...@@ -292,7 +290,7 @@ GEM ...@@ -292,7 +290,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.97.0) gitaly-proto (0.99.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -335,9 +333,8 @@ GEM ...@@ -335,9 +333,8 @@ GEM
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1) gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1) gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.1.0) gon (6.2.0)
actionpack (>= 3.0) actionpack (>= 3.0)
json
multi_json multi_json
request_store (>= 1.0) request_store (>= 1.0)
google-api-client (0.19.8) google-api-client (0.19.8)
...@@ -367,8 +364,8 @@ GEM ...@@ -367,8 +364,8 @@ GEM
rack (>= 1.3.0) rack (>= 1.3.0)
rack-accept rack-accept
virtus (>= 1.0.0) virtus (>= 1.0.0)
grape-entity (0.6.1) grape-entity (0.7.1)
activesupport (>= 5.0.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-route-helpers (2.1.0)
activesupport activesupport
...@@ -420,7 +417,7 @@ GEM ...@@ -420,7 +417,7 @@ GEM
json (~> 1.8) json (~> 1.8)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (0.9.5) i18n (1.0.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ice_nine (0.11.2) ice_nine (0.11.2)
influxdb (0.5.3) influxdb (0.5.3)
...@@ -515,7 +512,7 @@ GEM ...@@ -515,7 +512,7 @@ GEM
net-ldap (0.16.1) net-ldap (0.16.1)
net-ssh (4.2.0) net-ssh (4.2.0)
netrc (0.11.0) netrc (0.11.0)
nio4r (2.2.0) nio4r (2.3.1)
nokogiri (1.8.2) nokogiri (1.8.2)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
numerizer (0.1.1) numerizer (0.1.1)
...@@ -545,9 +542,9 @@ GEM ...@@ -545,9 +542,9 @@ GEM
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2) omniauth-github (1.3.0)
omniauth (~> 1.0) omniauth (~> 1.5)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.3) omniauth-gitlab (1.0.3)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
...@@ -633,7 +630,7 @@ GEM ...@@ -633,7 +630,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.1) prometheus-client-mmap (0.9.2)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
...@@ -644,7 +641,7 @@ GEM ...@@ -644,7 +641,7 @@ GEM
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.2) public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (2.0.4) rack (2.0.5)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
...@@ -662,17 +659,17 @@ GEM ...@@ -662,17 +659,17 @@ GEM
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (5.0.6) rails (5.0.7)
actioncable (= 5.0.6) actioncable (= 5.0.7)
actionmailer (= 5.0.6) actionmailer (= 5.0.7)
actionpack (= 5.0.6) actionpack (= 5.0.7)
actionview (= 5.0.6) actionview (= 5.0.7)
activejob (= 5.0.6) activejob (= 5.0.7)
activemodel (= 5.0.6) activemodel (= 5.0.7)
activerecord (= 5.0.6) activerecord (= 5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.0.6) railties (= 5.0.7)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1) actionpack (~> 5.x, >= 5.0.1)
...@@ -683,21 +680,21 @@ GEM ...@@ -683,21 +680,21 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.4)
loofah (~> 2.0) loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.1) rails-i18n (5.1.1)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
railties (5.0.6) railties (5.0.7)
actionpack (= 5.0.6) actionpack (= 5.0.7)
activesupport (= 5.0.6) activesupport (= 5.0.7)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.19.0) raindrops (0.19.0)
rake (12.3.0) rake (12.3.1)
rb-fsevent (0.10.3) rb-fsevent (0.10.3)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
...@@ -1001,7 +998,7 @@ DEPENDENCIES ...@@ -1001,7 +998,7 @@ DEPENDENCIES
asana (~> 0.6.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.6) asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0) asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
...@@ -1027,12 +1024,11 @@ DEPENDENCIES ...@@ -1027,12 +1024,11 @@ DEPENDENCIES
concurrent-ruby (~> 1.0.5) concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0) connection_pool (~> 2.0)
creole (~> 0.5.0) creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5) default_value_for (~> 3.0.5)
device_detector device_detector
devise (~> 4.2) devise (~> 4.4)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
...@@ -1063,7 +1059,7 @@ DEPENDENCIES ...@@ -1063,7 +1059,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.97.0) gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1071,12 +1067,12 @@ DEPENDENCIES ...@@ -1071,12 +1067,12 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3) gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.1.0) gon (~> 6.2)
google-api-client (~> 0.19.8) google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
...@@ -1117,7 +1113,7 @@ DEPENDENCIES ...@@ -1117,7 +1113,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9) omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4) omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1) omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
...@@ -1136,14 +1132,14 @@ DEPENDENCIES ...@@ -1136,14 +1132,14 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.1) prometheus-client-mmap (~> 0.9.2)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0) rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 5.0.6) rails (= 5.0.7)
rails-controller-testing rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
......
...@@ -140,7 +140,7 @@ export default { ...@@ -140,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, 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); this.editor.attachMergeRequestModel(this.model);
} else { } else {
this.editor.attachModel(this.model); this.editor.attachModel(this.model);
......
...@@ -44,6 +44,7 @@ export const dataStructure = () => ({ ...@@ -44,6 +44,7 @@ export const dataStructure = () => ({
size: 0, size: 0,
parentPath: null, parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
mrChange: null,
}); });
export const decorateData = entity => { export const decorateData = entity => {
......
<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>
<script> <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 { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
/** /**
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
*/ */
export default { export default {
components: { components: {
pipelinesTableRowComponent, PipelinesTableRowComponent,
DeprecatedModal, Modal,
}, },
props: { props: {
pipelines: { pipelines: {
...@@ -37,31 +37,19 @@ ...@@ -37,31 +37,19 @@
return { return {
pipelineId: '', pipelineId: '',
endpoint: '', endpoint: '',
type: '',
}; };
}, },
computed: { computed: {
modalTitle() { modalTitle() {
return this.type === 'stop' ? return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { pipelineId: `${this.pipelineId}`,
pipelineId: `'${this.pipelineId}'`,
}, false) :
sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false); }, false);
}, },
modalText() { modalText() {
return this.type === 'stop' ? return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
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>`, pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false); }, false);
}, },
primaryButtonLabel() {
return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
},
}, },
created() { created() {
eventHub.$on('openConfirmationModal', this.setModalData); eventHub.$on('openConfirmationModal', this.setModalData);
...@@ -73,7 +61,6 @@ ...@@ -73,7 +61,6 @@
setModalData(data) { setModalData(data) {
this.pipelineId = data.pipelineId; this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint; this.endpoint = data.endpoint;
this.type = data.type;
}, },
onSubmit() { onSubmit() {
eventHub.$emit('postAction', this.endpoint); eventHub.$emit('postAction', this.endpoint);
...@@ -120,20 +107,16 @@ ...@@ -120,20 +107,16 @@
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
/> />
<deprecated-modal
<modal
id="confirmation-modal" id="confirmation-modal"
:title="modalTitle" :header-title-text="modalTitle"
:text="modalText" footer-primary-button-variant="danger"
kind="danger" :footer-primary-button-text="s__('Pipeline|Stop pipeline')"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit" @submit="onSubmit"
> >
<template <span v-html="modalText"></span>
slot="body" </modal>
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</deprecated-modal>
</div> </div>
</template> </template>
<script> <script>
/* eslint-disable no-param-reassign */ import eventHub from '../event_hub';
import asyncButtonComponent from './async_button.vue'; import PipelinesActionsComponent from './pipelines_actions.vue';
import pipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; import PipelineStage from './stage.vue';
import pipelineStage from './stage.vue'; import PipelineUrl from './pipeline_url.vue';
import pipelineUrl from './pipeline_url.vue'; import PipelinesTimeago from './time_ago.vue';
import pipelinesTimeago from './time_ago.vue'; import CommitComponent from '../../vue_shared/components/commit.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. * Pipeline table row.
...@@ -16,14 +17,15 @@ ...@@ -16,14 +17,15 @@
*/ */
export default { export default {
components: { components: {
asyncButtonComponent, PipelinesActionsComponent,
pipelinesActionsComponent, PipelinesArtifactsComponent,
pipelinesArtifactsComponent, CommitComponent,
commitComponent, PipelineStage,
pipelineStage, PipelineUrl,
pipelineUrl, CiBadge,
ciBadge, PipelinesTimeago,
pipelinesTimeago, LoadingButton,
Icon,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -44,6 +46,12 @@ ...@@ -44,6 +46,12 @@
required: true, required: true,
}, },
}, },
data() {
return {
isRetrying: false,
isCancelling: false,
};
},
computed: { computed: {
/** /**
* If provided, returns the commit tag. * If provided, returns the commit tag.
...@@ -119,8 +127,10 @@ ...@@ -119,8 +127,10 @@
if (this.pipeline.ref) { if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') { if (prop === 'path') {
// eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop]; accumulator.ref_url = this.pipeline.ref[prop];
} else { } else {
// eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop]; accumulator[prop] = this.pipeline.ref[prop];
} }
return accumulator; return accumulator;
...@@ -216,6 +226,21 @@ ...@@ -216,6 +226,21 @@
return this.viewType === 'child'; 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> </script>
<template> <template>
...@@ -287,7 +312,8 @@ ...@@ -287,7 +312,8 @@
<div <div
v-if="displayPipelineActions" 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"> <div class="btn-group table-action-buttons">
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
...@@ -300,29 +326,27 @@ ...@@ -300,29 +326,27 @@
:artifacts="pipeline.details.artifacts" :artifacts="pipeline.details.artifacts"
/> />
<async-button-component <loading-button
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
:endpoint="pipeline.retry_path" @click="handleRetryClick"
css-class="js-pipelines-retry-button btn-default btn-retry" container-class="js-pipelines-retry-button btn btn-default btn-retry"
title="Retry" :loading="isRetrying"
icon="repeat" :disabled="isRetrying"
:pipeline-id="pipeline.id" >
data-toggle="modal" <icon name="repeat" />
data-target="#confirmation-modal" </loading-button>
type="retry"
/>
<async-button-component <loading-button
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:endpoint="pipeline.cancel_path" @click="handleCancelClick"
css-class="js-pipelines-cancel-button btn-remove"
title="Stop"
icon="close"
:pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
data-target="#confirmation-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> </div>
</div> </div>
......
...@@ -53,10 +53,12 @@ export default { ...@@ -53,10 +53,12 @@ export default {
}); });
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable); eventHub.$on('clickedDropdown', this.updateTable);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('postAction', this.postAction); eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable); eventHub.$off('clickedDropdown', this.updateTable);
}, },
destroyed() { destroyed() {
......
...@@ -111,7 +111,13 @@ ...@@ -111,7 +111,13 @@
</td> </td>
<td> <td>
<span
v-tooltip
:title="tooltipTitle(item.createdAt)"
data-placement="bottom"
>
{{ timeFormated(item.createdAt) }} {{ timeFormated(item.createdAt) }}
</span>
</td> </td>
<td class="content"> <td class="content">
......
...@@ -15,7 +15,7 @@ export default class UserCallout { ...@@ -15,7 +15,7 @@ export default class UserCallout {
init() { init() {
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { 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 { ...@@ -23,12 +23,15 @@ export default class UserCallout {
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) { 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 { } else {
Cookies.set(this.cookieName, 'true', { expires: 365 }); Cookies.set(this.cookieName, 'true', { expires: 365 });
} }
if ($currentTarget.hasClass('close')) { if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
} }
} }
......
...@@ -109,12 +109,12 @@ export default { ...@@ -109,12 +109,12 @@ export default {
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-url" class="deploy-link js-deploy-url"
> >
{{ deployment.external_url_formatted }}
<i <i
class="fa fa-external-link" class="fa fa-external-link"
aria-hidden="true" aria-hidden="true"
> >
</i> </i>
{{ deployment.external_url_formatted }}
</a> </a>
</template> </template>
<span <span
......
...@@ -70,12 +70,14 @@ ...@@ -70,12 +70,14 @@
/> />
</transition> </transition>
<transition name="fade"> <transition name="fade">
<slot>
<span <span
v-if="label" v-if="label"
class="js-loading-button-label" class="js-loading-button-label"
> >
{{ label }} {{ label }}
</span> </span>
</slot>
</transition> </transition>
</button> </button>
</template> </template>
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
line-height: $line-height-base;
.title { .title {
display: flex; display: flex;
...@@ -33,10 +34,14 @@ ...@@ -33,10 +34,14 @@
.navbar-collapse { .navbar-collapse {
padding-right: 0; padding-right: 0;
.navbar-nav {
margin: 0;
}
} }
.nav li a { .nav li {
color: $theme-gray-700; float: none;
} }
} }
......
...@@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base ...@@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user! before_action :authenticate_sessionless_user!
before_action :authenticate_user! before_action :authenticate_user!
before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms }, before_action :enforce_terms!, if: :should_enforce_terms?
unless: :peek_request?
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
before_action :ldap_security_check before_action :ldap_security_check
...@@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base ...@@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base
def peek_request? def peek_request?
request.path.start_with?('/-/peek') request.path.start_with?('/-/peek')
end end
def should_enforce_terms?
return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
!(peek_request? || devise_controller?)
end
end end
...@@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override :override
def index def index
can_manage_members = can?(current_user, :admin_group_member, @group)
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = GroupMembersFinder.new(@group).execute @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.search(params[:search]) if params[:search].present?
@members = @members.sort_by_attribute(@sort) @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 = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user)) @members = present_members(@members.includes(:user))
......
...@@ -3,6 +3,10 @@ module Users ...@@ -3,6 +3,10 @@ module Users
include InternalRedirect include InternalRedirect
skip_before_action :enforce_terms! 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 before_action :terms
layout 'terms' layout 'terms'
......
...@@ -42,22 +42,11 @@ module UsersHelper ...@@ -42,22 +42,11 @@ module UsersHelper
items << :sign_out if current_user items << :sign_out if current_user
# TODO: Remove these conditions when the permissions are prevented in return items if current_user&.required_terms_not_accepted?
# 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?
items << :help if required_terms_accepted items << :help
items << :profile if can?(current_user, :read_user, current_user)
if can?(current_user, :read_user, current_user) && required_terms_accepted items << :settings if can?(current_user, :update_user, current_user)
items << :profile
end
if can?(current_user, :update_user, current_user) && required_terms_accepted
items << :settings
end
items items
end end
......
...@@ -108,7 +108,13 @@ module Ci ...@@ -108,7 +108,13 @@ module Ci
end end
def assign_to(project, current_user = nil) def assign_to(project, current_user = nil)
if shared?
self.is_shared = false 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 self.save
project.runner_projects.create(runner_id: self.id) project.runner_projects.create(runner_id: self.id)
end end
......
...@@ -4,7 +4,9 @@ module Routable ...@@ -4,7 +4,9 @@ module Routable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do 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 has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :route, presence: true validates :route, presence: true
......
...@@ -96,6 +96,17 @@ class Member < ActiveRecord::Base ...@@ -96,6 +96,17 @@ class Member < ActiveRecord::Base
joins(:user).merge(User.search(query)) joins(:user).merge(User.search(query))
end 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) def sort_by_attribute(method)
case method.to_s case method.to_s
when 'access_level_asc' then reorder(access_level: :asc) when 'access_level_asc' then reorder(access_level: :asc)
......
...@@ -237,14 +237,18 @@ class User < ActiveRecord::Base ...@@ -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_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')) } 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") 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 end
def self.without_two_factor def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") 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 end
# #
...@@ -1193,6 +1197,11 @@ class User < ActiveRecord::Base ...@@ -1193,6 +1197,11 @@ class User < ActiveRecord::Base
accepted_term_id.present? accepted_term_id.present?
end end
def required_terms_not_accepted?
Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
!terms_accepted?
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
class GlobalPolicy < BasePolicy class GlobalPolicy < BasePolicy
desc "User is blocked" desc "User is blocked"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:blocked) { @user.blocked? } condition(:blocked) { @user&.blocked? }
desc "User is an internal user" desc "User is an internal user"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:internal) { @user.internal? } condition(:internal) { @user&.internal? }
desc "User's access has been locked" desc "User's access has been locked"
with_options scope: :user, score: 0 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 rule { anonymous }.policy do
prevent :log_in prevent :log_in
prevent :access_api
prevent :access_git
prevent :receive_notifications prevent :receive_notifications
prevent :use_quick_actions prevent :use_quick_actions
prevent :create_group prevent :create_group
...@@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy ...@@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy
prevent :use_quick_actions prevent :use_quick_actions
end end
rule { required_terms_not_accepted }.policy do
prevent :access_api
prevent :access_git
end
rule { can_create_group }.policy do rule { can_create_group }.policy do
enable :create_group enable :create_group
end end
......
- page_title "Members" - page_title "Members"
- can_manage_members = can?(current_user, :admin_group_member, @group)
.project-members-page.prepend-top-default .project-members-page.prepend-top-default
%h4 %h4
Members Members
%hr %hr
- if can?(current_user, :admin_group_member, @group) - if can_manage_members
.project-members-new.append-bottom-default .project-members-new.append-bottom-default
%p.clearfix %p.clearfix
Add new member to Add new member to
...@@ -13,20 +14,23 @@ ...@@ -13,20 +14,23 @@
= render 'shared/members/requests', membership_source: @group, requesters: @requesters = render 'shared/members/requests', membership_source: @group, requesters: @requesters
.append-bottom-default.clearfix .clearfix
%h5.member.existing-title %h5.member.existing-title
Existing members Existing members
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .panel.panel-default
.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 .form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } = 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" } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search") = icon("search")
- if can_manage_members
= render 'shared/members/filter_2fa_dropdown'
= render 'shared/members/sort_dropdown' = render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
Members with access to
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list.members-list %ul.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member = render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab' = paginate @members, theme: 'gitlab'
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .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') = icon('rss')
%span.icon-label %span.icon-label
Subscribe Subscribe
......
...@@ -20,10 +20,10 @@ ...@@ -20,10 +20,10 @@
= brand_header_logo = brand_header_logo
- logo_text = brand_header_logo_type - logo_text = brand_header_logo_type
- if logo_text.present? - if logo_text.present?
%span.logo-text.hidden-xs.prepend-left-8 %span.logo-text.prepend-left-8
= logo_text = logo_text
- if header_link?(:user_dropdown) - if header_link?(:user_dropdown)
.navbar-collapse.collapse .navbar-collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
......
- 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
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
%label.label.label-danger %label.label.label-danger
%strong Blocked %strong Blocked
- if user.two_factor_enabled?
%label.label.label-info
2FA
- if source.instance_of?(Group) && source != @group - if source.instance_of?(Group) && source != @group
&middot; &middot;
= link_to source.full_name, source, class: "member-group-link" = link_to source.full_name, source, class: "member-group-link"
......
---
title: Moves MR widget external link icon to the right
merge_request: 18828
author: Jacopo Beschi @jacopo-beschi
type: changed
---
title: Remove modalbox confirmation when retrying a pipeline
merge_request: 18879
author:
type: changed
---
title: 46210 Display logo and user dropdown on mobile for terms page and fix styling
merge_request:
author:
type: fixed
---
title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog'
merge_request: 18794
author: '@blackst0ne'
type: other
---
title: Block access to the API & git for users that did not accept enforced Terms
of Service
merge_request: 18816
author:
type: other
---
title: Expand documentation for Runners API
merge_request: 16484
author:
type: other
---
title: Add 2FA filter to the group members page
merge_request: 18483
author:
type: changed
---
title: 'Add missing tooltip to creation date on container registry overview'
merge_request: 18767
author: Lars Greiss
type: fixed
---
title: Finding a wiki page is done by Gitaly by default
merge_request:
author:
type: other
...@@ -26,17 +26,6 @@ def validate_storages_config ...@@ -26,17 +26,6 @@ def validate_storages_config
Gitlab.config.repositories.storages.each do |name, repository_storage| Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) 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| %w(failure_count_threshold failure_reset_time storage_timeout).each do |setting|
# Falling back to the defaults is fine! # Falling back to the defaults is fine!
next if repository_storage[setting].nil? next if repository_storage[setting].nil?
......
...@@ -411,3 +411,86 @@ DELETE /projects/:id/runners/:runner_id ...@@ -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" 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 |
...@@ -968,7 +968,7 @@ Group Chat Software ...@@ -968,7 +968,7 @@ Group Chat Software
Set Microsoft Teams service for a project. Set Microsoft Teams service for a project.
``` ```
PUT /projects/:id/services/microsoft_teams PUT /projects/:id/services/microsoft-teams
``` ```
Parameters: Parameters:
...@@ -982,7 +982,7 @@ Parameters: ...@@ -982,7 +982,7 @@ Parameters:
Delete Microsoft Teams service for a project. Delete Microsoft Teams service for a project.
``` ```
DELETE /projects/:id/services/microsoft_teams DELETE /projects/:id/services/microsoft-teams
``` ```
### Get Microsoft Teams service settings ### Get Microsoft Teams service settings
...@@ -990,7 +990,7 @@ DELETE /projects/:id/services/microsoft_teams ...@@ -990,7 +990,7 @@ DELETE /projects/:id/services/microsoft_teams
Get Microsoft Teams service settings for a project. Get Microsoft Teams service settings for a project.
``` ```
GET /projects/:id/services/microsoft_teams GET /projects/:id/services/microsoft-teams
``` ```
## Mattermost notifications ## Mattermost notifications
......
...@@ -129,8 +129,8 @@ You may see a temporary error message `SchedulerPredicates failed due to Persist ...@@ -129,8 +129,8 @@ You may see a temporary error message `SchedulerPredicates failed due to Persist
Add the GitLab Helm repository and initialize Helm: Add the GitLab Helm repository and initialize Helm:
```bash ```bash
helm repo add gitlab https://charts.gitlab.io
helm init 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. 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.
......
@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
...@@ -22,22 +22,10 @@ module SharedAuthentication ...@@ -22,22 +22,10 @@ module SharedAuthentication
sign_in(@user) sign_in(@user)
end 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 step 'I should be redirected to sign in page' do
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
end end
step "I logout" do
gitlab_sign_out
end
step "I logout directly" do step "I logout directly" do
gitlab_sign_out gitlab_sign_out
end end
......
...@@ -5,29 +5,6 @@ module SharedIssuable ...@@ -5,29 +5,6 @@ module SharedIssuable
find('.js-issuable-edit', visible: true).click find('.js-issuable-edit', visible: true).click
end 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 step 'I leave a comment referencing issue "Community issue"' do
leave_reference_comment( leave_reference_comment(
issuable: Issue.find_by(title: 'Community issue'), issuable: Issue.find_by(title: 'Community issue'),
...@@ -35,44 +12,6 @@ module SharedIssuable ...@@ -35,44 +12,6 @@ module SharedIssuable
) )
end 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 step 'I click link "Edit" for the merge request' do
edit_issuable edit_issuable
end end
......
...@@ -18,8 +18,4 @@ module SharedNote ...@@ -18,8 +18,4 @@ module SharedNote
expect(find('.js-md-preview')).to have_content('Nothing to preview.') expect(find('.js-md-preview')).to have_content('Nothing to preview.')
end end
end end
step 'I should see no notes at all' do
expect(page).not_to have_css('.note')
end
end end
...@@ -42,10 +42,6 @@ module SharedProject ...@@ -42,10 +42,6 @@ module SharedProject
# Visibility level # Visibility level
# ---------------------------------------- # ----------------------------------------
step 'private project "Enterprise"' do
create(:project, :private, :repository, name: 'Enterprise')
end
step 'I should see project "Enterprise"' do step 'I should see project "Enterprise"' do
expect(page).to have_content "Enterprise" expect(page).to have_content "Enterprise"
end end
...@@ -70,10 +66,6 @@ module SharedProject ...@@ -70,10 +66,6 @@ module SharedProject
end end
end end
step 'public project "Community"' do
create(:project, :public, :repository, name: 'Community')
end
step 'I should see project "Community"' do step 'I should see project "Community"' do
expect(page).to have_content "Community" expect(page).to have_content "Community"
end end
...@@ -89,13 +81,6 @@ module SharedProject ...@@ -89,13 +81,6 @@ module SharedProject
) )
end 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 step '"John Doe" owns internal project "Internal"' do
user_owns_project( user_owns_project(
user_name: 'John Doe', user_name: 'John Doe',
...@@ -104,14 +89,6 @@ module SharedProject ...@@ -104,14 +89,6 @@ module SharedProject
) )
end 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 step 'public empty project "Empty Public Project"' do
create :project_empty_repo, :public, name: "Empty Public Project" create :project_empty_repo, :public, name: "Empty Public Project"
end end
......
...@@ -45,7 +45,9 @@ module API ...@@ -45,7 +45,9 @@ module API
user = find_user_from_sources user = find_user_from_sources
return unless user 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 user
end end
...@@ -72,6 +74,14 @@ module API ...@@ -72,6 +74,14 @@ module API
end end
end 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 end
module ClassMethods module ClassMethods
......
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
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
...@@ -579,11 +579,6 @@ module Gitlab ...@@ -579,11 +579,6 @@ module Gitlab
count_commits(from: from, to: to, **options) count_commits(from: from, to: to, **options)
end 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 # old_rev and new_rev are commit ID's
# the result of this method is an array of Gitlab::Git::RawDiffChange # the result of this method is an array of Gitlab::Git::RawDiffChange
def raw_changes_between(old_rev, new_rev) def raw_changes_between(old_rev, new_rev)
......
...@@ -67,7 +67,8 @@ module Gitlab ...@@ -67,7 +67,8 @@ module Gitlab
end end
def page(title:, version: nil, dir: nil) 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 if is_enabled
gitaly_find_page(title: title, version: version, dir: dir) gitaly_find_page(title: title, version: version, dir: dir)
else else
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
# class return an instance of `GitlabAccessStatus` # class return an instance of `GitlabAccessStatus`
module Gitlab module Gitlab
class GitAccess class GitAccess
include Gitlab::Utils::StrongMemoize
UnauthorizedError = Class.new(StandardError) UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError) NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError)
...@@ -17,7 +15,6 @@ module Gitlab ...@@ -17,7 +15,6 @@ module Gitlab
deploy_key_upload: 'This deploy key does not have write access to this project.', 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.', no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.', 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.", command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP 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.', receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
...@@ -108,8 +105,11 @@ module Gitlab ...@@ -108,8 +105,11 @@ module Gitlab
end end
def check_active_user! def check_active_user!
if user && !user_access.allowed? return unless user
raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
unless user_access.allowed?
message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
raise UnauthorizedError, message
end end
end end
...@@ -340,6 +340,8 @@ module Gitlab ...@@ -340,6 +340,8 @@ module Gitlab
def user_access def user_access
@user_access ||= if ci? @user_access ||= if ci?
CiAccess.new CiAccess.new
elsif user && request_from_ci_build?
BuildAccess.new(user, project: project)
else else
UserAccess.new(user, project: project) UserAccess.new(user, project: project)
end end
......
...@@ -5,6 +5,14 @@ module Gitlab ...@@ -5,6 +5,14 @@ module Gitlab
# directly. # directly.
class StorageSettings class StorageSettings
DirectPathAccessError = Class.new(StandardError) 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 # This class will give easily recognizable NoMethodErrors
Deprecated = Class.new Deprecated = Class.new
...@@ -12,7 +20,8 @@ module Gitlab ...@@ -12,7 +20,8 @@ module Gitlab
attr_reader :legacy_disk_path attr_reader :legacy_disk_path
def initialize(storage) 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. # 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'] @legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
def initialize(*collections, per_page: nil) def initialize(*collections, per_page: nil)
raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2 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 @first_collection, @second_collection = collections
end end
......
...@@ -4,7 +4,8 @@ module Gitlab ...@@ -4,7 +4,8 @@ module Gitlab
def self.parse(repo_path) def self.parse(repo_path)
wiki = false 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) project, was_redirected = find_project(project_path)
if project_path.end_with?('.wiki') && project.nil? if project_path.end_with?('.wiki') && project.nil?
...@@ -17,22 +18,6 @@ module Gitlab ...@@ -17,22 +18,6 @@ module Gitlab
[project, wiki, redirected_path] [project, wiki, redirected_path]
end 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) def self.find_project(project_path)
project = Project.find_by_full_path(project_path, follow_redirects: true) project = Project.find_by_full_path(project_path, follow_redirects: true)
was_redirected = project && project.full_path.casecmp(project_path) != 0 was_redirected = project && project.full_path.casecmp(project_path) != 0
......
...@@ -490,43 +490,43 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -490,43 +490,43 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
id: job.id id: job.id
end 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) } let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do it 'returns a trace' do
response = subject response = subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8' expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq job.job_artifacts_trace.open.read expect(response.body).to eq(job.job_artifacts_trace.open.read)
end end
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) } let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'send a trace file' do it "send a trace file" do
response = subject response = subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8' expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq 'BUILD TRACE' expect(response.body).to eq("BUILD TRACE")
end end
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) } let(:job) { create(:ci_build, pipeline: pipeline) }
before do before do
job.update_column(:trace, 'Sample trace') job.update_column(:trace, "Sample trace")
end end
it 'send a trace file' do it "send a trace file" do
response = subject response = subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8' expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
expect(response.body).to eq 'Sample trace' expect(response.body).to eq("Sample trace")
end end
end end
......
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
...@@ -10,6 +10,7 @@ describe "Internal references", :js do ...@@ -10,6 +10,7 @@ describe "Internal references", :js do
let(:public_project_user) { public_project.owner } let(:public_project_user) { public_project.owner }
let(:public_project) { create(:project, :public, :repository) } let(:public_project) { create(:project, :public, :repository) }
let(:public_project_issue) { create(:issue, project: public_project) } 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 "when referencing to open issue" do
context "from private project" do context "from private project" do
...@@ -77,4 +78,63 @@ describe "Internal references", :js do ...@@ -77,4 +78,63 @@ describe "Internal references", :js do
end end
end 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 end
...@@ -125,7 +125,7 @@ describe 'Pipelines', :js do ...@@ -125,7 +125,7 @@ describe 'Pipelines', :js do
context 'when canceling' do context 'when canceling' do
before do before do
find('.js-pipelines-cancel-button').click find('.js-pipelines-cancel-button').click
find('.js-primary-button').click find('.js-modal-primary-action').click
wait_for_requests wait_for_requests
end end
...@@ -156,7 +156,6 @@ describe 'Pipelines', :js do ...@@ -156,7 +156,6 @@ describe 'Pipelines', :js do
context 'when retrying' do context 'when retrying' do
before do before do
find('.js-pipelines-retry-button').click find('.js-pipelines-retry-button').click
find('.js-primary-button').click
wait_for_requests wait_for_requests
end end
...@@ -256,7 +255,7 @@ describe 'Pipelines', :js do ...@@ -256,7 +255,7 @@ describe 'Pipelines', :js do
context 'when canceling' do context 'when canceling' do
before do before do
find('.js-pipelines-cancel-button').click find('.js-pipelines-cancel-button').click
find('.js-primary-button').click find('.js-modal-primary-action').click
end end
it 'indicates that pipeline was canceled' do it 'indicates that pipeline was canceled' do
......
...@@ -437,5 +437,107 @@ feature 'Login' do ...@@ -437,5 +437,107 @@ feature 'Login' do
expect(current_path).to eq(root_path) expect(current_path).to eq(root_path)
end 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
end end
...@@ -81,4 +81,22 @@ describe 'Users > Terms' do ...@@ -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") expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed")
end end
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 end
...@@ -42,26 +42,6 @@ describe '6_validations' do ...@@ -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.') 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
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 end
describe 'validate_storages_paths' do describe 'validate_storages_paths' do
......
...@@ -24,7 +24,7 @@ describe('RepoEditor', () => { ...@@ -24,7 +24,7 @@ describe('RepoEditor', () => {
f.active = true; f.active = true;
f.tempFile = true; f.tempFile = true;
vm.$store.state.openFiles.push(f); 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.monaco = true;
vm.$mount(); vm.$mount();
...@@ -215,6 +215,30 @@ describe('RepoEditor', () => { ...@@ -215,6 +215,30 @@ describe('RepoEditor', () => {
expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model); 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', () => { it('adds callback methods', () => {
spyOn(vm.editor, 'onPositionChange').and.callThrough(); spyOn(vm.editor, 'onPositionChange').and.callThrough();
......
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();
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => { describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
...@@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => { ...@@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => {
describe('actions column', () => { describe('actions column', () => {
beforeEach(() => { 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', () => { it('should render the provided actions', () => {
expect( expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull();
component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length, expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull();
).toEqual(pipeline.details.manual_actions.length); });
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);
}); });
}); });
}); });
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
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
require 'spec_helper' require 'spec_helper'
describe Gitlab::GitAccess do describe Gitlab::GitAccess do
set(:user) { create(:user) } include TermsHelper
let(:user) { create(:user) }
let(:actor) { user } let(:actor) { user }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
...@@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do ...@@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do
end end
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 private
def raise_unauthorized(message) def raise_unauthorized(message)
......
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
...@@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do ...@@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do
end end
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 describe '.find_project' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:redirect) { project.route.create_redirect('foo/bar/baz') } let(:redirect) { project.route.create_redirect('foo/bar/baz') }
......
...@@ -200,15 +200,29 @@ describe Ci::Runner do ...@@ -200,15 +200,29 @@ describe Ci::Runner do
describe '#assign_to' do describe '#assign_to' do
let!(:project) { FactoryBot.create(:project) } let!(:project) { FactoryBot.create(:project) }
let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) }
before do subject { runner.assign_to(project) }
shared_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
end
context 'with group runner' do
let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) }
it { expect(shared_runner).to be_specific } it 'raises an error' do
it { expect(shared_runner.projects).to eq([project]) } expect { subject }
it { expect(shared_runner.only_for?(project)).to be_truthy } .to raise_error(ArgumentError, 'Transitioning a group runner to a project runner is not supported')
end
end
end end
describe '.online' do describe '.online' do
......
...@@ -55,13 +55,9 @@ describe Clusters::Applications::Runner do ...@@ -55,13 +55,9 @@ describe Clusters::Applications::Runner do
context 'without a runner' do context 'without a runner' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:cluster) { create(:cluster) } let(:cluster) { create(:cluster, projects: [project]) }
let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) } let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) }
before do
cluster.projects << project
end
it 'creates a runner' do it 'creates a runner' do
expect do expect do
subject subject
......
...@@ -239,17 +239,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -239,17 +239,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
context 'when kubernetes responds with valid pods' do context 'when kubernetes responds with valid pods and deployments' do
before do before do
stub_kubeclient_pods stub_kubeclient_pods
stub_kubeclient_deployments
end end
it { is_expected.to eq(pods: [kube_pod]) } it { is_expected.to include(pods: [kube_pod]) }
end end
context 'when kubernetes responds with 500s' do context 'when kubernetes responds with 500s' do
before do before do
stub_kubeclient_pods(status: 500) stub_kubeclient_pods(status: 500)
stub_kubeclient_deployments(status: 500)
end end
it { expect { subject }.to raise_error(Kubeclient::HttpError) } it { expect { subject }.to raise_error(Kubeclient::HttpError) }
...@@ -258,9 +260,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -258,9 +260,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do context 'when kubernetes responds with 404s' do
before do before do
stub_kubeclient_pods(status: 404) stub_kubeclient_pods(status: 404)
stub_kubeclient_deployments(status: 404)
end end
it { is_expected.to eq(pods: []) } it { is_expected.to include(pods: []) }
end end
end end
end end
...@@ -2,6 +2,7 @@ require 'spec_helper' ...@@ -2,6 +2,7 @@ require 'spec_helper'
describe User do describe User do
include ProjectForksHelper include ProjectForksHelper
include TermsHelper
describe 'modules' do describe 'modules' do
subject { described_class } subject { described_class }
...@@ -2728,4 +2729,30 @@ describe User do ...@@ -2728,4 +2729,30 @@ describe User do
.to change { RedirectRoute.where(path: 'foo').count }.by(-1) .to change { RedirectRoute.where(path: 'foo').count }.by(-1)
end end
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 end
...@@ -90,4 +90,94 @@ describe GlobalPolicy do ...@@ -90,4 +90,94 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:update_custom_attribute) } it { is_expected.to be_allowed(:update_custom_attribute) }
end end
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 end
...@@ -6,6 +6,7 @@ describe API::Helpers do ...@@ -6,6 +6,7 @@ describe API::Helpers do
include API::APIGuard::HelperMethods include API::APIGuard::HelperMethods
include described_class include described_class
include SentryHelper include SentryHelper
include TermsHelper
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
...@@ -163,6 +164,23 @@ describe API::Helpers do ...@@ -163,6 +164,23 @@ describe API::Helpers do
expect { current_user }.to raise_error /403/ expect { current_user }.to raise_error /403/
end 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 it "sets current_user" do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user) expect(current_user).to eq(user)
......
require "spec_helper" require "spec_helper"
describe 'Git HTTP requests' do describe 'Git HTTP requests' do
include TermsHelper
include GitHttpHelpers include GitHttpHelpers
include WorkhorseHelpers include WorkhorseHelpers
include UserActivitiesHelpers include UserActivitiesHelpers
...@@ -824,4 +825,56 @@ describe 'Git HTTP requests' do ...@@ -824,4 +825,56 @@ describe 'Git HTTP requests' do
end end
end 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 end
...@@ -9,8 +9,13 @@ module KubernetesHelpers ...@@ -9,8 +9,13 @@ module KubernetesHelpers
kube_response(kube_pods_body) kube_response(kube_pods_body)
end end
def kube_deployments_response
kube_response(kube_deployments_body)
end
def stub_kubeclient_discover(api_url) 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 + '/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 end
def stub_kubeclient_pods(response = nil) def stub_kubeclient_pods(response = nil)
...@@ -20,6 +25,13 @@ module KubernetesHelpers ...@@ -20,6 +25,13 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end 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) def stub_kubeclient_get_secrets(api_url, **options)
WebMock.stub_request(:get, api_url + '/api/v1/secrets') WebMock.stub_request(:get, api_url + '/api/v1/secrets')
.to_return(kube_response(kube_v1_secrets_body(options))) .to_return(kube_response(kube_v1_secrets_body(options)))
...@@ -53,6 +65,18 @@ module KubernetesHelpers ...@@ -53,6 +65,18 @@ module KubernetesHelpers
"kind" => "APIResourceList", "kind" => "APIResourceList",
"resources" => [ "resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "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" } { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
] ]
} }
...@@ -65,14 +89,25 @@ module KubernetesHelpers ...@@ -65,14 +89,25 @@ module KubernetesHelpers
} }
end 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 # This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment # 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" => { "metadata" => {
"name" => name, "name" => name,
"generate_name" => "generated-name-with-suffix",
"creationTimestamp" => "2016-11-25T19:55:19Z", "creationTimestamp" => "2016-11-25T19:55:19Z",
"labels" => { "app" => app } "labels" => {
"app" => app,
"track" => track
}
}, },
"spec" => { "spec" => {
"containers" => [ "containers" => [
...@@ -80,7 +115,27 @@ module KubernetesHelpers ...@@ -80,7 +115,27 @@ module KubernetesHelpers
{ "name" => "container-1" } { "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 end
...@@ -101,4 +156,12 @@ module KubernetesHelpers ...@@ -101,4 +156,12 @@ module KubernetesHelpers
terminal terminal
end end
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 end
...@@ -12,8 +12,10 @@ ...@@ -12,8 +12,10 @@
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project # AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
# level, or manually added below. # level, or manually added below.
# #
# If you want to deploy to staging first, or enable canary deploys, # Continuous deployment to production is enabled by default.
# uncomment the relevant jobs in the pipeline below. # 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 # 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 # specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
...@@ -88,14 +90,6 @@ codequality: ...@@ -88,14 +90,6 @@ codequality:
artifacts: artifacts:
paths: [codeclimate.json] 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: performance:
stage: performance stage: performance
image: docker:stable image: docker:stable
...@@ -223,8 +217,8 @@ stop_review: ...@@ -223,8 +217,8 @@ stop_review:
# Staging deploys are disabled by default since # Staging deploys are disabled by default since
# continuous deployment to production is enabled by default # continuous deployment to production is enabled by default
# If you prefer to automatically deploy to staging and # If you prefer to automatically deploy to staging and
# only manually promote to production, enable this job by removing the dot (.), # only manually promote to production, enable this job by setting
# and uncomment the `when: manual` line in the `production` job. # STAGING_ENABLED.
staging: staging:
stage: staging stage: staging
...@@ -245,13 +239,9 @@ staging: ...@@ -245,13 +239,9 @@ staging:
kubernetes: active kubernetes: active
variables: variables:
- $STAGING_ENABLED - $STAGING_ENABLED
except:
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
# Canaries are disabled by default, but if you want them, # Canaries are disabled by default, but if you want them,
# and know what the downsides are, enable this job by removing the dot (.), # and know what the downsides are, enable this job by removing the dot (.).
# and uncomment the `when: manual` line in the `production` job.
.canary: .canary:
stage: canary stage: canary
...@@ -272,11 +262,6 @@ staging: ...@@ -272,11 +262,6 @@ staging:
- master - master
kubernetes: active 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 .production: &production_template
stage: production stage: production
script: script:
...@@ -310,6 +295,7 @@ production: ...@@ -310,6 +295,7 @@ production:
production_manual: production_manual:
<<: *production_template <<: *production_template
when: manual when: manual
allow_failure: false
only: only:
refs: refs:
- master - master
...@@ -345,6 +331,7 @@ rollout 10%: ...@@ -345,6 +331,7 @@ rollout 10%:
<<: *rollout_template <<: *rollout_template
variables: variables:
ROLLOUT_PERCENTAGE: 10 ROLLOUT_PERCENTAGE: 10
when: manual
only: only:
refs: refs:
- master - master
...@@ -379,6 +366,7 @@ rollout 50%: ...@@ -379,6 +366,7 @@ rollout 50%:
rollout 100%: rollout 100%:
<<: *production_template <<: *production_template
when: manual when: manual
allow_failure: false
only: only:
refs: refs:
- master - master
...@@ -428,14 +416,6 @@ rollout 100%: ...@@ -428,14 +416,6 @@ rollout 100%:
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code "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() { function sast() {
case "$CI_SERVER_VERSION" in case "$CI_SERVER_VERSION" in
*-ee) *-ee)
...@@ -562,12 +542,14 @@ rollout 100%: ...@@ -562,12 +542,14 @@ rollout 100%:
replicas=$(get_replicas "$track" "$percentage") replicas=$(get_replicas "$track" "$percentage")
if [[ -n "$(helm ls -q "^$name$")" ]]; then
helm upgrade --reuse-values \ helm upgrade --reuse-values \
--wait \ --wait \
--set replicaCount="$replicas" \ --set replicaCount="$replicas" \
--namespace="$KUBE_NAMESPACE" \ --namespace="$KUBE_NAMESPACE" \
"$name" \ "$name" \
chart/ chart/
fi
} }
function install_dependencies() { function install_dependencies() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment