Commit a8fe17de authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 6078-add-permissions-checks-dismiss-issue

* master: (144 commits)
  resolve conflict in app/assets/javascripts/boards/components/sidebar/remove_issue.vue
  Resolve conflicts in app/assets/javascripts/boards/components/board_sidebar.js
  Resolve conflicts in config/sidekiq_queues.yml
  Resolve conflicts in app/workers/all_queues.yml
  Resolve conflicts in .gitignore
  Fix conflict on saml.md
  fix conflict
  Rails5 fix NoMethodError: undefined method `join' for "":String
  Add CHANGELOG
  Improve shelling out in bin/changelog
  Fix sorting by name on explore projects page
  Do not style alert links that mimic buttons
  Resolve "100% CPU for webpack-dev-server running in GDK"
  Add back copy for existing gcp accounts within offer banner
  Resolve "Introduce hover, active, focus states for files in sidebar of Web IDE"
  Adds a status prop to report_issues.vue and passes it through to modal.vue
  Bring changes from EE
  Adds a `.modal--hide-footer` class to the security modal so we can conditionally hide it with CSS
  Adds the changelog entry for MR!6169
  Adds a "resolved" key to resolved vulnerabilities and conditionally displays action button based on that key
  ...
parents 21f1b16e c488e07b
......@@ -29,8 +29,7 @@ eslint-report.html
/app/assets/javascripts/locale/**/app.js
/backups/*
/config/aws.yml
/config/database.yml
/config/database_geo.yml
/config/database*.yml
/config/gitlab.yml
/config/gitlab_ci.yml
/config/initializers/rack_attack.rb
......
......@@ -308,18 +308,6 @@ stages:
paths:
- log/development.log
# Review docs base
.review-docs: &review-docs
<<: *dedicated-runner
<<: *except-qa
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
when: manual
only:
- branches
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
......@@ -379,20 +367,44 @@ package-and-qa:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
# Trigger a docs build in gitlab-docs
# Useful to preview the docs changes live
review-docs-deploy:
<<: *review-docs
stage: build
# Review docs base
.review-docs: &review-docs
<<: *dedicated-runner
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
environment:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy-manual:
<<: *review-docs
stage: build
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
when: manual
only:
- branches
<<: *except-docs-and-qa
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy:
<<: *review-docs
stage: post-test
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
only:
- /(^docs[\/-].*|.*-docs$)/
<<: *except-qa
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
......@@ -401,9 +413,10 @@ review-docs-cleanup:
environment:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
when: manual
script:
- gem install gitlab --no-ri --no-rdoc
- ./SCRIPT_NAME cleanup
- ./$SCRIPT_NAME cleanup
##
# Trigger a docker image build in CNG (Cloud Native GitLab) repository
......
Please view this file on the master branch, on stable branches it's out of date.
## 11.0.1 (2018-06-21)
- No changes.
## 11.0.0 (2018-06-22)
### Security (2 changes)
......@@ -71,6 +75,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Allow viewing only one when multiple issue boards is not enabled.
## 10.8.5 (2018-06-21)
- No changes.
## 10.8.4 (2018-06-06)
### Fixed (4 changes)
......@@ -191,6 +199,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove `features/group_active_tab.feature`. !5554 (@blackst0ne)
## 10.7.6 (2018-06-21)
- No changes.
## 10.7.5 (2018-05-28)
### Security (3 changes)
......
......@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.0.1 (2018-06-21)
### Security (5 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
## 11.0.0 (2018-06-22)
### Security (3 changes)
......@@ -242,6 +253,17 @@ entry.
- Workhorse to send raw diff and patch for commits.
## 10.8.5 (2018-06-21)
### Security (5 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
## 10.8.4 (2018-06-06)
- No changes.
......@@ -460,6 +482,22 @@ entry.
- Gitaly handles repository forks by default.
## 10.7.6 (2018-06-21)
### Security (6 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
- XSS fix to use safe_params instead of params in url_for helpers.
### Other (1 change)
- Replacing gollum libraries for gitlab custom libs. !18343
## 10.7.5 (2018-05-28)
### Security (3 changes)
......
......@@ -240,7 +240,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0'
# Sanitize user input
gem 'sanitize', '~> 2.0'
gem 'sanitize', '~> 4.6.5'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
......
......@@ -319,13 +319,13 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.4)
gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
sanitize (~> 2.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
......@@ -543,6 +543,8 @@ GEM
netrc (0.11.0)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
......@@ -833,8 +835,10 @@ GEM
et-orbi (~> 1.0)
rugged (0.27.2)
safe_yaml (1.0.4)
sanitize (2.1.0)
sanitize (4.6.5)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
......@@ -1188,7 +1192,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 2.0)
sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
......
......@@ -322,13 +322,13 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.4)
gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
sanitize (~> 2.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
......@@ -547,6 +547,8 @@ GEM
nio4r (2.3.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
......@@ -842,8 +844,10 @@ GEM
et-orbi (~> 1.0)
rugged (0.27.1)
safe_yaml (1.0.4)
sanitize (2.1.0)
sanitize (4.6.5)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
......@@ -1199,7 +1203,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 2.0)
sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
......
......@@ -2,18 +2,18 @@
import $ from 'jquery';
import Vue from 'vue';
import weight from 'ee/sidebar/components/weight/weight.vue';
import Weight from 'ee/sidebar/components/weight/weight.vue';
import Flash from '../../flash';
import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
import assignees from '../../sidebar/components/assignees/assignees.vue';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
import Assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import RemoveBtn from './sidebar/remove_issue.vue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;
......@@ -23,11 +23,11 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
subscriptions,
weight,
AssigneeTitle,
Assignees,
RemoveBtn,
Subscriptions,
Weight,
},
props: {
currentUser: {
......
import Vue from 'vue';
<script>
import Flash from '../../../flash';
import { __ } from '../../../locale';
import './lists_dropdown';
import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalFooter = Vue.extend({
export default {
components: {
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
ListsDropdown,
},
mixins: [modalMixin],
data() {
......@@ -61,28 +61,32 @@ gl.issueBoards.ModalFooter = Vue.extend({
this.toggleModal(false);
},
},
template: `
<footer
class="form-actions add-issues-footer">
<div class="float-left">
<button
class="btn btn-success"
type="button"
:disabled="submitDisabled"
@click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown></lists-dropdown>
</div>
};
</script>
<template>
<footer
class="form-actions add-issues-footer"
>
<div class="float-left">
<button
class="btn btn-default float-right"
:disabled="submitDisabled"
class="btn btn-success"
type="button"
@click="toggleModal(false)">
Cancel
@click="addIssues"
>
{{ submitText }}
</button>
</footer>
`,
});
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown/>
</div>
<button
class="btn btn-default float-right"
type="button"
@click="toggleModal(false)"
>
Cancel
</button>
</footer>
</template>
import Vue from 'vue';
import modalFilters from './filters';
import './tabs';
import modalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({
components: {
'modal-tabs': gl.issueBoards.ModalTabs,
modalTabs,
modalFilters,
},
mixins: [modalMixin],
......
......@@ -5,7 +5,7 @@ import queryData from '~/boards/utils/query_data';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
......@@ -14,7 +14,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
EmptyState,
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter,
ModalFooter,
loadingIcon,
},
props: {
......
import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
this.modal.selectedList = null;
},
template: `
<div class="dropdown inline">
<button
class="dropdown-menu-toggle"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<span
class="dropdown-label-box"
:style="{ backgroundColor: selected.label.color }">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="list in state.lists"
v-if="list.type == 'label'">
<a
href="#"
role="button"
:class="{ 'is-active': list.id == selected.id }"
@click.prevent="modal.selectedList = list">
<span
class="dropdown-label-box"
:style="{ backgroundColor: list.label.color }">
</span>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div>
`,
});
<script>
import ModalStore from '../../stores/modal_store';
export default {
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
this.modal.selectedList = null;
},
};
</script>
<template>
<div class="dropdown inline">
<button
class="dropdown-menu-toggle"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<span
:style="{ backgroundColor: selected.label.color }"
class="dropdown-label-box">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="(list, i) in state.lists"
v-if="list.type == 'label'"
:key="i">
<a
:class="{ 'is-active': list.id == selected.id }"
href="#"
role="button"
@click.prevent="modal.selectedList = list">
<span
:style="{ backgroundColor: list.label.color }"
class="dropdown-label-box">
</span>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div>
</template>
import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalTabs = Vue.extend({
mixins: [modalMixin],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
template: `
<div class="top-area prepend-top-10 append-bottom-10">
<ul class="nav-links issues-state-filters">
<li :class="{ 'active': activeTab == 'all' }">
<a
href="#"
role="button"
@click.prevent="changeTab('all')">
Open issues
<span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
</li>
<li :class="{ 'active': activeTab == 'selected' }">
<a
href="#"
role="button"
@click.prevent="changeTab('selected')">
Selected issues
<span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
</li>
</ul>
</div>
`,
});
<script>
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
mixins: [modalMixin],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
};
</script>
<template>
<div class="top-area prepend-top-10 append-bottom-10">
<ul class="nav-links issues-state-filters">
<li :class="{ 'active': activeTab == 'all' }">
<a
href="#"
role="button"
@click.prevent="changeTab('all')"
>
Open issues
<span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
</li>
<li :class="{ 'active': activeTab == 'selected' }">
<a
href="#"
role="button"
@click.prevent="changeTab('selected')"
>
Selected issues
<span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
</li>
</ul>
</div>
</template>
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.RemoveIssueBtn = Vue.extend({
props: {
issue: {
type: Object,
required: true,
},
list: {
type: Object,
required: true,
},
},
computed: {
updateUrl() {
return this.issue.path;
},
},
methods: {
removeIssue() {
const board = Store.state.currentBoard;
const issue = this.issue;
const lists = issue.getLists();
const boardLabelIds = board.labels.map(label => label.id);
const listLabelIds = lists.map(list => list.label.id);
let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id))
.filter(id => !boardLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
let assigneeIds = issue.assignees
.map(assignee => assignee.id)
.filter(id => id !== board.assignee.id);
if (assigneeIds.length === 0) {
// for backend to explicitly set No Assignee
assigneeIds = ['0'];
}
const data = {
issue: {
label_ids: labelIds,
assignee_ids: assigneeIds,
},
};
if (board.milestone_id) {
data.issue.milestone_id = -1;
}
if (board.weight) {
data.issue.weight = null;
}
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach((list) => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach((list) => {
list.removeIssue(issue);
});
Store.detail.issue = {};
},
},
template: `
<div
class="block list">
<button
class="btn btn-default btn-block"
type="button"
@click="removeIssue">
Remove from board
</button>
</div>
`,
});
<script>
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore;
export default {
props: {
issue: {
type: Object,
required: true,
},
list: {
type: Object,
required: true,
},
},
computed: {
updateUrl() {
return this.issue.path;
},
},
methods: {
removeIssue() {
const board = Store.state.currentBoard;
const issue = this.issue;
const lists = issue.getLists();
const boardLabelIds = board.labels.map(label => label.id);
const listLabelIds = lists.map(list => list.label.id);
let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id))
.filter(id => !boardLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
let assigneeIds = issue.assignees
.map(assignee => assignee.id)
.filter(id => id !== board.assignee.id);
if (assigneeIds.length === 0) {
// for backend to explicitly set No Assignee
assigneeIds = ['0'];
}
const data = {
issue: {
label_ids: labelIds,
assignee_ids: assigneeIds,
},
};
if (board.milestone_id) {
data.issue.milestone_id = -1;
}
if (board.weight) {
data.issue.weight = null;
}
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach(list => {
list.removeIssue(issue);
});
Store.detail.issue = {};
},
},
};
</script>
<template>
<div
class="block list"
>
<button
class="btn btn-default btn-block"
type="button"
@click="removeIssue"
>
Remove from board
</button>
</div>
</template>
......@@ -7,6 +7,16 @@ function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
issues: true,
mergeRequests: true,
epics: false,
milestones: true,
labels: true,
};
class GfmAutoComplete {
constructor(dataSources) {
this.dataSources = dataSources || {};
......@@ -14,14 +24,7 @@ class GfmAutoComplete {
this.isLoadingData = {};
}
setup(input, enableMap = {
emojis: true,
members: true,
issues: true,
milestones: true,
mergeRequests: true,
labels: true,
}) {
setup(input, enableMap = defaultAutocompleteConfig) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
......
import $ from 'jquery';
import autosize from 'autosize';
import GfmAutoComplete from './gfm_auto_complete';
import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = enableGFM;
this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
......@@ -34,14 +34,7 @@ export default class GLForm {
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true,
members: this.enableGFM,
issues: this.enableGFM,
milestones: this.enableGFM,
mergeRequests: this.enableGFM,
labels: this.enableGFM,
});
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
autosize(this.textarea);
}
......
......@@ -89,14 +89,14 @@ export default {
<template>
<div class="multi-file-commit-list-item position-relative">
<button
<div
v-tooltip
:title="tooltipTitle"
:class="{
'is-active': isActive
}"
type="button"
class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
role="button"
@dblclick="fileAction"
@click="openFileInEditor"
>
......@@ -107,7 +107,7 @@ export default {
:css-classes="iconClass"
/>{{ file.name }}
</span>
</button>
</div>
<component
:is="actionComponent"
:path="file.path"
......
......@@ -44,6 +44,8 @@ export default {
methods: {
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
clickFile(tab) {
if (tab.active) return;
this.updateDelayViewerUpdated(true);
if (tab.pending) {
......
......@@ -7,10 +7,10 @@ export default () => {
notesIds,
now,
diffView,
autocomplete,
enableGFM,
} = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance
Notes.initialize(notesUrl, notesIds, now, diffView, autocomplete);
Notes.initialize(notesUrl, notesIds, now, diffView, enableGFM);
};
......@@ -20,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
import { defaultAutocompleteConfig } from './gfm_auto_complete';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
......@@ -45,7 +46,7 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM) {
if (!this.instance) {
this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
}
......@@ -55,7 +56,7 @@ export default class Notes {
return this.instance;
}
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this);
......@@ -94,7 +95,7 @@ export default class Notes {
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
this.setupMainTargetNoteForm();
this.setupMainTargetNoteForm(enableGFM);
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
......@@ -598,14 +599,14 @@ export default class Notes {
*
* Sets some hidden fields in the form.
*/
setupMainTargetNoteForm() {
setupMainTargetNoteForm(enableGFM) {
var form;
// find the form
form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
this.setupNoteForm(form);
this.setupNoteForm(form, enableGFM);
// fix classes
form.removeClass('js-new-note-form');
form.addClass('js-main-target-form');
......@@ -633,9 +634,9 @@ export default class Notes {
* setup GFM auto complete
* show the form
*/
setupNoteForm(form) {
setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
var textarea, key;
this.glForm = new GLForm(form, this.enableGFM);
this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
......
......@@ -194,7 +194,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
@click="cancelHandler()">
Cancel
{{ __('Discard draft') }}
</button>
</div>
</form>
......
......@@ -3,5 +3,5 @@ import GLForm from '~/gl_form';
export default function ($formEl) {
new ZenMode(); // eslint-disable-line no-new
new GLForm($formEl, true); // eslint-disable-line no-new
new GLForm($formEl); // eslint-disable-line no-new
}
......@@ -11,7 +11,7 @@ import WeightSelect from 'ee/weight_select';
export default () => {
new ShortcutsNavigation();
new GLForm($('.issue-form'), true);
new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
......
......@@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new Diff();
new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
......
......@@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.tag-form'), true); // eslint-disable-line no-new
new GLForm($('.tag-form')); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
});
......@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
new GLForm($('.wiki-form')); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button');
......
......@@ -3,6 +3,13 @@ import GLForm from '~/gl_form';
import ZenMode from '~/zen_mode';
export default () => {
new GLForm($('.snippet-form'), false); // eslint-disable-line no-new
// eslint-disable-next-line no-new
new GLForm($('.snippet-form'), {
members: false,
issues: false,
mergeRequests: false,
milestones: false,
labels: false,
});
new ZenMode(); // eslint-disable-line no-new
};
......@@ -6,5 +6,13 @@ import GLForm from '../../gl_form';
export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new
new DueDateSelectors(); // eslint-disable-line no-new
new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new
// eslint-disable-next-line no-new
new GLForm($('.milestone-form'), {
emojis: initGFM,
members: initGFM,
issues: initGFM,
mergeRequests: initGFM,
milestones: initGFM,
labels: initGFM,
});
};
......@@ -62,7 +62,14 @@
/*
GLForm class handles all the toolbar buttons
*/
return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
});
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
......
......@@ -310,7 +310,7 @@ pre code {
color: $white-light;
h4,
a,
a:not(.btn),
.alert-link {
color: $white-light;
}
......
......@@ -180,10 +180,6 @@
color: $border-and-box-shadow;
}
.ide-file-list .file.file-active {
color: $border-and-box-shadow;
}
.ide-sidebar-link {
&.active {
color: $border-and-box-shadow;
......
......@@ -23,6 +23,7 @@
margin-top: 0;
border-top: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height;
color: $gl-text-color;
&.is-collapsed {
.ide-file-list {
......@@ -45,12 +46,8 @@
.file {
cursor: pointer;
&.file-open {
background: $white-normal;
}
&.file-active {
font-weight: $gl-font-weight-bold;
background: $theme-gray-100;
}
.ide-file-name {
......@@ -58,7 +55,9 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
line-height: 22px;
line-height: 16px;
display: inline-block;
height: 18px;
svg {
vertical-align: middle;
......@@ -86,12 +85,14 @@
.ide-new-btn {
display: none;
.btn {
padding: 2px 5px;
}
}
&:hover,
&:focus {
background: $white-normal;
.ide-new-btn {
display: block;
}
......@@ -281,8 +282,8 @@
}
.margin {
background-color: $gray-light;
border-right: 1px solid $white-normal;
background-color: $white-light;
border-right: 1px solid $theme-gray-100;
.line-insert {
border-right: 1px solid $line-added-dark;
......@@ -303,6 +304,15 @@
.multi-file-editor-holder {
height: 100%;
min-height: 0;
&.is-readonly,
.editor.original {
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
background-color: $theme-gray-50;
}
}
}
.preview-container {
......@@ -587,11 +597,17 @@
&:hover,
&:focus {
background: $white-normal;
background: $theme-gray-100;
}
&:active {
background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
cursor: pointer;
&.is-active {
background-color: $white-normal;
}
......@@ -611,10 +627,6 @@
.multi-file-commit-list-file-path {
@include str-truncated(calc(100% - 30px));
&:hover {
text-decoration: underline;
}
&:active {
text-decoration: none;
}
......
......@@ -11,6 +11,5 @@ class Admin::DashboardController < Admin::ApplicationController
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
@license = License.current
end
end
......@@ -130,7 +130,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
set_remember_me(user)
if user.two_factor_enabled?
if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user)
else
sign_in_and_redirect(user)
......
......@@ -56,7 +56,7 @@ class UserRecentEventsFinder
visible = target_user
.project_interactions
.where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC])
.where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
.select(:id)
Gitlab::SQL::Union.new([authorized, visible]).to_sql
......
......@@ -145,7 +145,14 @@ module NotesHelper
notesIds: @notes.map(&:id),
now: Time.now.to_i,
diffView: diff_view,
autocomplete: autocomplete
enableGFM: {
emojis: true,
members: autocomplete,
issues: autocomplete,
mergeRequests: autocomplete,
milestones: autocomplete,
labels: autocomplete
}
}
end
......
......@@ -42,7 +42,8 @@ module ProjectsHelper
name_tag_options[:class] << 'has-tooltip'
end
content_tag(:span, sanitize(username), name_tag_options)
# NOTE: ActionView::Helpers::TagHelper#content_tag HTML escapes username
content_tag(:span, username, name_tag_options)
end
def link_to_member(project, author, opts = {}, &block)
......@@ -508,6 +509,14 @@ module ProjectsHelper
end
end
def sidebar_projects_paths
%w[
projects#show
projects#activity
cycle_analytics#show
]
end
def sidebar_settings_paths
%w[
projects#edit
......
......@@ -578,9 +578,9 @@ module Ci
.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
end
def queued_duration
......
......@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder("lower(name) asc") }
scope :order_name_desc, -> { reorder("lower(name) desc") }
scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end
module ClassMethods
......
......@@ -133,9 +133,7 @@ class MergeRequest < ActiveRecord::Base
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
begin
# Merge request can become unmergeable due to many reasons.
# We only notify if it is due to conflict.
unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
if merge_request.notify_conflict?
NotificationService.new.merge_request_unmergeable(merge_request)
TodoService.new.merge_request_became_unmergeable(merge_request)
end
......@@ -714,6 +712,10 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
def notify_conflict?
(opened? || locked?) && !project.repository.can_be_merged?(diff_head_sha, target_branch)
end
def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
......
......@@ -29,8 +29,8 @@ class ProjectAutoDevops < ActiveRecord::Base
end
if manual?
variables.append(key: 'STAGING_ENABLED', value: 1)
variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1)
variables.append(key: 'STAGING_ENABLED', value: '1')
variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
end
end
end
......
......@@ -26,7 +26,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository
delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
......
......@@ -82,7 +82,7 @@ class WebHookService
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password)
password: CGI.unescape(parsed_url.password.presence || '')
}
make_request(post_url, basic_auth)
end
......
......@@ -373,3 +373,5 @@
= _('Geo allows you to replicate your GitLab instance to other geographical locations.')
.settings-content
= render partial: 'slack'
= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
......@@ -8,7 +8,7 @@
.sidebar-context-title
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
= sprite_icon('project')
......@@ -29,13 +29,13 @@
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span= _('Activity')
= render_if_exists 'projects/sidebar/security_dashboard'
- if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics')
= render_if_exists 'projects/sidebar/security_dashboard'
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree' do
......
......@@ -6,7 +6,7 @@
= image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
.col-sm-10
%h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
Apply for credit
......@@ -30,7 +30,7 @@
#{@commits_graph.start_date.strftime('%b %d')}
- end_time = capture do
#{@commits_graph.end_date.strftime('%b %d')}
= (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{@ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
= (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{h @ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
.col-md-6
.tree-ref-container
......
......@@ -40,5 +40,5 @@
= yield(:note_actions)
%a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
%a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Discard draft" } }
Discard draft
......@@ -145,6 +145,7 @@
- cronjob:ldap_all_groups_sync
- cronjob:ldap_sync
- cronjob:update_all_mirrors
- cronjob:pseudonymizer
- geo:geo_scheduler_scheduler
- geo:geo_scheduler_primary_scheduler
......
......@@ -19,7 +19,24 @@ Options = Struct.new(
)
INVALID_TYPE = -1
module ChangelogHelpers
Abort = Class.new(StandardError)
Done = Class.new(StandardError)
def capture_stdout(cmd)
output = IO.popen(cmd, &:read)
fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
output
end
def fail_with(message)
raise Abort, "\e[31merror\e[0m #{message}"
end
end
class ChangelogOptionParser
extend ChangelogHelpers
Type = Struct.new(:name, :description)
TYPES = [
Type.new('added', 'New feature'),
......@@ -68,7 +85,7 @@ class ChangelogOptionParser
opts.on('-h', '--help', 'Print help message') do
$stdout.puts opts
exit
raise Done.new
end
end
......@@ -108,18 +125,19 @@ class ChangelogOptionParser
def assert_valid_type!(type)
unless type
$stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}"
exit 1
raise Abort, "Invalid category index, please select an index between 1 and #{TYPES.length}"
end
end
def git_user_name
%x{git config user.name}.strip
capture_stdout(%w[git config user.name]).strip
end
end
end
class ChangelogEntry
include ChangelogHelpers
attr_reader :options
def initialize(options)
......@@ -159,13 +177,9 @@ class ChangelogEntry
end
def amend_commit
%x{git add #{file_path}}
exec("git commit --amend")
end
fail_with "git add failed" unless system(*%W[git add #{file_path}])
def fail_with(message)
$stderr.puts "\e[31merror\e[0m #{message}"
exit 1
Kernel.exec(*%w[git commit --amend])
end
def assert_feature_branch!
......@@ -203,7 +217,7 @@ class ChangelogEntry
end
def last_commit_subject
%x{git log --format="%s" -1}.strip
capture_stdout(%w[git log --format=%s -1]).strip
end
def file_path
......@@ -225,7 +239,7 @@ class ChangelogEntry
end
def branch_name
@branch_name ||= %x{git symbolic-ref --short HEAD}.strip
@branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
end
def remove_trailing_whitespace(yaml_content)
......@@ -234,8 +248,15 @@ class ChangelogEntry
end
if $0 == __FILE__
options = ChangelogOptionParser.parse(ARGV)
ChangelogEntry.new(options)
begin
options = ChangelogOptionParser.parse(ARGV)
ChangelogEntry.new(options)
rescue ChangelogHelpers::Abort => ex
$stderr.puts ex.message
exit 1
rescue ChangelogHelpers::Done
exit
end
end
# vim: ft=ruby
---
title: Fade uneditable area in Web IDE
merge_request: 20008
author:
type: changed
---
title: Update Web IDE file tree styles
merge_request: 19969
author:
type: changed
---
title: Fix webhook error when password is not present
merge_request: 19945
author: Jan Beckmann
type: fixed
---
title: Fix sorting by name on explore projects page
merge_request: 20162
author:
type: fixed
---
title: Only load Omniauth if enabled
merge_request: 20132
author:
type: fixed
---
title: Notify conflict for only open merge request
merge_request: 20125
author:
type: fixed
---
title: Fix incremental rollouts for Auto DevOps
merge_request: 20061
author:
type: fixed
---
title: Add back copy for existing gcp accounts within offer banner
merge_request:
author:
type: changed
---
title: Fix alert button styling so that they don't show up white
merge_request:
author:
type: fixed
---
title: Fix XSS vulnerability for table of content generation
merge_request:
author:
type: security
---
title: Update sanitize gem to 4.6.5 to fix HTML injection vulnerability
merge_request:
author:
type: security
---
title: HTML escape branch name in project graphs page
merge_request:
author:
type: security
---
title: HTML escape the name of the user in ProjectsHelper#link_to_member
merge_request:
author:
type: security
---
title: Don't show events from internal projects for anonymous users in public feed
merge_request:
author:
type: security
......@@ -7,6 +7,12 @@ Bundler.require(:default, Rails.env)
require 'elasticsearch/rails/instrumentation'
module Gitlab
# This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
def self.rails5?
ENV["RAILS5"].in?(%w[1 true])
end
class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache')
......@@ -16,6 +22,11 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/current_settings')
require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
# This needs to be loaded before DB connection is made
# to make sure that all connections have NO_ZERO_DATE
# setting disabled
require_dependency Rails.root.join('lib/mysql_zero_date')
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
......@@ -239,10 +250,4 @@ module Gitlab
Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end
end
# This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
def self.rails5?
ENV["RAILS5"].in?(%w[1 true])
end
end
......@@ -311,6 +311,10 @@ production: &base
geo_migrated_local_files_clean_up_worker:
cron: "15 */6 * * *"
# Export pseudonymized data in CSV format for analysis
pseudonymizer_worker:
cron: "0 * * * *"
registry:
# enabled: true
# host: registry.example.com
......@@ -726,6 +730,20 @@ production: &base
# # Specifies Amazon S3 storage class to use for backups, this is optional
# # storage_class: 'STANDARD'
## Pseudonymizer exporter
pseudonymizer:
# Tables manifest that specifies the fields to extract and pseudonymize.
manifest: config/pseudonymizer.yml
upload:
# remote_directory: 'gitlab-elt'
# Fog storage connection settings, see http://fog.io/storage/ .
connection:
# provider: AWS
# region: eu-west-1
# aws_access_key_id: AKIAKIAKI
# aws_secret_access_key: 'secret123'
# # The remote 'directory' to store the CSV files. For S3, this would be the bucket name.
## GitLab Shell settings
gitlab_shell:
path: /home/git/gitlab-shell/
......@@ -876,6 +894,17 @@ test:
token: secret
backup:
path: tmp/tests/backups
pseudonymizer:
manifest: config/pseudonymizer.yml
upload:
# The remote 'directory' to store the CSV files. For S3, this would be the bucket name.
remote_directory: gitlab-elt.test
# Fog storage connection settings, see http://fog.io/storage/
connection:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: us-east-1
gitlab_shell:
path: tmp/tests/gitlab-shell/
hooks_path: tmp/tests/gitlab-shell/hooks/
......
......@@ -370,6 +370,10 @@ Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_for_usage_ping)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
Settings.cron_jobs['pseudonymizer_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['pseudonymizer_worker']['cron'] ||= '0 23 * * *'
Settings.cron_jobs['pseudonymizer_worker']['job_class'] ||= 'PseudonymizerWorker'
Settings.cron_jobs['schedule_update_user_activity_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['schedule_update_user_activity_worker']['cron'] ||= '30 0 * * *'
Settings.cron_jobs['schedule_update_user_activity_worker']['job_class'] = 'ScheduleUpdateUserActivityWorker'
......@@ -470,6 +474,14 @@ Settings.backup['upload']['multipart_chunk_size'] ||= 104857600
Settings.backup['upload']['encryption'] ||= nil
Settings.backup['upload']['storage_class'] ||= nil
#
# Pseudonymizer
#
Settings['pseudonymizer'] ||= Settingslogic.new({})
Settings.pseudonymizer['manifest'] = Settings.absolute(Settings.pseudonymizer['manifest'] || Rails.root.join("config/pseudonymizer.yml"))
Settings.pseudonymizer['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil })
# Settings.pseudonymizer['upload']['multipart_chunk_size'] ||= 104857600
#
# Git
#
......
......@@ -65,7 +65,7 @@ elsif Gitlab::Database.mysql?
prepend RegisterDateTimeWithTimeZone
# Add the class `DateTimeWithTimeZone` so we can map `timestamp` to it.
class MysqlDateTimeWithTimeZone < MysqlDateTime
class MysqlDateTimeWithTimeZone < (Gitlab.rails5? ? ActiveRecord::Type::DateTime : MysqlDateTime)
def type
:datetime_with_timezone
end
......
......@@ -219,5 +219,7 @@ Devise.setup do |config|
end
end
Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
if Gitlab.config.omniauth.enabled
Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
end
end
This diff is collapsed.
......@@ -4,7 +4,7 @@ class MergeRequestDiffFileLimitsToMysql < ActiveRecord::Migration
def up
return unless Gitlab::Database.mysql?
change_column :merge_request_diff_files, :diff, :text, limit: 2147483647
change_column :merge_request_diff_files, :diff, :text, limit: 2147483647, default: nil
end
def down
......
......@@ -206,6 +206,7 @@ ActiveRecord::Schema.define(version: 20180612175636) do
t.string "encrypted_external_auth_client_key_pass_iv"
t.string "email_additional_text"
t.boolean "enforce_terms", default: false
t.boolean "pseudonymizer_enabled", default: false, null: false
end
create_table "approvals", force: :cascade do |t|
......
......@@ -167,6 +167,10 @@ created in snippets, wikis, and repos.
- [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
## Analytics
- [Pseudonymizer](pseudonymizer.md): Export data from GitLab's database to CSV files in a secure way.
## Troubleshooting
- [Debugging tips](troubleshooting/debug.md): Tips to debug problems when things go wrong
......
# Pseudonymizer
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5532) in [GitLab Ultimate][ee] 11.1.
As GitLab's database hosts sensitive information, using it unfiltered for analytics
implies high security requirements. To help alleviate this constraint, the Pseudonymizer
service is used to export GitLab's data in a pseudonymized way.
CAUTION: **Warning:**
This process is not impervious. If the source data is available, it's possible for
a user to correlate data to the pseudonymized version.
The Pseudonymizer currently uses `HMAC(SHA256)` to mutate fields that shouldn't
be textually exported. This ensures that:
- the end-user of the data source cannot infer/revert the pseudonymized fields
- the referential integrity is maintained
## Configuration
To configure the pseudonymizer, you need to:
- Provide a manifest file that describes which fields should be included or
pseudonymized ([example `manifest.yml` file](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/config/pseudonymizer.yml)).
A default manifest is provided with the GitLab installation. Using a relative file path will be resolved from the Rails root.
Alternatively, you can use an absolute file path.
- Use an object storage and specify the connection parameters in the `pseudonymizer.upload.connection` configuration option.
**For Omnibus installations:**
1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
the values you want:
```ruby
gitlab_rails['pseudonymizer_manifest'] = 'config/pseudonymizer.yml'
gitlab_rails['pseudonymizer_upload_remote_directory'] = 'gitlab-elt'
gitlab_rails['pseudonymizer_upload_connection'] = {
'provider' => 'AWS',
'region' => 'eu-central-1',
'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY'
}
```
NOTE: **Note:**
If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs.
```ruby
gitlab_rails['pseudonymizer_upload_connection'] = {
'provider' => 'AWS',
'region' => 'eu-central-1',
'use_iam_profile' => true
}
```
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect.
---
**For installations from source:**
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
lines:
```yaml
pseudonymizer:
manifest: config/pseudonymizer.yml
upload:
remote_directory: 'gitlab-elt' # The bucket name
connection:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: eu-central-1
```
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source)
for the changes to take effect.
## Usage
You can optionally run the pseudonymizer using the following environment variables:
- `PSEUDONYMIZER_OUTPUT_DIR` - where to store the output CSV files (defaults to `/tmp`)
- `PSEUDONYMIZER_BATCH` - the batch size when querying the DB (defaults to `100000`)
```bash
## Omnibus
sudo gitlab-rake gitlab:db:pseudonymizer
## Source
sudo -u git -H bundle exec rake gitlab:db:pseudonymizer RAILS_ENV=production
```
This will produce some CSV files that might be very large, so make sure the
`PSEUDONYMIZER_OUTPUT_DIR` has sufficient space. As a rule of thumb, at least
10% of the database size is recommended.
After the pseudonymizer has run, the output CSV files should be uploaded to the
configured object storage and deleted from the local disk.
[ee]: https://about.gitlab.com/pricing/
......@@ -322,50 +322,49 @@ to EE only.
## Previewing the changes live
To preview your changes to documentation locally, please follow
this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
NOTE: **Note:**
To preview your changes to documentation locally, follow this
[development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
If you want to preview the doc changes of your merge request live, you can use
the manual `review-docs-deploy` job in your merge request. You will need at
least Maintainer permissions to be able to run it and is currently enabled for the
following projects:
The live preview is currently enabled for the following projects:
- https://gitlab.com/gitlab-org/gitlab-ce
- https://gitlab.com/gitlab-org/gitlab-ee
- https://gitlab.com/gitlab-org/gitlab-runner
NOTE: **Note:**
You will need to push a branch to those repositories, it doesn't work for forks.
TIP: **Tip:**
If your branch contains only documentation changes, you can use
[special branch names](#branch-naming) to avoid long running pipelines.
In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
reveal the `review-docs-deploy` job. Hit the play button for the job to start.
For [docs-only changes](#branch-naming), the review app is run automatically.
For all other branches, you can use the manual `review-docs-deploy-manual` job
in your merge request. You will need at least Maintainer permissions to be able
to run it. In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
reveal the `review-docs-deploy-manual` job. Hit the play button for the job to start.
![Manual trigger a docs build](img/manual_build_docs.png)
This job will:
NOTE: **Note:**
You will need to push a branch to those repositories, it doesn't work for forks.
The `review-docs-deploy*` job will:
1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
project named after the scheme: `preview-<branch-slug>`
project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
CE, etc.
1. Trigger a cross project pipeline and build the docs site with your changes
After a few minutes, the Review App will be deployed and you will be able to
preview the changes. The docs URL can be found in two places:
- In the merge request widget
- In the output of the `review-docs-deploy` job, which also includes the
- In the output of the `review-docs-deploy*` job, which also includes the
triggered pipeline so that you can investigate whether something went wrong
In case the Review App URL returns 404, follow these steps to debug:
1. **Did you follow the URL from the merge request widget?** If yes, then check if
the link is the same as the one in the job output. It can happen that if the
branch name slug is longer than 35 characters, it is automatically
truncated. That means that the merge request widget will not show the proper
URL due to a limitation of how `environment: url` works, but you can find the
real URL from the output of the `review-docs-deploy` job.
the link is the same as the one in the job output.
1. **Did you follow the URL from the job output?** If yes, then it means that
either the site is not yet deployed or something went wrong with the remote
pipeline. Give it a few minutes and it should appear online, otherwise you
......
......@@ -21,7 +21,7 @@ and click a button to begin the upgrade process.
## Features
The GitLab Pivotal Tile is based on [GitLab Premium] and includes nearly all of its features. The features in Premium but _not_ supported on the Tile are:
The GitLab Pivotal Tile is based on [GitLab Premium][eep] and includes nearly all of its features. The features in Premium but _not_ supported on the Tile are:
* PostgreSQL
* Pages
......@@ -41,5 +41,5 @@ website:
- [Product page](https://network.pivotal.io/products/p-gitlab/)
- [Documentation](https://docs.pivotal.io/partners/gitlab/index.html)
[premium]: https://about.gitlab.com/products/
[eep]: https://about.gitlab.com/products/
[pcf]: https://pivotal.io/platform
......@@ -102,5 +102,5 @@ Click the links to see your GitLab repository data.
[existing-jira]: ../user/project/integrations/jira.md
[jira-development-panel]: https://confluence.atlassian.com/adminjiraserver070/integrating-with-development-tools-776637096.html#Integratingwithdevelopmenttools-Developmentpanelonissues
[eep]: https://about.gitlab.com/products/
[ee-2786]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2786
[ee-2381]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2381
[relative-url]: https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab
......@@ -230,6 +230,81 @@ considered `admin groups`.
} }
```
## Bypass two factor authentication
If you want some SAML authentication methods to count as 2FA on a per session basis, you can register them in the
`upstream_two_factor_authn_contexts` list:
**For Omnibus installations:**
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['omniauth_providers'] = [
{
name: 'saml',
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
upstream_two_factor_authn_contexts:
%w(
urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
)
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
]
```
1. Save the file and [reconfigure][] GitLab for the changes to take effect.
---
**For installations from source:**
1. Edit `config/gitlab.yml`:
```yaml
- {
name: 'saml',
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
upstream_two_factor_authn_contexts:
[
'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
]
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
```
1. Save the file and [restart GitLab][] for the changes ot take effect
In addition to the changes in GitLab, make sure that your Idp is returning the
`AuthnContext`. For example:
```xml
<saml:AuthnStatement>
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:MediumStrongCertificateProtectedTransport</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
```
## Customization
### `auto_sign_in_with_provider`
......
......@@ -113,7 +113,7 @@ by yourself (except when an issue is due). You will only receive automatic
notifications when somebody else comments or adds changes to the ones that
you've created or mentions you.
If a merge request becomes unmergeable, its author will be notified about the cause.
If an open merge request becomes unmergeable due to conflict, its author will be notified about the cause.
If a user has also set the merge request to automatically merge once pipeline succeeds,
then that user will also be notified.
......
......@@ -31,7 +31,7 @@ A Todo appears in your Todos dashboard when:
- you are `@mentioned` in a comment on a commit,
- a job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail.
- a merge request becomes unmergeable, and you are either:
- an open merge request becomes unmergeable due to conflict, and you are either:
- the author, or
- have set it to automatically merge once pipeline succeeds.
......
......@@ -21,6 +21,11 @@ export default {
type: Number,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
};
</script>
......@@ -31,6 +36,7 @@ export default {
<modal-open-name
:issue="issue"
:status="status"
class="js-modal-dast"
/>
</div>
......
......@@ -64,6 +64,7 @@
<modal
id="modal-mrwidget-security-issue"
:header-title-text="modal.title"
:class="{'modal-hide-footer': modal.isResolved}"
class="modal-security-report-dast"
>
<slot>
......@@ -202,39 +203,41 @@
</div>
</slot>
<div slot="footer">
<button
type="button"
class="btn btn-default"
data-dismiss="modal"
>
{{ __('Cancel' ) }}
</button>
<template v-if="!modal.isResolved">
<button
type="button"
class="btn btn-default"
data-dismiss="modal"
>
{{ __('Cancel' ) }}
</button>
<loading-button
v-if="canCreateFeedbackPermission"
:loading="modal.isDismissingIssue"
:disabled="modal.isDismissingIssue"
:label="revertTitle"
container-class="js-dismiss-btn btn btn-close"
@click="handleDismissClick"
/>
<loading-button
v-if="canCreateFeedbackPermission"
:loading="modal.isDismissingIssue"
:disabled="modal.isDismissingIssue"
:label="revertTitle"
container-class="js-dismiss-btn btn btn-close"
@click="handleDismissClick"
/>
<a
v-if="modal.vulnerability.hasIssue"
:href="modal.vulnerability.issueFeedback && modal.vulnerability.issueFeedback.issue_url"
rel="noopener noreferrer nofollow"
class="btn btn-success btn-inverted"
>
{{ __('View issue' ) }}
</a>
<loading-button
v-else-if="!modal.vulnerability.hasIssue && canCreateIssuePermission"
:loading="modal.isCreatingNewIssue"
:disabled="modal.isCreatingNewIssue"
:label="__('Create issue')"
container-class="js-create-issue-btn btn btn-success btn-inverted"
@click="createNewIssue"
/>
<a
v-if="modal.vulnerability.hasIssue"
:href="modal.vulnerability.issueFeedback && modal.vulnerability.issueFeedback.issue_url"
rel="noopener noreferrer nofollow"
class="btn btn-success btn-inverted"
>
{{ __('View issue' ) }}
</a>
<loading-button
v-else-if="!modal.vulnerability.hasIssue && canCreateIssuePermission"
:loading="modal.isCreatingNewIssue"
:disabled="modal.isCreatingNewIssue"
:label="__('Create issue')"
container-class="btn btn-success btn-inverted"
@click="createNewIssue"
/>
</template>
</div>
</modal>
</template>
......@@ -7,11 +7,17 @@ export default {
type: Object,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
methods: {
...mapActions(['openModal']),
handleIssueClick() {
this.openModal(this.issue);
const { issue, status, openModal } = this;
openModal({ issue, status });
},
},
};
......
......@@ -109,17 +109,20 @@ export default {
<sast-issue
v-if="isTypeSast"
:issue="issue"
:status="status"
/>
<dast-issue
v-else-if="isTypeDast"
:issue="issue"
:issue-index="index"
:status="status"
/>
<sast-container-issue
v-else-if="isTypeSastContainer"
:issue="issue"
:status="status"
/>
<codequality-issue
......
......@@ -17,6 +17,11 @@ export default {
type: Object,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
};
</script>
......@@ -25,7 +30,10 @@ export default {
<div class="report-block-list-issue-description-text">
<template v-if="issue.severity">{{ issue.severity }}:</template>
<modal-open-name :issue="issue" />
<modal-open-name
:issue="issue"
:status="status"
/>
</div>
<report-link
......
......@@ -19,6 +19,11 @@ export default {
type: Object,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
};
</script>
......@@ -36,7 +41,10 @@ export default {
</template>
<template v-else-if="issue.priority">{{ issue.priority }}:</template>
<modal-open-name :issue="issue" />
<modal-open-name
:issue="issue"
:status="status"
/>
</div>
<report-link
......
......@@ -204,13 +204,13 @@ export const fetchDependencyScanningReports = ({ state, dispatch }) => {
export const updateDependencyScanningIssue = ({ commit }, issue) =>
commit(types.UPDATE_DEPENDENCY_SCANNING_ISSUE, issue);
export const openModal = ({ dispatch }, issue) => {
dispatch('setModalData', issue);
export const openModal = ({ dispatch }, payload) => {
dispatch('setModalData', payload);
$('#modal-mrwidget-security-issue').modal('show');
};
export const setModalData = ({ commit }, issue) => commit(types.SET_ISSUE_MODAL_DATA, issue);
export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
export const requestDismissIssue = ({ commit }) => commit(types.REQUEST_DISMISS_ISSUE);
export const receiveDismissIssue = ({ commit }) => commit(types.RECEIVE_DISMISS_ISSUE_SUCCESS);
export const receiveDismissIssueError = ({ commit }, error) =>
......
......@@ -255,7 +255,9 @@ export default {
state.dependencyScanning.hasError = true;
},
[types.SET_ISSUE_MODAL_DATA](state, issue) {
[types.SET_ISSUE_MODAL_DATA](state, payload) {
const { issue, status } = payload;
state.modal.title = issue.title;
state.modal.data.description.value = issue.description;
state.modal.data.file.value = issue.location && issue.location.file;
......@@ -280,6 +282,7 @@ export default {
}
state.modal.data.instances.value = issue.instances;
state.modal.vulnerability = issue;
state.modal.isResolved = status === 'success';
// clear previous state
state.modal.error = null;
......
......@@ -139,4 +139,9 @@
width: $modal-lg;
max-width: $modal-lg;
}
// This is temporary till we get the new modals hooked up
&.modal-hide-footer .modal-footer {
display: none;
}
}
......@@ -20,6 +20,10 @@ module EE
attrs << :email_additional_text
end
if License.feature_available?(:pseudonymizer)
attrs << :pseudonymizer_enabled
end
attrs
end
end
......
......@@ -3,6 +3,14 @@ module EE
module Admin
module DashboardController
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :index
def index
super
@license = License.current
end
def stats
@admin_count = ::User.admins.count
......
......@@ -35,6 +35,18 @@ module EE
"and the value is encrypted at rest.")
end
def pseudonymizer_enabled_help_text
_("Enable Pseudonymizer data collection")
end
def pseudonymizer_description_text
_("GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.")
end
def pseudonymizer_disabled_description_text
_("The pseudonymizer data collection is disabled. When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.")
end
override :visible_attributes
def visible_attributes
super + [
......@@ -55,7 +67,8 @@ module EE
:slack_app_id,
:slack_app_secret,
:slack_app_verification_token,
:allow_group_owners_to_manage_ldap
:allow_group_owners_to_manage_ldap,
:pseudonymizer_enabled
]
end
......
......@@ -100,11 +100,20 @@ module EE
slack_app_enabled: false,
slack_app_id: nil,
slack_app_secret: nil,
slack_app_verification_token: nil
slack_app_verification_token: nil,
pseudonymizer_enabled: false
)
end
end
def pseudonymizer_available?
License.feature_available?(:pseudonymizer)
end
def pseudonymizer_enabled?
pseudonymizer_available? && super
end
def should_check_namespace_plan?
check_namespace_plan? && (Rails.env.test? || ::Gitlab.dev_env_or_com?)
end
......
......@@ -73,6 +73,7 @@ class License < ActiveRecord::Base
ide
chatops
pod_logs
pseudonymizer
].freeze
# List all features available for early adopters,
......
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group.row
.offset-sm-2.col-sm-10
- is_enabled = @application_setting.pseudonymizer_enabled?
.form-check
= f.label :pseudonymizer_enabled do
= f.check_box :pseudonymizer_enabled
= pseudonymizer_enabled_help_text
.form-text.text-muted
- if is_enabled
= pseudonymizer_description_text
- else
= pseudonymizer_disabled_description_text
= f.submit 'Save changes', class: "btn btn-success"
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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