diff options
517 files changed, 7056 insertions, 2665 deletions
diff --git a/.gitignore b/.gitignore index 104c6930050..3ffe4263c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,5 @@ package-lock.json /junit_*.xml /coverage-frontend/ jsdoc/ -**/tmp/rubocop_cache/**
\ No newline at end of file +**/tmp/rubocop_cache/** +.overcommit.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 27992024265..5b5527284d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,3 +41,4 @@ include: - local: .gitlab/ci/setup.gitlab-ci.yml - local: .gitlab/ci/test-metadata.gitlab-ci.yml - local: .gitlab/ci/yaml.gitlab-ci.yml + - local: .gitlab/ci/ee-specific-checks.gitlab-ci.yml diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index e77c773824f..3f29adddf73 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -74,7 +74,7 @@ docs lint: script: - scripts/lint-doc.sh # Lint Markdown - - markdownlint --config .markdownlint.json doc/**/*.md + - markdownlint --config .markdownlint.json 'doc/**/*.md' # Prepare docs for build - mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX - cd /tmp/gitlab-docs diff --git a/.gitlab/ci/ee-specific-checks.gitlab-ci.yml b/.gitlab/ci/ee-specific-checks.gitlab-ci.yml new file mode 100644 index 00000000000..babb89b4606 --- /dev/null +++ b/.gitlab/ci/ee-specific-checks.gitlab-ci.yml @@ -0,0 +1,22 @@ +.ee-specific-check: + extends: .default-tags + dependencies: [] + only: + - branches@gitlab-org/gitlab-ee + except: + - master + - tags + - /[\d-]+-stable(-ee)?/ + - /[\d-]+-auto-deploy-\d{7}/ + - /^security-/ + - /\bce\-to\-ee\b/ + +ee-files-location-check: + extends: .ee-specific-check + script: + - scripts/ee-files-location-check + +ee-specific-lines-check: + extends: .ee-specific-check + script: + - scripts/ee-specific-lines-check diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index efe33049939..5622cd232ca 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -26,7 +26,9 @@ dependency_scanning: extends: .reports dast: - extends: .reports + extends: + - .reports + - .review-only stage: qa dependencies: ["review-deploy"] before_script: diff --git a/.overcommit.yml.example b/.overcommit.yml.example new file mode 100644 index 00000000000..25823b9a8b3 --- /dev/null +++ b/.overcommit.yml.example @@ -0,0 +1,28 @@ +# Use this file to configure the Overcommit hooks you wish to use. This will +# extend the default configuration defined in: +# https://github.com/sds/overcommit/blob/master/config/default.yml +# +# At the topmost level of this YAML file is a key representing type of hook +# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can +# customize each hook, such as whether to only run it on certain files (via +# `include`), whether to only display output if it fails (via `quiet`), etc. +# +# For a complete list of hooks, see: +# https://github.com/sds/overcommit/tree/master/lib/overcommit/hook +# +# For a complete list of options that you can use to customize hooks, see: +# https://github.com/sds/overcommit#configuration +# +# Uncomment the following lines to make the configuration take effect. + +PreCommit: + RuboCop: + enabled: true +# on_warn: fail # Treat all warnings as failures +# +#PostCheckout: +# ALL: # Special hook name that customizes all hooks of this type +# quiet: true # Change all post-checkout hooks to only display output on failure +# +# IndexTags: +# enabled: true # Generate a tags file with `ctags` each time HEAD changes diff --git a/CHANGELOG.md b/CHANGELOG.md index ffca09a92e7..c4d238b2999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 12.2.3 + +### Security (22 changes) + +- Ensure only authorised users can create notes on Merge Requests and Issues. +- Gitaly: ignore git redirects. +- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. +- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth. +- Limit the size of issuable description and comments. +- Send TODOs for comments on commits correctly. +- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds. +- Added image proxy to mitigate potential stealing of IP addresses. +- Filter out old system notes for epics in notes api endpoint response. +- Avoid exposing unaccessible repo data upon GFM post processing. +- Fix HTML injection for label description. +- Make sure HTML text is always escaped when replacing label/milestone references. +- Prevent DNS rebind on JIRA service integration. +- Use admin_group authorization in Groups::RunnersController. +- Prevent disclosure of merge request ID via email. +- Show cross-referenced MR-id in issues' activities only to authorized users. +- Enforce max chars and max render time in markdown math. +- Check permissions before responding in MergeController#pipeline_status. +- Remove EXIF from users/personal snippet uploads. +- Fix project import restricted visibility bypass via API. +- Fix weak session management by clearing password reset tokens after login (username/email) are updated. +- Fix SSRF via DNS rebinding in Kubernetes Integration. + + +## 12.2.2 + +- Unreleased due to QA failure. + ## 12.2.1 ### Fixed (3 changes) @@ -591,6 +623,34 @@ entry. - Removes EE differences for app/views/admin/users/show.html.haml. +## 12.0.7 + +### Security (22 changes) + +- Ensure only authorised users can create notes on Merge Requests and Issues. +- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. +- Queries for Upload should be scoped by model. +- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth. +- Limit the size of issuable description and comments. +- Send TODOs for comments on commits correctly. +- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds. +- Added image proxy to mitigate potential stealing of IP addresses. +- Filter out old system notes for epics in notes api endpoint response. +- Avoid exposing unaccessible repo data upon GFM post processing. +- Fix HTML injection for label description. +- Make sure HTML text is always escaped when replacing label/milestone references. +- Prevent DNS rebind on JIRA service integration. +- Use admin_group authorization in Groups::RunnersController. +- Prevent disclosure of merge request ID via email. +- Show cross-referenced MR-id in issues' activities only to authorized users. +- Enforce max chars and max render time in markdown math. +- Check permissions before responding in MergeController#pipeline_status. +- Remove EXIF from users/personal snippet uploads. +- Fix project import restricted visibility bypass via API. +- Fix weak session management by clearing password reset tokens after login (username/email) are updated. +- Fix SSRF via DNS rebinding in Kubernetes Integration. + + ## 12.0.6 - No changes. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4d5fde5bd16..91951fd8ad7 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.60.0 +1.61.0 diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION index 26aaba0e866..f0bb29e7638 100644 --- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION +++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION @@ -1 +1 @@ -1.2.0 +1.3.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index ccfb75e5120..3c40359d3dc 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -9.4.1 +9.4.2 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index e5c15102d9b..7f6758ef97b 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.9.0 +8.10.0 @@ -215,7 +215,7 @@ gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false gem 'hipchat', '~> 1.5.0' # Jira integration -gem 'jira-ruby', '~> 1.4' +gem 'jira-ruby', '~> 1.7' # Flowdock integration gem 'flowdock', '~> 0.7' diff --git a/Gemfile.lock b/Gemfile.lock index dac68eac5b0..ea2c44a2992 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,8 @@ GEM asciidoctor-plantuml (0.0.9) asciidoctor (>= 1.5.6, < 3.0.0) ast (2.4.0) + atlassian-jwt (0.2.0) + jwt (~> 2.1.0) attr_encrypted (3.1.0) encryptor (~> 3.0.0) attr_required (1.0.1) @@ -444,8 +446,9 @@ GEM opentracing (~> 0.3) thrift jaro_winkler (1.5.3) - jira-ruby (1.4.1) + jira-ruby (1.7.1) activesupport + atlassian-jwt multipart-post oauth (~> 0.5, >= 0.5.0) js_regex (3.1.1) @@ -1124,7 +1127,7 @@ DEPENDENCIES icalendar influxdb (~> 0.2) invisible_captcha (~> 0.12.1) - jira-ruby (~> 1.4) + jira-ruby (~> 1.7) js_regex (~> 3.1) json-schema (~> 2.8.0) jwt (~> 2.1.0) diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index a68936d79e2..b5dbdbb7e86 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,6 +1,5 @@ -import $ from 'jquery'; -import { __ } from '~/locale'; import flash from '~/flash'; +import { s__, sprintf } from '~/locale'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -10,21 +9,131 @@ import flash from '~/flash'; // <code class="js-render-math"></div> // -// Loop over all math elements and render math -function renderWithKaTeX(elements, katex) { - elements.each(function katexElementsLoop() { - const mathNode = $('<span></span>'); - const $this = $(this); - - const display = $this.attr('data-math-style') === 'display'; - try { - katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false }); - mathNode.insertAfter($this); - $this.remove(); - } catch (err) { - throw err; +const MAX_MATH_CHARS = 1000; +const MAX_RENDER_TIME_MS = 2000; + +// These messages might be used with inline errors in the future. Keep them around. For now, we will +// display a single error message using flash(). + +// const CHAR_LIMIT_EXCEEDED_MSG = sprintf( +// s__( +// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.', +// ), +// { maxChars: MAX_MATH_CHARS }, +// ); +// const RENDER_TIME_EXCEEDED_MSG = s__( +// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.", +// ); + +const RENDER_FLASH_MSG = sprintf( + s__( + 'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.', + ), + { maxChars: MAX_MATH_CHARS }, +); + +// Wait for the browser to reflow the layout. Reflowing SVG takes time. +// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object". +const waitForReflow = fn => { + window.requestAnimationFrame(fn); +}; + +/** + * Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown. + */ +class SafeMathRenderer { + /* + How this works: + + The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG. + During this time, the JS is blocked and the page becomes unresponsive. + We want to render math blocks one by one until a certain time is exceeded, after which we stop + rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an + asynchronous task, so we can't time it synchronously. + + SafeMathRenderer essentially does the following: + 1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice. + 2. Places each placeholder element in a queue. + 3. Renders the element at the head of the queue and waits for reflow. + 4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty. + */ + queue = []; + totalMS = 0; + + constructor(elements, katex) { + this.elements = elements; + this.katex = katex; + + this.renderElement = this.renderElement.bind(this); + this.render = this.render.bind(this); + } + + renderElement() { + if (!this.queue.length) { + return; } - }); + + const el = this.queue.shift(); + const text = el.textContent; + + el.removeAttribute('style'); + + if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) { + if (!this.flashShown) { + flash(RENDER_FLASH_MSG); + this.flashShown = true; + } + + // Show unrendered math code + const codeElement = document.createElement('pre'); + codeElement.className = 'code'; + codeElement.textContent = el.textContent; + el.parentNode.replaceChild(codeElement, el); + + // Render the next math + this.renderElement(); + } else { + this.startTime = Date.now(); + + try { + el.innerHTML = this.katex.renderToString(text, { + displayMode: el.getAttribute('data-math-style') === 'display', + throwOnError: true, + maxSize: 20, + maxExpand: 20, + }); + } catch (e) { + // Don't show a flash for now because it would override an existing flash message + el.textContent = s__('math|There was an error rendering this math block'); + // el.style.color = '#d00'; + el.className = 'katex-error'; + } + + // Give the browser time to reflow the svg + waitForReflow(() => { + const deltaTime = Date.now() - this.startTime; + this.totalMS += deltaTime; + + this.renderElement(); + }); + } + } + + render() { + // Replace math blocks with a placeholder so they aren't rendered twice + this.elements.forEach(el => { + const placeholder = document.createElement('span'); + placeholder.style.display = 'none'; + placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style')); + placeholder.textContent = el.textContent; + el.parentNode.replaceChild(placeholder, el); + this.queue.push(placeholder); + }); + + // If we wait for the browser thread to settle down a bit, math rendering becomes 5-10x faster + // and less prone to timeouts. + setTimeout(this.renderElement, 400); + } } export default function renderMath($els) { @@ -34,7 +143,8 @@ export default function renderMath($els) { import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'), ]) .then(([katex]) => { - renderWithKaTeX($els, katex); + const renderer = new SafeMathRenderer($els.get(), katex); + renderer.render(); }) - .catch(() => flash(__('An error occurred while rendering KaTeX'))); + .catch(() => {}); } diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index b84722244d1..71e5d8058da 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -1,10 +1,10 @@ <script> -import Vue from 'vue'; +import axios from '~/lib/utils/axios_utils'; import Flash from '../../../flash'; import { __ } from '../../../locale'; import boardsStore from '../../stores/boards_store'; -export default Vue.extend({ +export default { props: { issue: { type: Object, @@ -35,7 +35,7 @@ export default Vue.extend({ } // Post the remove data - Vue.http.patch(this.updateUrl, data).catch(() => { + axios.patch(this.updateUrl, data).catch(() => { Flash(__('Failed to remove issue from board, please try again.')); lists.forEach(list => { @@ -71,7 +71,7 @@ export default Vue.extend({ return req; }, }, -}); +}; </script> <template> <div class="block list"> diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index ada5a49e246..772f16cab4e 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -55,7 +55,7 @@ export default class ClusterStore { ...applicationInitialState, title: s__('ClusterIntegration|GitLab Runner'), version: null, - chartRepo: 'https://gitlab.com/charts/gitlab-runner', + chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner', updateAvailable: null, updateSuccessful: false, updateFailed: false, diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js index 1339e28d8b8..33c05404493 100644 --- a/app/assets/javascripts/droplab/drop_lab.js +++ b/app/assets/javascripts/droplab/drop_lab.js @@ -60,7 +60,7 @@ class DropLab { addEvents() { this.eventWrapper.documentClicked = this.documentClicked.bind(this); - document.addEventListener('click', this.eventWrapper.documentClicked); + document.addEventListener('mousedown', this.eventWrapper.documentClicked); } documentClicked(e) { @@ -74,7 +74,7 @@ class DropLab { } removeEvents() { - document.removeEventListener('click', this.eventWrapper.documentClicked); + document.removeEventListener('mousedown', this.eventWrapper.documentClicked); } changeHookList(trigger, list, plugins, config) { diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue index 22113692968..500f6737839 100644 --- a/app/assets/javascripts/ide/components/error_message.vue +++ b/app/assets/javascripts/ide/components/error_message.vue @@ -44,7 +44,7 @@ export default { <template> <div class="flash-container flash-container-page" @click="clickFlash"> - <div class="flash-alert"> + <div class="flash-alert" data-qa-selector="flash_alert"> <span v-html="message.text"> </span> <button v-if="message.action" diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 7254c50a568..48be97c8952 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -86,7 +86,7 @@ export default { v-else-if="showChangedFileIcon" :file="file" :show-tooltip="true" - :show-staged-icon="true" + :show-staged-icon="false" /> <new-dropdown :type="file.type" diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 2e6bd85feec..200391282e7 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -89,7 +89,7 @@ export default { </script> <template> - <div class="multi-file-commit-panel ide-right-sidebar"> + <div class="multi-file-commit-panel ide-right-sidebar" data-qa-selector="ide_right_sidebar"> <resizable-panel v-show="isOpen" :collapsible="false" @@ -120,6 +120,7 @@ export default { }" data-container="body" data-placement="left" + :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`" class="ide-sidebar-link is-right" type="button" @click="clickTab($event, tab)" diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 9ca38d6bbfa..88975c2cc73 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -300,9 +300,9 @@ export default { this.closeRecaptcha(); }, - deleteIssuable() { + deleteIssuable(payload) { this.service - .deleteIssuable() + .deleteIssuable(payload) .then(res => res.data) .then(data => { // Stop the poll so we don't get 404's with the issuable not existing diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index eb51a074f84..ce867f16acf 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -55,7 +55,7 @@ export default { if (window.confirm(confirmMessage)) { this.deleteLoading = true; - eventHub.$emit('delete.issuable'); + eventHub.$emit('delete.issuable', { destroy_confirm: true }); } }, }, diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js index 9546eb22c27..3c8334bee50 100644 --- a/app/assets/javascripts/issue_show/services/index.js +++ b/app/assets/javascripts/issue_show/services/index.js @@ -10,8 +10,8 @@ export default class Service { return axios.get(this.realtimeEndpoint); } - deleteIssuable() { - return axios.delete(this.endpoint); + deleteIssuable(payload) { + return axios.delete(this.endpoint, { params: payload }); } updateIssuable(data) { diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 8da87f424c4..ad1072366f3 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -12,7 +12,6 @@ import createStore from '../store'; import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; import ErasedBlock from './erased_block.vue'; -import Log from './job_log.vue'; import LogTopBar from './job_log_controllers.vue'; import StuckBlock from './stuck_block.vue'; import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue'; @@ -30,7 +29,10 @@ export default { EnvironmentsBlock, ErasedBlock, Icon, - Log, + Log: () => + gon && gon.features && gon.features.jobLogJson + ? import('./job_log_json.vue') + : import('./job_log.vue'), LogTopBar, StuckBlock, UnmetPrerequisitesBlock, diff --git a/app/assets/javascripts/jobs/components/job_log_json.vue b/app/assets/javascripts/jobs/components/job_log_json.vue new file mode 100644 index 00000000000..2198b20eb8f --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log_json.vue @@ -0,0 +1,10 @@ +<script> +export default { + name: 'JobLogJSON', +}; +</script> +<template> + <pre> + {{ __('This feature is in development. Please disable the `job_log_json` feature flag') }} + </pre> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index a223a8f5b08..ea867d30ce8 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -144,6 +144,10 @@ export default { visibilityLevelDescription() { return visibilityLevelDescriptions[this.visibilityLevel]; }, + + showContainerRegistryPublicNote() { + return this.visibilityLevel === visibilityOptions.PUBLIC; + }, }, watch: { @@ -286,6 +290,9 @@ export default { label="Container registry" help-text="Every project can have its own space to store its Docker images" > + <div v-if="showContainerRegistryPublicNote" class="text-muted"> + {{ __('Note: the container registry is always visible when a project is public') }} + </div> <project-feature-toggle v-model="containerRegistryEnabled" :disabled-input="!repositoryEnabled" diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 60d3d83a4b2..765cb868f80 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -113,7 +113,7 @@ export default class ProjectFindFile { if (searchText) { matches = fuzzaldrinPlus.match(filePath, searchText); } - blobItemUrl = this.options.blobUrlTemplate + '/' + filePath; + blobItemUrl = this.options.blobUrlTemplate + '/' + encodeURIComponent(filePath); html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); results.push(this.element.find('.tree-table > tbody').append(html)); } diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 7580c2d0ad0..88b6b4732b1 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -53,7 +53,7 @@ export default { }; </script> <template> - <div class="card"> + <div :id="release.tag_name" class="card"> <div class="card-body"> <h2 class="card-title mt-0"> {{ release.name }} diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 3e65bdf0cb0..6f6d145815e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -5,11 +5,7 @@ export const WARNING_MESSAGE_CLASS = 'warning_message'; export const DANGER_MESSAGE_CLASS = 'danger_message'; export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds'; -export const ATMTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds'; +export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds'; export const MT_MERGE_STRATEGY = 'merge_train'; -export const AUTO_MERGE_STRATEGIES = [ - MWPS_MERGE_STRATEGY, - ATMTWPS_MERGE_STRATEGY, - MT_MERGE_STRATEGY, -]; +export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY]; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 7843409f4a7..699d41494bf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -3,7 +3,7 @@ import _ from 'underscore'; import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; -import { ATMTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants'; +import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants'; export default class MergeRequestStore { constructor(data) { @@ -217,8 +217,8 @@ export default class MergeRequestStore { } static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) { - if (_.includes(availableAutoMergeStrategies, ATMTWPS_MERGE_STRATEGY)) { - return ATMTWPS_MERGE_STRATEGY; + if (_.includes(availableAutoMergeStrategies, MTWPS_MERGE_STRATEGY)) { + return MTWPS_MERGE_STRATEGY; } else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) { return MT_MERGE_STRATEGY; } else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) { diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index beb2ac09992..a97538d813a 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -24,7 +24,7 @@ export default { showStagedIcon: { type: Boolean, required: false, - default: false, + default: true, }, size: { type: Number, @@ -41,7 +41,7 @@ export default { changedIcon() { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings - const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; + const suffix = !this.file.changed && this.file.staged && this.showStagedIcon ? '-solid' : ''; return `${getCommitIconMap(this.file).icon}${suffix}`; }, diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index afcc7f8a1db..33caac4d725 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -384,8 +384,18 @@ @extend .fa-exclamation-circle; } } -} + .prometheus-graph-embed { + h3.popover-header { + /** Override <h3> .popover-header + * as embed metrics do not follow the same + * style as default md <h3> (which are deeply nested) + */ + margin: 0; + font-size: $gl-font-size-small; + } + } +} /** * Headers diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 74380ec995a..2d2f0c531c7 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -45,8 +45,7 @@ input[type='checkbox']:hover { border: 0; border-radius: $border-radius-default; transition: border-color ease-in-out $default-transition-duration, - background-color ease-in-out $default-transition-duration, - width ease-in-out $default-transition-duration; + background-color ease-in-out $default-transition-duration; @include media-breakpoint-up(xl) { width: $search-input-xl-width; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index af6644b8fcc..2f7ac41781a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -47,8 +47,8 @@ class ApplicationController < ActionController::Base # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security # concerns due to caching private data. - DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze - DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache".freeze + DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store" + DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache" rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -143,7 +143,7 @@ class ApplicationController < ActionController::Base payload[:username] = logged_user.try(:username) end - if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze + if response.status == 422 && response.body.present? && response.content_type == 'application/json' payload[:response] = response.body end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index b86e4451a7e..e537c11096c 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -6,6 +6,7 @@ module IssuableActions included do before_action :authorize_destroy_issuable!, only: :destroy + before_action :check_destroy_confirmation!, only: :destroy before_action :authorize_admin_issuable!, only: :bulk_update before_action only: :show do push_frontend_feature_flag(:scoped_labels, default_enabled: true) @@ -91,6 +92,33 @@ module IssuableActions end end + def check_destroy_confirmation! + return true if params[:destroy_confirm] + + error_message = "Destroy confirmation not provided for #{issuable.human_class_name}" + exception = RuntimeError.new(error_message) + Gitlab::Sentry.track_acceptable_exception( + exception, + extra: { + project_path: issuable.project.full_path, + issuable_type: issuable.class.name, + issuable_id: issuable.id + } + ) + + index_path = polymorphic_path([parent, issuable.class]) + + respond_to do |format| + format.html do + flash[:notice] = error_message + redirect_to index_path + end + format.json do + render json: { errors: error_message }, status: :unprocessable_entity + end + end + end + def bulk_update result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name) quantity = result[:count] @@ -110,7 +138,7 @@ module IssuableActions end notes = prepare_notes_for_rendering(notes) - notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } + notes = notes.select { |n| n.visible_for?(current_user) } discussions = Discussion.build_collection(notes, issuable) diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index f7137a04437..bff0715f192 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -12,7 +12,7 @@ module LfsRequest extend ActiveSupport::Concern - CONTENT_TYPE = 'application/vnd.git-lfs+json'.freeze + CONTENT_TYPE = 'application/vnd.git-lfs+json' included do before_action :require_lfs_enabled! diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 4b7899d469b..fbae4c53c31 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -29,7 +29,7 @@ module NotesActions end notes = prepare_notes_for_rendering(notes) - notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } + notes = notes.select { |n| n.visible_for?(current_user) } notes_json[:notes] = if use_note_serializer? diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index f5d35379e10..60a68cec3c3 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -127,4 +127,8 @@ module UploadsActions def model strong_memoize(:model) { find_model } end + + def workhorse_authorize_request? + action_name == 'authorize' + end end diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index f8e32451b02..af2b2cbd1fd 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -3,7 +3,7 @@ class Groups::RunnersController < Groups::ApplicationController # Proper policies should be implemented per # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 - before_action :authorize_admin_pipeline! + before_action :authorize_admin_group! before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] @@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController @runner ||= @group.runners.find(params[:id]) end - def authorize_admin_pipeline! - return render_404 unless can?(current_user, :admin_pipeline, group) - end - def runner_params params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index adbc0159358..06d7579aff4 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -11,6 +11,9 @@ class Projects::JobsController < Projects::ApplicationController before_action :authorize_erase_build!, only: [:erase] before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize] before_action :verify_api_request!, only: :terminal_websocket_authorize + before_action only: [:trace] do + push_frontend_feature_flag(:job_log_json) + end layout 'project' @@ -64,6 +67,14 @@ class Projects::JobsController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord def trace + if Feature.enabled?(:job_log_json, @project) + json_trace + else + html_trace + end + end + + def html_trace build.trace.read do |stream| respond_to do |format| format.json do @@ -84,6 +95,10 @@ class Projects::JobsController < Projects::ApplicationController end end + def json_trace + # will be implemented with https://gitlab.com/gitlab-org/gitlab-ce/issues/66454 + end + def retry return respond_422 unless @build.retryable? diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 42c415757f9..c16736a756a 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -3,7 +3,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController include LfsRequest - LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'.freeze + LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream' skip_before_action :lfs_check_access!, only: [:deprecated] before_action :lfs_check_batch_operation!, only: [:batch] diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d492c5227cf..ea1dd7d19d5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo skip_before_action :merge_request, only: [:index, :bulk_update] before_action :whitelist_query_limiting, only: [:assign_related_issues, :update] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :authorize_test_reports!, only: [:test_reports] before_action :set_issuables_index, only: [:index] before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] @@ -189,7 +190,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo def pipeline_status render json: PipelineSerializer .new(project: @project, current_user: @current_user) - .represent_status(@merge_request.head_pipeline) + .represent_status(head_pipeline) end def ci_environments_status @@ -239,6 +240,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private + def head_pipeline + strong_memoize(:head_pipeline) do + pipeline = @merge_request.head_pipeline + pipeline if can?(current_user, :read_pipeline, pipeline) + end + end + def ci_environments_status_on_merge_result? params[:environment_target] == 'merge_commit' end @@ -337,4 +345,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo render json: { status_reason: 'Unknown error' }, status: :internal_server_error end end + + def authorize_test_reports! + # MergeRequest#actual_head_pipeline is the pipeline accessed in MergeRequest#compare_reports. + return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline) + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1880bead3ee..a6dd811ab8b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -21,10 +21,13 @@ class SessionsController < Devise::SessionsController prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? } before_action :auto_sign_in_with_provider, only: [:new] + before_action :store_unauthenticated_sessions, only: [:new] + before_action :save_failed_login, if: :action_new_and_failed_login? before_action :load_recaptcha - after_action :log_failed_login, if: -> { action_name == 'new' && failed_login? } - helper_method :captcha_enabled? + after_action :log_failed_login, if: :action_new_and_failed_login? + + helper_method :captcha_enabled?, :captcha_on_login_required? # protect_from_forgery is already prepended in ApplicationController but # authenticate_with_two_factor which signs in the user is prepended before @@ -37,7 +40,8 @@ class SessionsController < Devise::SessionsController # token mismatch. protect_from_forgery with: :exception, prepend: true - CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze + CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha' + MAX_FAILED_LOGIN_ATTEMPTS = 5 def new set_minimum_password_length @@ -81,10 +85,14 @@ class SessionsController < Devise::SessionsController request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled? end + def captcha_on_login_required? + Gitlab::Recaptcha.enabled_on_login? && unverified_anonymous_user? + end + # From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller def check_captcha return unless user_params[:password].present? - return unless captcha_enabled? + return unless captcha_enabled? || captcha_on_login_required? return unless Gitlab::Recaptcha.load_configurations! if verify_recaptcha @@ -103,14 +111,14 @@ class SessionsController < Devise::SessionsController def increment_failed_login_captcha_counter Gitlab::Metrics.counter( :failed_login_captcha_total, - 'Number of failed CAPTCHA attempts for logins'.freeze + 'Number of failed CAPTCHA attempts for logins' ).increment end def increment_successful_login_captcha_counter Gitlab::Metrics.counter( :successful_login_captcha_total, - 'Number of successful CAPTCHA attempts for logins'.freeze + 'Number of successful CAPTCHA attempts for logins' ).increment end @@ -126,10 +134,28 @@ class SessionsController < Devise::SessionsController Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}") end + def action_new_and_failed_login? + action_name == 'new' && failed_login? + end + + def save_failed_login + session[:failed_login_attempts] ||= 0 + session[:failed_login_attempts] += 1 + end + def failed_login? (options = request.env["warden.options"]) && options[:action] == "unauthenticated" end + # storing sessions per IP lets us check if there are associated multiple + # anonymous sessions with one IP and prevent situations when there are + # multiple attempts of logging in + def store_unauthenticated_sessions + return if current_user + + Gitlab::AnonymousSession.new(request.remote_ip, session_id: request.session.id).store_session_id_per_ip + end + # Handle an "initial setup" state, where there's only one user, it's an admin, # and they require a password change. # rubocop: disable CodeReuse/ActiveRecord @@ -240,6 +266,18 @@ class SessionsController < Devise::SessionsController @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers end + def unverified_anonymous_user? + exceeded_failed_login_attempts? || exceeded_anonymous_sessions? + end + + def exceeded_failed_login_attempts? + session.fetch(:failed_login_attempts, 0) > MAX_FAILED_LOGIN_ATTEMPTS + end + + def exceeded_anonymous_sessions? + Gitlab::AnonymousSession.new(request.remote_ip).stored_sessions >= MAX_FAILED_LOGIN_ATTEMPTS + end + def authentication_method if user_params[:otp_attempt] "two-factor" diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 94bd18f70d4..2adfeab182e 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -2,6 +2,7 @@ class UploadsController < ApplicationController include UploadsActions + include WorkhorseRequest UnknownUploadModelError = Class.new(StandardError) @@ -21,7 +22,8 @@ class UploadsController < ApplicationController before_action :upload_mount_satisfied? before_action :find_model before_action :authorize_access!, only: [:show] - before_action :authorize_create_access!, only: [:create] + before_action :authorize_create_access!, only: [:create, :authorize] + before_action :verify_workhorse_api!, only: [:authorize] def uploader_class PersonalFileUploader @@ -72,7 +74,7 @@ class UploadsController < ApplicationController end def render_unauthorized - if current_user + if current_user || workhorse_authorize_request? render_404 else authenticate_user! diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 0ab19f1d2d2..84021d0da56 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -164,6 +164,10 @@ module ApplicationSettingsHelper :allow_local_requests_from_system_hooks, :dns_rebinding_protection_enabled, :archive_builds_in_human_readable, + :asset_proxy_enabled, + :asset_proxy_secret_key, + :asset_proxy_url, + :asset_proxy_whitelist, :authorized_keys_enabled, :auto_devops_enabled, :auto_devops_domain, @@ -231,6 +235,7 @@ module ApplicationSettingsHelper :recaptcha_enabled, :recaptcha_private_key, :recaptcha_site_key, + :login_recaptcha_protection_enabled, :receive_max_input_size, :repository_checks_enabled, :repository_storages, diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 81ff359556d..b7f7e617825 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -56,13 +56,13 @@ module AvatarsHelper })) end - def user_avatar_url_for(options = {}) + def user_avatar_url_for(only_path: true, **options) if options[:url] options[:url] elsif options[:user] - avatar_icon_for_user(options[:user], options[:size]) + avatar_icon_for_user(options[:user], options[:size], only_path: only_path) else - avatar_icon_for_email(options[:user_email], options[:size]) + avatar_icon_for_email(options[:user_email], options[:size], only_path: only_path) end end @@ -75,6 +75,7 @@ module AvatarsHelper has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip] data_attributes = options[:data] || {} css_class = %W[avatar s#{avatar_size}].push(*options[:css_class]) + alt_text = user_name ? "#{user_name}'s avatar" : "default avatar" if has_tooltip css_class.push('has-tooltip') @@ -88,7 +89,7 @@ module AvatarsHelper end image_options = { - alt: "#{user_name}'s avatar", + alt: alt_text, src: avatar_url, data: data_attributes, class: css_class, diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 36122d3a22a..23596769738 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -90,6 +90,8 @@ module EmailsHelper when MergeRequest merge_request = MergeRequest.find(closed_via[:id]).present + return "" unless Ability.allowed?(@recipient, :read_merge_request, merge_request) + case format when :html merge_request_link = link_to(merge_request.to_reference, merge_request.web_url) @@ -102,6 +104,8 @@ module EmailsHelper # Technically speaking this should be Commit but per # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339 # we can't deserialize Commit without custom serializer for ActiveJob + return "" unless Ability.allowed?(@recipient, :download_code, @project) + _("via %{closed_via}") % { closed_via: closed_via } else "" diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 2ed016beea4..c5a3507637e 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -71,7 +71,7 @@ module LabelsHelper end def label_tooltip_title(label) - label.description + Sanitize.clean(label.description) end def suggested_colors diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 33bf2d57fae..14f947a03a3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -448,7 +448,7 @@ module ProjectsHelper def git_user_email if current_user - current_user.email + current_user.commit_email else "your@email.com" end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 91c83380b62..2e2d324ab62 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -30,7 +30,46 @@ module SearchHelper to = collection.offset_value + collection.to_a.size count = collection.total_count - s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"") % { from: from, to: to, count: count, scope: scope.humanize(capitalize: false), term: term } + search_entries_info_template(collection) % { + from: from, + to: to, + count: count, + scope: search_entries_info_label(scope, count), + term: term + } + end + + def search_entries_info_label(scope, count) + case scope + when 'blobs', 'snippet_blobs', 'wiki_blobs' + ns_('SearchResults|result', 'SearchResults|results', count) + when 'commits' + ns_('SearchResults|commit', 'SearchResults|commits', count) + when 'issues' + ns_('SearchResults|issue', 'SearchResults|issues', count) + when 'merge_requests' + ns_('SearchResults|merge request', 'SearchResults|merge requests', count) + when 'milestones' + ns_('SearchResults|milestone', 'SearchResults|milestones', count) + when 'notes' + ns_('SearchResults|comment', 'SearchResults|comments', count) + when 'projects' + ns_('SearchResults|project', 'SearchResults|projects', count) + when 'snippet_titles' + ns_('SearchResults|snippet', 'SearchResults|snippets', count) + when 'users' + ns_('SearchResults|user', 'SearchResults|users', count) + else + raise "Unrecognized search scope '#{scope}'" + end + end + + def search_entries_info_template(collection) + if collection.total_pages > 1 + s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"") + else + s_("SearchResults|Showing %{count} %{scope} for \"%{term}\"") + end end def find_project_for_result_blob(projects, result) diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index f3a3203f7ad..47d15836da0 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -11,7 +11,7 @@ module Emails def issue_due_email(recipient_id, issue_id, reason = nil) setup_issue_mail(issue_id, recipient_id) - mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) + mail_answer_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) end def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) @@ -34,6 +34,8 @@ module Emails setup_issue_mail(issue_id, recipient_id, closed_via: closed_via) @updated_by = User.find(updated_by_user_id) + @recipient = User.find(recipient_id) + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2a99c6e5c59..e39d655325f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -4,7 +4,6 @@ class ApplicationSetting < ApplicationRecord include CacheableAttributes include CacheMarkdownField include TokenAuthenticatable - include IgnorableColumn include ChronicDurationAttribute add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required } @@ -18,19 +17,28 @@ class ApplicationSetting < ApplicationRecord # fix a lot of tests using allow_any_instance_of include ApplicationSettingImplementation + attr_encrypted :asset_proxy_secret_key, + mode: :per_attribute_iv, + insecure_mode: true, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc' + serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize + serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize - ignore_column :koding_url - ignore_column :koding_enabled - ignore_column :sentry_enabled - ignore_column :sentry_dsn - ignore_column :clientside_sentry_enabled - ignore_column :clientside_sentry_dsn + self.ignored_columns += %i[ + clientside_sentry_dsn + clientside_sentry_enabled + koding_enabled + koding_url + sentry_dsn + sentry_enabled + ] cache_markdown_field :sign_in_text cache_markdown_field :help_page_text @@ -75,11 +83,11 @@ class ApplicationSetting < ApplicationRecord validates :recaptcha_site_key, presence: true, - if: :recaptcha_enabled + if: :recaptcha_or_login_protection_enabled validates :recaptcha_private_key, presence: true, - if: :recaptcha_enabled + if: :recaptcha_or_login_protection_enabled validates :akismet_api_key, presence: true, @@ -192,6 +200,17 @@ class ApplicationSetting < ApplicationRecord allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } + validates :asset_proxy_url, + presence: true, + allow_blank: false, + url: true, + if: :asset_proxy_enabled? + + validates :asset_proxy_secret_key, + presence: true, + allow_blank: false, + if: :asset_proxy_enabled? + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -292,4 +311,8 @@ class ApplicationSetting < ApplicationRecord def self.cache_backend Gitlab::ThreadMemoryCache.cache_backend end + + def recaptcha_or_login_protection_enabled + recaptcha_enabled || login_recaptcha_protection_enabled + end end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 55ac1e129cf..f402c0e2775 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -23,8 +23,9 @@ module ApplicationSettingImplementation akismet_enabled: false, allow_local_requests_from_web_hooks_and_services: false, allow_local_requests_from_system_hooks: true, - dns_rebinding_protection_enabled: true, + asset_proxy_enabled: false, authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand + commit_email_hostname: default_commit_email_hostname, container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], @@ -33,7 +34,9 @@ module ApplicationSettingImplementation default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, disabled_oauth_sign_in_sources: [], + dns_rebinding_protection_enabled: true, domain_whitelist: Settings.gitlab['domain_whitelist'], dsa_key_restriction: 0, ecdsa_key_restriction: 0, @@ -52,9 +55,11 @@ module ApplicationSettingImplementation housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, import_sources: Settings.gitlab['import_sources'], + local_markdown_version: 0, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], mirror_available: true, + outbound_local_requests_whitelist: [], password_authentication_enabled_for_git: true, password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], performance_bar_allowed_group_id: nil, @@ -63,7 +68,10 @@ module ApplicationSettingImplementation plantuml_url: nil, polling_interval_multiplier: 1, project_export_enabled: true, + protected_ci_variables: false, + raw_blob_request_limit: 300, recaptcha_enabled: false, + login_recaptcha_protection_enabled: false, repository_checks_enabled: true, repository_storages: ['default'], require_two_factor_authentication: false, @@ -95,16 +103,10 @@ module ApplicationSettingImplementation user_default_internal_regex: nil, user_show_add_ssh_key_message: true, usage_stats_set_by_user_id: nil, - diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, - commit_email_hostname: default_commit_email_hostname, snowplow_collector_hostname: nil, snowplow_cookie_domain: nil, snowplow_enabled: false, - snowplow_site_id: nil, - protected_ci_variables: false, - local_markdown_version: 0, - outbound_local_requests_whitelist: [], - raw_blob_request_limit: 300 + snowplow_site_id: nil } end @@ -198,6 +200,15 @@ module ApplicationSettingImplementation end end + def asset_proxy_whitelist=(values) + values = domain_strings_to_array(values) if values.is_a?(String) + + # make sure we always whitelist the running host + values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host) + + self[:asset_proxy_whitelist] = values + end + def repository_storages Array(read_attribute(:repository_storages)) end @@ -306,6 +317,7 @@ module ApplicationSettingImplementation values .split(DOMAIN_LIST_SEPARATOR) + .map(&:strip) .reject(&:empty?) .uniq end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 0ab302a0f3e..24fcb97db6e 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class AwardEmoji < ApplicationRecord - DOWNVOTE_NAME = "thumbsdown".freeze - UPVOTE_NAME = "thumbsup".freeze + DOWNVOTE_NAME = "thumbsdown" + UPVOTE_NAME = "thumbsup" include Participable include GhostUser diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb index df6b9bb2f0b..1c3a6599f36 100644 --- a/app/models/blob_viewer/base.rb +++ b/app/models/blob_viewer/base.rb @@ -2,7 +2,7 @@ module BlobViewer class Base - PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze + PARTIAL_PATH_PREFIX = 'projects/blob/viewers' class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_types, :load_async, :binary, :switcher_icon, :switcher_title, :collapse_limit, :size_limit diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index da4584228ce..1338a585c9e 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -16,7 +16,7 @@ class BroadcastMessage < ApplicationRecord default_value_for :color, '#E75E40' default_value_for :font, '#FFFFFF' - CACHE_KEY = 'broadcast_message_current_json'.freeze + CACHE_KEY = 'broadcast_message_current_json' after_commit :flush_redis_cache diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7930bef5cf2..d558f66154e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -11,19 +11,20 @@ module Ci include ObjectStorage::BackgroundMove include Presentable include Importable - include IgnorableColumn include Gitlab::Utils::StrongMemoize include Deployable include HasRef BuildArchivedError = Class.new(StandardError) - ignore_column :commands - ignore_column :artifacts_file - ignore_column :artifacts_metadata - ignore_column :artifacts_file_store - ignore_column :artifacts_metadata_store - ignore_column :artifacts_size + self.ignored_columns += %i[ + artifacts_file + artifacts_file_store + artifacts_metadata + artifacts_metadata_store + artifacts_size + commands + ] belongs_to :project, inverse_of: :builds belongs_to :runner @@ -444,7 +445,7 @@ module Ci end end - CI_REGISTRY_USER = 'gitlab-ci-token'.freeze + CI_REGISTRY_USER = 'gitlab-ci-token' def persisted_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 997bf298025..8075c15bbaf 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -6,7 +6,7 @@ module Ci class BuildRunnerSession < ApplicationRecord extend Gitlab::Ci::Model - TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'.freeze + TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' self.table_name = 'ci_builds_runner_session' diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0a943a33bbb..64e372878e6 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -203,6 +203,7 @@ module Ci scope :for_sha, -> (sha) { where(sha: sha) } scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) } scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } + scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } scope :triggered_by_merge_request, -> (merge_request) do where(source: :merge_request_event, merge_request: merge_request) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 1c1c7a5ae7a..e0e905ebfa8 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -4,7 +4,6 @@ module Ci class Runner < ApplicationRecord extend Gitlab::Ci::Model include Gitlab::SQL::Pattern - include IgnorableColumn include RedisCacheable include ChronicDurationAttribute include FromUnion @@ -36,7 +35,7 @@ module Ci FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze - ignore_column :is_shared + self.ignored_columns = %i[is_shared] has_many :builds has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 6bd7473c8ff..27d4180e5b9 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -3,7 +3,8 @@ module Clusters module Applications class CertManager < ApplicationRecord - VERSION = 'v0.5.2'.freeze + VERSION = 'v0.9.1' + CRD_VERSION = '0.9' self.table_name = 'clusters_applications_cert_managers' @@ -21,16 +22,22 @@ module Clusters validates :email, presence: true def chart - 'stable/cert-manager' + 'certmanager/cert-manager' + end + + def repository + 'https://charts.jetstack.io' end def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: 'certmanager', + repository: repository, version: VERSION, rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files.merge(cluster_issuer_file), + preinstall: pre_install_script, postinstall: post_install_script ) end @@ -46,16 +53,30 @@ module Clusters private + def pre_install_script + [ + apply_file("https://raw.githubusercontent.com/jetstack/cert-manager/release-#{CRD_VERSION}/deploy/manifests/00-crds.yaml"), + "kubectl label --overwrite namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} certmanager.k8s.io/disable-validation=true" + ] + end + def post_install_script - ["kubectl create -f /data/helm/certmanager/config/cluster_issuer.yaml"] + [retry_command(apply_file('/data/helm/certmanager/config/cluster_issuer.yaml'))] + end + + def retry_command(command) + "for i in $(seq 1 30); do #{command} && break; sleep 1s; echo \"Retrying ($i)...\"; done" end def post_delete_script [ delete_private_key, delete_crd('certificates.certmanager.k8s.io'), + delete_crd('certificaterequests.certmanager.k8s.io'), + delete_crd('challenges.certmanager.k8s.io'), delete_crd('clusterissuers.certmanager.k8s.io'), - delete_crd('issuers.certmanager.k8s.io') + delete_crd('issuers.certmanager.k8s.io'), + delete_crd('orders.certmanager.k8s.io') ].compact end @@ -75,6 +96,10 @@ module Clusters Gitlab::Kubernetes::KubectlCmd.delete("crd", definition, "--ignore-not-found") end + def apply_file(filename) + Gitlab::Kubernetes::KubectlCmd.apply_file(filename) + end + def cluster_issuer_file { 'cluster_issuer.yaml': cluster_issuer_yaml_content diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 1430b82c2f2..50def3ba38c 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Ingress < ApplicationRecord - VERSION = '1.1.2'.freeze + VERSION = '1.1.2' self.table_name = 'clusters_applications_ingress' diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 9ede0615fa3..fb74d96efe3 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -5,7 +5,7 @@ require 'securerandom' module Clusters module Applications class Jupyter < ApplicationRecord - VERSION = '0.9-174bbd5'.freeze + VERSION = '0.9-174bbd5' self.table_name = 'clusters_applications_jupyter' diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 244fe738396..a9b9374622d 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -3,9 +3,9 @@ module Clusters module Applications class Knative < ApplicationRecord - VERSION = '0.6.0'.freeze - REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze - METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze + VERSION = '0.6.0' + REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts' + METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml' FETCH_IP_ADDRESS_DELAY = 30.seconds API_RESOURCES_PATH = 'config/knative/api_resources.yml' diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 329250255fd..2d6af8f4f0b 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.8.0'.freeze + VERSION = '0.8.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 97d39491b73..444e1a82c97 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -20,8 +20,8 @@ module Clusters Applications::Runner.application_name => Applications::Runner, Applications::Prometheus.application_name => Applications::Prometheus }.merge(PROJECT_ONLY_APPLICATIONS).freeze - DEFAULT_ENVIRONMENT = '*'.freeze - KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze + DEFAULT_ENVIRONMENT = '*' + KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN' belongs_to :user diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index 0c800621a55..d459af23a2f 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -11,7 +11,7 @@ module CacheableAttributes class_methods do def cache_key - "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze + "#{name}:#{Gitlab::VERSION}:#{Rails.version}" end # Can be overridden diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 71ebb586c13..cf88076ac74 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -3,7 +3,7 @@ module HasStatus extend ActiveSupport::Concern - DEFAULT_STATUS = 'created'.freeze + DEFAULT_STATUS = 'created' BLOCKED_STATUS = %w[manual scheduled].freeze AVAILABLE_STATUSES = %w[created preparing pending running success failed canceled skipped manual scheduled].freeze STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb deleted file mode 100644 index 3bec44dc79b..00000000000 --- a/app/models/concerns/ignorable_column.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -# Module that can be included into a model to make it easier to ignore database -# columns. -# -# Example: -# -# class User < ApplicationRecord -# include IgnorableColumn -# -# ignore_column :updated_at -# end -# -module IgnorableColumn - extend ActiveSupport::Concern - - class_methods do - def columns - super.reject { |column| ignored_columns.include?(column.name) } - end - - def ignored_columns - @ignored_columns ||= Set.new - end - - def ignore_column(*names) - ignored_columns.merge(names.map(&:to_s)) - end - end -end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index db46d7afbb9..eefe9f00836 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -73,6 +73,7 @@ module Issuable validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } + validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true validate :milestone_is_valid scope :authored, ->(user) { where(author_id: user) } diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 583751ea6ac..208937f2aff 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -4,9 +4,9 @@ module ProtectedRefAccess extend ActiveSupport::Concern HUMAN_ACCESS_LEVELS = { - Gitlab::Access::MAINTAINER => "Maintainers".freeze, - Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze, - Gitlab::Access::NO_ACCESS => "No one".freeze + Gitlab::Access::MAINTAINER => "Maintainers", + Gitlab::Access::DEVELOPER => "Developers + Maintainers", + Gitlab::Access::NO_ACCESS => "No one" }.freeze class_methods do diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 116e8967651..3a486632800 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -33,8 +33,17 @@ module Routable # # Returns a single object, or nil. def find_by_full_path(path, follow_redirects: false) - order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)") - found = where_full_path_in([path]).reorder(order_sql).take + increment_counter(:routable_find_by_full_path, 'Number of calls to Routable.find_by_full_path') + + if Feature.enabled?(:routable_two_step_lookup) + # Case sensitive match first (it's cheaper and the usual case) + # If we didn't have an exact match, we perform a case insensitive search + found = joins(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take + else + order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)") + found = where_full_path_in([path]).reorder(order_sql).take + end + return found if found if follow_redirects @@ -52,12 +61,23 @@ module Routable def where_full_path_in(paths) return none if paths.empty? + increment_counter(:routable_where_full_path_in, 'Number of calls to Routable.where_full_path_in') + wheres = paths.map do |path| "(LOWER(routes.path) = LOWER(#{connection.quote(path)}))" end joins(:route).where(wheres.join(' OR ')) end + + # Temporary instrumentation of method calls + def increment_counter(counter, description) + @counters[counter] ||= Gitlab::Metrics.counter(counter, description) + + @counters[counter].increment + rescue + # ignore the error + end end def full_name diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index 8b536a123fc..98842242eb6 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -9,8 +9,8 @@ require 'task_list/filter' # # Used by MergeRequest and Issue module Taskable - COMPLETED = 'completed'.freeze - INCOMPLETE = 'incomplete'.freeze + COMPLETED = 'completed' + INCOMPLETE = 'incomplete' COMPLETE_PATTERN = /(\[[xX]\])/.freeze INCOMPLETE_PATTERN = /(\[[\s]\])/.freeze ITEM_PATTERN = %r{ diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 0bd90bd28e3..22ab326a0ab 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DeployKey < Key - include IgnorableColumn include FromUnion has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -11,7 +10,7 @@ class DeployKey < Key scope :are_public, -> { where(public: true) } scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, :namespace] }) } - ignore_column :can_push + self.ignored_columns += %i[can_push] accepts_nested_attributes_for :deploy_keys_projects diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 85f5a2040c0..20e1d802178 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -8,7 +8,7 @@ class DeployToken < ApplicationRecord add_authentication_token_field :token, encrypted: :optional AVAILABLE_SCOPES = %i(read_repository read_registry).freeze - GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze + GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token' default_value_for(:expires_at) { Forever.date } diff --git a/app/models/diff_viewer/base.rb b/app/models/diff_viewer/base.rb index 527ee33b83b..22c8fe73563 100644 --- a/app/models/diff_viewer/base.rb +++ b/app/models/diff_viewer/base.rb @@ -2,7 +2,7 @@ module DiffViewer class Base - PARTIAL_PATH_PREFIX = 'projects/diffs/viewers'.freeze + PARTIAL_PATH_PREFIX = 'projects/diffs/viewers' class_attribute :partial_name, :type, :extensions, :file_types, :binary, :switcher_icon, :switcher_title diff --git a/app/models/event.rb b/app/models/event.rb index 738080eb584..392d7368033 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -2,7 +2,6 @@ class Event < ApplicationRecord include Sortable - include IgnorableColumn include FromUnion default_scope { reorder(nil) } diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 116beac5c2a..995baf8565c 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class GpgKey < ApplicationRecord - KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze - KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze + KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----' + KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----' include ShaAttribute diff --git a/app/models/group.rb b/app/models/group.rb index 6c868b1d1f0..abe93cf3c84 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -15,6 +15,8 @@ class Group < Namespace include WithUploads include Gitlab::Utils::StrongMemoize + ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 + has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members has_many :users, through: :group_members @@ -365,6 +367,8 @@ class Group < Namespace end def max_member_access_for_user(user) + return GroupMember::NO_ACCESS unless user + return GroupMember::OWNER if user.admin? members_with_parents @@ -427,6 +431,10 @@ class Group < Namespace super || ::Gitlab::Access::OWNER_SUBGROUP_ACCESS end + def access_request_approvers_to_be_notified + members.owners.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) + end + private def update_two_factor_requirement diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb index a9b1962f24c..f401c23e453 100644 --- a/app/models/instance_configuration.rb +++ b/app/models/instance_configuration.rb @@ -4,8 +4,8 @@ require 'resolv' class InstanceConfiguration SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze - SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze - CACHE_KEY = 'instance_configuration'.freeze + SSH_ALGORITHMS_PATH = '/etc/ssh/' + CACHE_KEY = 'instance_configuration' EXPIRATION_TIME = 24.hours def settings diff --git a/app/models/issue.rb b/app/models/issue.rb index caea8eadd18..75d4fc8c1c5 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -178,7 +178,7 @@ class Issue < ApplicationRecord end def moved? - !moved_to.nil? + !moved_to_id.nil? end def can_move?(user, to_project = nil) diff --git a/app/models/label.rb b/app/models/label.rb index d9455b36242..dc9f0a3d1a9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -199,7 +199,11 @@ class Label < ApplicationRecord end def title=(value) - write_attribute(:title, sanitize_title(value)) if value.present? + write_attribute(:title, sanitize_value(value)) if value.present? + end + + def description=(value) + write_attribute(:description, sanitize_value(value)) if value.present? end ## @@ -260,7 +264,7 @@ class Label < ApplicationRecord end end - def sanitize_title(value) + def sanitize_value(value) CGI.unescapeHTML(Sanitize.clean(value.to_s)) end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 3d6f397e599..ed5832ff989 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -3,7 +3,7 @@ class GroupMember < Member include FromUnion - SOURCE_TYPE = 'Namespace'.freeze + SOURCE_TYPE = 'Namespace' belongs_to :group, foreign_key: 'source_id' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index c64e2669b6a..2bb5806cd21 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ProjectMember < Member - SOURCE_TYPE = 'Project'.freeze + SOURCE_TYPE = 'Project' belongs_to :project, foreign_key: 'source_id' diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 2c9dbf2585c..2402fa8e38f 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -4,7 +4,6 @@ class MergeRequestDiff < ApplicationRecord include Sortable include Importable include ManualInverseAssociation - include IgnorableColumn include EachBatch include Gitlab::Utils::StrongMemoize include ObjectStorage::BackgroundMove diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb index 61a7eb4b576..ed61c807519 100644 --- a/app/models/namespace/aggregation_schedule.rb +++ b/app/models/namespace/aggregation_schedule.rb @@ -7,7 +7,7 @@ class Namespace::AggregationSchedule < ApplicationRecord self.primary_key = :namespace_id DEFAULT_LEASE_TIMEOUT = 1.5.hours.to_i - REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'.freeze + REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay' belongs_to :namespace diff --git a/app/models/note.rb b/app/models/note.rb index a12d1eb7243..ebd13675dc9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -14,7 +14,6 @@ class Note < ApplicationRecord include CacheMarkdownField include AfterCommitQueue include ResolvableNote - include IgnorableColumn include Editable include Gitlab::SQL::Pattern include ThrottledTouch @@ -34,7 +33,7 @@ class Note < ApplicationRecord end end - ignore_column :original_discussion_id + self.ignored_columns += %i[original_discussion_id] cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true @@ -89,6 +88,7 @@ class Note < ApplicationRecord delegate :title, to: :noteable, allow_nil: true validates :note, presence: true + validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT } validates :project, presence: true, if: :for_project_noteable? # Attachments are deprecated and are handled by Markdown uploader @@ -331,6 +331,10 @@ class Note < ApplicationRecord cross_reference? && !all_referenced_mentionables_allowed?(user) end + def visible_for?(user) + !cross_reference_not_visible_for?(user) + end + def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/notification_reason.rb b/app/models/notification_reason.rb index 0a13487574f..6856d397413 100644 --- a/app/models/notification_reason.rb +++ b/app/models/notification_reason.rb @@ -3,9 +3,9 @@ # Holds reasons for a notification to have been sent as well as a priority list to select which reason to use # above the rest class NotificationReason - OWN_ACTIVITY = 'own_activity'.freeze - ASSIGNED = 'assigned'.freeze - MENTIONED = 'mentioned'.freeze + OWN_ACTIVITY = 'own_activity' + ASSIGNED = 'assigned' + MENTIONED = 'mentioned' # Priority list for selecting which reason to return in the notification REASON_PRIORITY = [ diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 8306b11a7b6..637c017a342 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class NotificationSetting < ApplicationRecord - include IgnorableColumn - - ignore_column :events + self.ignored_columns += %i[events] enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 } diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 27c122d3559..12ce717efd7 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class PagesDomain < ApplicationRecord - VERIFICATION_KEY = 'gitlab-pages-verification-code'.freeze + VERIFICATION_KEY = 'gitlab-pages-verification-code' VERIFICATION_THRESHOLD = 3.days.freeze SSL_RENEWAL_THRESHOLD = 30.days.freeze diff --git a/app/models/project.rb b/app/models/project.rb index c67c5c7bc8c..17b52d0578e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,8 +37,8 @@ class Project < ApplicationRecord BoardLimitExceeded = Class.new(StandardError) - STATISTICS_ATTRIBUTE = 'repositories_count'.freeze - UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze + STATISTICS_ATTRIBUTE = 'repositories_count' + UNKNOWN_IMPORT_URL = 'http://unknown.git' # Hashed Storage versions handle rolling out new storage to project and dependents models: # nil: legacy # 1: repository @@ -55,12 +55,16 @@ class Project < ApplicationRecord VALID_MIRROR_PORTS = [22, 80, 443].freeze VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze + ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 + SORTING_PREFERENCE_FIELD = :projects_sort cache_markdown_field :description, pipeline: :description delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?, + :merge_requests_access_level, :issues_access_level, :wiki_access_level, + :snippets_access_level, :builds_access_level, :repository_access_level, to: :project_feature, allow_nil: true delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage @@ -2191,6 +2195,10 @@ class Project < ApplicationRecord pool_repository.present? end + def access_request_approvers_to_be_notified + members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) + end + private def merge_requests_allowing_collaboration(source_branch = nil) diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 43edfde851c..d058904dd9e 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -5,7 +5,7 @@ require "addressable/uri" class BuildkiteService < CiService include ReactiveService - ENDPOINT = "https://buildkite.com".freeze + ENDPOINT = "https://buildkite.com" prop_accessor :project_url, :token boolean_accessor :enable_ssl_verification diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index d08fcd8954d..0728c83005e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -64,7 +64,12 @@ class JiraService < IssueTrackerService end def client - @client ||= JIRA::Client.new(options) + @client ||= begin + JIRA::Client.new(options).tap do |client| + # Replaces JIRA default http client with our implementation + client.request_client = Gitlab::Jira::HttpClient.new(client.options) + end + end end def help diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index c15993bdc06..d3fff100964 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class PivotaltrackerService < Service - API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze + API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits' prop_accessor :token, :restrict_to_branch validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 0d35bab7f80..7324890551c 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class PushoverService < Service - BASE_URI = 'https://api.pushover.net/1'.freeze + BASE_URI = 'https://api.pushover.net/1' prop_accessor :api_key, :user_key, :device, :priority, :sound validates :api_key, :user_key, :priority, presence: true, if: :activated? diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index cb16ad75d14..5bfd06476f0 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -35,7 +35,9 @@ class SlashCommandsService < Service chat_user = find_chat_user(params) if chat_user&.user - return Gitlab::SlashCommands::Presenters::Access.new.access_denied unless chat_user.user.can?(:use_slash_commands) + unless chat_user.user.can?(:use_slash_commands) + return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project) + end Gitlab::SlashCommands::Command.new(project, chat_user, params).execute else diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index c9ee0653d86..41e63986286 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -200,6 +200,7 @@ class RemoteMirror < ApplicationRecord result.password = '*****' if result.password result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user result.to_s + rescue URI::Error end def ensure_remote! diff --git a/app/models/repository.rb b/app/models/repository.rb index b957b9b0bdd..7882b2b3036 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -3,9 +3,9 @@ require 'securerandom' class Repository - REF_MERGE_REQUEST = 'merge-requests'.freeze - REF_KEEP_AROUND = 'keep-around'.freeze - REF_ENVIRONMENTS = 'environments'.freeze + REF_MERGE_REQUEST = 'merge-requests' + REF_KEEP_AROUND = 'keep-around' + REF_ENVIRONMENTS = 'environments' ARCHIVE_CACHE_TIME = 60 # Cache archives referred to by a (mutable) ref for 1 minute ARCHIVE_CACHE_TIME_IMMUTABLE = 3600 # Cache archives referred to by an immutable reference for 1 hour @@ -239,13 +239,13 @@ class Repository def branch_exists?(branch_name) return false unless raw_repository - branch_names_include?(branch_name) + branch_names.include?(branch_name) end def tag_exists?(tag_name) return false unless raw_repository - tag_names_include?(tag_name) + tag_names.include?(tag_name) end def ref_exists?(ref) @@ -565,10 +565,10 @@ class Repository end delegate :branch_names, to: :raw_repository - cache_method_as_redis_set :branch_names, fallback: [] + cache_method :branch_names, fallback: [] delegate :tag_names, to: :raw_repository - cache_method_as_redis_set :tag_names, fallback: [] + cache_method :tag_names, fallback: [] delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository cache_method :branch_count, fallback: 0 @@ -1130,10 +1130,6 @@ class Repository @cache ||= Gitlab::RepositoryCache.new(self) end - def redis_set_cache - @redis_set_cache ||= Gitlab::RepositorySetCache.new(self) - end - def request_store_cache @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 9a2640db9ca..a19755d286a 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord TYPES_WITH_CROSS_REFERENCES = %w[ commit cross_reference close duplicate - moved + moved merge ].freeze ICON_TYPES = %w[ diff --git a/app/models/user.rb b/app/models/user.rb index 6107aaa7fca..67d730e2fa3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,7 +13,6 @@ class User < ApplicationRecord include Sortable include CaseSensitivity include TokenAuthenticatable - include IgnorableColumn include FeatureGate include CreatedAtFilterable include BulkMemberAccessLoad @@ -24,9 +23,11 @@ class User < ApplicationRecord DEFAULT_NOTIFICATION_LEVEL = :participating - ignore_column :external_email - ignore_column :email_provider - ignore_column :authentication_token + self.ignored_columns += %i[ + authentication_token + email_provider + external_email + ] add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token @@ -58,7 +59,7 @@ class User < ApplicationRecord :validatable, :omniauthable, :confirmable, :registerable BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \ - "administrator if you think this is an error.".freeze + "administrator if you think this is an error." # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour @@ -493,7 +494,7 @@ class User < ApplicationRecord def by_login(login) return unless login - if login.include?('@'.freeze) + if login.include?('@') unscoped.iwhere(email: login).take else unscoped.iwhere(username: login).take @@ -645,6 +646,13 @@ class User < ApplicationRecord end end + # will_save_change_to_attribute? is used by Devise to check if it is necessary + # to clear any existing reset_password_tokens before updating an authentication_key + # and login in our case is a virtual attribute to allow login by username or email. + def will_save_change_to_login? + will_save_change_to_username? || will_save_change_to_email? + end + def unique_email if !emails.exists?(email: email) && Email.exists?(email: email) errors.add(:email, _('has already been taken')) diff --git a/app/models/user_status.rb b/app/models/user_status.rb index 6ced4f56823..016b89bae81 100644 --- a/app/models/user_status.rb +++ b/app/models/user_status.rb @@ -5,7 +5,7 @@ class UserStatus < ApplicationRecord self.primary_key = :user_id - DEFAULT_EMOJI = 'speech_balloon'.freeze + DEFAULT_EMOJI = 'speech_balloon' belongs_to :user diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index dd8c5d49cf4..fa252af55e4 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -5,6 +5,8 @@ class IssuePolicy < IssuablePolicy # Make sure to sync this class checks with issue.rb to avoid security problems. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. + extend ProjectPolicy::ClassMethods + desc "User can read confidential issues" condition(:can_read_confidential) do @user && IssueCollection.new([@subject]).visible_to(@user).any? @@ -14,13 +16,12 @@ class IssuePolicy < IssuablePolicy condition(:confidential, scope: :subject) { @subject.confidential? } rule { confidential & ~can_read_confidential }.policy do - prevent :read_issue + prevent(*create_read_update_admin_destroy(:issue)) prevent :read_issue_iid - prevent :update_issue - prevent :admin_issue - prevent :create_note end + rule { ~can?(:read_issue) }.prevent :create_note + rule { locked }.policy do prevent :reopen_issue end diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index a3692857ff4..5ad7bdabdff 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -4,4 +4,10 @@ class MergeRequestPolicy < IssuablePolicy rule { locked }.policy do prevent :reopen_merge_request end + + # Only users who can read the merge request can comment. + # Although :read_merge_request is computed in the policy context, + # it would not be safe to prevent :create_note there, since + # note permissions are shared, and this would apply too broadly. + rule { ~can?(:read_merge_request) }.prevent :create_note end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 8b967459173..94a827658f0 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -23,7 +23,7 @@ class DeploymentEntity < Grape::Entity expose :last? expose :deployed_by, as: :user, using: UserEntity - expose :deployable do |deployment, opts| + expose :deployable, if: -> (deployment) { deployment.deployable.present? } do |deployment, opts| deployment.deployable.yield_self do |deployable| if include_details? JobEntity.represent(deployable, opts) diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 8115585b7a8..e06a87c4763 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -6,6 +6,8 @@ module ApplicationSettings attr_reader :params, :application_setting + MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist).freeze + def execute validate_classification_label(application_setting, :external_authorization_service_default_label) unless bypass_external_auth? @@ -25,7 +27,13 @@ module ApplicationSettings params[:usage_stats_set_by_user_id] = current_user.id end - @application_setting.update(@params) + @application_setting.assign_attributes(params) + + if invalidate_markdown_cache? + @application_setting[:local_markdown_version] = @application_setting.local_markdown_version + 1 + end + + @application_setting.save end private @@ -41,6 +49,11 @@ module ApplicationSettings @application_setting.add_to_outbound_local_requests_whitelist(values_array) end + def invalidate_markdown_cache? + !params.key?(:local_markdown_version) && + (@application_setting.changes.keys & MARKDOWN_CACHE_INVALIDATING_PARAMS).any? + end + def update_terms(terms) return unless terms.present? diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 3e968c8f707..c39edd5c114 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -44,6 +44,10 @@ class BaseService model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator") end + def visibility_level + params[:visibility].is_a?(String) ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level] + end + private def error(message, http_status = nil) diff --git a/app/services/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb index 78b53cb3637..f7780488923 100644 --- a/app/services/chat_names/authorize_user_service.rb +++ b/app/services/chat_names/authorize_user_service.rb @@ -24,16 +24,16 @@ module ChatNames end def chat_name_token - Gitlab::ChatNameToken.new + @chat_name_token ||= Gitlab::ChatNameToken.new end def chat_name_params { - service_id: @service.id, - team_id: @params[:team_id], + service_id: @service.id, + team_id: @params[:team_id], team_domain: @params[:team_domain], - chat_id: @params[:user_id], - chat_name: @params[:user_name] + chat_id: @params[:user_id], + chat_name: @params[:user_name] } end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index cdcc4b15bea..29317f1176e 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -15,7 +15,8 @@ module Ci Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Create, - Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze + Gitlab::Ci::Pipeline::Chain::Limit::Activity, + Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block) @pipeline = Ci::Pipeline.new diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index 3c6803d24e6..65d08966802 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -2,24 +2,7 @@ module Clusters module Applications - class CheckInstallationProgressService < BaseHelmService - def execute - return unless operation_in_progress? - - case installation_phase - when Gitlab::Kubernetes::Pod::SUCCEEDED - on_success - when Gitlab::Kubernetes::Pod::FAILED - on_failed - else - check_timeout - end - rescue Kubeclient::HttpError => e - log_error(e) - - app.make_errored!("Kubernetes error: #{e.error_code}") - end - + class CheckInstallationProgressService < CheckProgressService private def operation_in_progress? @@ -32,10 +15,6 @@ module Clusters remove_installation_pod end - def on_failed - app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.") - end - def check_timeout if timed_out? begin @@ -54,18 +33,6 @@ module Clusters def timed_out? Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT end - - def remove_installation_pod - helm_api.delete_pod!(pod_name) - end - - def installation_phase - helm_api.status(pod_name) - end - - def installation_errors - helm_api.log(pod_name) - end end end end diff --git a/app/services/clusters/applications/check_progress_service.rb b/app/services/clusters/applications/check_progress_service.rb new file mode 100644 index 00000000000..4a07b955f8e --- /dev/null +++ b/app/services/clusters/applications/check_progress_service.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class CheckProgressService < BaseHelmService + def execute + return unless operation_in_progress? + + case pod_phase + when Gitlab::Kubernetes::Pod::SUCCEEDED + on_success + when Gitlab::Kubernetes::Pod::FAILED + on_failed + else + check_timeout + end + rescue Kubeclient::HttpError => e + log_error(e) + + app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code }) + end + + private + + def operation_in_progress? + raise NotImplementedError + end + + def on_success + raise NotImplementedError + end + + def pod_name + raise NotImplementedError + end + + def on_failed + app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) + end + + def timed_out? + raise NotImplementedError + end + + def pod_phase + helm_api.status(pod_name) + end + end + end +end diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index e51d84ef052..6a618d61c4f 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -2,26 +2,13 @@ module Clusters module Applications - class CheckUninstallProgressService < BaseHelmService - def execute - return unless app.uninstalling? - - case installation_phase - when Gitlab::Kubernetes::Pod::SUCCEEDED - on_success - when Gitlab::Kubernetes::Pod::FAILED - on_failed - else - check_timeout - end - rescue Kubeclient::HttpError => e - log_error(e) + class CheckUninstallProgressService < CheckProgressService + private - app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code }) + def operation_in_progress? + app.uninstalling? end - private - def on_success app.post_uninstall app.destroy! @@ -31,10 +18,6 @@ module Clusters remove_installation_pod end - def on_failed - app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) - end - def check_timeout if timed_out? app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) @@ -50,14 +33,6 @@ module Clusters def timed_out? Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT end - - def remove_installation_pod - helm_api.delete_pod!(pod_name) - end - - def installation_phase - helm_api.status(pod_name) - end end end end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index e5a5b73321a..bbbeb4b30e4 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -37,7 +37,7 @@ module Clusters end def global_params - { user: current_user, namespace_per_environment: Feature.enabled?(:kubernetes_namespace_per_environment, default_enabled: true) } + { user: current_user } end def clusterable_params diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 2f3c1df7651..c5cde831964 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -26,7 +26,7 @@ module Clusters private def create_gitlab_service_account! - Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator( + Clusters::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator( kube_client, rbac: create_rbac_cluster? ).execute @@ -49,10 +49,10 @@ module Clusters end def request_kubernetes_token - Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new( + Clusters::Kubernetes::FetchKubernetesTokenService.new( kube_client, - Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, - Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE + Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE ).execute end diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb deleted file mode 100644 index 85711764785..00000000000 --- a/app/services/clusters/gcp/kubernetes.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - module Kubernetes - GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab' - GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default' - GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token' - GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' - GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin' - PROJECT_CLUSTER_ROLE_NAME = 'edit' - GITLAB_KNATIVE_SERVING_ROLE_NAME = 'gitlab-knative-serving-role' - GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' - end - end -end diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb deleted file mode 100644 index c45dac7b273..00000000000 --- a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - module Kubernetes - class CreateOrUpdateNamespaceService - def initialize(cluster:, kubernetes_namespace:) - @cluster = cluster - @kubernetes_namespace = kubernetes_namespace - @platform = cluster.platform - end - - def execute - create_project_service_account - configure_kubernetes_token - - kubernetes_namespace.save! - end - - private - - attr_reader :cluster, :kubernetes_namespace, :platform - - def create_project_service_account - Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator( - platform.kubeclient, - service_account_name: kubernetes_namespace.service_account_name, - service_account_namespace: kubernetes_namespace.namespace, - rbac: platform.rbac? - ).execute - end - - def configure_kubernetes_token - kubernetes_namespace.service_account_token = fetch_service_account_token - end - - def fetch_service_account_token - Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new( - platform.kubeclient, - kubernetes_namespace.token_name, - kubernetes_namespace.namespace - ).execute - end - end - end - end -end diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_service_account_service.rb deleted file mode 100644 index 7c5450dbcd6..00000000000 --- a/app/services/clusters/gcp/kubernetes/create_or_update_service_account_service.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - module Kubernetes - class CreateOrUpdateServiceAccountService - def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil) - @kubeclient = kubeclient - @service_account_name = service_account_name - @service_account_namespace = service_account_namespace - @token_name = token_name - @rbac = rbac - @namespace_creator = namespace_creator - @role_binding_name = role_binding_name - end - - def self.gitlab_creator(kubeclient, rbac:) - self.new( - kubeclient, - service_account_name: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME, - service_account_namespace: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE, - token_name: Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, - rbac: rbac - ) - end - - def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:) - self.new( - kubeclient, - service_account_name: service_account_name, - service_account_namespace: service_account_namespace, - token_name: "#{service_account_namespace}-token", - rbac: rbac, - namespace_creator: true, - role_binding_name: "gitlab-#{service_account_namespace}" - ) - end - - def execute - ensure_project_namespace_exists if namespace_creator - - kubeclient.create_or_update_service_account(service_account_resource) - kubeclient.create_or_update_secret(service_account_token_resource) - - return unless rbac - - create_role_or_cluster_role_binding - - return unless namespace_creator - - create_or_update_knative_serving_role - create_or_update_knative_serving_role_binding - end - - private - - attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name - - def ensure_project_namespace_exists - Gitlab::Kubernetes::Namespace.new( - service_account_namespace, - kubeclient - ).ensure_exists! - end - - def create_role_or_cluster_role_binding - if namespace_creator - kubeclient.create_or_update_role_binding(role_binding_resource) - else - kubeclient.create_or_update_cluster_role_binding(cluster_role_binding_resource) - end - end - - def create_or_update_knative_serving_role - kubeclient.update_role(knative_serving_role_resource) - end - - def create_or_update_knative_serving_role_binding - kubeclient.update_role_binding(knative_serving_role_binding_resource) - end - - def service_account_resource - Gitlab::Kubernetes::ServiceAccount.new( - service_account_name, - service_account_namespace - ).generate - end - - def service_account_token_resource - Gitlab::Kubernetes::ServiceAccountToken.new( - token_name, - service_account_name, - service_account_namespace - ).generate - end - - def cluster_role_binding_resource - subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] - - Gitlab::Kubernetes::ClusterRoleBinding.new( - Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME, - Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME, - subjects - ).generate - end - - def role_binding_resource - Gitlab::Kubernetes::RoleBinding.new( - name: role_binding_name, - role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME, - role_kind: :ClusterRole, - namespace: service_account_namespace, - service_account_name: service_account_name - ).generate - end - - def knative_serving_role_resource - Gitlab::Kubernetes::Role.new( - name: Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, - namespace: service_account_namespace, - rules: [{ - apiGroups: %w(serving.knative.dev), - resources: %w(configurations configurationgenerations routes revisions revisionuids autoscalers services), - verbs: %w(get list create update delete patch watch) - }] - ).generate - end - - def knative_serving_role_binding_resource - Gitlab::Kubernetes::RoleBinding.new( - name: Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, - role_name: Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, - role_kind: :Role, - namespace: service_account_namespace, - service_account_name: service_account_name - ).generate - end - end - end - end -end diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb deleted file mode 100644 index 5d9bdc52d37..00000000000 --- a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - module Kubernetes - class FetchKubernetesTokenService - DEFAULT_TOKEN_RETRY_DELAY = 5.seconds - TOKEN_RETRY_LIMIT = 5 - - attr_reader :kubeclient, :service_account_token_name, :namespace - - def initialize(kubeclient, service_account_token_name, namespace, token_retry_delay: DEFAULT_TOKEN_RETRY_DELAY) - @kubeclient = kubeclient - @service_account_token_name = service_account_token_name - @namespace = namespace - @token_retry_delay = token_retry_delay - end - - def execute - # Kubernetes will create the Secret and set the token asynchronously - # so it is necessary to retry - # https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#token-controller - TOKEN_RETRY_LIMIT.times do - token_base64 = get_secret&.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - - sleep @token_retry_delay - end - - nil - end - - private - - def get_secret - kubeclient.get_secret(service_account_token_name, namespace).as_json - rescue Kubeclient::ResourceNotFoundError - end - end - end - end -end diff --git a/app/services/clusters/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/kubernetes/create_or_update_namespace_service.rb new file mode 100644 index 00000000000..15be8446cc0 --- /dev/null +++ b/app/services/clusters/kubernetes/create_or_update_namespace_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Clusters + module Kubernetes + class CreateOrUpdateNamespaceService + def initialize(cluster:, kubernetes_namespace:) + @cluster = cluster + @kubernetes_namespace = kubernetes_namespace + @platform = cluster.platform + end + + def execute + create_project_service_account + configure_kubernetes_token + + kubernetes_namespace.save! + end + + private + + attr_reader :cluster, :kubernetes_namespace, :platform + + def create_project_service_account + Clusters::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator( + platform.kubeclient, + service_account_name: kubernetes_namespace.service_account_name, + service_account_namespace: kubernetes_namespace.namespace, + rbac: platform.rbac? + ).execute + end + + def configure_kubernetes_token + kubernetes_namespace.service_account_token = fetch_service_account_token + end + + def fetch_service_account_token + Clusters::Kubernetes::FetchKubernetesTokenService.new( + platform.kubeclient, + kubernetes_namespace.token_name, + kubernetes_namespace.namespace + ).execute + end + end + end +end diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb new file mode 100644 index 00000000000..8b8ad924b64 --- /dev/null +++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module Clusters + module Kubernetes + class CreateOrUpdateServiceAccountService + def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil) + @kubeclient = kubeclient + @service_account_name = service_account_name + @service_account_namespace = service_account_namespace + @token_name = token_name + @rbac = rbac + @namespace_creator = namespace_creator + @role_binding_name = role_binding_name + end + + def self.gitlab_creator(kubeclient, rbac:) + self.new( + kubeclient, + service_account_name: Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME, + service_account_namespace: Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE, + token_name: Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + rbac: rbac + ) + end + + def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:) + self.new( + kubeclient, + service_account_name: service_account_name, + service_account_namespace: service_account_namespace, + token_name: "#{service_account_namespace}-token", + rbac: rbac, + namespace_creator: true, + role_binding_name: "gitlab-#{service_account_namespace}" + ) + end + + def execute + ensure_project_namespace_exists if namespace_creator + + kubeclient.create_or_update_service_account(service_account_resource) + kubeclient.create_or_update_secret(service_account_token_resource) + + return unless rbac + + create_role_or_cluster_role_binding + + return unless namespace_creator + + create_or_update_knative_serving_role + create_or_update_knative_serving_role_binding + end + + private + + attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name + + def ensure_project_namespace_exists + Gitlab::Kubernetes::Namespace.new( + service_account_namespace, + kubeclient + ).ensure_exists! + end + + def create_role_or_cluster_role_binding + if namespace_creator + kubeclient.create_or_update_role_binding(role_binding_resource) + else + kubeclient.create_or_update_cluster_role_binding(cluster_role_binding_resource) + end + end + + def create_or_update_knative_serving_role + kubeclient.update_role(knative_serving_role_resource) + end + + def create_or_update_knative_serving_role_binding + kubeclient.update_role_binding(knative_serving_role_binding_resource) + end + + def service_account_resource + Gitlab::Kubernetes::ServiceAccount.new( + service_account_name, + service_account_namespace + ).generate + end + + def service_account_token_resource + Gitlab::Kubernetes::ServiceAccountToken.new( + token_name, + service_account_name, + service_account_namespace + ).generate + end + + def cluster_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + Clusters::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME, + Clusters::Kubernetes::GITLAB_CLUSTER_ROLE_NAME, + subjects + ).generate + end + + def role_binding_resource + Gitlab::Kubernetes::RoleBinding.new( + name: role_binding_name, + role_name: Clusters::Kubernetes::PROJECT_CLUSTER_ROLE_NAME, + role_kind: :ClusterRole, + namespace: service_account_namespace, + service_account_name: service_account_name + ).generate + end + + def knative_serving_role_resource + Gitlab::Kubernetes::Role.new( + name: Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, + namespace: service_account_namespace, + rules: [{ + apiGroups: %w(serving.knative.dev), + resources: %w(configurations configurationgenerations routes revisions revisionuids autoscalers services), + verbs: %w(get list create update delete patch watch) + }] + ).generate + end + + def knative_serving_role_binding_resource + Gitlab::Kubernetes::RoleBinding.new( + name: Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, + role_name: Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, + role_kind: :Role, + namespace: service_account_namespace, + service_account_name: service_account_name + ).generate + end + end + end +end diff --git a/app/services/clusters/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/kubernetes/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..aaf437abfad --- /dev/null +++ b/app/services/clusters/kubernetes/fetch_kubernetes_token_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Clusters + module Kubernetes + class FetchKubernetesTokenService + DEFAULT_TOKEN_RETRY_DELAY = 5.seconds + TOKEN_RETRY_LIMIT = 5 + + attr_reader :kubeclient, :service_account_token_name, :namespace + + def initialize(kubeclient, service_account_token_name, namespace, token_retry_delay: DEFAULT_TOKEN_RETRY_DELAY) + @kubeclient = kubeclient + @service_account_token_name = service_account_token_name + @namespace = namespace + @token_retry_delay = token_retry_delay + end + + def execute + # Kubernetes will create the Secret and set the token asynchronously + # so it is necessary to retry + # https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#token-controller + TOKEN_RETRY_LIMIT.times do + token_base64 = get_secret&.dig('data', 'token') + return Base64.decode64(token_base64) if token_base64 + + sleep @token_retry_delay + end + + nil + end + + private + + def get_secret + kubeclient.get_secret(service_account_token_name, namespace).as_json + rescue Kubeclient::ResourceNotFoundError + end + end + end +end diff --git a/app/services/clusters/kubernetes/kubernetes.rb b/app/services/clusters/kubernetes/kubernetes.rb new file mode 100644 index 00000000000..7d5d0c2c1d6 --- /dev/null +++ b/app/services/clusters/kubernetes/kubernetes.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Clusters + module Kubernetes + GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab' + GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default' + GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token' + GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' + GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin' + PROJECT_CLUSTER_ROLE_NAME = 'edit' + GITLAB_KNATIVE_SERVING_ROLE_NAME = 'gitlab-knative-serving-role' + GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' + end +end diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 6e5bf823cc7..0aa76df35ba 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -12,7 +12,7 @@ class CreateSnippetService < BaseService PersonalSnippet.new(params) end - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + unless Gitlab::VisibilityLevel.allowed_for?(current_user, snippet.visibility_level) deny_visibility_level(snippet) return snippet end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index e78e5d5fc2c..1dd22d7a3ae 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -68,9 +68,5 @@ module Groups true end - - def visibility_level - params[:visibility].present? ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level] - end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 83710ffce2f..5b8c1288854 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -293,11 +293,16 @@ class NotificationService def new_access_request(member) return true unless member.notifiable?(:subscription) - recipients = member.source.members.active_without_invites_and_requests.owners_and_maintainers - if fallback_to_group_owners_maintainers?(recipients, member) - recipients = member.source.group.members.active_without_invites_and_requests.owners_and_maintainers + source = member.source + + recipients = source.access_request_approvers_to_be_notified + + if fallback_to_group_access_request_approvers?(recipients, source) + recipients = source.group.access_request_approvers_to_be_notified end + return true if recipients.empty? + recipients.each { |recipient| deliver_access_request_email(recipient, member) } end @@ -611,9 +616,9 @@ class NotificationService mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.id).deliver_later end - def fallback_to_group_owners_maintainers?(recipients, member) + def fallback_to_group_access_request_approvers?(recipients, source) return false if recipients.present? - member.source.respond_to?(:group) && member.source.group + source.respond_to?(:group) && source.group end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 89dc4375c63..942a45286b2 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -5,9 +5,11 @@ module Projects include ValidatesClassificationLabel def initialize(user, params) - @current_user, @params = user, params.dup - @skip_wiki = @params.delete(:skip_wiki) + @current_user, @params = user, params.dup + @skip_wiki = @params.delete(:skip_wiki) @initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme)) + @import_data = @params.delete(:import_data) + @relations_block = @params.delete(:relations_block) end def execute @@ -15,14 +17,11 @@ module Projects return ::Projects::CreateFromTemplateService.new(current_user, params).execute end - import_data = params.delete(:import_data) - relations_block = params.delete(:relations_block) - @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level - unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level) - deny_visibility_level(@project) + if project_visibility.restricted? + deny_visibility_level(@project, project_visibility.visibility_level) return @project end @@ -44,7 +43,7 @@ module Projects @project.namespace_id = current_user.namespace_id end - relations_block&.call(@project) + @relations_block&.call(@project) yield(@project) if block_given? validate_classification_label(@project, :external_authorization_classification_label) @@ -54,7 +53,7 @@ module Projects @project.creator = current_user - save_project_and_import_data(import_data) + save_project_and_import_data after_create_actions if @project.persisted? @@ -129,9 +128,9 @@ module Projects !@project.feature_available?(:wiki, current_user) || @skip_wiki end - def save_project_and_import_data(import_data) + def save_project_and_import_data Project.transaction do - @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data + @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data if @project.save unless @project.gitlab_project_import? @@ -192,5 +191,11 @@ module Projects fail(error: @project.errors.full_messages.join(', ')) end end + + def project_visibility + @project_visibility ||= Gitlab::VisibilityLevelChecker + .new(current_user, @project, project_params: { import_data: @import_data }) + .level_restricted? + end end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 0ea230a44a1..b1256df35d6 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -314,11 +314,9 @@ class TodoService end def reject_users_without_access(users, parent, target) - if target.is_a?(Note) && target.for_issuable? - target = target.noteable - end + target = target.noteable if target.is_a?(Note) - if target.is_a?(Issuable) + if target.respond_to?(:to_ability_name) select_users(users, :"read_#{target.to_ability_name}", target) else select_users(users, :read_project, parent) diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb index 2969c360de5..a294812ef9e 100644 --- a/app/services/update_snippet_service.rb +++ b/app/services/update_snippet_service.rb @@ -12,7 +12,7 @@ class UpdateSnippetService < BaseService def execute # check that user is allowed to set specified visibility_level - new_visibility = params[:visibility_level] + new_visibility = visibility_level if new_visibility && new_visibility.to_i != snippet.visibility_level unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index 1ac69601d18..3efdd0aa1d9 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -6,6 +6,10 @@ class PersonalFileUploader < FileUploader options.storage_path end + def self.workhorse_local_upload_path + File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH) + end + def self.base_dir(model, _store = nil) # base_dir is the path seen by the user when rendering Markdown, so # it should be the same for both local and object storage. It is diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index d24e46b2815..f0a19075115 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -7,11 +7,15 @@ = f.check_box :recaptcha_enabled, class: 'form-check-input' = f.label :recaptcha_enabled, class: 'form-check-label' do Enable reCAPTCHA - - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions' - - recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url } %span.form-text.text-muted#recaptcha_help_block - = _('Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe } - + = _('Helps prevent bots from creating accounts.') + .form-group + .form-check + = f.check_box :login_recaptcha_protection_enabled, class: 'form-check-input' + = f.label :login_recaptcha_protection_enabled, class: 'form-check-label' do + Enable reCAPTCHA for login + %span.form-text.text-muted#recaptcha_help_block + = _('Helps prevent bots from brute-force attacks.') .form-group = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold' = f.text_field :recaptcha_site_key, class: 'form-control' @@ -21,6 +25,7 @@ .form-group = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold' + .form-group = f.text_field :recaptcha_private_key, class: 'form-control' .form-group diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml index 46e3d1c4570..c60e44b3864 100644 --- a/app/views/admin/application_settings/reporting.html.haml +++ b/app/views/admin/application_settings/reporting.html.haml @@ -9,7 +9,9 @@ %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded_by_default? ? _('Collapse') : _('Expand') %p - = _('Enable reCAPTCHA or Akismet and set IP limits.') + - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions' + - recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url } + = _('Enable reCAPTCHA or Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe } .settings-content = render 'spam' diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 0b1d3d1ddb3..6e9efcb0597 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -16,7 +16,7 @@ - else = link_to _('Forgot your password?'), new_password_path(:user) %div - - if captcha_enabled? + - if captcha_enabled? || captcha_on_login_required? = recaptcha_tags .submit-container.move-submit-down diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index 733cb36cc3d..c3c7d102b28 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -9,7 +9,7 @@ Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/ - %span.descr This setting can be overridden in each project. + %span This setting can be overridden in each project. .form-group.row .col-sm-2.col-form-label = f.label s_('ProjectCreationLevel|Allowed to create projects') diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index c55730ccd31..4c88660ccb9 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -14,7 +14,7 @@ %span.d-block - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %span.descr.text-muted= share_with_group_lock_help_text(@group) + %span.js-descr.text-muted= share_with_group_lock_help_text(@group) .form-group.append-bottom-default .form-check diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index e6a235e39da..ba5cd0fdd41 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -47,6 +47,7 @@ = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref = hidden_field_tag :nav_source, 'navbar' - -# workaround for non-JS feature specs, for JS you need to use find('#search').send_keys(:enter) - = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' + -# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb + - if ENV['RAILS_ENV'] == 'test' + %noscript= button_tag 'Search' .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index b93d95ef02f..bd61db3ee76 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -1,9 +1,5 @@ -<%= sanitize_name(@issue.author_name) %> <%= 'created an issue:' %> +<%= sanitize_name(@issue.author_name) %> <%= 'created an issue:' %> <%= url_for(project_issue_url(@issue.project, @issue)) %> -<% if @issue.assignees.any? -%> - <%= assignees_label(@issue) %> -<% end %> +<%= assignees_label(@issue) if @issue.assignees.any? %> -<% if @issue.description -%> - <%= @issue.description %> -<% end %> +<%= @issue.description %> diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index 6c0d7b1e60b..f4b0ed0f886 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -1,8 +1,8 @@ -<%= @merge_request.author_name %> <%= 'created a merge request:' %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> +<%= sanitize_name(@merge_request.author_name) %> <%= 'created a merge request:' %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> <%= merge_path_description(@merge_request, 'to') %> <%= 'Author:' %> <%= @merge_request.author_name %> -<%= assignees_label(@merge_request) %> +<%= assignees_label(@merge_request) if @merge_request.assignees.any? %> <%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %> <%= @merge_request.description %> diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml index c21d333f21a..d3fcb52422b 100644 --- a/app/views/projects/_merge_request_merge_checks_settings.html.haml +++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml @@ -7,7 +7,7 @@ = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input' = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do = s_('ProjectSettings|Pipelines must succeed') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|Pipelines need to be configured to enable this feature.') = link_to icon('question-circle'), help_page_path('ci/merge_request_pipelines/index.md', diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index 47c311f42d0..1be7f7bb418 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -7,14 +7,14 @@ = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input" = label_tag :project_merge_method_merge, class: 'form-check-label' do = s_('ProjectSettings|Merge commit') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|Every merge creates a merge commit') .form-check.mb-2 = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input" = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do = s_('ProjectSettings|Merge commit with semi-linear history') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|Every merge creates a merge commit') %br = s_('ProjectSettings|Fast-forward merges only') @@ -25,7 +25,7 @@ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input" = label_tag :project_merge_method_ff, class: 'form-check-label' do = s_('ProjectSettings|Fast-forward merge') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|No merge commits are created') %br = s_('ProjectSettings|Fast-forward merges only') diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index a766dd51463..6c77036a85b 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -5,7 +5,7 @@ = render partial: 'signature', object: @commit.signature %strong #{ s_('CommitBoxTitle|Commit') } - %span.commit-sha= @commit.short_id + %span.commit-sha{ data: { qa_selector: 'commit_sha_content' } }= @commit.short_id = clipboard_button(text: @commit.id, title: _('Copy commit SHA to clipboard')) %span.d-none.d-sm-inline= _('authored') #{time_ago_with_tooltip(@commit.authored_date)} diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 6f8a93fbcf5..84f0900d9c1 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -50,7 +50,7 @@ - @project.remote_mirrors.each_with_index do |mirror, index| - next if mirror.new_record? %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } - %td.qa-mirror-repository-url= mirror.safe_url + %td.qa-mirror-repository-url= mirror.safe_url || _('Invalid URL') %td= _('Push') %td = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index de1b95692d6..2f277e8147a 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -15,7 +15,7 @@ .footer-block.row-content-block = service_save_button(@service) - = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel' + = link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel' - if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true) %hr diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml index 16e48814578..7748a7a6a8e 100644 --- a/app/views/projects/services/_index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -1,8 +1,8 @@ .row.prepend-top-default.append-bottom-default .col-lg-4 %h4.prepend-top-0 - Project services - %p Project services allow you to integrate GitLab with other applications + = s_("ProjectService|Project services") + %p= s_("ProjectService|Project services allow you to integrate GitLab with other applications") .col-lg-8 %table.table %colgroup @@ -13,12 +13,12 @@ %thead %tr %th - %th Service - %th.d-none.d-sm-block Description - %th Last edit + %th= s_("ProjectService|Service") + %th.d-none.d-sm-block= _("Description") + %th= s_("ProjectService|Last edit") - @services.sort_by(&:title).each do |service| %tr - %td{ "aria-label" => "#{service.title}: status " + (service.activated? ? "on" : "off") } + %td{ "aria-label" => (service.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: service.title } } = boolean_to_icon service.activated? %td = link_to edit_project_service_path(@project, service.to_param) do diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index df1fd583670..fc20bc52d1c 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -1,6 +1,6 @@ -- breadcrumb_title "Integrations" -- page_title @service.title, "Services" -- add_to_breadcrumbs("Settings", edit_project_path(@project)) +- breadcrumb_title s_("ProjectService|Integrations") +- page_title @service.title, s_("ProjectService|Services") +- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project)) = render 'deprecated_message' if @service.deprecation_message diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 82c1d57c97e..395df502ddb 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -1,6 +1,6 @@ -- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}" +- run_actions_text = s_("ProjectService|Perform common operations on GitLab project: %{project_name}") % { project_name: @project.full_name } -%p To set up this service: +%p= s_("ProjectService|To set up this service:") %ul.list-unstyled.indent-list %li 1. @@ -18,67 +18,67 @@ .help-form .form-group - = label_tag :display_name, 'Display name', class: 'col-12 col-form-label label-bold' + = label_tag :display_name, _('Display name'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#display_name', class: 'input-group-text') .form-group - = label_tag :description, 'Description', class: 'col-12 col-form-label label-bold' + = label_tag :description, _('Description'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#description', class: 'input-group-text') .form-group - = label_tag nil, 'Command trigger word', class: 'col-12 col-form-label label-bold' + = label_tag nil, s_('MattermostService|Command trigger word'), class: 'col-12 col-form-label label-bold' .col-12 - %p Fill in the word that works best for your team. + %p= s_('MattermostService|Fill in the word that works best for your team.') %p - Suggestions: + = s_('MattermostService|Suggestions:') %code= 'gitlab' %code= @project.path # Path contains no spaces, but dashes %code= @project.full_path .form-group - = label_tag :request_url, 'Request URL', class: 'col-12 col-form-label label-bold' + = label_tag :request_url, s_('MattermostService|Request URL'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#request_url', class: 'input-group-text') .form-group - = label_tag nil, 'Request method', class: 'col-12 col-form-label label-bold' + = label_tag nil, s_('MattermostService|Request method'), class: 'col-12 col-form-label label-bold' .col-12 POST .form-group - = label_tag :response_username, 'Response username', class: 'col-12 col-form-label label-bold' + = label_tag :response_username, s_('MattermostService|Response username'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#response_username', class: 'input-group-text') .form-group - = label_tag :response_icon, 'Response icon', class: 'col-12 col-form-label label-bold' + = label_tag :response_icon, s_('MattermostService|Response icon'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#response_icon', class: 'input-group-text') .form-group - = label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold' + = label_tag nil, _('Autocomplete'), class: 'col-12 col-form-label label-bold' .col-12 Yes .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-12 col-12 col-form-label label-bold' + = label_tag :autocomplete_hint, _('Autocomplete hint'), class: 'col-12 col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#autocomplete_hint', class: 'input-group-text') .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold' + = label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index f51dd581d29..cc005dd69b7 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -3,14 +3,12 @@ .info-well .well-segment %p - This service allows users to perform common operations on this - project by entering slash commands in Mattermost. + = s_("MattermostService|This service allows users to perform common operations on this project by entering slash commands in Mattermost.") = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do - View documentation + = _("View documentation") = sprite_icon('external-link', size: 16) %p.inline - See list of available commands in Mattermost after setting up this service, - by entering + = s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering") %kbd.inline /<trigger> help - unless enabled || @service.template? = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index 2da8e5428ec..aee81ea744a 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -4,4 +4,4 @@ .col-sm-9.offset-sm-3 = link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do = custom_icon('mattermost_logo', size: 15) - Add to Mattermost + = s_("MattermostService|Add to Mattermost") diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 9b7732abc62..7f6717e298c 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -4,17 +4,15 @@ .info-well .well-segment %p - This service allows users to perform common operations on this - project by entering slash commands in Slack. + = s_("SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack.") = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do - View documentation + = _("View documentation") = sprite_icon('external-link', size: 16) %p.inline - See list of available commands in Slack after setting up this service, - by entering + = s_("SlackService|See list of available commands in Slack after setting up this service, by entering") %kbd.inline /<command> help - unless @service.template? - %p To set up this service: + %p= _("To set up this service:") %ul.list-unstyled.indent-list %li 1. @@ -27,11 +25,11 @@ .help-form .form-group - = label_tag nil, 'Command', class: 'col-12 col-form-label label-bold' + = label_tag nil, _('Command'), class: 'col-12 col-form-label label-bold' .col-12 - %p Fill in the word that works best for your team. + %p= s_('SlackService|Fill in the word that works best for your team.') %p - Suggestions: + = _("Suggestions:") %code= 'gitlab' %code= @project.path # Path contains no spaces, but dashes %code= @project.full_path @@ -44,44 +42,44 @@ = clipboard_button(target: '#url', class: 'input-group-text') .form-group - = label_tag nil, 'Method', class: 'col-12 col-form-label label-bold' + = label_tag nil, _('Method'), class: 'col-12 col-form-label label-bold' .col-12 POST .form-group - = label_tag :customize_name, 'Customize name', class: 'col-12 col-form-label label-bold' + = label_tag :customize_name, _('Customize name'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#customize_name', class: 'input-group-text') .form-group - = label_tag nil, 'Customize icon', class: 'col-12 col-form-label label-bold' + = label_tag nil, _('Customize icon'), class: 'col-12 col-form-label label-bold' .col-12 = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36, class: 'mr-3') - = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') + = link_to(_('Download image'), asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') .form-group - = label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold' + = label_tag nil, _('Autocomplete'), class: 'col-12 col-form-label label-bold' .col-12 Show this command in the autocomplete list .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold' + = label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') .form-group - = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-12 col-form-label label-bold' + = label_tag :autocomplete_usage_hint, _('Autocomplete usage hint'), class: 'col-12 col-form-label label-bold' .col-12.input-group = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') .form-group - = label_tag :descriptive_label, 'Descriptive label', class: 'col-12 col-form-label label-bold' + = label_tag :descriptive_label, _('Descriptive label'), class: 'col-12 col-form-label label-bold' .col-12.input-group - = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly' + = text_field_tag :descriptive_label, _('Perform common operations on GitLab project'), class: 'form-control form-control-sm', readonly: 'readonly' .input-group-append = clipboard_button(target: '#descriptive_label', class: 'input-group-text') @@ -89,12 +87,6 @@ %ul.list-unstyled.indent-list %li - 2. Paste the - %strong Token - into the field below + = s_("SlackService|2. Paste the <strong>Token</strong> into the field below").html_safe %li - 3. Select the - %strong Active - checkbox, press - %strong Save changes - and start using GitLab inside Slack! + = s_("SlackService|3. Select the <strong>Active</strong> checkbox, press <strong>Save changes</strong> and start using GitLab inside Slack!").html_safe diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 498a9744783..430d6071468 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -14,14 +14,14 @@ = f.label :build_allow_git_fetch_false, class: 'form-check-label' do %strong git clone %br - %span.descr + %span = _("Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job") .form-check = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' } = f.label :build_allow_git_fetch_true, class: 'form-check-label' do %strong git fetch %br - %span.descr + %span = _("Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)") %hr diff --git a/app/views/shared/empty_states/_profile_tabs.html.haml b/app/views/shared/empty_states/_profile_tabs.html.haml index 6da40e1b059..98a5a5953d0 100644 --- a/app/views/shared/empty_states/_profile_tabs.html.haml +++ b/app/views/shared/empty_states/_profile_tabs.html.haml @@ -12,7 +12,7 @@ %p= current_user_empty_message_description - if secondary_button_link.present? - = link_to secondary_button_label, secondary_button_link, class: 'btn btn-create btn-inverted' + = link_to secondary_button_label, secondary_button_link, class: 'btn btn-success btn-inverted' = link_to primary_button_label, primary_button_link, class: 'btn btn-success' - else diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 4f6a71b6071..875cacd1f4f 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -9,7 +9,7 @@ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" - if can_reopen = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, - class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" + class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}", data: { qa_selector: 'reopen_issue_button' } - else - if can_update && !are_close_and_open_buttons_hidden = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 214e87052da..04a70e406ca 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -66,7 +66,7 @@ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) - = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable], params: { destroy_confirm: true }), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' %span.append-right-10 diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 71123740ee4..93408e0bfc0 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -17,7 +17,7 @@ #{issuables_state_counter_text(type, :closed, display_count)} - else %li{ class: active_when(params[:state] == 'closed') }> - = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do + = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed', qa_selector: 'closed_issues_link' } do #{issuables_state_counter_text(type, :closed, display_count)} = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count) diff --git a/changelogs/unreleased/28643-access-request-emails-limit-to-ten-owners.yml b/changelogs/unreleased/28643-access-request-emails-limit-to-ten-owners.yml new file mode 100644 index 00000000000..25e83eaf68e --- /dev/null +++ b/changelogs/unreleased/28643-access-request-emails-limit-to-ten-owners.yml @@ -0,0 +1,5 @@ +--- +title: Limit access request emails to ten most recently active owners or maintainers +merge_request: 32141 +author: +type: changed diff --git a/changelogs/unreleased/36383-improve-search-result-labels.yml b/changelogs/unreleased/36383-improve-search-result-labels.yml new file mode 100644 index 00000000000..b17c173af7a --- /dev/null +++ b/changelogs/unreleased/36383-improve-search-result-labels.yml @@ -0,0 +1,5 @@ +--- +title: Improve search result labels +merge_request: 32101 +author: +type: changed diff --git a/changelogs/unreleased/56295-some-avatars-not-visible-in-commit-trailers.yml b/changelogs/unreleased/56295-some-avatars-not-visible-in-commit-trailers.yml new file mode 100644 index 00000000000..23450c0fdc3 --- /dev/null +++ b/changelogs/unreleased/56295-some-avatars-not-visible-in-commit-trailers.yml @@ -0,0 +1,5 @@ +--- +title: Fix for missing avatar images dislpayed in commit trailers. +merge_request: 32374 +author: Jesse Hall @jessehall3 +type: fixed diff --git a/changelogs/unreleased/62055-find-file-links-encoding.yml b/changelogs/unreleased/62055-find-file-links-encoding.yml new file mode 100644 index 00000000000..20a359a9d64 --- /dev/null +++ b/changelogs/unreleased/62055-find-file-links-encoding.yml @@ -0,0 +1,5 @@ +--- +title: Fix encoding of special characters in "Find File" +merge_request: 31311 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/62673-clean-note-app-tests.yml b/changelogs/unreleased/62673-clean-note-app-tests.yml new file mode 100644 index 00000000000..abfcf8fe364 --- /dev/null +++ b/changelogs/unreleased/62673-clean-note-app-tests.yml @@ -0,0 +1,5 @@ +--- +title: make test of note app with comments disabled dry +merge_request: 32383 +author: Romain Maneschi +type: other diff --git a/changelogs/unreleased/64251-branch-name-set-cache.yml b/changelogs/unreleased/64251-branch-name-set-cache.yml deleted file mode 100644 index 6ce4bdf5e43..00000000000 --- a/changelogs/unreleased/64251-branch-name-set-cache.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cache branch and tag names as Redis sets -merge_request: 30476 -author: -type: performance diff --git a/changelogs/unreleased/65063-Embeded-metrics-inconsistent-styles.yml b/changelogs/unreleased/65063-Embeded-metrics-inconsistent-styles.yml new file mode 100644 index 00000000000..126cf29afc0 --- /dev/null +++ b/changelogs/unreleased/65063-Embeded-metrics-inconsistent-styles.yml @@ -0,0 +1,5 @@ +--- +title: Fixed embeded metrics tooltip inconsistent styling +merge_request: 31517 +author: +type: fixed diff --git a/changelogs/unreleased/65251-default-clusters-namespace_per_environment-column-to-true.yml b/changelogs/unreleased/65251-default-clusters-namespace_per_environment-column-to-true.yml new file mode 100644 index 00000000000..c0bc20b2485 --- /dev/null +++ b/changelogs/unreleased/65251-default-clusters-namespace_per_environment-column-to-true.yml @@ -0,0 +1,5 @@ +--- +title: Default clusters namespace_per_environment column to true +merge_request: 32139 +author: +type: other diff --git a/changelogs/unreleased/66264-moved-issue-reference.yml b/changelogs/unreleased/66264-moved-issue-reference.yml new file mode 100644 index 00000000000..5876e9bbf79 --- /dev/null +++ b/changelogs/unreleased/66264-moved-issue-reference.yml @@ -0,0 +1,5 @@ +--- +title: Use moved instead of closed in issue references +merge_request: 32277 +author: juliette-derancourt +type: changed diff --git a/changelogs/unreleased/66524-issue-due-notification-emails-are-threaded-incorrectly.yml b/changelogs/unreleased/66524-issue-due-notification-emails-are-threaded-incorrectly.yml new file mode 100644 index 00000000000..a6a0ba4b4f4 --- /dev/null +++ b/changelogs/unreleased/66524-issue-due-notification-emails-are-threaded-incorrectly.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue due notification emails not being threaded correctly +merge_request: 32325 +author: +type: fixed diff --git a/changelogs/unreleased/66715-delete-search-animation.yml b/changelogs/unreleased/66715-delete-search-animation.yml new file mode 100644 index 00000000000..9cb796e4164 --- /dev/null +++ b/changelogs/unreleased/66715-delete-search-animation.yml @@ -0,0 +1,5 @@ +--- +title: delete animation width on global search input +merge_request: 32399 +author: Romain Maneschi +type: other diff --git a/changelogs/unreleased/add-warning-note-to-project-container-registry-setting.yml b/changelogs/unreleased/add-warning-note-to-project-container-registry-setting.yml new file mode 100644 index 00000000000..0663788b680 --- /dev/null +++ b/changelogs/unreleased/add-warning-note-to-project-container-registry-setting.yml @@ -0,0 +1,6 @@ +--- +title: Added warning note on the project container registry setting informing users + that the registry is public for public projects +merge_request: 32447 +author: +type: other diff --git a/changelogs/unreleased/ce-60465-prevent-comments-on-private-mrs.yml b/changelogs/unreleased/ce-60465-prevent-comments-on-private-mrs.yml new file mode 100644 index 00000000000..ba970162447 --- /dev/null +++ b/changelogs/unreleased/ce-60465-prevent-comments-on-private-mrs.yml @@ -0,0 +1,3 @@ +--- +title: Ensure only authorised users can create notes on Merge Requests and Issues +type: security diff --git a/changelogs/unreleased/ce-slack-close-command.yml b/changelogs/unreleased/ce-slack-close-command.yml new file mode 100644 index 00000000000..c20e623b117 --- /dev/null +++ b/changelogs/unreleased/ce-slack-close-command.yml @@ -0,0 +1,5 @@ +--- +title: Add a close issue slack slash command +merge_request: 32150 +author: +type: added diff --git a/changelogs/unreleased/cert_manager_v0_9.yml b/changelogs/unreleased/cert_manager_v0_9.yml new file mode 100644 index 00000000000..bda5bbffab5 --- /dev/null +++ b/changelogs/unreleased/cert_manager_v0_9.yml @@ -0,0 +1,5 @@ +--- +title: Install cert-manager v0.9.1 +merge_request: 32243 +author: +type: changed diff --git a/changelogs/unreleased/fix-create-milestone-btn-success.yml b/changelogs/unreleased/fix-create-milestone-btn-success.yml new file mode 100644 index 00000000000..a1b1d305ce3 --- /dev/null +++ b/changelogs/unreleased/fix-create-milestone-btn-success.yml @@ -0,0 +1,5 @@ +--- +title: Fix style of secondary profile tab buttons. +merge_request: 32010 +author: Wolfgang Faust +type: fixed diff --git a/changelogs/unreleased/fix-dropdown-closing.yml b/changelogs/unreleased/fix-dropdown-closing.yml new file mode 100644 index 00000000000..5ce3a6b478e --- /dev/null +++ b/changelogs/unreleased/fix-dropdown-closing.yml @@ -0,0 +1,5 @@ +--- +title: Fix dropdowns closing when click is released outside the dropdown +merge_request: 32084 +author: +type: fixed diff --git a/changelogs/unreleased/fix-nil-deployable-exception-on-job-controller-show.yml b/changelogs/unreleased/fix-nil-deployable-exception-on-job-controller-show.yml new file mode 100644 index 00000000000..b79317e3ab7 --- /dev/null +++ b/changelogs/unreleased/fix-nil-deployable-exception-on-job-controller-show.yml @@ -0,0 +1,5 @@ +--- +title: Fix users cannot access job detail page when deployable does not exist +merge_request: 32247 +author: +type: fixed diff --git a/changelogs/unreleased/georgekoltsov-13698-override-params.yml b/changelogs/unreleased/georgekoltsov-13698-override-params.yml new file mode 100644 index 00000000000..1bbde160e18 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-13698-override-params.yml @@ -0,0 +1,5 @@ +--- +title: Allow project feature permissions to be overridden during import with override_params +merge_request: 32348 +author: +type: fixed diff --git a/changelogs/unreleased/handle-invalid-mirror-url.yml b/changelogs/unreleased/handle-invalid-mirror-url.yml new file mode 100644 index 00000000000..c72707a2cad --- /dev/null +++ b/changelogs/unreleased/handle-invalid-mirror-url.yml @@ -0,0 +1,5 @@ +--- +title: Handle invalid mirror url +merge_request: 32353 +author: Lee Tickett +type: fixed diff --git a/changelogs/unreleased/improve-chatops-help.yml b/changelogs/unreleased/improve-chatops-help.yml new file mode 100644 index 00000000000..77e6f2e5308 --- /dev/null +++ b/changelogs/unreleased/improve-chatops-help.yml @@ -0,0 +1,5 @@ +--- +title: Improve chatops help output +merge_request: 32208 +author: +type: changed diff --git a/changelogs/unreleased/issue_10770.yml b/changelogs/unreleased/issue_10770.yml new file mode 100644 index 00000000000..728160b24ad --- /dev/null +++ b/changelogs/unreleased/issue_10770.yml @@ -0,0 +1,5 @@ +--- +title: Rename epic column state to state_id +merge_request: 32270 +author: +type: changed diff --git a/changelogs/unreleased/refactor-showStagedIcon.yml b/changelogs/unreleased/refactor-showStagedIcon.yml new file mode 100644 index 00000000000..94c58c4dc45 --- /dev/null +++ b/changelogs/unreleased/refactor-showStagedIcon.yml @@ -0,0 +1,5 @@ +--- +title: Refactor showStagedIcon property to reflect the behavior its name represents. +merge_request: 32333 +author: Arun Kumar Mohan +type: other diff --git a/changelogs/unreleased/remove-vue-resource-from-remove-issue.yml b/changelogs/unreleased/remove-vue-resource-from-remove-issue.yml new file mode 100644 index 00000000000..5be1c832ac7 --- /dev/null +++ b/changelogs/unreleased/remove-vue-resource-from-remove-issue.yml @@ -0,0 +1,5 @@ +--- +title: Remove vue resource from remove issue +merge_request: 32425 +author: Lee Tickett +type: other diff --git a/changelogs/unreleased/runner-chart-repo-use-new-location.yml b/changelogs/unreleased/runner-chart-repo-use-new-location.yml new file mode 100644 index 00000000000..790e5704c04 --- /dev/null +++ b/changelogs/unreleased/runner-chart-repo-use-new-location.yml @@ -0,0 +1,5 @@ +--- +title: Use new location for gitlab-runner helm charts +merge_request: 32384 +author: +type: other diff --git a/changelogs/unreleased/security-59549-add-capcha-for-failed-logins.yml b/changelogs/unreleased/security-59549-add-capcha-for-failed-logins.yml new file mode 100644 index 00000000000..55f9e36c39c --- /dev/null +++ b/changelogs/unreleased/security-59549-add-capcha-for-failed-logins.yml @@ -0,0 +1,5 @@ +--- +title: Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-61974-limit-issue-comment-size-2.yml b/changelogs/unreleased/security-61974-limit-issue-comment-size-2.yml new file mode 100644 index 00000000000..962171dc6f8 --- /dev/null +++ b/changelogs/unreleased/security-61974-limit-issue-comment-size-2.yml @@ -0,0 +1,5 @@ +--- +title: Speed up regexp in namespace format by failing fast after reaching maximum namespace depth +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-61974-limit-issue-comment-size.yml b/changelogs/unreleased/security-61974-limit-issue-comment-size.yml new file mode 100644 index 00000000000..6d5ef057d83 --- /dev/null +++ b/changelogs/unreleased/security-61974-limit-issue-comment-size.yml @@ -0,0 +1,5 @@ +--- +title: Limit the size of issuable description and comments +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-64711-fix-commit-todos.yml b/changelogs/unreleased/security-64711-fix-commit-todos.yml new file mode 100644 index 00000000000..ce4b3cdeeaf --- /dev/null +++ b/changelogs/unreleased/security-64711-fix-commit-todos.yml @@ -0,0 +1,5 @@ +--- +title: Send TODOs for comments on commits correctly +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-ci-metrics-permissions.yml b/changelogs/unreleased/security-ci-metrics-permissions.yml new file mode 100644 index 00000000000..51c6493442a --- /dev/null +++ b/changelogs/unreleased/security-ci-metrics-permissions.yml @@ -0,0 +1,6 @@ +--- +title: Restrict MergeRequests#test_reports to authenticated users with read-access + on Builds +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-enable-image-proxy.yml b/changelogs/unreleased/security-enable-image-proxy.yml new file mode 100644 index 00000000000..88b49ffd9e8 --- /dev/null +++ b/changelogs/unreleased/security-enable-image-proxy.yml @@ -0,0 +1,5 @@ +--- +title: Added image proxy to mitigate potential stealing of IP addresses +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ce-master.yml b/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ce-master.yml new file mode 100644 index 00000000000..c639098721e --- /dev/null +++ b/changelogs/unreleased/security-epic-notes-api-reveals-historical-info-ce-master.yml @@ -0,0 +1,5 @@ +--- +title: Filter out old system notes for epics in notes api endpoint response +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-exposed-default-branch.yml b/changelogs/unreleased/security-exposed-default-branch.yml new file mode 100644 index 00000000000..bf32617ee8a --- /dev/null +++ b/changelogs/unreleased/security-exposed-default-branch.yml @@ -0,0 +1,5 @@ +--- +title: Avoid exposing unaccessible repo data upon GFM post processing +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-html-injection-for-label-description-ce-master.yml b/changelogs/unreleased/security-fix-html-injection-for-label-description-ce-master.yml new file mode 100644 index 00000000000..07124ac399b --- /dev/null +++ b/changelogs/unreleased/security-fix-html-injection-for-label-description-ce-master.yml @@ -0,0 +1,5 @@ +--- +title: Fix HTML injection for label description +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-markdown-xss.yml b/changelogs/unreleased/security-fix-markdown-xss.yml new file mode 100644 index 00000000000..7ef19f13fd5 --- /dev/null +++ b/changelogs/unreleased/security-fix-markdown-xss.yml @@ -0,0 +1,5 @@ +--- +title: Make sure HTML text is always escaped when replacing label/milestone references. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix_jira_ssrf_vulnerability.yml b/changelogs/unreleased/security-fix_jira_ssrf_vulnerability.yml new file mode 100644 index 00000000000..25518dd2d05 --- /dev/null +++ b/changelogs/unreleased/security-fix_jira_ssrf_vulnerability.yml @@ -0,0 +1,5 @@ +--- +title: Prevent DNS rebind on JIRA service integration +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-gitaly-1-61-0.yml b/changelogs/unreleased/security-gitaly-1-61-0.yml new file mode 100644 index 00000000000..cbcd5f545a0 --- /dev/null +++ b/changelogs/unreleased/security-gitaly-1-61-0.yml @@ -0,0 +1,5 @@ +--- +title: "Gitaly: ignore git redirects" +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-group-runners-permissions.yml b/changelogs/unreleased/security-group-runners-permissions.yml new file mode 100644 index 00000000000..6c74be30b6d --- /dev/null +++ b/changelogs/unreleased/security-group-runners-permissions.yml @@ -0,0 +1,5 @@ +--- +title: Use admin_group authorization in Groups::RunnersController +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-hide_merge_request_ids_on_emails.yml b/changelogs/unreleased/security-hide_merge_request_ids_on_emails.yml new file mode 100644 index 00000000000..cd8c9590a70 --- /dev/null +++ b/changelogs/unreleased/security-hide_merge_request_ids_on_emails.yml @@ -0,0 +1,5 @@ +--- +title: Prevent disclosure of merge request ID via email +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-id-filter-timeline-activities-for-guests.yml b/changelogs/unreleased/security-id-filter-timeline-activities-for-guests.yml new file mode 100644 index 00000000000..0fa5f89e2c0 --- /dev/null +++ b/changelogs/unreleased/security-id-filter-timeline-activities-for-guests.yml @@ -0,0 +1,5 @@ +--- +title: Show cross-referenced MR-id in issues' activities only to authorized users +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-katex-dos-master.yml b/changelogs/unreleased/security-katex-dos-master.yml new file mode 100644 index 00000000000..df803a5eafd --- /dev/null +++ b/changelogs/unreleased/security-katex-dos-master.yml @@ -0,0 +1,5 @@ +--- +title: Enforce max chars and max render time in markdown math +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-mr-head-pipeline-leak.yml b/changelogs/unreleased/security-mr-head-pipeline-leak.yml new file mode 100644 index 00000000000..b15b353ff41 --- /dev/null +++ b/changelogs/unreleased/security-mr-head-pipeline-leak.yml @@ -0,0 +1,5 @@ +--- +title: Check permissions before responding in MergeController#pipeline_status +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-personal-snippets.yml b/changelogs/unreleased/security-personal-snippets.yml new file mode 100644 index 00000000000..95f61993b98 --- /dev/null +++ b/changelogs/unreleased/security-personal-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Remove EXIF from users/personal snippet uploads. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-project-import-bypass.yml b/changelogs/unreleased/security-project-import-bypass.yml new file mode 100644 index 00000000000..fc7b823509c --- /dev/null +++ b/changelogs/unreleased/security-project-import-bypass.yml @@ -0,0 +1,5 @@ +--- +title: Fix project import restricted visibility bypass via API +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-sarcila-fix-weak-session-management.yml b/changelogs/unreleased/security-sarcila-fix-weak-session-management.yml new file mode 100644 index 00000000000..a37a3099519 --- /dev/null +++ b/changelogs/unreleased/security-sarcila-fix-weak-session-management.yml @@ -0,0 +1,6 @@ +--- +title: Fix weak session management by clearing password reset tokens after login (username/email) + are updated +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-ssrf-kubernetes-dns.yml b/changelogs/unreleased/security-ssrf-kubernetes-dns.yml new file mode 100644 index 00000000000..4d6335e4b08 --- /dev/null +++ b/changelogs/unreleased/security-ssrf-kubernetes-dns.yml @@ -0,0 +1,5 @@ +--- +title: Fix SSRF via DNS rebinding in Kubernetes Integration +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-add-delete-confirmation.yml b/changelogs/unreleased/sh-add-delete-confirmation.yml new file mode 100644 index 00000000000..21c383183e7 --- /dev/null +++ b/changelogs/unreleased/sh-add-delete-confirmation.yml @@ -0,0 +1,5 @@ +--- +title: Make it harder to delete issuables accidentally +merge_request: 32376 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-ci-lint-500-error.yml b/changelogs/unreleased/sh-fix-ci-lint-500-error.yml new file mode 100644 index 00000000000..74d9f980d46 --- /dev/null +++ b/changelogs/unreleased/sh-fix-ci-lint-500-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix 500 error in CI lint when included templates are an array +merge_request: 32232 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-snippet-visibility-api.yml b/changelogs/unreleased/sh-fix-snippet-visibility-api.yml new file mode 100644 index 00000000000..5cfb9cdedc0 --- /dev/null +++ b/changelogs/unreleased/sh-fix-snippet-visibility-api.yml @@ -0,0 +1,5 @@ +--- +title: Fix snippets API not working with visibility level +merge_request: 32286 +author: +type: fixed diff --git a/changelogs/unreleased/sh-support-content-for-snippets-api.yml b/changelogs/unreleased/sh-support-content-for-snippets-api.yml new file mode 100644 index 00000000000..60a5d98da46 --- /dev/null +++ b/changelogs/unreleased/sh-support-content-for-snippets-api.yml @@ -0,0 +1,5 @@ +--- +title: Standardize use of `content` parameter in snippets API +merge_request: 32296 +author: +type: changed diff --git a/changelogs/unreleased/tc-cleanup-issue-created-text-mail.yml b/changelogs/unreleased/tc-cleanup-issue-created-text-mail.yml new file mode 100644 index 00000000000..cc590ee38eb --- /dev/null +++ b/changelogs/unreleased/tc-cleanup-issue-created-text-mail.yml @@ -0,0 +1,5 @@ +--- +title: Bring text mail for new issue & MR more in line +merge_request: 32254 +author: +type: changed diff --git a/changelogs/unreleased/update-workhorse.yml b/changelogs/unreleased/update-workhorse.yml new file mode 100644 index 00000000000..e6e5720e9b6 --- /dev/null +++ b/changelogs/unreleased/update-workhorse.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Workhorse to v8.10.0 +merge_request: 32501 +author: +type: other diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 40a80429afa..33c90989548 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -606,3 +606,10 @@ :why: https://github.com/egonSchiele/contracts.ruby/blob/master/LICENSE :versions: [] :when: 2019-04-01 11:29:39.361015000 Z +- - :license + - atlassian-jwt + - Apache 2.0 + - :who: Takuya Noguchi + :why: https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/master/LICENSE.txt + :versions: [] + :when: 2019-08-30 05:45:35.317663000 Z diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 81433b620bc..4160f488a7a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -515,7 +515,7 @@ Settings['sidekiq']['log_format'] ||= 'default' Settings['gitlab_shell'] ||= Settingslogic.new({}) Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || Settings.gitlab['user_home'] + '/gitlab-shell/') Settings.gitlab_shell['hooks_path'] = :deprecated_use_gitlab_shell_path_instead -Settings.gitlab_shell['authorized_keys_file'] ||= nil +Settings.gitlab_shell['authorized_keys_file'] ||= File.join(Dir.home, '.ssh', 'authorized_keys') Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret') Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? diff --git a/config/initializers/asset_proxy_settings.rb b/config/initializers/asset_proxy_settings.rb new file mode 100644 index 00000000000..92247aba1b8 --- /dev/null +++ b/config/initializers/asset_proxy_settings.rb @@ -0,0 +1,6 @@ +# +# Asset proxy settings +# +ActiveSupport.on_load(:active_record) do + Banzai::Filter::AssetProxyFilter.initialize_settings +end diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb index 18e067c8854..cad662e12f3 100644 --- a/config/initializers/fill_shards.rb +++ b/config/initializers/fill_shards.rb @@ -1,3 +1,5 @@ -if Shard.connected? && !Gitlab::Database.read_only? +# The `table_exists?` check is needed because during our migration rollback testing, +# `Shard.connected?` could be cached and return true even though the table doesn't exist +if Shard.connected? && Shard.table_exists? && !Gitlab::Database.read_only? Shard.populate! end diff --git a/config/initializers/rest-client-hostname_override.rb b/config/initializers/rest-client-hostname_override.rb new file mode 100644 index 00000000000..80b123ebe61 --- /dev/null +++ b/config/initializers/rest-client-hostname_override.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module RestClient + class Request + attr_accessor :hostname_override + + module UrlBlocker + def transmit(uri, req, payload, &block) + begin + ip, hostname_override = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_settings_local_requests?, + allow_localhost: allow_settings_local_requests?, + dns_rebind_protection: dns_rebind_protection?) + + self.hostname_override = hostname_override + rescue Gitlab::UrlBlocker::BlockedUrlError => e + raise ArgumentError, "URL '#{uri}' is blocked: #{e.message}" + end + + # Gitlab::UrlBlocker returns a Addressable::URI which we need to coerce + # to URI so that rest-client can use it to determine if it's a + # URI::HTTPS or not. It uses it to set `net.use_ssl` to true or not: + # + # https://github.com/rest-client/rest-client/blob/f450a0f086f1cd1049abbef2a2c66166a1a9ba71/lib/restclient/request.rb#L656 + ip_as_uri = URI.parse(ip) + super(ip_as_uri, req, payload, &block) + end + + def net_http_object(hostname, port) + super.tap do |http| + http.hostname_override = hostname_override if hostname_override + end + end + + private + + def dns_rebind_protection? + return false if Gitlab.http_proxy_env? + + Gitlab::CurrentSettings.dns_rebinding_protection_enabled? + end + + def allow_settings_local_requests? + Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? + end + end + + prepend UrlBlocker + end +end diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index 1d2bb2bce0a..d8a4da8cdf9 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -19,6 +19,7 @@ Rails.application.configure do |config| Warden::Manager.after_authentication(scope: :user) do |user, auth, opts| ActiveSession.cleanup(user) + Gitlab::AnonymousSession.new(auth.request.remote_ip, session_id: auth.request.session.id).cleanup_session_per_ip_entries end Warden::Manager.after_set_user(scope: :user, only: :fetch) do |user, auth, opts| diff --git a/config/routes.rb b/config/routes.rb index d633228a495..c333550f758 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -110,6 +110,7 @@ Rails.application.routes.draw do draw :smartcard draw :jira_connect draw :username + draw :trial_registration end Gitlab.ee do diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb index 920f8454ce2..096ef146e07 100644 --- a/config/routes/uploads.rb +++ b/config/routes/uploads.rb @@ -30,6 +30,10 @@ scope path: :uploads do to: 'uploads#create', constraints: { model: /personal_snippet|user/, id: /\d+/ }, as: 'upload' + + post ':model/authorize', + to: 'uploads#authorize', + constraints: { model: /personal_snippet|user/ } end # Redirect old note attachments path to new uploads path. diff --git a/db/fixtures/development/98_gitlab_instance_administration_project.rb b/db/fixtures/development/98_gitlab_instance_administration_project.rb new file mode 100644 index 00000000000..6cf3ae95cee --- /dev/null +++ b/db/fixtures/development/98_gitlab_instance_administration_project.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute! diff --git a/db/fixtures/production/998_gitlab_instance_administration_project.rb b/db/fixtures/production/998_gitlab_instance_administration_project.rb new file mode 100644 index 00000000000..6cf3ae95cee --- /dev/null +++ b/db/fixtures/production/998_gitlab_instance_administration_project.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute! diff --git a/db/migrate/20190219201635_add_asset_proxy_settings.rb b/db/migrate/20190219201635_add_asset_proxy_settings.rb new file mode 100644 index 00000000000..9de38cf8a89 --- /dev/null +++ b/db/migrate/20190219201635_add_asset_proxy_settings.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddAssetProxySettings < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :application_settings, :asset_proxy_enabled, :boolean, default: false, null: false + add_column :application_settings, :asset_proxy_url, :string # rubocop:disable Migration/AddLimitToStringColumns + add_column :application_settings, :asset_proxy_whitelist, :text + add_column :application_settings, :encrypted_asset_proxy_secret_key, :text + add_column :application_settings, :encrypted_asset_proxy_secret_key_iv, :string # rubocop:disable Migration/AddLimitToStringColumns + end +end diff --git a/db/migrate/20190719122333_add_login_recaptcha_protection_enabled_to_application_settings.rb b/db/migrate/20190719122333_add_login_recaptcha_protection_enabled_to_application_settings.rb new file mode 100644 index 00000000000..4561e1e8aa9 --- /dev/null +++ b/db/migrate/20190719122333_add_login_recaptcha_protection_enabled_to_application_settings.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLoginRecaptchaProtectionEnabledToApplicationSettings < ActiveRecord::Migration[5.1] + DOWNTIME = false + + def change + add_column :application_settings, :login_recaptcha_protection_enabled, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb b/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb new file mode 100644 index 00000000000..951ff41f1a8 --- /dev/null +++ b/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddActiveJobsLimitToPlans < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :plans, :active_jobs_limit, :integer, default: 0 + end + + def down + remove_column :plans, :active_jobs_limit + end +end diff --git a/db/migrate/20190822175441_rename_epics_state_to_state_id.rb b/db/migrate/20190822175441_rename_epics_state_to_state_id.rb new file mode 100644 index 00000000000..7f40d164a8e --- /dev/null +++ b/db/migrate/20190822175441_rename_epics_state_to_state_id.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class RenameEpicsStateToStateId < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + rename_column_concurrently :epics, :state, :state_id + end + + def down + cleanup_concurrent_column_rename :epics, :state_id, :state + end +end diff --git a/db/migrate/20190823055948_change_clusters_namespace_per_environment_default.rb b/db/migrate/20190823055948_change_clusters_namespace_per_environment_default.rb new file mode 100644 index 00000000000..919ce807869 --- /dev/null +++ b/db/migrate/20190823055948_change_clusters_namespace_per_environment_default.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ChangeClustersNamespacePerEnvironmentDefault < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + change_column_default :clusters, :namespace_per_environment, from: false, to: true + end +end diff --git a/db/post_migrate/20190822185441_cleanup_epics_state_id_rename.rb b/db/post_migrate/20190822185441_cleanup_epics_state_id_rename.rb new file mode 100644 index 00000000000..471b2ab9ca2 --- /dev/null +++ b/db/post_migrate/20190822185441_cleanup_epics_state_id_rename.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CleanupEpicsStateIdRename < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + cleanup_concurrent_column_rename :epics, :state, :state_id + end + + def down + rename_column_concurrently :epics, :state_id, :state + end +end diff --git a/db/schema.rb b/db/schema.rb index 54774b0a65b..5999a859e77 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -272,12 +272,18 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.boolean "lock_memberships_to_ldap", default: false, null: false t.boolean "time_tracking_limit_to_hours", default: false, null: false t.string "grafana_url", default: "/-/grafana", null: false + t.boolean "login_recaptcha_protection_enabled", default: false, null: false t.string "outbound_local_requests_whitelist", limit: 255, default: [], null: false, array: true t.integer "raw_blob_request_limit", default: 300, null: false t.boolean "allow_local_requests_from_web_hooks_and_services", default: false, null: false t.boolean "allow_local_requests_from_system_hooks", default: true, null: false t.bigint "instance_administration_project_id" t.string "snowplow_collector_hostname" + t.boolean "asset_proxy_enabled", default: false, null: false + t.string "asset_proxy_url" + t.text "asset_proxy_whitelist" + t.text "encrypted_asset_proxy_secret_key" + t.string "encrypted_asset_proxy_secret_key_iv" t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" @@ -929,7 +935,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.integer "cluster_type", limit: 2, default: 3, null: false t.string "domain" t.boolean "managed", default: true, null: false - t.boolean "namespace_per_environment", default: false, null: false + t.boolean "namespace_per_environment", default: true, null: false t.index ["enabled"], name: "index_clusters_on_enabled" t.index ["user_id"], name: "index_clusters_on_user_id" end @@ -1280,11 +1286,11 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.date "due_date_fixed" t.boolean "start_date_is_fixed" t.boolean "due_date_is_fixed" - t.integer "state", limit: 2, default: 1, null: false t.integer "closed_by_id" t.datetime "closed_at" t.integer "parent_id" t.integer "relative_position" + t.integer "state_id", limit: 2, default: 1, null: false t.index ["assignee_id"], name: "index_epics_on_assignee_id" t.index ["author_id"], name: "index_epics_on_author_id" t.index ["closed_by_id"], name: "index_epics_on_closed_by_id" @@ -2514,6 +2520,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.string "title" t.integer "active_pipelines_limit" t.integer "pipeline_size_limit" + t.integer "active_jobs_limit", default: 0 t.index ["name"], name: "index_plans_on_name" end diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif Binary files differindex d35cf55804f..a0ec2d4f10a 100644 --- a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif +++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif Binary files differindex 29b28df1cbd..a9d5dd7e73e 100644 --- a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif +++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md index 39174780e24..4d678030531 100644 --- a/doc/administration/geo/replication/updating_the_geo_nodes.md +++ b/doc/administration/geo/replication/updating_the_geo_nodes.md @@ -1,15 +1,18 @@ # Updating the Geo nodes **(PREMIUM ONLY)** -Depending on which version of Geo you are updating to/from, there may be -different steps. +Depending on which version of Geo you are updating to/from, there may be different steps. ## General update steps -In order to update the Geo nodes when a new GitLab version is released, -all you need to do is update GitLab itself: +NOTE: **Note:** These general update steps are not intended for [high-availability deployments](https://docs.gitlab.com/omnibus/update/README.html#multi-node--ha-deployment), and will cause downtime. If you want to avoid downtime, consider using [zero downtime updates](https://docs.gitlab.com/omnibus/update/README.html#zero-downtime-updates). -1. Log into each node (**primary** and **secondary** nodes). -1. [Update GitLab][update]. +To update the Geo nodes when a new GitLab version is released, update **primary** +and all **secondary** nodes: + +1. Log into the **primary** node. +1. [Update GitLab on the **primary** node using Omnibus](https://docs.gitlab.com/omnibus/update/README.html). +1. Log into each **secondary** node. +1. [Update GitLab on each **secondary** node using Omnibus](https://docs.gitlab.com/omnibus/update/README.html). 1. [Test](#check-status-after-updating) **primary** and **secondary** nodes, and check version in each. ### Check status after updating @@ -27,6 +30,8 @@ everything is working correctly: 1. Test the data replication by pushing code to the **primary** node and see if it is received by **secondary** nodes. +If you encounter any issues, please consult the [Geo troubleshooting guide](troubleshooting.md). + ## Upgrading to GitLab 12.1 By default, GitLab 12.1 will attempt to automatically upgrade the embedded PostgreSQL server to 10.7 from 9.6. Please see [the omnibus documentation](https://docs.gitlab.com/omnibus/settings/database.html#upgrading-a-geo-instance) for the recommended procedure. diff --git a/doc/administration/raketasks/uploads/sanitize.md b/doc/administration/raketasks/uploads/sanitize.md index ae5ccfb9e37..7574660d848 100644 --- a/doc/administration/raketasks/uploads/sanitize.md +++ b/doc/administration/raketasks/uploads/sanitize.md @@ -37,6 +37,8 @@ Parameter | Type | Description `stop_id` | integer | Only uploads with equal or smaller ID will be processed `dry_run` | boolean | Do not remove EXIF data, only check if EXIF data are present or not, default: true `sleep_time` | float | Pause for number of seconds after processing each image, default: 0.3 seconds +`uploader` | string | Run sanitization only for uploads of the given uploader (`FileUploader`, `PersonalFileUploader`, `NamespaceFileUploader`) +`since` | date | Run sanitization only for uploads newer than given date (e.g. `2019-05-01`) If you have too many uploads, you can speed up sanitization by setting `sleep_time` to a lower value or by running multiple rake tasks in parallel, diff --git a/doc/api/applications.md b/doc/api/applications.md index 82955f0c1db..807a0e57e8b 100644 --- a/doc/api/applications.md +++ b/doc/api/applications.md @@ -88,7 +88,9 @@ DELETE /applications/:id Parameters: -- `id` (required) - The id of the application (not the application_id) +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------| +| `id` | integer | yes | The id of the application (not the application_id). | Example request: diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index df8cf06fc4a..2e2e1bb5e1e 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -212,22 +212,25 @@ group, this can be achieved quite easily with the API. First, find the ID of the projects you're interested in, by either listing all projects: -``` +```bash curl --header 'PRIVATE-TOKEN: <your_access_token>' https://gitlab.example.com/api/v4/projects ``` -Or finding the ID of a group and then listing all projects in that group: +Or finding the ID of a group: -``` +```bash curl --header 'PRIVATE-TOKEN: <your_access_token>' https://gitlab.example.com/api/v4/groups +``` -# For group 1234: +Then listing all projects in that group (for example, group 1234): + +```bash curl --header 'PRIVATE-TOKEN: <your_access_token>' https://gitlab.example.com/api/v4/groups/1234 ``` With those IDs, add the same deploy key to all: -``` +```bash for project_id in 321 456 987; do curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \ --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v4/projects/${project_id}/deploy_keys diff --git a/doc/api/epics.md b/doc/api/epics.md index 87ae0c48199..675b88649e0 100644 --- a/doc/api/epics.md +++ b/doc/api/epics.md @@ -165,7 +165,7 @@ POST /groups/:id/epics | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `title` | string | yes | The title of the epic | | `labels` | string | no | The comma separated list of labels | -| `description` | string | no | The description of the epic | +| `description` | string | no | The description of the epic. Limited to 1 000 000 characters. | | `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | | `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | | `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) | @@ -231,7 +231,7 @@ PUT /groups/:id/epics/:epic_iid | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `epic_iid` | integer/string | yes | The internal ID of the epic | | `title` | string | no | The title of an epic | -| `description` | string | no | The description of an epic | +| `description` | string | no | The description of an epic. Limited to 1 000 000 characters. | | `labels` | string | no | The comma separated list of labels | | `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | | `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | diff --git a/doc/api/issues.md b/doc/api/issues.md index cadc9291489..7498d2d840b 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -590,7 +590,7 @@ POST /projects/:id/issues | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `iid` | integer/string | no | The internal ID of the project's issue (requires admin or project owner rights) | | `title` | string | yes | The title of an issue | -| `description` | string | no | The description of an issue | +| `description` | string | no | The description of an issue. Limited to 1 000 000 characters. | | `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | | `assignee_ids` | integer array | no | The ID of a user to assign issue | | `milestone_id` | integer | no | The global ID of a milestone to assign issue | @@ -691,7 +691,7 @@ PUT /projects/:id/issues/:issue_iid | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `issue_iid` | integer | yes | The internal ID of a project's issue | | `title` | string | no | The title of an issue | -| `description` | string | no | The description of an issue | +| `description` | string | no | The description of an issue. Limited to 1 000 000 characters. | | `confidential` | boolean | no | Updates an issue to be confidential | | `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | | `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index bf83bfd7e6a..b73fe38f53e 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -152,8 +152,8 @@ POST /projects/:id/approval_rules | `id` | integer | yes | The ID of a project | | `name` | string | yes | The name of the approval rule | | `approvals_required` | integer | yes | The number of required approvals for this rule | -| `users` | integer | no | The ids of users as approvers | -| `groups` | integer | no | The ids of groups as approvers | +| `user_ids` | Array | no | The ids of users as approvers | +| `group_ids` | Array | no | The ids of groups as approvers | ```json { @@ -231,8 +231,8 @@ PUT /projects/:id/approval_rules/:approval_rule_id | `approval_rule_id` | integer | yes | The ID of a approval rule | | `name` | string | yes | The name of the approval rule | | `approvals_required` | integer | yes | The number of required approvals for this rule | -| `users` | integer | no | The ids of users as approvers | -| `groups` | integer | no | The ids of groups as approvers | +| `user_ids` | Array | no | The ids of users as approvers | +| `group_ids` | Array | no | The ids of groups as approvers | ```json { @@ -525,6 +525,270 @@ PUT /projects/:id/merge_requests/:merge_request_iid/approvers } ``` +### Get merge request level rules + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can request information about a merge request's approval rules using the following endpoint: + +``` +GET /projects/:id/merge_requests/:merge_request_iid/approval_rules +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|---------------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | + +```json +[ + { + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 5, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" + } + ], + "approvals_required": 3, + "source_rule": null, + "users": [ + { + "id": 5, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + ], + "contains_hidden_groups": false + } +] +``` + +### Create merge request level rule + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can create merge request approval rules using the following endpoint: + +``` +POST /projects/:id/merge_requests/:merge_request_iid/approval_rules +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------------|---------|----------|------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | +| `name` | string | yes | The name of the approval rule | +| `approvals_required` | integer | yes | The number of required approvals for this rule | +| `approval_project_rule_id` | integer | no | The ID of a project-level approval rule | +| `user_ids` | Array | no | The ids of users as approvers | +| `group_ids` | Array | no | The ids of groups as approvers | + +**Important:** When `approval_project_rule_id` is set, the `name`, `users` and +`groups` of project-level rule will be copied. The `approvals_required` specified +will be used. + +```json +{ + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" + } + ], + "approvals_required": 1, + "source_rule": null, + "users": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + ], + "contains_hidden_groups": false +} +``` + +### Update merge request level rule + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can update merge request approval rules using the following endpoint: + +``` +PUT /projects/:id/merge_request/:merge_request_iid/approval_rules/:approval_rule_id +``` + +**Important:** Approvers and groups not in the `users`/`groups` param will be **removed** + +**Important:** Updating a `report_approver` or `code_owner` rule is not allowed. +These are system generated rules. + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The ID of MR | +| `approval_rule_id` | integer | yes | The ID of a approval rule | +| `name` | string | yes | The name of the approval rule | +| `approvals_required` | integer | yes | The number of required approvals for this rule | +| `user_ids` | Array | no | The ids of users as approvers | +| `group_ids` | Array | no | The ids of groups as approvers | + +```json +{ + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" + } + ], + "approvals_required": 1, + "source_rule": null, + "users": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + ], + "contains_hidden_groups": false +} +``` + +### Delete merge request level rule + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can delete merge request approval rules using the following endpoint: + +``` +DELETE /projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id +``` + +**Important:** Deleting a `report_approver` or `code_owner` rule is not allowed. +These are system generated rules. + +**Parameters:** + +| Attribute | Type | Required | Description | +|---------------------|---------|----------|---------------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The ID of MR | +| `approval_rule_id` | integer | yes | The ID of a approval rule | + ## Approve Merge Request >**Note:** This API endpoint is only available on 8.9 Starter and above. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 49ed4968b0d..0d030ef30c8 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -837,7 +837,7 @@ POST /projects/:id/merge_requests | `title` | string | yes | Title of MR | | `assignee_id` | integer | no | Assignee user ID | | `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | -| `description` | string | no | Description of MR | +| `description` | string | no | Description of MR. Limited to 1 000 000 characters. | | `target_project_id` | integer | no | The target project (numeric id) | | `labels` | string | no | Labels for MR as a comma-separated list | | `milestone_id` | integer | no | The global ID of a milestone | @@ -990,7 +990,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | | `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | -| `description` | string | no | Description of MR | +| `description` | string | no | Description of MR. Limited to 1 000 000 characters. | | `state_event` | string | no | New state (close/reopen) | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `squash` | boolean | no | Squash commits into a single commit when merging | diff --git a/doc/api/notes.md b/doc/api/notes.md index acbf0334563..1f5baf7d0e1 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -113,7 +113,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `issue_iid` (required) - The IID of an issue -- `body` (required) - The content of a note +- `body` (required) - The content of a note. Limited to 1 000 000 characters. - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) ```bash @@ -133,7 +133,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `issue_iid` (required) - The IID of an issue - `note_id` (required) - The ID of a note -- `body` (required) - The content of a note +- `body` (required) - The content of a note. Limited to 1 000 000 characters. ```bash curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note @@ -231,7 +231,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `snippet_id` (required) - The ID of a snippet -- `body` (required) - The content of a note +- `body` (required) - The content of a note. Limited to 1 000 000 characters. - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ```bash @@ -251,7 +251,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `snippet_id` (required) - The ID of a snippet - `note_id` (required) - The ID of a note -- `body` (required) - The content of a note +- `body` (required) - The content of a note. Limited to 1 000 000 characters. ```bash curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/notes?body=note @@ -354,7 +354,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `merge_request_iid` (required) - The IID of a merge request -- `body` (required) - The content of a note +- `body` (required) - The content of a note. Limited to 1 000 000 characters. - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ### Modify existing merge request note @@ -370,7 +370,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `merge_request_iid` (required) - The IID of a merge request - `note_id` (required) - The ID of a note -- `body` (required) - The content of a note +- `body` (required) - The content of a note. Limited to 1 000 000 characters. ```bash curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/notes?body=note @@ -472,7 +472,7 @@ Parameters: | --------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | | `epic_id` | integer | yes | The ID of an epic | -| `body` | string | yes | The content of a note | +| `body` | string | yes | The content of a note. Limited to 1 000 000 characters. | ```bash curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note @@ -493,7 +493,7 @@ Parameters: | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | | `epic_id` | integer | yes | The ID of an epic | | `note_id` | integer | yes | The ID of a note | -| `body` | string | yes | The content of a note | +| `body` | string | yes | The content of a note. Limited to 1 000 000 characters. | ```bash curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index e74b35fd959..850cf57a06f 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -12,14 +12,14 @@ Paginated list of Releases, sorted by `released_at`. GET /projects/:id/releases ``` -| Attribute | Type | Required | Description | -| ------------- | -------------- | -------- | --------------------------------------- | +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases" ``` Example response: @@ -39,7 +39,7 @@ Example response: "username":"root", "state":"active", "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url":"http://localhost:3000/root" + "web_url":"https://gitlab.example.com/root" }, "commit":{ "id":"079e90101242458910cccd35eab0e211dfc359c0", @@ -62,19 +62,19 @@ Example response: "sources":[ { "format":"zip", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.zip" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.zip" }, { "format":"tar.gz", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar.gz" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar.gz" }, { "format":"tar.bz2", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar.bz2" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar.bz2" }, { "format":"tar", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar" } ], "links":[ @@ -106,7 +106,7 @@ Example response: "username":"root", "state":"active", "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url":"http://localhost:3000/root" + "web_url":"https://gitlab.example.com/root" }, "commit":{ "id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4", @@ -129,19 +129,19 @@ Example response: "sources":[ { "format":"zip", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" }, { "format":"tar.gz", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" }, { "format":"tar.bz2", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" }, { "format":"tar", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" } ], "links":[ @@ -160,15 +160,15 @@ Get a Release for the given tag. GET /projects/:id/releases/:tag_name ``` -| Attribute | Type | Required | Description | -| ------------- | -------------- | -------- | --------------------------------------- | +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | -| `tag_name` | string | yes | The tag where the release will be created from. | +| `tag_name` | string | yes | The tag where the release will be created from. | Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1" ``` Example response: @@ -187,7 +187,7 @@ Example response: "username":"root", "state":"active", "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url":"http://localhost:3000/root" + "web_url":"https://gitlab.example.com/root" }, "commit":{ "id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4", @@ -210,19 +210,19 @@ Example response: "sources":[ { "format":"zip", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" }, { "format":"tar.gz", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" }, { "format":"tar.bz2", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" }, { "format":"tar", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" } ], "links":[ @@ -240,24 +240,24 @@ Create a Release. You need push access to the repository to create a Release. POST /projects/:id/releases ``` -| Attribute | Type | Required | Description | -| ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | -| `name` | string | yes | The release name. | -| `tag_name` | string | yes | The tag where the release will be created from. | -| `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). | -| `ref` | string | no | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. | -| `assets:links`| array of hash | no | An array of assets links. | -| `assets:links:name`| string | no (if `assets:links` specified, it's required) | The name of the link. | -| `assets:links:url`| string | no (if `assets:links` specified, it's required) | The url of the link. | -| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| Attribute | Type | Required | Description | +| -------------------| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | +| `name` | string | yes | The release name. | +| `tag_name` | string | yes | The tag where the release will be created from. | +| `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). | +| `ref` | string | no | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. | +| `assets:links` | array of hash | no | An array of assets links. | +| `assets:links:name`| string | required by: `assets:links` | The name of the link. | +| `assets:links:url` | string | required by: `assets:links` | The url of the link. | +| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | Example request: ```sh curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ --data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "assets": { "links": [{ "name": "hoge", "url": "https://google.com" }] } }' \ - --request POST http://localhost:3000/api/v4/projects/24/releases + --request POST https://gitlab.example.com/api/v4/projects/24/releases ``` Example response: @@ -276,7 +276,7 @@ Example response: "username":"root", "state":"active", "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url":"http://localhost:3000/root" + "web_url":"https://gitlab.example.com/root" }, "commit":{ "id":"079e90101242458910cccd35eab0e211dfc359c0", @@ -299,19 +299,19 @@ Example response: "sources":[ { "format":"zip", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.zip" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.zip" }, { "format":"tar.gz", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.tar.gz" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.tar.gz" }, { "format":"tar.bz2", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.tar.bz2" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.tar.bz2" }, { "format":"tar", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.tar" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.3/awesome-app-v0.3.tar" } ], "links":[ @@ -334,18 +334,18 @@ Update a Release. PUT /projects/:id/releases/:tag_name ``` -| Attribute | Type | Required | Description | -| ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | -| `tag_name` | string | yes | The tag where the release will be created from. | -| `name` | string | no | The release name. | -| `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). | +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag where the release will be created from. | +| `name` | string | no | The release name. | +| `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). | | `released_at` | datetime | no | The date when the release will be/was ready. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | Example request: ```sh -curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1" +curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1" ``` Example response: @@ -364,7 +364,7 @@ Example response: "username":"root", "state":"active", "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url":"http://localhost:3000/root" + "web_url":"https://gitlab.example.com/root" }, "commit":{ "id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4", @@ -387,19 +387,19 @@ Example response: "sources":[ { "format":"zip", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" }, { "format":"tar.gz", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" }, { "format":"tar.bz2", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" }, { "format":"tar", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" } ], "links":[ @@ -417,15 +417,15 @@ Delete a Release. Deleting a Release will not delete the associated tag. DELETE /projects/:id/releases/:tag_name ``` -| Attribute | Type | Required | Description | -| ------------- | -------------- | -------- | --------------------------------------- | +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | -| `tag_name` | string | yes | The tag where the release will be created from. | +| `tag_name` | string | yes | The tag where the release will be created from. | Example request: ```sh -curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1" +curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1" ``` Example response: @@ -444,7 +444,7 @@ Example response: "username":"root", "state":"active", "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url":"http://localhost:3000/root" + "web_url":"https://gitlab.example.com/root" }, "commit":{ "id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4", @@ -467,19 +467,19 @@ Example response: "sources":[ { "format":"zip", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip" }, { "format":"tar.gz", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz" }, { "format":"tar.bz2", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2" }, { "format":"tar", - "url":"http://localhost:3000/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" + "url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar" } ], "links":[ diff --git a/doc/api/settings.md b/doc/api/settings.md index 710b63c9a2f..a14b0d3632a 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -67,7 +67,10 @@ Example response: "local_markdown_version": 0, "allow_local_requests_from_hooks_and_services": true, "allow_local_requests_from_web_hooks_and_services": true, - "allow_local_requests_from_system_hooks": false + "allow_local_requests_from_system_hooks": false, + "asset_proxy_enabled": true, + "asset_proxy_url": "https://assets.example.com", + "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"] } ``` @@ -121,6 +124,10 @@ Example response: "domain_whitelist": [], "domain_blacklist_enabled" : false, "domain_blacklist" : [], + "external_authorization_service_enabled": true, + "external_authorization_service_url": "https://authorize.me", + "external_authorization_service_default_label": "default", + "external_authorization_service_timeout": 0.5, "user_oauth_applications": true, "after_sign_out_path": "", "container_registry_token_expire_delay": 5, @@ -141,6 +148,9 @@ Example response: "user_show_add_ssh_key_message": true, "file_template_project_id": 1, "local_markdown_version": 0, + "asset_proxy_enabled": true, + "asset_proxy_url": "https://assets.example.com", + "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"], "geo_node_allowed_ips": "0.0.0.0/0, ::/0", "allow_local_requests_from_hooks_and_services": true, "allow_local_requests_from_web_hooks_and_services": true, @@ -151,20 +161,13 @@ Example response: Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see these parameters: -- `external_authorization_service_enabled` -- `external_authorization_service_url` -- `external_authorization_service_default_label` -- `external_authorization_service_timeout` - `file_template_project_id` - `geo_node_allowed_ips` +- `geo_status_timeout` Example responses: **(PREMIUM ONLY)** ```json - "external_authorization_service_enabled": true, - "external_authorization_service_url": "https://authorize.me", - "external_authorization_service_default_label": "default", - "external_authorization_service_timeout": 0.5, "file_template_project_id": 1, "geo_node_allowed_ips": "0.0.0.0/0, ::/0" ``` @@ -184,57 +187,66 @@ are listed in the descriptions of the relevant settings. | `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable akismet spam protection. | | `allow_group_owners_to_manage_ldap` | boolean | no | **(PREMIUM)** Set to `true` to allow group owners to manage LDAP | | `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from hooks and services. | -| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. | | `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. | +| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. | +| `archive_builds_in_human_readable` | string | no | Set the duration for which the jobs will be considered as old and expired. Once that time passes, the jobs will be archived and no longer able to be retried. Make it empty to never expire jobs. It has to be no less than 1 day, for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>. | +| `asset_proxy_enabled` | boolean | no | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. GitLab restart is required to apply changes. | +| `asset_proxy_secret_key` | string | no | Shared secret with the asset proxy server. GitLab restart is required to apply changes. | +| `asset_proxy_url` | string | no | URL of the asset proxy server. GitLab restart is required to apply changes. | +| `asset_proxy_whitelist` | string or array of strings | no | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. GitLab restart is required to apply changes. | | `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. | | `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. | | `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. | | `check_namespace_plan` | boolean | no | **(PREMIUM)** Enabling this will make only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. | -| `clientside_sentry_dsn` | string | required by: `clientside_sentry_enabled` | Clientside Sentry Data Source Name. | -| `clientside_sentry_enabled` | boolean | no | (**If enabled, requires:** `clientside_sentry_dsn`) Enable Sentry error reporting for the client side. | +| `commit_email_hostname` | string | no | Custom hostname (for private commit emails). | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts. | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take: `0` _(not protected, both developers and maintainers can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and maintainers can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but maintainers can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. | | `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | -| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `default_project_creation` | integer | no | Default project creation protection. Can take: `0` _(No one)_, `1` _(Maintainers)_ or `2` _(Developers + Maintainers)_| | `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. | +| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). | | `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. | +| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. | | `domain_blacklist` | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. | | `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. | | `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. | -| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or ip addresses to which local requests are allowed when local requests for hooks and services are disabled. | `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | | `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. | | `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. | -| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch | | `elasticsearch_aws_access_key` | string | no | **(PREMIUM)** AWS IAM access key | +| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch | | `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the elasticsearch domain is configured | | `elasticsearch_aws_secret_access_key` | string | no | **(PREMIUM)** AWS IAM secret access key | | `elasticsearch_experimental_indexer` | boolean | no | **(PREMIUM)** Use the experimental elasticsearch indexer. More info: <https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer> | | `elasticsearch_indexing` | boolean | no | **(PREMIUM)** Enable Elasticsearch indexing | -| `elasticsearch_search` | boolean | no | **(PREMIUM)** Enable Elasticsearch search | -| `elasticsearch_url` | string | no | **(PREMIUM)** The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (e.g., `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). | | `elasticsearch_limit_indexing` | boolean | no | **(PREMIUM)** Limit Elasticsearch to index certain namespaces and projects | -| `elasticsearch_project_ids` | array of integers | no | **(PREMIUM)** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | | `elasticsearch_namespace_ids` | array of integers | no | **(PREMIUM)** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | +| `elasticsearch_project_ids` | array of integers | no | **(PREMIUM)** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | +| `elasticsearch_search` | boolean | no | **(PREMIUM)** Enable Elasticsearch search | +| `elasticsearch_url` | string | no | **(PREMIUM)** The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (e.g., `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). | | `email_additional_text` | string | no | **(PREMIUM)** Additional text added to the bottom of every email for legal/auditing/compliance reasons | | `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. | -| `external_auth_client_cert` | string | no | **(PREMIUM)** (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service | -| `external_auth_client_key` | string | required by: `external_auth_client_cert` | **(PREMIUM)** Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored | -| `external_auth_client_key_pass` | string | no | **(PREMIUM)** Passphrase to use for the private key when authenticating with the external service this is encrypted when stored | -| `external_authorization_service_enabled` | boolean | no | **(PREMIUM)** (**If enabled, requires:** `external_authorization_service_default_label`, `external_authorization_service_timeout` and `external_authorization_service_url` ) Enable using an external authorization service for accessing projects | -| `external_authorization_service_default_label` | string | required by: `external_authorization_service_enabled` | **(PREMIUM)** The default classification label to use when requesting authorization and no classification label has been specified on the project | -| `external_authorization_service_timeout` | float | required by: `external_authorization_service_enabled` | **(PREMIUM)** The timeout after which an authorization request is aborted, in seconds. When a request times out, access is denied to the user. (min: 0.001, max: 10, step: 0.001) | -| `external_authorization_service_url` | string | required by: `external_authorization_service_enabled` | **(PREMIUM)** URL to which authorization requests will be directed | +| `external_auth_client_cert` | string | no | (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service | +| `external_auth_client_key_pass` | string | no | Passphrase to use for the private key when authenticating with the external service this is encrypted when stored | +| `external_auth_client_key` | string | required by: `external_auth_client_cert` | Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored | +| `external_authorization_service_default_label` | string | required by: `external_authorization_service_enabled` | The default classification label to use when requesting authorization and no classification label has been specified on the project | +| `external_authorization_service_enabled` | boolean | no | (**If enabled, requires:** `external_authorization_service_default_label`, `external_authorization_service_timeout` and `external_authorization_service_url` ) Enable using an external authorization service for accessing projects | +| `external_authorization_service_timeout` | float | required by: `external_authorization_service_enabled` | The timeout after which an authorization request is aborted, in seconds. When a request times out, access is denied to the user. (min: 0.001, max: 10, step: 0.001) | +| `external_authorization_service_url` | string | required by: `external_authorization_service_enabled` | URL to which authorization requests will be directed | | `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from | | `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. | +| `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | | `geo_status_timeout` | integer | no | **(PREMIUM)** The amount of seconds after which a request to get a secondary node status will time out. | | `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for Git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. | | `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. | | `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. | +| `grafana_enabled` | boolean | no | Enable Grafana. | +| `grafana_url` | string | no | Grafana URL. | | `gravatar_enabled` | boolean | no | Enable Gravatar. | | `hashed_storage_enabled` | boolean | no | Create new projects using hashed storage paths: Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. (EXPERIMENTAL) | | `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. | @@ -249,8 +261,10 @@ are listed in the descriptions of the relevant settings. | `housekeeping_gc_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which `git gc` is run. | | `housekeeping_incremental_repack_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which an incremental `git repack` is run. | | `html_emails_enabled` | boolean | no | Enable HTML emails. | +| `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `bitbucket_server`, `gitlab`, `google_code`, `fogbugz`, `git`, `gitlab_project`, `gitea`, `manifest`, and `phabricator`. | + | `instance_statistics_visibility_private` | boolean | no | When set to `true` Instance statistics will only be available to admins. | -| `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `gitlab`, `google_code`, `fogbugz`, `git`, and `gitlab_project`. | +| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | | `max_artifacts_size` | integer | no | Maximum artifacts size in MB | | `max_attachment_size` | integer | no | Limit attachment size in MB | | `max_pages_size` | integer | no | Maximum size of pages repositories in MB | @@ -266,6 +280,7 @@ are listed in the descriptions of the relevant settings. | `mirror_capacity_threshold` | integer | no | **(PREMIUM)** Minimum capacity to be available before scheduling more mirrors preemptively | | `mirror_max_capacity` | integer | no | **(PREMIUM)** Maximum number of mirrors that can be synchronizing at the same time. | | `mirror_max_delay` | integer | no | **(PREMIUM)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. | +| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or ip addresses to which local requests are allowed when local requests for hooks and services are disabled. | `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. | | `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. | | `password_authentication_enabled_for_web` | boolean | no | Enable authentication for the web interface via a GitLab account password. Default is `true`. | @@ -277,10 +292,12 @@ are listed in the descriptions of the relevant settings. | `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. | | `project_export_enabled` | boolean | no | Enable project export. | | `prometheus_metrics_enabled` | boolean | no | Enable prometheus metrics. | +| `protected_ci_variables` | boolean | no | Environment variables are protected by default. | | `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** 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. | `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable recaptcha. | | `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for recaptcha. | | `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for recaptcha. | +| `receive_max_input_size` | integer | no | Maximum push size (MB). | | `repository_checks_enabled` | boolean | no | GitLab will periodically run `git fsck` in all project and wiki repositories to look for silent disk corruption issues. | | `repository_size_limit` | integer | no | **(PREMIUM)** Size limit per repository (MB) | | `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. | @@ -292,13 +309,17 @@ are listed in the descriptions of the relevant settings. | `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. | | `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **(PREMIUM)** Set the maximum number of pipeline minutes that a group can use on shared Runners per month. | | `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. | -| `sign_in_text` | string | no | Text on the login page. | | `signin_enabled` | string | no | (Deprecated: Use `password_authentication_enabled_for_web` instead) Flag indicating if password authentication is enabled for the web interface. | +| `sign_in_text` | string | no | Text on the login page. | | `signup_enabled` | boolean | no | Enable registration. Default is `true`. | | `slack_app_enabled` | boolean | no | **(PREMIUM)** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. | | `slack_app_id` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app id of the Slack-app. | | `slack_app_secret` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app secret of the Slack-app. | | `slack_app_verification_token` | string | required by: `slack_app_enabled` | **(PREMIUM)** The verification token of the Slack-app. | +| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) | +| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) | +| `snowplow_enabled` | boolean | no | Enable snowplow tracking. | +| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) | | `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. | | `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. | | `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). | @@ -317,12 +338,8 @@ are listed in the descriptions of the relevant settings. | `unique_ips_limit_time_window` | integer | required by: `unique_ips_limit_enabled` | How many seconds an IP will be counted towards the limit. | | `usage_ping_enabled` | boolean | no | Every week GitLab will report license usage back to GitLab, Inc. | | `user_default_external` | boolean | no | Newly registered users will be external by default. | +| `user_default_internal_regex` | string | no | Specify an e-mail address regex pattern to identify default internal users. | | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider. | | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | -| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | -| `snowplow_enabled` | boolean | no | Enable snowplow tracking. | -| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) | -| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) | -| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) | -| `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | +| `web_ide_clientside_preview_enabled` | boolean | no | Client side evaluation (allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation). | diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 1ce0b1e7a62..f90447e124e 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -165,15 +165,15 @@ Parameters: |:--------------|:-------|:---------|:---------------------------------------------------| | `title` | string | yes | Title of a snippet. | | `file_name` | string | yes | Name of a snippet file. | -| `code` | string | yes | Content of a snippet. | +| `content` | string | yes | Content of a snippet. | | `description` | string | no | Description of a snippet. | -| `visibility` | string | yes | Snippet's [visibility](#snippet-visibility-level). | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). | Example request: ```sh curl --request POST \ - --data '{"title": "This is a snippet", "code": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \ + --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \ --header 'Content-Type: application/json' \ --header "PRIVATE-TOKEN: valid_api_token" \ https://gitlab.example.com/api/v4/snippets @@ -222,14 +222,14 @@ Parameters: | `title` | string | no | Title of a snippet. | | `file_name` | string | no | Name of a snippet file. | | `description` | string | no | Description of a snippet. | -| `code` | string | no | Content of a snippet. | +| `content` | string | no | Content of a snippet. | | `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). | Example request: ```sh curl --request PUT \ - --data '{"title": "foo", "code": "bar"}' \ + --data '{"title": "foo", "content": "bar"}' \ --header 'Content-Type: application/json' \ --header "PRIVATE-TOKEN: valid_api_token" \ https://gitlab.example.com/api/v4/snippets/1 diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md index a59a0477b80..ab9fa517e23 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -175,7 +175,7 @@ job: ### Inherit global config, but override specific settings per job You can override cache settings without overwriting the global cache by using -[anchors](../yaml/README.md#anchors). For example, if you want to override the +[anchors](../yaml/README.md#anchors). For example, if you want to override the `policy` for one job: ```yaml diff --git a/doc/ci/ci_cd_for_external_repos/github_integration.md b/doc/ci/ci_cd_for_external_repos/github_integration.md index f639e3dadee..fa56503072d 100644 --- a/doc/ci/ci_cd_for_external_repos/github_integration.md +++ b/doc/ci/ci_cd_for_external_repos/github_integration.md @@ -11,35 +11,10 @@ GitLab. <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a video on [Using GitLab CI/CD pipelines with GitHub repositories](https://www.youtube.com/watch?v=qgl3F2j-1cI). -## Connect with GitHub integration - -If the [GitHub integration](../../integration/github.md) has been enabled by your GitLab -administrator: - -1. In GitLab create a **CI/CD for external repo** project and select - **GitHub**. - - ![Create project](img/github_omniauth.png) - -1. Once authenticated, you will be redirected to a list of your repositories to - connect. Click **Connect** to select the repository. - - ![Create project](img/github_repo_list.png) - -1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/README.md). - -GitLab will: - -1. Import the project. -1. Enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter). -1. Enable [GitHub project integration](../../user/project/integrations/github.md). -1. Create a web hook on GitHub to notify GitLab of new commits. - -CAUTION: **Caution:** -Due to a 10-token limitation on the [GitHub OAuth Implementation](https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#creating-multiple-tokens-for-oauth-apps), -if you import more than 10 times, your oldest imported project's token will be -revoked. See issue [#9147](https://gitlab.com/gitlab-org/gitlab-ee/issues/9147) -for more information. +NOTE: **Note:** +Because of [GitHub limitations](https://gitlab.com/gitlab-org/gitlab-ee/issues/9147), +[GitHub OAuth](../../integration/github.html#enabling-github-oauth) +cannot be used to authenticate with GitHub as an external CI/CD repository. ## Connect with Personal Access Token @@ -47,8 +22,7 @@ NOTE: **Note:** Personal access tokens can only be used to connect GitHub.com repositories to GitLab. -If you are not using the [GitHub integration](../../integration/github.md), you can -still perform a one-off authorization with GitHub to grant GitLab access your +To perform a one-off authorization with GitHub to grant GitLab access your repositories: 1. Open <https://github.com/settings/tokens/new> to create a **Personal Access @@ -69,18 +43,19 @@ repositories: 1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/README.md). -GitLab will import the project, enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter), enable -[GitHub project integration](../../user/project/integrations/github.md), and create a web hook -on GitHub to notify GitLab of new commits. +GitLab will: + +1. Import the project. +1. Enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter) +1. Enable [GitHub project integration](../../user/project/integrations/github.md) +1. Create a web hook on GitHub to notify GitLab of new commits. ## Connect manually NOTE: **Note:** -To use **GitHub Enterprise** with **GitLab.com** use this method. +To use **GitHub Enterprise** with **GitLab.com**, use this method. -If the [GitHub integration](../../integration/github.md) is not enabled, or is enabled -for a different GitHub instance, you GitLab CI/CD can be manually enabled for -your repository: +To manually enable GitLab CI/CD for your repository: 1. In GitHub open <https://github.com/settings/tokens/new> create a **Personal Access Token.** GitLab will use this token to access your repository and diff --git a/doc/ci/ci_cd_for_external_repos/img/github_repo_list.png b/doc/ci/ci_cd_for_external_repos/img/github_repo_list.png Binary files differdeleted file mode 100644 index 73579dd3cf1..00000000000 --- a/doc/ci/ci_cd_for_external_repos/img/github_repo_list.png +++ /dev/null diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 2cbad5f101c..730e46f994e 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -575,6 +575,42 @@ For private and internal projects: docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY ``` +### Using docker-in-docker image from Container Registry + +If you want to use your own Docker images for docker-in-docker there are a few things you need to do in addition to the steps in the [docker-in-docker](#use-docker-in-docker-workflow-with-docker-executor) section: + +1. Update the `image` and `service` to point to your registry. +1. Add a service [alias](https://docs.gitlab.com/ee/ci/yaml/#servicesalias) + +Below is an example of how your `.gitlab-ci.yml` should look like, assuming you have it configured with [TLS enabled](#tls-enabled): + +```yaml + build: + image: $CI_REGISTRY/group/project/docker:19.03.1 + services: + - name: $CI_REGISTRY/group/project/docker:19.03.1-dind + alias: docker + variables: + # Specify to Docker where to create the certificates, Docker will + # create them automatically on boot, and will create + # `/certs/client` that will be shared between the service and + # build container. + DOCKER_TLS_CERTDIR: "/certs" + DOCKER_DRIVER: overlay2 + stage: build + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests +``` + +If you forget to set the service alias the `docker:19.03.1` image won't find the +`dind` service, and an error like the following is thrown: + +```sh +$ docker info +error during connect: Get http://docker:2376/v1.39/info: dial tcp: lookup docker on 192.168.0.1:53: no such host +``` + ### Container Registry examples If you're using docker-in-docker on your Runners, this is how your `.gitlab-ci.yml` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d8c6e525142..06bd9e68a18 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1989,6 +1989,7 @@ Marking a job to be run in parallel requires only a simple addition to your conf script: rspec + parallel: 5 ``` + TIP: **Tip:** Parallelize tests suites across parallel jobs. Different languages have different tools to facilitate this. @@ -2170,6 +2171,14 @@ include: - template: Auto-DevOps.gitlab-ci.yml ``` +Multiple `include:template` files: + +```yaml +include: + - template: Android-Fastlane.gitlab-ci.yml + - template: Auto-DevOps.gitlab-ci.yml +``` + All [nested includes](#nested-includes) will be executed only with the permission of the user, so it is possible to use project, remote or template includes. diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index f00a810ec42..8b5d380ad9e 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -106,7 +106,7 @@ Stage labels specify which [stage](https://about.gitlab.com/handbook/product/cat Stage labels respects the `devops::<stage_key>` naming convention. `<stage_key>` is the stage key as it is in the single source of truth for stages at <https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml> -with `_` replaced with ` `. +with `_` replaced with a space. For instance, the "Manage" stage is represented by the ~"devops::manage" label in the `gitlab-org` group since its key under `stages` is `manage`. @@ -132,7 +132,7 @@ Group labels respects the `group::<group_key>` naming convention and their color is `#A8D695`. `<group_key>` is the group key as it is in the single source of truth for groups at <https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml>, -with `_` replaced with ` `. +with `_` replaced with a space. For instance, the "Continuous Integration" group is represented by the ~"group::continuous integration" label in the `gitlab-org` group since its key diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md index 7832850a9f0..f825b3d7088 100644 --- a/doc/development/contributing/style_guides.md +++ b/doc/development/contributing/style_guides.md @@ -25,8 +25,13 @@ 1. [Python](../python_guide/index.md) 1. [Shell scripting](../shell_scripting_guide/index.md) +## Checking the style and other issues + This is also the style used by linting tools such as [RuboCop](https://github.com/rubocop-hq/rubocop) and [Hound CI](https://houndci.com). +You can run RuboCop by hand or install a tool like [Overcommit](https://github.com/sds/overcommit) to run it for you. +Overcommit will automatically run the configured checks (like Rubocop) on every modified file before commit. You can use the example overcommit configuration found in `.overcommit.yml.example` as a quickstart. +This saves you time as you don't have to wait for the same errors to be detected by the CI. --- diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index c9ae00d148a..edd83f67d3b 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -88,7 +88,7 @@ in an EE MR. To pass the test, simply remove the docs changes from the EE MR, an ## Changing document location Changing a document's location requires specific steps to be followed to ensure that -users can seamlessly access the new doc page, whether they are accesing content +users can seamlessly access the new doc page, whether they are accessing content on a GitLab instance domain at `/help` or at docs.gitlab.com. Be sure to ping a GitLab technical writer if you have any questions during the process (such as whether the move is necessary), and ensure that a technical writer reviews this diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md index 025a946da0e..158e69df2a6 100644 --- a/doc/development/documentation/structure.md +++ b/doc/development/documentation/structure.md @@ -13,7 +13,7 @@ and the section on Content in the [Style Guide](styleguide.md). ## Components of a documentation page -Most pages will be dedicated to a specifig GitLab feature or to a use case that involves +Most pages will be dedicated to a specific GitLab feature or to a use case that involves one or more features, potentially in conjunction with third-party tools. Every feature or use case document should include the following content in the following sequence, diff --git a/doc/development/git_object_deduplication.md b/doc/development/git_object_deduplication.md index 4dd1edf9b5a..e8af6346524 100644 --- a/doc/development/git_object_deduplication.md +++ b/doc/development/git_object_deduplication.md @@ -186,7 +186,7 @@ When a pool repository record is created in SQL on a Geo primary, this will eventually trigger an event on the Geo secondary. The Geo secondary will then create the pool repository in Gitaly. This leads to an "eventually consistent" situation because as each pool participant gets -synchronized, Geo will eventuall trigger garbage collection in Gitaly on +synchronized, Geo will eventually trigger garbage collection in Gitaly on the secondary, at which stage Git objects will get deduplicated. > TODO How do we handle the edge case where at the time the Geo diff --git a/doc/development/python_guide/index.md b/doc/development/python_guide/index.md index a80bee27d4a..47d9d96766c 100644 --- a/doc/development/python_guide/index.md +++ b/doc/development/python_guide/index.md @@ -7,9 +7,9 @@ As of GitLab 11.10, we require Python 3. ## Installation -There are several ways of installing python on your system. To be able to use the same version we use in production, -we suggest you use [pyenv](https://github.com/pyenv/pyenv). It works and behave similar to its counterpart in the -ruby world: [rbenv](https://github.com/rbenv/rbenv). +There are several ways of installing Python on your system. To be able to use the same version we use in production, +we suggest you use [pyenv](https://github.com/pyenv/pyenv). It works and behaves similarly to its counterpart in the +Ruby world: [rbenv](https://github.com/rbenv/rbenv). ### macOS @@ -67,7 +67,7 @@ Running this command will install both the required Python version as well as re ## Use instructions -To run any python code under the Pipenv environment, you need to first start a `virtualenv` based on the dependencies +To run any Python code under the Pipenv environment, you need to first start a `virtualenv` based on the dependencies of the application. With Pipenv, this is a simple as running: ```bash diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index f30a83a4c71..0f982c3a48b 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -13,7 +13,7 @@ a level that is difficult to manage. Test heuristics can help solve this problem. They concisely address many of the common ways bugs manifest themselves within our code. When designing our tests, take time to review known test heuristics to inform our test design. We can find some helpful heuristics documented in the Handbook in the -[Test Design](https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/test-design/) section. +[Test Engineering](https://about.gitlab.com/handbook/engineering/quality/test-engineering/#test-heuristics) section. ## Test speed diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 944bf5900c5..f4cee410066 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -45,15 +45,12 @@ rule. The first step is to ignore the column in the application code. This is necessary because Rails caches the columns and re-uses this cache in various -places. This can be done by including the `IgnorableColumn` module into the -model, followed by defining the columns to ignore. For example, to ignore +places. This can be done by defining the columns to ignore. For example, to ignore `updated_at` in the User model you'd use the following: ```ruby -class User < ActiveRecord::Base - include IgnorableColumn - - ignore_column :updated_at +class User < ApplicationRecord + self.ignored_columns += %i[updated_at] end ``` @@ -64,8 +61,7 @@ column. Both these changes should be submitted in the same merge request. Once the changes from step 1 have been released & deployed you can set up a separate merge request that removes the ignore rule. This merge request can -simply remove the `ignore_column` line, and the `include IgnorableColumn` line -if no other `ignore_column` calls remain. +simply remove the `self.ignored_columns` line. ## Renaming Columns diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md index c5939dc6856..dfb1dbcf1ed 100644 --- a/doc/install/azure/index.md +++ b/doc/install/azure/index.md @@ -179,7 +179,7 @@ to make sure your VM is configured to use a _static_ public IP address (i.e. not or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more. -## Let's open some ports! +## Let's open some ports At this stage you should have a running and fully operational VM. However, none of the services on your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the @@ -333,6 +333,7 @@ If you're running Windows, you'll need to connect using [PuTTY] or an equivalent If you're running Linux or macOS, then you already have an SSH client installed. > **Note:** +> > - Remember that you will need to login with the username and password you specified > [when you created](#basics) your Azure VM > - If you need to reset your VM password, read diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index de49508b47a..c8c1bb00d83 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -92,10 +92,11 @@ export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig:$PKG_CONFIG_PATH" To build and install the indexer, run: ```sh -git clone https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer.git -cd gitlab-elasticsearch-indexer -make -sudo make install +indexer_path=/home/git/gitlab-elasticsearch-indexer + +# Run the installation task for gitlab-elasticsearch-indexer: +sudo -u git -H bundle exec rake gitlab:indexer:install[$indexer_path] RAILS_ENV=production +cd $indexer_path && sudo make install ``` The `gitlab-elasticsearch-indexer` will be installed to `/usr/local/bin`. @@ -128,8 +129,10 @@ total are being tracked in [epic &153](https://gitlab.com/groups/gitlab-org/-/ep ## Enabling Elasticsearch -In order to enable Elasticsearch, you need to have admin access. Go to -**Admin > Settings > Integrations** and find the "Elasticsearch" section. +In order to enable Elasticsearch, you need to have admin access. Navigate to +**Admin Area** (wrench icon), then **Settings > Integrations** and expand the **Elasticsearch** section. + +Click **Save changes** for the changes to take effect. The following Elasticsearch settings are available: @@ -171,171 +174,222 @@ from the Elasticsearch index as expected. To disable the Elasticsearch integration: -1. Navigate to the **Admin > Settings > Integrations** -1. Find the 'Elasticsearch' section and uncheck 'Search with Elasticsearch enabled' - and 'Elasticsearch indexing' -1. Click **Save** for the changes to take effect -1. (Optional) Delete the existing index by running the command `sudo gitlab-rake gitlab:elastic:delete_index` +1. Navigate to the **Admin Area** (wrench icon), then **Settings > Integrations**. +1. Expand the **Elasticsearch** section and uncheck **Elasticsearch indexing** + and **Search with Elasticsearch enabled**. +1. Click **Save changes** for the changes to take effect. +1. (Optional) Delete the existing index by running one of these commands: + + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:delete_index + + # Installations from source + bundle exec rake gitlab:elastic:delete_index RAILS_ENV=production + ``` ## Adding GitLab's data to the Elasticsearch index -### Indexing small instances (database size less than 500 MiB, size of repos less than 5 GiB) +While Elasticsearch indexing is enabled, new changes in your GitLab instance will be automatically indexed as they happen. +To backfill existing data, you can use one of the methods below to index it in background jobs. -Configure Elasticsearch's host and port in **Admin > Settings**. Then index the data using one of the following commands: +### Indexing through the administration UI -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:index +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15390) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3. -# Installations from source -bundle exec rake gitlab:elastic:index RAILS_ENV=production -``` +To index via the admin area: + +1. Navigate to the **Admin Area** (wrench icon), then **Settings > Integrations** and expand the **Elasticsearch** section. +1. [Enable **Elasticsearch indexing** and configure your host and port](#enabling-elasticsearch). +1. Create empty indexes using one of the following commands: + + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:create_empty_index + + # Installations from source + bundle exec rake gitlab:elastic:create_empty_index RAILS_ENV=production + ``` + +1. Click **Index all projects**. +1. Click **Check progress** in the confirmation message to see the status of the background jobs. +1. Personal snippets need to be indexed manually by running one of these commands: + + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index_snippets + + # Installations from source + bundle exec rake gitlab:elastic:index_snippets RAILS_ENV=production + ``` + +1. After the indexing has completed, enable [**Search with Elasticsearch**](#enabling-elasticsearch). + +### Indexing through Rake tasks + +#### Indexing small instances + +CAUTION: **Warning**: +This will delete your existing indexes. + +If the database size is less than 500 MiB, and the size of all hosted repos is less than 5 GiB: + +1. [Enable **Elasticsearch indexing** and configure your host and port](#enabling-elasticsearch). +1. Index your data using one of the following commands: + + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index + + # Installations from source + bundle exec rake gitlab:elastic:index RAILS_ENV=production + ``` -After it completes the indexing process, [enable Elasticsearch searching](elasticsearch.md#enabling-elasticsearch). +1. After the indexing has completed, enable [**Search with Elasticsearch**](#enabling-elasticsearch). -### Indexing large instances +#### Indexing large instances -WARNING: **Warning**: -Performing asynchronous indexing, as this will describe, will generate a lot of sidekiq jobs. +CAUTION: **Warning**: +Performing asynchronous indexing will generate a lot of Sidekiq jobs. Make sure to prepare for this task by either [Horizontally Scaling](../administration/high_availability/README.md#basic-scaling) -or creating [extra sidekiq processes](../administration/operations/extra_sidekiq_processes.md) +or creating [extra Sidekiq processes](../administration/operations/extra_sidekiq_processes.md) -Configure Elasticsearch's host and port in **Admin > Settings > Integrations**. Then create empty indexes using one of the following commands: +1. [Enable **Elasticsearch indexing** and configure your host and port](#enabling-elasticsearch). +1. Create empty indexes using one of the following commands: -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:create_empty_index + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:create_empty_index -# Installations from source -bundle exec rake gitlab:elastic:create_empty_index RAILS_ENV=production -``` + # Installations from source + bundle exec rake gitlab:elastic:create_empty_index RAILS_ENV=production + ``` -Indexing large Git repositories can take a while. To speed up the process, you -can temporarily disable auto-refreshing and replicating. In our experience, you can expect a 20% -decrease in indexing time. We'll enable them when indexing is done. This step is optional! +1. Indexing large Git repositories can take a while. To speed up the process, you + can temporarily disable auto-refreshing and replicating. In our experience, you can expect a 20% + decrease in indexing time. We'll enable them when indexing is done. This step is optional! -```bash -curl --request PUT localhost:9200/gitlab-production/_settings --data '{ - "index" : { - "refresh_interval" : "-1", - "number_of_replicas" : 0 - } }' -``` + ```bash + curl --request PUT localhost:9200/gitlab-production/_settings --data '{ + "index" : { + "refresh_interval" : "-1", + "number_of_replicas" : 0 + } }' + ``` -Then enable Elasticsearch indexing and run project indexing tasks: +1. Index projects and their associated data: -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:index_projects + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index_projects -# Installations from source -bundle exec rake gitlab:elastic:index_projects RAILS_ENV=production -``` + # Installations from source + bundle exec rake gitlab:elastic:index_projects RAILS_ENV=production + ``` -This enqueues a Sidekiq job for each project that needs to be indexed. -You can view the jobs in the admin panel (they are placed in the `elastic_indexer` -queue), or you can query indexing status using a rake task: + This enqueues a Sidekiq job for each project that needs to be indexed. + You can view the jobs in **Admin Area > Monitoring > Background Jobs > Queues Tab** + and click `elastic_indexer`, or you can query indexing status using a rake task: -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:index_projects_status + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index_projects_status -# Installations from source -bundle exec rake gitlab:elastic:index_projects_status RAILS_ENV=production + # Installations from source + bundle exec rake gitlab:elastic:index_projects_status RAILS_ENV=production -Indexing is 65.55% complete (6555/10000 projects) -``` + Indexing is 65.55% complete (6555/10000 projects) + ``` -If you want to limit the index to a range of projects you can provide the -`ID_FROM` and `ID_TO` parameters: + If you want to limit the index to a range of projects you can provide the + `ID_FROM` and `ID_TO` parameters: -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:index_projects ID_FROM=1001 ID_TO=2000 + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index_projects ID_FROM=1001 ID_TO=2000 -# Installations from source -bundle exec rake gitlab:elastic:index_projects ID_FROM=1001 ID_TO=2000 RAILS_ENV=production -``` + # Installations from source + bundle exec rake gitlab:elastic:index_projects ID_FROM=1001 ID_TO=2000 RAILS_ENV=production + ``` -Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional. -The above examples will index all projects starting with ID `1001` up to (and including) ID `2000`. + Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional. + The above example will index all projects from ID `1001` up to (and including) ID `2000`. -TIP: **Troubleshooting:** -Sometimes the project indexing jobs queued by `gitlab:elastic:index_projects` -can get interrupted. This may happen for many reasons, but it's always safe -to run the indexing task again - it will skip those repositories that have -already been indexed. + TIP: **Troubleshooting:** + Sometimes the project indexing jobs queued by `gitlab:elastic:index_projects` + can get interrupted. This may happen for many reasons, but it's always safe + to run the indexing task again. It will skip repositories that have + already been indexed. -As the indexer stores the last commit SHA of every indexed repository in the -database, you can run the indexer with the special parameter `UPDATE_INDEX` and -it will check every project repository again to make sure that every commit in -that repository is indexed, it can be useful in case if your index is outdated: + As the indexer stores the last commit SHA of every indexed repository in the + database, you can run the indexer with the special parameter `UPDATE_INDEX` and + it will check every project repository again to make sure that every commit in + a repository is indexed, which can be useful in case if your index is outdated: -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:index_projects UPDATE_INDEX=true ID_TO=1000 + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index_projects UPDATE_INDEX=true ID_TO=1000 -# Installations from source -bundle exec rake gitlab:elastic:index_projects UPDATE_INDEX=true ID_TO=1000 RAILS_ENV=production -``` + # Installations from source + bundle exec rake gitlab:elastic:index_projects UPDATE_INDEX=true ID_TO=1000 RAILS_ENV=production + ``` -You can also use the `gitlab:elastic:clear_index_status` Rake task to force the -indexer to "forget" all progress, so retrying the indexing process from the -start. + You can also use the `gitlab:elastic:clear_index_status` Rake task to force the + indexer to "forget" all progress, so it will retry the indexing process from the + start. -The `index_projects` command enqueues jobs to index all project and wiki -repositories, and most database content. However, snippets still need to be -indexed separately. To do so, run one of these commands: +1. Personal snippets are not associated with a project and need to be indexed separately + by running one of these commands: -```sh -# Omnibus installations -sudo gitlab-rake gitlab:elastic:index_snippets + ```sh + # Omnibus installations + sudo gitlab-rake gitlab:elastic:index_snippets -# Installations from source -bundle exec rake gitlab:elastic:index_snippets RAILS_ENV=production -``` + # Installations from source + bundle exec rake gitlab:elastic:index_snippets RAILS_ENV=production + ``` -Enable replication and refreshing again after indexing (only if you previously disabled it): +1. Enable replication and refreshing again after indexing (only if you previously disabled it): -```bash -curl --request PUT localhost:9200/gitlab-production/_settings --data '{ - "index" : { - "number_of_replicas" : 1, - "refresh_interval" : "1s" - } }' -``` + ```bash + curl --request PUT localhost:9200/gitlab-production/_settings --data '{ + "index" : { + "number_of_replicas" : 1, + "refresh_interval" : "1s" + } }' + ``` -A force merge should be called after enabling the refreshing above. + A force merge should be called after enabling the refreshing above. -For Elasticsearch 6.x, before proceeding with the force merge, the index should be in read-only mode: + For Elasticsearch 6.x, the index should be in read-only mode before proceeding with the force merge: -```bash -curl --request PUT localhost:9200/gitlab-production/_settings --data '{ - "settings": { - "index.blocks.write": true - } }' -``` + ```bash + curl --request PUT localhost:9200/gitlab-production/_settings --data '{ + "settings": { + "index.blocks.write": true + } }' + ``` -Then, initiate the force merge: + Then, initiate the force merge: -```bash -curl --request POST 'http://localhost:9200/gitlab-production/_forcemerge?max_num_segments=5' -``` + ```bash + curl --request POST 'http://localhost:9200/gitlab-production/_forcemerge?max_num_segments=5' + ``` -After this, if your index is in read-only, switch back to read-write: + After this, if your index is in read-only mode, switch back to read-write: -```bash -curl --request PUT localhost:9200/gitlab-production/_settings --data '{ - "settings": { - "index.blocks.write": false - } }' -``` + ```bash + curl --request PUT localhost:9200/gitlab-production/_settings --data '{ + "settings": { + "index.blocks.write": false + } }' + ``` -Enable Elasticsearch search in **Admin > Settings > Integrations**. That's it. Enjoy it! +1. After the indexing has completed, enable [**Search with Elasticsearch**](#enabling-elasticsearch). -### Index limit +### Indexing limitations -Currently for repository and snippet files, GitLab would only index up to 1 MB of content, in order to avoid indexing timeout. +For repository and snippet files, GitLab will only index up to 1 MiB of content, in order to avoid indexing timeouts. ## GitLab Elasticsearch Rake Tasks @@ -352,7 +406,7 @@ There are several rake tasks available to you via the command line: - [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake) - This determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. - [`sudo gitlab-rake gitlab:elastic:create_empty_index`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake) - - This generates an empty index on the Elasticsearch side. + - This generates an empty index on the Elasticsearch side, deleting the existing one if present. - [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake) - This deletes all instances of IndexStatus for all projects. - [`sudo gitlab-rake gitlab:elastic:delete_index`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake) @@ -468,7 +522,7 @@ Here are some common pitfalls and how to overcome them: pp s.search_objects.to_a ``` - See [Elasticsearch Index Scopes](elasticsearch.md#elasticsearch-index-scopes) for more information on searching for specific types of data. + See [Elasticsearch Index Scopes](#elasticsearch-index-scopes) for more information on searching for specific types of data. - **I indexed all the repositories but then switched Elasticsearch servers and now I can't find anything** diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 71ea2e25533..86a66dc4569 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -15,6 +15,7 @@ Taking the trigger term as `project-name`, the commands are: | `/project-name help` | Shows all available slash commands | | `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` | | `/project-name issue show <id>` | Shows the issue with id `<id>` | +| `/project-name issue close <id>` | Closes the issue with id `<id>` | | `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` | | `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` | | `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment | diff --git a/doc/security/README.md b/doc/security/README.md index e3fb07c69c2..fe96f7f2846 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -18,3 +18,4 @@ type: index - [Enforce Two-factor authentication](two_factor_authentication.md) - [Send email confirmation on sign-up](user_email_confirmation.md) - [Security of running jobs](https://docs.gitlab.com/runner/security/) +- [Proxying images](asset_proxy.md) diff --git a/doc/security/asset_proxy.md b/doc/security/asset_proxy.md new file mode 100644 index 00000000000..6a2341c28c8 --- /dev/null +++ b/doc/security/asset_proxy.md @@ -0,0 +1,28 @@ +A possible security concern when managing a public facing GitLab instance is +the ability to steal a users IP address by referencing images in issues, comments, etc. + +For example, adding `![Example image](http://example.com/example.png)` to +an issue description will cause the image to be loaded from the external +server in order to be displayed. However this also allows the external server +to log the IP address of the user. + +One way to mitigate this is by proxying any external images to a server you +control. GitLab handles this by allowing you to run the "Camo" server +[cactus/go-camo](https://github.com/cactus/go-camo#how-it-works). +The image request is sent to the Camo server, which then makes the request for +the original image. This way an attacker only ever seems the IP address +of your Camo server. + +Once you have your Camo server up and running, you can configure GitLab to +proxy image requests to it. The following settings are supported: + +| Attribute | Description | +| ------------------------- | ----------- | +| `asset_proxy_enabled` | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. | +| `asset_proxy_secret_key` | Shared secret with the asset proxy server. | +| `asset_proxy_url` | URL of the asset proxy server. | +| `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. | + +These can be set via the [Application setting API](../api/settings.md) + +Note that a GitLab restart is required to apply any changes. diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 87dae0e7e1f..15fdb52ac00 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -706,6 +706,34 @@ will build a Docker image based on the Dockerfile rather than using buildpacks. This can be much faster and result in smaller images, especially if your Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/). +### Passing arguments to `docker build` + +Arguments can be passed to the `docker build` command using the +`AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` project variable. + +For example, to build a Docker image based on based on the `ruby:alpine` +instead of the default `ruby:latest`: + +1. Set `AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` to `--build-arg=RUBY_VERSION=alpine`. +1. Add the following to a custom `Dockerfile`: + + ```docker + ARG RUBY_VERSION=latest + FROM ruby:$RUBY_VERSION + + # ... put your stuff here + ``` + +NOTE: **Note:** +Passing in complex values (newlines and spaces, for example) will likely +cause escaping issues due to the way this argument is used in Auto DevOps. +Consider using Base64 encoding of such values to avoid this problem. + +CAUTION: **Warning:** +Avoid passing secrets as Docker build arguments if possible, as they may be +persisted in your image. See +[this discussion](https://github.com/moby/moby/issues/13490) for details. + ### Custom Helm Chart Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernetes. @@ -794,6 +822,7 @@ applications. |-----------------------------------------|------------------------------------| | `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. | | `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. | +| `AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` | Extra arguments to be passed to the `docker build` command. Note that using quotes will not prevent word splitting. [More details](#passing-arguments-to-docker-build). | | `AUTO_DEVOPS_CHART` | Helm Chart used to deploy your apps. Defaults to the one [provided by GitLab](https://gitlab.com/gitlab-org/charts/auto-deploy-app). | | `AUTO_DEVOPS_CHART_REPOSITORY` | Helm Chart repository used to search for charts. Defaults to `https://charts.gitlab.io`. | | `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | From Gitlab 11.11, used to set the name of the helm repository. Defaults to `gitlab`. | diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md index ea4223355d8..c9a5430b2c6 100644 --- a/doc/topics/git/partial_clone.md +++ b/doc/topics/git/partial_clone.md @@ -112,7 +112,7 @@ enabled on the Git server: git init # Add the remote - git remote add origin git@gitlab.com/example/jumbo-repo + git remote add origin <url> # Enable partial clone support for the remote git config --local extensions.partialClone origin diff --git a/doc/university/process/README.md b/doc/university/process/README.md index b278e02ccd5..1a555f065ed 100644 --- a/doc/university/process/README.md +++ b/doc/university/process/README.md @@ -2,10 +2,6 @@ comments: false --- ---- -title: University | Process ---- - # Suggesting improvements If you would like to teach a class or participate or help in any way please diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md index 0ce92be4994..66e645a0af8 100644 --- a/doc/university/training/gitlab_flow.md +++ b/doc/university/training/gitlab_flow.md @@ -23,8 +23,6 @@ type: reference as opposed to individual stable branches - Consider creating a tag for each version that gets deployed -## Production branch - ![inline](gitlab_flow/production_branch.png) ## Release branch @@ -36,8 +34,6 @@ type: reference - Cherry-pick critical bug fixes to stable branch for patch release - Never commit bug fixes directly to stable branch -## Release branch - ![inline](gitlab_flow/release_branches.png) ## More details diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md index 845bb7f0a81..c9a1cbb7839 100644 --- a/doc/university/training/topics/git_intro.md +++ b/doc/university/training/topics/git_intro.md @@ -15,7 +15,7 @@ comments: false - Adapts to nearly any workflow - Fast, reliable and stable file format -## Help! +## Help Use the tools at your disposal when you get stuck. diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md index e440652edfc..37107e13584 100644 --- a/doc/university/training/user_training.md +++ b/doc/university/training/user_training.md @@ -23,7 +23,7 @@ type: reference - Adapts to nearly any workflow. - Fast, reliable and stable file format. -## Help! +## Help Use the tools at your disposal when you get stuck. diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md index f5864e1f828..dbcf250bc57 100644 --- a/doc/user/admin_area/license.md +++ b/doc/user/admin_area/license.md @@ -117,4 +117,4 @@ questions that you know someone might ask. Each scenario can be a third-level heading, e.g. `### Getting error message X`. If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. -->
\ No newline at end of file +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 5e385b7216d..20691210fbd 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -44,12 +44,19 @@ there are no restrictions. These settings can be found within: -- Each project's settings. -- A group's settings. -- The **Size limit per repository (MB)** field in the **Account and limit** section of a GitLab instance's - settings by navigating to either: - - **Admin Area > Settings > General**. - - The path `/admin/application_settings`. +- Each project's settings: + 1. From the Project's homepage, navigate to **Settings > General**. + 1. Fill in the **Repository size limit (MB)** field in the **Naming, topics, avatar** section. + 1. Click **Save changes**. +- Each group's settings: + 1. From the Group's homepage, navigate to **Settings > General**. + 1. Fill in the **Repository size limit (MB)** field in the **Naming, visibility** section. + 1. Click **Save changes**. +- GitLab's global settings: + 1. From the Dashboard, navigate to **Admin Area > Settings > General**. + 1. Expand the **Account and limit** section. + 1. Fill in the **Size limit per repository (MB)** field. + 1. Click **Save changes**. The first push of a new project, including LFS objects, will be checked for size and **will** be rejected if the sum of their sizes exceeds the maximum allowed diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index bd76b052422..fa14ecdf7cb 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -127,7 +127,7 @@ but commented out to help encourage others to add to it in the future. --> GitLab administrators can force a pipeline configuration to run on every pipeline. -The configuration applies to all pipelines for a GitLab instance and is +The configuration applies to all pipelines for a GitLab instance and is sourced from: - The [instance template repository](instance_template_repository.md). diff --git a/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md index 8e53a6995fb..6d2f74af660 100644 --- a/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md +++ b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md @@ -6,7 +6,7 @@ type: reference > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/30829) in GitLab 12.2. -This setting allows you to rate limit the requests to raw endpoints, defaults to `300` requests per minute. +This setting allows you to rate limit the requests to raw endpoints, defaults to `300` requests per minute. It can be modified in **Admin Area > Network > Performance Optimization**. For example, requests over `300` per minute to `https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/controllers/application_controller.rb` will be blocked. Access to the raw file will be released after 1 minute. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 7b631a5a1cd..a030f8d96ef 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -96,7 +96,7 @@ them in a YAML file named `clair-whitelist.yml`. Read more in the ## Example -The following is a sample `.gitlab-ci.yml` that will build your Docker Image, push it to the container registry and run Container Scanning. +The following is a sample `.gitlab-ci.yml` that will build your Docker Image, push it to the container registry and run Container Scanning. ```yaml variables: @@ -155,4 +155,4 @@ docker: Error response from daemon: failed to copy xattrs: failed to set xattr " This is a result of a bug in Docker which is now [fixed](https://github.com/containerd/continuity/pull/138 "fs: add WithAllowXAttrErrors CopyOpt"). To prevent the error, ensure the Docker version that the Runner is using is `18.09.03` or higher. For more information, see -[issue #10241](https://gitlab.com/gitlab-org/gitlab-ee/issues/10241 "Investigate why Container Scanning is not working with NFS mounts").
\ No newline at end of file +[issue #10241](https://gitlab.com/gitlab-org/gitlab-ee/issues/10241 "Investigate why Container Scanning is not working with NFS mounts"). diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index fcd683ca2db..5a1cc0561fc 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -153,7 +153,7 @@ Clicking on this button will create a merge request to apply the solution onto t > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9928) in [GitLab Ultimate](https://about.gitlab.com/pricing) 12.2. -Merge Request Approvals can be configured to require approval from a member +Merge Request Approvals can be configured to require approval from a member of your security team when a vulnerability would be introduced by a merge request. This threshold is defined as `high`, `critical`, or `unknown` diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard.png Binary files differdeleted file mode 100644 index 85ab124f74c..00000000000 --- a/doc/user/application_security/security_dashboard/img/group_security_dashboard.png +++ /dev/null diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png Binary files differnew file mode 100644 index 00000000000..61f683c1335 --- /dev/null +++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 314f7c1766f..e7cda35eb98 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -63,18 +63,13 @@ Once you're on the dashboard, at the top you should see a series of filters for: - Project NOTE: **Note:** -The dashboard only shows projects with [security reports](#supported-reports) enabled in a group. +The dashboard only shows projects with [security reports](#supported-reports) enabled in a group. -![dashboard with action buttons and metrics](img/group_security_dashboard.png) +![dashboard with action buttons and metrics](img/group_security_dashboard_v12_3.png) Selecting one or more filters will filter the results in this page. -The first section is an overview of all the vulnerabilities, grouped by severity. -Underneath this overview is a timeline chart that shows how many open -vulnerabilities your projects had at various points in time. You can filter among 30, 60, and -90 days, with the default being 90. Hover over the chart to get more details about -the open vulnerabilities at a specific time. -Finally, there is a list of all the vulnerabilities in the group, sorted by severity. +The main section is a list of all the vulnerabilities in the group, sorted by severity. In that list, you can see the severity of the vulnerability, its name, its confidence (likelihood of the vulnerability to be a positive one), and the project it's from. @@ -85,6 +80,11 @@ If you hover over a row, there will appear some actions you can take: - "Create issue" - "Dismiss vulnerability" +Next to the list is a timeline chart that shows how many open +vulnerabilities your projects had at various points in time. You can filter among 30, 60, and +90 days, with the default being 90. Hover over the chart to get more details about +the open vulnerabilities at a specific time. + Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities). ## Keeping the dashboards up to date diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 743d708399c..40ed0db4c57 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -103,7 +103,7 @@ implications](../project/clusters/index.md#security-implications) before doing s NOTE: **Note:** The -[runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) +[runner/gitlab-runner](https://gitlab.com/gitlab-org/charts/gitlab-runner) chart is used to install this application with a [`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml) file. diff --git a/doc/user/clusters/img/jupyter-git-extension.gif b/doc/user/clusters/img/jupyter-git-extension.gif Binary files differindex 13a88d97425..14dc567af2a 100644 --- a/doc/user/clusters/img/jupyter-git-extension.gif +++ b/doc/user/clusters/img/jupyter-git-extension.gif diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 6891682141c..6b748981106 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -452,7 +452,7 @@ Replying to a non-thread comment will convert the non-thread comment to a thread once the reply is submitted. This conversion is considered an edit to the original comment, so a note about when it was last edited will appear underneath it. -This feature only exists for Issues, Merge requests, and Epics. Commits, Snippets and Merge request diff threads are +This feature only exists for Issues, Merge requests, and Epics. Commits, Snippets and Merge request diff threads are not supported yet. [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 72beb38fe76..2f2955f5a1c 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -331,13 +331,17 @@ No response headers are provided. ### Admin Area settings -GitLab.com does not currently use these settings. +GitLab.com: + +- Has [rate limits on raw endpoints](../../user/admin_area/settings/rate_limits_on_raw_endpoints.md) + set to the default. +- Does not have the user and IP rate limits settings enabled. ## GitLab.com at scale In addition to the GitLab Enterprise Edition Omnibus install, GitLab.com uses the following applications and settings to achieve scale. All settings are -located publicly available [chef cookbooks](https://gitlab.com/gitlab-cookbooks). +publicly available at [chef cookbooks](https://gitlab.com/gitlab-cookbooks). ### ELK diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md index 5968b91c9b7..b947a587b2b 100644 --- a/doc/user/group/epics/index.md +++ b/doc/user/group/epics/index.md @@ -4,7 +4,7 @@ type: reference, howto # Epics **(ULTIMATE)** -> Introduced in [GitLab Ultimate][ee] 10.2. +> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2. Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and @@ -116,7 +116,7 @@ To apply labels across multiple epics: ## Deleting an epic NOTE: **Note:** -To delete an epic, you need to be an [Owner][permissions] of a group/subgroup. +To delete an epic, you need to be an [Owner](../../permissions.md#group-members-permissions) of a group/subgroup. When inside a single epic view, click the **Delete** button to delete the epic. A modal will pop-up to confirm your action. @@ -154,7 +154,7 @@ link in the issue sidebar. If you have [permissions](../../permissions.md) to close an issue and create an epic in the parent group, you can promote an issue to an epic with the `/promote` -[quick action](../../project/quick_actions.md#quick-actions-for-epics-ultimate). +[quick action](../../project/quick_actions.md#quick-actions-for-issues-merge-requests-and-epics). Only issues from projects that are in groups can be promoted. When the quick action is executed: @@ -171,7 +171,7 @@ The following issue metadata will be copied to the epic: ## Searching for an epic from epics list page -> Introduced in [GitLab Ultimate][ee] 10.5. +> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.5. You can search for an epic from the list of epics using filtered search bar (similar to that of Issues and Merge requests) based on following parameters: @@ -210,10 +210,7 @@ Note that for a given group, the visibility of all projects must be the same as the group, or less restrictive. That means if you have access to a group's epic, then you already have access to its projects' issues. -You may also consult the [group permissions table][permissions]. - -[ee]: https://about.gitlab.com/pricing/ -[permissions]: ../../permissions.md#group-members-permissions +You may also consult the [group permissions table](../../permissions.md#group-members-permissions). ## Thread diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 864f1a397d5..86fb7533e70 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -150,8 +150,11 @@ side of your screen. ![Request access button](img/request_access_button.png) -Group owners and maintainers will be notified of your request and will be able to approve or -decline it on the members page. +Once access is requested: + +- Up to ten group owners are notified of your request via email. + Email is sent to the most recently active group owners. +- Any group owner can approve or decline your request on the members page. ![Manage access requests](img/access_requests_management.png) @@ -346,7 +349,7 @@ Add one or more whitelisted IP subnets using CIDR notation in comma separated fo coming from a different IP address won't be able to access the restricted content. -Restriction currently applies to UI, API access is not restricted. +Restriction currently applies to UI and API access, Git actions via ssh are not restricted. To avoid accidental lock-out, admins and group owners are are able to access the group regardless of the IP restriction. @@ -358,7 +361,7 @@ the group regardless of the IP restriction. You can restrict access to groups and their underlying projects by allowing only users with email addresses in particular domains to be added to the group. -Add email domains you want to whitelist and users with emails from different +Add email domains you want to whitelist and users with emails from different domains won't be allowed to be added to this group. Some domains cannot be restricted. These are the most popular public email domains, such as: @@ -417,7 +420,7 @@ You can disable all email notifications related to the group, which also include it's subgroups and projects. To enable this feature: - + 1. Navigate to the group's **Settings > General** page. 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**. 1. Click **Save changes**. diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index 96c4f16fe04..79f0bb4db72 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -58,7 +58,7 @@ Example `CODEOWNERS` file: \#file_with_pound.rb @owner-file-with-pound # Multiple codeowners can be specified, separated by spaces or tabs -CODEOWNERS @multiple @code @owners +CODEOWNERS @multiple @code @owners # Both usernames or email addresses can be used to match # users. Everything else will be ignored. For example this will diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index 3206a39dc08..94e0c9fd886 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -57,5 +57,5 @@ service in GitLab. If builds are not triggered, ensure you entered the right GitLab IP address in Bamboo under 'Trigger IP addresses'. -> **Note:** -> - Starting with GitLab 8.14.0, builds are triggered on push events. +NOTE: **Note:** +Starting with GitLab 8.14.0, builds are triggered on push events. diff --git a/doc/user/project/integrations/img/grafana_live_embed.png b/doc/user/project/integrations/img/grafana_live_embed.png Binary files differnew file mode 100644 index 00000000000..91970cd379a --- /dev/null +++ b/doc/user/project/integrations/img/grafana_live_embed.png diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md index 886094a6531..b06ccda8287 100644 --- a/doc/user/project/integrations/mock_ci.md +++ b/doc/user/project/integrations/mock_ci.md @@ -6,7 +6,7 @@ To set up the mock CI service server, respond to the following endpoints - `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json` - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success-with-warnings'|'skipped'|'not_found'] }` - - If the service returns a 404, it is interpreted as `pending` + - If the service returns a 404, it is interpreted as `pending` - `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}` - Just where the build is linked to, doesn't matter if implemented diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index d13592559b9..3583c0554ee 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -393,6 +393,27 @@ The following requirements must be met for the metric to unfurl: ![Embedded Metrics](img/embed_metrics.png) +### Embedding live Grafana charts + +It is also possible to embed live [Grafana](https://docs.gitlab.com/omnibus/settings/grafana.html) charts within issues, as a [Direct Linked Rendered Image](https://grafana.com/docs/reference/sharing/#direct-link-rendered-image). + +The sharing dialog within Grafana provides the link, as highlighted below. + +![Grafana Direct Linked Rendered Image](img/grafana_live_embed.png) + +NOTE: **Note:** +For this embed to display correctly the Grafana instance must be available to the target user, either as a public dashboard or on the same network. + +Copy the link and add an image tag as [inline HTML](../../markdown.md#inline-html) in your markdown. You may tweak the query parameters as required. For instance, removing the `&from=` and `&to=` parameters will give you a live chart. Here is example markup for a live chart from GitLab's public dashboard: + +```html +<img src="https://dashboards.gitlab.com/render/d-solo/RZmbBr7mk/gitlab-triage?orgId=1&refresh=30s&var-env=gprd&var-environment=gprd&var-prometheus=prometheus-01-inf-gprd&var-prometheus_app=prometheus-app-01-inf-gprd&var-backend=All&var-type=All&var-stage=main&panelId=1247&width=1000&height=300"/> +``` + +This will render like so: + +<img src="https://dashboards.gitlab.com/render/d-solo/RZmbBr7mk/gitlab-triage?orgId=1&refresh=30s&var-env=gprd&var-environment=gprd&var-prometheus=prometheus-01-inf-gprd&var-prometheus_app=prometheus-app-01-inf-gprd&var-backend=All&var-type=All&var-stage=main&panelId=1247&width=1000&height=300"/> + ## Troubleshooting If the "No data found" screen continues to appear, it could be due to: diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 84adb9637fc..f5bc6cbd988 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -2,6 +2,7 @@ > **Note:** > Starting from GitLab 8.5: +> > - the `repository` key is deprecated in favor of the `project` key > - the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key > - the `project.http_url` key is deprecated in favor of the `project.git_http_url` key @@ -12,6 +13,7 @@ > > **Note:** > Starting from GitLab 11.2: +> > - The `description` field for issues, merge requests, comments, and wiki pages > is rewritten so that simple Markdown image references (like > `![](/uploads/...)`) have their target URL changed to an absolute URL. See @@ -98,11 +100,12 @@ Below are described the supported events. Triggered when you push to the repository except when pushing tags. -> **Note:** When more than 20 commits are pushed at once, the `commits` webhook - attribute will only contain the first 20 for performance reasons. Loading - detailed commit data is expensive. Note that despite only 20 commits being - present in the `commits` attribute, the `total_commits_count` attribute will - contain the actual total. +NOTE: **Note:** +When more than 20 commits are pushed at once, the `commits` webhook +attribute will only contain the first 20 for performance reasons. Loading +detailed commit data is expensive. Note that despite only 20 commits being +present in the `commits` attribute, the `total_commits_count` attribute will +contain the actual total. **Request header**: @@ -1181,20 +1184,20 @@ X-Gitlab-Event: Job Hook ```json { - "object_kind": "job", + "object_kind": "build", "ref": "gitlab-script-trigger", "tag": false, "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "job_id": 1977, - "job_name": "test", - "job_stage": "test", - "job_status": "created", - "job_started_at": null, - "job_finished_at": null, - "job_duration": null, - "job_allow_failure": false, - "job_failure_reason": "script_failure", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": null, + "build_allow_failure": false, + "build_failure_reason": "script_failure", "project_id": 380, "project_name": "gitlab-org/gitlab-test", "user": { diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index d7d168710ef..41a7ed09281 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -192,7 +192,7 @@ You can also click the `+` to add more related issues. #### 19. Related Merge Requests -Merge requests that were mentioned in that issue's description or in the issue thread +Merge requests that were mentioned in that issue's description or in the issue thread are listed as [related merge requests](crosslinking_issues.md#from-merge-requests) here. Also, if the current issue was mentioned as related in another merge request, that merge request will be listed here. diff --git a/doc/user/project/issues/sorting_issue_lists.md b/doc/user/project/issues/sorting_issue_lists.md index 0fe86e6f410..6e31acf80bc 100644 --- a/doc/user/project/issues/sorting_issue_lists.md +++ b/doc/user/project/issues/sorting_issue_lists.md @@ -15,14 +15,14 @@ When you select **Manual** sorting, you can change the order by dragging and dropping the issues. The changed order will persist. Everyone who visits the same list will see the reordered list, with some exceptions. Each issue is assigned a relative order value, representing its relative -order with respect to the other issues in the list. When you drag-and-drop reorder +order with respect to the other issues in the list. When you drag-and-drop reorder an issue, its relative order value changes accordingly. In addition, any time that issue appears in a manually sorted list, the updated relative order value will be used for the ordering. This means that if issue `A` is drag-and-drop reordered to be above issue `B` by any user in a given list inside your GitLab instance, any time those two issues are subsequently -loaded in any list in the same instance (could be a different project issue list or a +loaded in any list in the same instance (could be a different project issue list or a different group issue list, for example), that ordering will be maintained. This ordering also affects [issue boards](../issue_board.md#issue-ordering-in-a-list). diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index e343fd45488..21016dca358 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -79,8 +79,15 @@ side of your screen. ![Request access button](img/request_access_button.png) -Project owners & maintainers will be notified of your request and will be able to approve or -decline it on the members page. +Once access is requested: + +- Up to ten project maintainers are notified of your request via email. + Email is sent to the most recently active project maintainers. +- Any project maintainer can approve or decline your request on the members page. + +NOTE: **Note:** +If a project does not have any maintainers, the notification is sent to the +most recently active owners of the project's group. ![Manage access requests](img/access_requests_management.png) diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index e94125e658d..a88fb4fc6f8 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -4,8 +4,7 @@ type: reference, howto # Allow collaboration on merge requests across forks -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395) - in GitLab 10.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395) in GitLab 10.6. When a user opens a merge request from a fork, they are given the option to allow upstream members to collaborate with them on the source branch. This allows diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index 3a409bab19d..3c6b660c63d 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -5,8 +5,7 @@ disqus_identifier: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_ # Code Quality **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1984) -in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3. With the help of [GitLab CI/CD](../../../ci/README.md), you can analyze your source code quality using GitLab Code Quality. @@ -26,7 +25,7 @@ Code Quality: Going a step further, GitLab can show the Code Quality report right in the merge request widget area: -![Code Quality Widget](img/code_quality.gif) +![Code Quality Widget](img/code_quality.png) ## Use cases diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png Binary files differindex 32b9a3b9ce4..bbb131e86e9 100644 --- a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png +++ b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png diff --git a/doc/user/project/merge_requests/img/code_quality.gif b/doc/user/project/merge_requests/img/code_quality.gif Binary files differdeleted file mode 100644 index bab921cf38b..00000000000 --- a/doc/user/project/merge_requests/img/code_quality.gif +++ /dev/null diff --git a/doc/user/project/merge_requests/img/code_quality.png b/doc/user/project/merge_requests/img/code_quality.png Binary files differnew file mode 100644 index 00000000000..a20f6476fb8 --- /dev/null +++ b/doc/user/project/merge_requests/img/code_quality.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index a94057dc3a1..d6da8cb99c7 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -218,7 +218,7 @@ Similarly, assignees are removed by deselecting them from the same dropdown menu It's also possible to manage multiple assignees: - When creating a merge request. -- Using [quick actions](../quick_actions.md#quick-actions-for-issues-and-merge-requests). +- Using [quick actions](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics). ## Resolve conflicts diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index a0432414bcf..ca9ddd91e0d 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -330,7 +330,7 @@ the dropdown) `approver` and select the user. Merge Request Approvals can be configured to require approval from a member of your security team when a vulnerability would be introduced by a merge request. -For more information, see +For more information, see [Security approvals in merge requests](../../application_security/index.md#security-approvals-in-merge-requests-ultimate). <!-- ## Troubleshooting diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md index ea59644fce6..8ac4131e10b 100644 --- a/doc/user/project/merge_requests/work_in_progress_merge_requests.md +++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md @@ -18,7 +18,7 @@ There are several ways to flag a merge request as a Work In Progress: - Add `[WIP]` or `WIP:` to the start of the merge request's title. Clicking on **Start the title with WIP:**, under the title box, when editing the merge request's description will have the same effect. -- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-and-merge-requests) +- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) in a comment in the merge request. This is a toggle, and can be repeated to change the status back. Note that any other text in the comment will be discarded. - Add "wip" or "WIP" to the start of a commit message targeting the merge request's @@ -33,7 +33,7 @@ Similar to above, when a Merge Request is ready to be merged, you can remove the - Remove `[WIP]` or `WIP:` from the start of the merge request's title. Clicking on **Remove the WIP: prefix from the title**, under the title box, when editing the merge request's description, will have the same effect. -- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-and-merge-requests) +- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) in a comment in the merge request. This is a toggle, and can be repeated to change the status back. Note that any other text in the comment will be discarded. - Click on the **Resolve WIP status** button near the bottom of the merge request description, diff --git a/doc/user/project/packages/maven_repository.md b/doc/user/project/packages/maven_repository.md index c61402afb2c..491ebc0c068 100644 --- a/doc/user/project/packages/maven_repository.md +++ b/doc/user/project/packages/maven_repository.md @@ -1,7 +1,6 @@ # GitLab Maven Repository **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5811) in - [GitLab Premium](https://about.gitlab.com/pricing/) 11.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5811) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.3. With the GitLab [Maven](https://maven.apache.org) Repository, every project can have its own space to store its Maven artifacts. diff --git a/doc/user/project/packages/npm_registry.md b/doc/user/project/packages/npm_registry.md index e01bac6b26f..48186688da9 100644 --- a/doc/user/project/packages/npm_registry.md +++ b/doc/user/project/packages/npm_registry.md @@ -1,7 +1,6 @@ # GitLab NPM Registry **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5934) - in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7. With the GitLab NPM Registry, every project can have its own space to store NPM packages. diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md index 3b80370d4a9..dc75eb450a3 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md @@ -92,4 +92,4 @@ You can have one DNS record or more than one combined: - `example.com` => `A` => `192.192.192.192` - `www` => `CNAME` => `example.com` - `MX` => `mail.example.com` -- `example.com`=> TXT => `"google-site-verification=6P08Ow5E-8Q0m6vQ7FMAqAYIDprkVV8fUf_7hZ4Qvc8"`
\ No newline at end of file +- `example.com`=> TXT => `"google-site-verification=6P08Ow5E-8Q0m6vQ7FMAqAYIDprkVV8fUf_7hZ4Qvc8"` diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md index 1821d954af3..f68dfdf02cc 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md @@ -24,7 +24,7 @@ and steps below. (`*.gitlab.io`, for GitLab.com). - A custom domain name `example.com` or subdomain `subdomain.example.com`. - Access to your domain's server control panel to set up DNS records: - - A DNS A or CNAME record poiting your domain to GitLab Pages server. + - A DNS A or CNAME record pointing your domain to GitLab Pages server. - A DNS TXT record to verify your domain's ownership. ### Steps diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md index 255d535645d..ffd9bc04c3e 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md @@ -21,6 +21,9 @@ such as [#64870](https://gitlab.com/gitlab-org/gitlab-ce/issues/64870). See all the related issues linked from this [issue's description](https://gitlab.com/gitlab-org/gitlab-ce/issues/28996) for more information. +Note: **Note:** +Using this feature requires **2 IP addresses** to be configured to the machine. + ## Requirements Before you can enable automatic provisioning of a SSL certificate for your domain, make sure you have: diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 6d538ca2455..45fdab1ca3a 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -100,4 +100,4 @@ _Read on about [Projects for GitLab Pages and URL structure](getting_started_par - Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site - You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- Fork an [example project](https://gitlab.com/pages) to build your website based upon
\ No newline at end of file +- Fork an [example project](https://gitlab.com/pages) to build your website based upon diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index a0bc10b0ed7..a9fd6544e64 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -106,12 +106,14 @@ To get started with GitLab Pages, you can either: 1. From the left sidebar, navigate to your project's **CI/CD > Pipelines** and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your site to the server. -1. Once the pipeline has finished successfully, find the link to visit your - website from your project's **Settings > Pages**. +1. After the pipeline has finished successfully, wait approximately 30 minutes + for your website to be visible. After waiting 30 minutes, find the link to + visit your website from your project's **Settings > Pages**. If the link + leads to a 404 page, wait a few minutes and try again. -Your website is then visible on your domain, and you can modify yourfiles +Your website is then visible on your domain and you can modify your files as you wish. For every modification pushed to your repository, GitLab CI/CD -will run a new pipeline to publish your changes to the server. +will run a new pipeline to immediately publish your changes to the server. _Advanced options:_ diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 456209c2180..78b6d202b32 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -107,11 +107,11 @@ in the pipelines settings page. ### Removing color codes Some test coverage tools output with ANSI color codes that won't be -parsed correctly by the regular expression and will cause coverage -parsing to fail. +parsed correctly by the regular expression and will cause coverage +parsing to fail. If your coverage tool doesn't provide an option to disable color -codes in the output, you can pipe the output of the coverage tool through a +codes in the output, you can pipe the output of the coverage tool through a small one line script that will strip the color codes off. For example: diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 647250bd02a..e373d605098 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -12,59 +12,63 @@ on a separate line in order to be properly detected and executed. Once executed, > From [GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26672), an alert is displayed when a quick action is successfully applied. -## Quick Actions for issues and merge requests - -The following quick actions are applicable to both issues and merge requests threads, -discussions, and descriptions: - -| Command | Action | Issue | Merge request | -|:---------------------------|:------------------------------ |:------|:--------------| -| `/tableflip <Comment>` | Append the comment with `(╯°□°)╯︵ ┻━┻` | ✓ | ✓ | -| `/shrug <Comment>` | Append the comment with `¯\_(ツ)_/¯` | ✓ | ✓ | -| `/todo` | Add a To Do | ✓ | ✓ | -| `/done` | Mark To Do as done | ✓ | ✓ | -| `/subscribe` | Subscribe | ✓ | ✓ | -| `/unsubscribe` | Unsubscribe | ✓ | ✓ | -| `/close` | Close | ✓ | ✓ | -| `/reopen` | Reopen | ✓ | ✓ | -| `/title <New title>` | Change title | ✓ | ✓ | -| `/award :emoji:` | Toggle emoji award | ✓ | ✓ | -| `/assign me` | Assign yourself | ✓ | ✓ | -| `/assign @user` | Assign one user | ✓ | ✓ | -| `/assign @user1 @user2` | Assign multiple users **(STARTER)** | ✓ | ✓ | -| `/unassign @user1 @user2` | Remove assignee(s) **(STARTER)** | ✓ | ✓ | -| `/reassign @user1 @user2` | Change assignee **(STARTER)** | ✓ | ✓ | -| `/unassign` | Remove current assignee | ✓ | ✓ | -| `/milestone %milestone` | Set milestone | ✓ | ✓ | -| `/remove_milestone` | Remove milestone | ✓ | ✓ | -| `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ | -| `/unlabel ~label1 ~label2` | Remove all or specific label(s)| ✓ | ✓ | -| `/relabel ~label1 ~label2` | Replace existing label(s) with those specified | ✓ | ✓ | -| `/copy_metadata <#issue>` | Copy labels and milestone from another issue in the project | ✓ | ✓ | -| `/copy_metadata <!merge_request>` | Copy labels and milestone from another merge request in the project | ✓ | ✓ | -| `/estimate <1w 3d 2h 14m>` | Set time estimate | ✓ | ✓ | -| `/remove_estimate` | Remove time estimate | ✓ | ✓ | -| `/spend <time(1h 30m)> <date(YYYY-MM-DD)>` | Add spent time; optionally, specify the date that time was spent on | ✓ | ✓ | -| `/spend <time(-1h 5m)> <date(YYYY-MM-DD)>` | Subtract spent time; optionally, specify the date that time was spent on | ✓ | ✓ | -| `/remove_time_spent` | Remove time spent | ✓ | ✓ | -| `/lock` | Lock the thread | ✓ | ✓ | -| `/unlock` | Unlock the thread | ✓ | ✓ | -| `/due <date>` | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. | ✓ | | -| `/remove_due_date` | Remove due date | ✓ | | -| `/weight <value>` | Set weight. Valid options for `<value>` include `0`, `1`, `2`, etc. **(STARTER)** | ✓ | | -| `/clear_weight` | Clears weight **(STARTER)** | ✓ | | -| `/epic <epic>` | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. **(ULTIMATE)** | ✓ | | -| `/remove_epic` | Removes from epic **(ULTIMATE)** | ✓ | | -| `/promote` | Promote issue to epic **(ULTIMATE)** | ✓ | | -| `/confidential` | Make confidential | ✓ | | -| `/duplicate <#issue>` | Mark this issue as a duplicate of another issue | ✓ | -| `/move <path/to/project>` | Move this issue to another project | ✓ | | -| `/target_branch <Local branch Name>` | Set target branch | | ✓ | -| `/wip` | Toggle the Work In Progress status | | ✓ | -| `/approve` | Approve the merge request | | ✓ | -| `/merge` | Merge (when pipeline succeeds) | | ✓ | -| `/create_merge_request <branch name>` | Create a new merge request starting from the current issue | ✓ | | -| `/relate #issue1 #issue2` | Mark issues as related **(STARTER)** | ✓ | | +## Quick Actions for issues, merge requests and epics + +The following quick actions are applicable to descriptions, discussions and threads +in issues and merge requests, as well as epics.**(ULTIMATE)** + +| Command | Issue | Merge request | Epic | Action | +|:--------------------------------------|:------|:--------------|:-----|:------ | +| `/tableflip <comment>` | ✓ | ✓ | ✓ | Append the comment with `(╯°□°)╯︵ ┻━┻` | +| `/shrug <comment>` | ✓ | ✓ | ✓ | Append the comment with `¯\_(ツ)_/¯` | +| `/todo` | ✓ | ✓ | ✓ | Add a To Do | +| `/done` | ✓ | ✓ | ✓ | Mark To Do as done | +| `/subscribe` | ✓ | ✓ | ✓ | Subscribe | +| `/unsubscribe` | ✓ | ✓ | ✓ | Unsubscribe | +| `/close` | ✓ | ✓ | ✓ | Close | +| `/reopen` | ✓ | ✓ | ✓ | Reopen | +| `/title <new title>` | ✓ | ✓ | ✓ | Change title | +| `/award :emoji:` | ✓ | ✓ | ✓ | Toggle emoji award | +| `/assign me` | ✓ | ✓ | | Assign yourself | +| `/assign @user` | ✓ | ✓ | | Assign one user | +| `/assign @user1 @user2` | ✓ | ✓ | | Assign multiple users **(STARTER)** | +| `/reassign @user1 @user2` | ✓ | ✓ | | Change assignee **(STARTER)** | +| `/unassign` | ✓ | ✓ | | Remove current assignee | +| `/unassign @user1 @user2` | ✓ | ✓ | | Remove assignee(s) **(STARTER)** | +| `/milestone %milestone` | ✓ | ✓ | | Set milestone | +| `/remove_milestone` | ✓ | ✓ | | Remove milestone | +| `/label ~label1 ~label2` | ✓ | ✓ | ✓ | Add label(s). Label names can also start without `~` but mixed syntax is not supported | +| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace existing label(s) with those specified | +| `/unlabel ~label1 ~label2` | ✓ | ✓ | ✓ | Remove all or specific label(s) | +| `/copy_metadata <#issue>` | ✓ | ✓ | | Copy labels and milestone from another issue in the project | +| `/copy_metadata <!merge_request>` | ✓ | ✓ | | Copy labels and milestone from another merge request in the project | +| `/estimate <<W>w <DD>d <hh>h <mm>m>` | ✓ | ✓ | | Set time estimate. For example, `/estimate 1w 3d 2h 14m` | +| `/remove_estimate` | ✓ | ✓ | | Remove time estimate | +| `/spend <time(<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | ✓ | ✓ | | Add spent time; optionally specify the date that time was spent on. For example, `/spend time(1h 30m)` or `/spend time(1h 30m) date(2018-08-26)` | +| `/spend <time(-<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | ✓ | ✓ | | Subtract spent time; optionally specify the date that time was spent on. For example, `/spend time(-1h 30m)` or `/spend time(-1h 30m) date(2018-08-26)` | +| `/remove_time_spent` | ✓ | ✓ | | Remove time spent | +| `/lock` | ✓ | ✓ | | Lock the thread | +| `/unlock` | ✓ | ✓ | | Unlock the thread | +| `/due <date>` | ✓ | | | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st` | +| `/remove_due_date` | ✓ | | | Remove due date | +| `/weight <value>` | ✓ | | | Set weight. Valid options for `<value>` include `0`, `1`, `2`, etc **(STARTER)** | +| `/clear_weight` | ✓ | | | Clear weight **(STARTER)** | +| `/epic <epic>` | ✓ | | | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. **(ULTIMATE)** | +| `/remove_epic` | ✓ | | | Remove from epic **(ULTIMATE)** | +| `/promote` | ✓ | | | Promote issue to epic **(ULTIMATE)** | +| `/confidential` | ✓ | | | Make confidential | +| `/duplicate <#issue>` | ✓ | | | Mark this issue as a duplicate of another issue | +| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue | +| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** | +| `/move <path/to/project>` | ✓ | | | Move this issue to another project | +| `/target_branch <local branch name>` | | ✓ | | Set target branch | +| `/wip` | | ✓ | | Toggle the Work In Progress status | +| `/approve` | | ✓ | | Approve the merge request | +| `/merge` | | ✓ | | Merge (when pipeline succeeds) | +| `/child_epic <epic>` | | | ✓ | Add child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) **(ULTIMATE)** | +| `/remove_child_epic <epic>` | | | ✓ | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) **(ULTIMATE)** | +| `/parent_epic <epic>` | | | ✓ | Set parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) **(ULTIMATE)** | +| `/remove_parent_epic` | | | ✓ | Remove parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) **(ULTIMATE)** | ## Autocomplete characters @@ -93,30 +97,6 @@ The following quick actions are applicable for commit messages: |:------------------------|:------------------------------------------| | `/tag v1.2.3 <message>` | Tags this commit with an optional message | -## Quick actions for Epics **(ULTIMATE)** - -The following quick actions are applicable for epics threads and description: - -| Command | Action | -|:---------------------------|:----------------------------------------| -| `/tableflip <Comment>` | Append the comment with `(╯°□°)╯︵ ┻━┻` | -| `/shrug <Comment>` | Append the comment with `¯\_(ツ)_/¯` | -| `/todo` | Add a To Do | -| `/done` | Mark To Do as done | -| `/subscribe` | Subscribe | -| `/unsubscribe` | Unsubscribe | -| `/close` | Close | -| `/reopen` | Reopen | -| `/title <New title>` | Change title | -| `/award :emoji:` | Toggle emoji award | -| `/label ~label1 ~label2` | Add label(s) | -| `/unlabel ~label1 ~label2` | Remove all or specific label(s) | -| `/relabel ~label1 ~label2` | Replace existing label(s) with those specified | -| `/child_epic <epic>` | Adds child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) **(ULTIMATE)**| -| `/remove_child_epic <epic>` | Removes child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) **(ULTIMATE)** | -| `/parent_epic <epic>` | Sets parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) **(ULTIMATE)** | -| `/remove_parent_epic` | Removes parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | - <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 4e3db95b6d6..58ccd8bf2ae 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -34,7 +34,7 @@ Issues or Merge Requests, both of which use Labels and Milestones, then you shou #### Disabling email notifications -You can disable all email notifications related to the project by selecting the +You can disable all email notifications related to the project by selecting the **Disable email notifications** checkbox. Only the project owner is allowed to change this setting. diff --git a/doc/user/search/advanced_search_syntax.md b/doc/user/search/advanced_search_syntax.md index c3d22e4fd29..d65dd32fe11 100644 --- a/doc/user/search/advanced_search_syntax.md +++ b/doc/user/search/advanced_search_syntax.md @@ -1,6 +1,7 @@ # Advanced Syntax Search **(STARTER ONLY)** > **Notes:** +> > - Introduced in [GitLab Enterprise Starter][ee] 9.2 > - This is the user documentation. To install and configure Elasticsearch, > visit the [administrator documentation](../../integration/elasticsearch.md). diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 6c1acc3963f..9125207167c 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -239,7 +239,7 @@ module API # because notes are redacted if they point to projects that # cannot be accessed by the user. notes = prepare_notes_for_rendering(notes) - notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } + notes.select { |n| n.visible_for?(current_user) } end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 88be76d3c78..cfcf6228225 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1174,6 +1174,9 @@ module API attributes.delete(:performance_bar_enabled) attributes.delete(:allow_local_requests_from_hooks_and_services) + # let's not expose the secret key in a response + attributes.delete(:asset_proxy_secret_key) + attributes end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index b2bf6bf7417..f445834323d 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -12,7 +12,7 @@ module API end def update_note(noteable, note_id) - note = noteable.notes.find(params[:note_id]) + note = noteable.notes.find(note_id) authorize! :admin_note, note @@ -61,8 +61,8 @@ module API end def get_note(noteable, note_id) - note = noteable.notes.with_metadata.find(params[:note_id]) - can_read_note = !note.cross_reference_not_visible_for?(current_user) + note = noteable.notes.with_metadata.find(note_id) + can_read_note = note.visible_for?(current_user) if can_read_note present note, with: Entities::Note diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 84563d66ee8..16fca9acccb 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -42,7 +42,7 @@ module API # array returned, but this is really a edge-case. notes = paginate(raw_notes) notes = prepare_notes_for_rendering(notes) - notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } + notes = notes.select { |note| note.visible_for?(current_user) } present notes, with: Entities::Note end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index a607df411a6..b4545295d54 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -51,16 +51,18 @@ module API params do requires :title, type: String, desc: 'The title of the snippet' requires :file_name, type: String, desc: 'The file name of the snippet' - requires :code, type: String, allow_blank: false, desc: 'The content of the snippet' + optional :code, type: String, allow_blank: false, desc: 'The content of the snippet (deprecated in favor of "content")' + optional :content, type: String, allow_blank: false, desc: 'The content of the snippet' optional :description, type: String, desc: 'The description of a snippet' requires :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' + mutually_exclusive :code, :content end post ":id/snippets" do authorize! :create_project_snippet, user_project - snippet_params = declared_params.merge(request: request, api: true) - snippet_params[:content] = snippet_params.delete(:code) + snippet_params = declared_params(include_missing: false).merge(request: request, api: true) + snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present? snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute @@ -80,12 +82,14 @@ module API requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' optional :title, type: String, desc: 'The title of the snippet' optional :file_name, type: String, desc: 'The file name of the snippet' - optional :code, type: String, allow_blank: false, desc: 'The content of the snippet' + optional :code, type: String, allow_blank: false, desc: 'The content of the snippet (deprecated in favor of "content")' + optional :content, type: String, allow_blank: false, desc: 'The content of the snippet' optional :description, type: String, desc: 'The description of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :code, :visibility_level + at_least_one_of :title, :file_name, :code, :content, :visibility_level + mutually_exclusive :code, :content end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c36ee5af63f..dd27ebab83d 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -36,6 +36,10 @@ module API given akismet_enabled: ->(val) { val } do requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' end + optional :asset_proxy_enabled, type: Boolean, desc: 'Enable proxying of assets' + optional :asset_proxy_url, type: String, desc: 'URL of the asset proxy server' + optional :asset_proxy_secret_key, type: String, desc: 'Shared secret with the asset proxy server' + optional :asset_proxy_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted.' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' @@ -104,6 +108,11 @@ module API requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' end + optional :login_recaptcha_protection_enabled, type: Boolean, desc: 'Helps prevent brute-force attacks' + given login_recaptcha_protection_enabled: ->(val) { val } do + requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' + requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' + end optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." optional :repository_storages, type: Array[String], desc: 'Storage paths for new projects' optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication' @@ -123,7 +132,7 @@ module API optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' - optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" + optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated' optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' given snowplow_enabled: ->(val) { val } do diff --git a/lib/api/validations/types/comma_separated_to_array.rb b/lib/api/validations/types/comma_separated_to_array.rb new file mode 100644 index 00000000000..b551878abd1 --- /dev/null +++ b/lib/api/validations/types/comma_separated_to_array.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Validations + module Types + class CommaSeparatedToArray + def self.coerce + lambda do |value| + case value + when String + value.split(',').map(&:strip) + when Array + value.map { |v| v.to_s.split(',').map(&:strip) }.flatten + else + [] + end + end + end + end + end + end +end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 52af28ce8ec..a0439089879 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -7,6 +7,14 @@ module Banzai class AbstractReferenceFilter < ReferenceFilter include CrossProjectReference + # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found + # reference (which we replace with placeholder during re-scaping). The + # random number helps ensure it's pretty close to unique. Since it's a + # transitory value (it never gets saved) we can initialize once, and it + # doesn't matter if it changes on a restart. + REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_" + REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze + def self.object_class # Implement in child class # Example: MergeRequest @@ -389,6 +397,14 @@ module Banzai def escape_html_entities(text) CGI.escapeHTML(text.to_s) end + + def escape_with_placeholders(text, placeholder_data) + escaped = escape_html_entities(text) + + escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match| + placeholder_data[$1.to_i] + end + end end end end diff --git a/lib/banzai/filter/asset_proxy_filter.rb b/lib/banzai/filter/asset_proxy_filter.rb new file mode 100644 index 00000000000..0a9a52a73a1 --- /dev/null +++ b/lib/banzai/filter/asset_proxy_filter.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # Proxy's images/assets to another server. Reduces mixed content warnings + # as well as hiding the customer's IP address when requesting images. + # Copies the original img `src` to `data-canonical-src` then replaces the + # `src` with a new url to the proxy server. + class AssetProxyFilter < HTML::Pipeline::CamoFilter + def initialize(text, context = nil, result = nil) + super + end + + def validate + needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled? + end + + def asset_host_whitelisted?(host) + context[:asset_proxy_domain_regexp] ? context[:asset_proxy_domain_regexp].match?(host) : false + end + + def self.transform_context(context) + context[:disable_asset_proxy] = !Gitlab.config.asset_proxy.enabled + + unless context[:disable_asset_proxy] + context[:asset_proxy_enabled] = !context[:disable_asset_proxy] + context[:asset_proxy] = Gitlab.config.asset_proxy.url + context[:asset_proxy_secret_key] = Gitlab.config.asset_proxy.secret_key + context[:asset_proxy_domain_regexp] = Gitlab.config.asset_proxy.domain_regexp + end + + context + end + + # called during an initializer. Caching the values in Gitlab.config significantly increased + # performance, rather than querying Gitlab::CurrentSettings.current_application_settings + # over and over. However, this does mean that the Rails servers need to get restarted + # whenever the application settings are changed + def self.initialize_settings + application_settings = Gitlab::CurrentSettings.current_application_settings + Gitlab.config['asset_proxy'] ||= Settingslogic.new({}) + + if application_settings.respond_to?(:asset_proxy_enabled) + Gitlab.config.asset_proxy['enabled'] = application_settings.asset_proxy_enabled + Gitlab.config.asset_proxy['url'] = application_settings.asset_proxy_url + Gitlab.config.asset_proxy['secret_key'] = application_settings.asset_proxy_secret_key + Gitlab.config.asset_proxy['whitelist'] = application_settings.asset_proxy_whitelist || [Gitlab.config.gitlab.host] + Gitlab.config.asset_proxy['domain_regexp'] = compile_whitelist(Gitlab.config.asset_proxy.whitelist) + else + Gitlab.config.asset_proxy['enabled'] = ::ApplicationSetting.defaults[:asset_proxy_enabled] + end + end + + def self.compile_whitelist(domain_list) + return if domain_list.empty? + + escaped = domain_list.map { |domain| Regexp.escape(domain).gsub('\*', '.*?') } + Regexp.new("^(#{escaped.join('|')})$", Regexp::IGNORECASE) + end + end + end +end diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb index f49c4b403db..02a47556151 100644 --- a/lib/banzai/filter/commit_trailers_filter.rb +++ b/lib/banzai/filter/commit_trailers_filter.rb @@ -88,7 +88,8 @@ module Banzai user: user, user_email: email, css_class: 'avatar-inline', - has_tooltip: false + has_tooltip: false, + only_path: false ) link_href = user.nil? ? "mailto:#{email}" : urls.user_url(user) diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 61ee3eac216..fb721fe12b1 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -14,10 +14,10 @@ module Banzai # such as on `mailto:` links. Since we've been using it, do an # initial parse for validity and then use Addressable # for IDN support, etc - uri = uri_strict(node['href'].to_s) + uri = uri_strict(node_src(node)) if uri - node.set_attribute('href', uri.to_s) - addressable_uri = addressable_uri(node['href']) + node.set_attribute(node_src_attribute(node), uri.to_s) + addressable_uri = addressable_uri(node_src(node)) else addressable_uri = nil end @@ -35,6 +35,16 @@ module Banzai private + # if this is a link to a proxied image, then `src` is already the correct + # proxied url, so work with the `data-canonical-src` + def node_src_attribute(node) + node['data-canonical-src'] ? 'data-canonical-src' : 'href' + end + + def node_src(node) + node[node_src_attribute(node)] + end + def uri_strict(href) URI.parse(href) rescue URI::Error @@ -72,7 +82,7 @@ module Banzai return unless uri return unless context[:emailable_links] - unencoded_uri_str = Addressable::URI.unencode(node['href']) + unencoded_uri_str = Addressable::URI.unencode(node_src(node)) if unencoded_uri_str == node.content && idn?(uri) node.content = uri.normalize diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index 01237303c27..ed0a01e6277 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -18,6 +18,9 @@ module Banzai rel: 'noopener noreferrer' ) + # make sure the original non-proxied src carries over to the link + link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src'] + link.children = img.clone img.replace(link) diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index 8e2358694d4..9dfd77b1759 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -21,7 +21,8 @@ module Banzai next if !can_read_cross_project? && cross_reference?(issuable) if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable) - node.content += " (#{issuable.state})" + state = moved_issue?(issuable) ? s_("IssuableStatus|moved") : issuable.state + node.content += " (#{state})" end end @@ -30,6 +31,10 @@ module Banzai private + def moved_issue?(issuable) + issuable.instance_of?(Issue) && issuable.moved? + end + def issuable_reference?(text, issuable) CGI.unescapeHTML(text) == issuable.reference_link_text(project || group) end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 4892668fc22..a0789b7ca06 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -14,24 +14,24 @@ module Banzai find_labels(parent_object).find(id) end - def self.references_in(text, pattern = Label.reference_pattern) - unescape_html_entities(text).gsub(pattern) do |match| - yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~ - end - end - def references_in(text, pattern = Label.reference_pattern) - unescape_html_entities(text).gsub(pattern) do |match| + labels = {} + unescaped_html = unescape_html_entities(text).gsub(pattern) do |match| namespace, project = $~[:namespace], $~[:project] project_path = full_project_path(namespace, project) label = find_label(project_path, $~[:label_id], $~[:label_name]) if label - yield match, label.id, project, namespace, $~ + labels[label.id] = yield match, label.id, project, namespace, $~ + "#{REFERENCE_PLACEHOLDER}#{label.id}" else - escape_html_entities(match) + match end end + + return text if labels.empty? + + escape_with_placeholders(unescaped_html, labels) end def find_label(parent_ref, label_id, label_name) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 08969753d75..4c47ee4dba1 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -51,15 +51,21 @@ module Banzai # default implementation. return super(text, pattern) if pattern != Milestone.reference_pattern - unescape_html_entities(text).gsub(pattern) do |match| + milestones = {} + unescaped_html = unescape_html_entities(text).gsub(pattern) do |match| milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) if milestone - yield match, milestone.id, $~[:project], $~[:namespace], $~ + milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~ + "#{REFERENCE_PLACEHOLDER}#{milestone.id}" else - escape_html_entities(match) + match end end + + return text if milestones.empty? + + escape_with_placeholders(unescaped_html, milestones) end def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 86f18679496..846a7d46aad 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -9,6 +9,7 @@ module Banzai # Context options: # :commit # :group + # :current_user # :project # :project_wiki # :ref @@ -18,6 +19,7 @@ module Banzai def call return doc if context[:system_note] + return doc unless visible_to_user? @uri_types = {} clear_memoization(:linkable_files) @@ -166,6 +168,16 @@ module Banzai Gitlab.config.gitlab.relative_url_root.presence || '/' end + def visible_to_user? + if project + Ability.allowed?(current_user, :download_code, project) + elsif group + Ability.allowed?(current_user, :read_group, group) + else # Objects detached from projects or groups, e.g. Personal Snippets. + true + end + end + def ref context[:ref] || project.default_branch end @@ -178,6 +190,10 @@ module Banzai context[:project] end + def current_user + context[:current_user] + end + def repository @repository ||= project&.repository end diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb index 0fff104cf91..a278fcfdb47 100644 --- a/lib/banzai/filter/video_link_filter.rb +++ b/lib/banzai/filter/video_link_filter.rb @@ -23,6 +23,14 @@ module Banzai "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})" end + if context[:asset_proxy_enabled].present? + src_query.concat( + UploaderHelper::VIDEO_EXT.map do |ext| + "'.#{ext}' = substring(@data-canonical-src, string-length(@data-canonical-src) - #{ext.size})" + end + ) + end + "descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]" end end @@ -48,6 +56,13 @@ module Banzai target: '_blank', rel: 'noopener noreferrer', title: "Download '#{element['title'] || element['alt']}'") + + # make sure the original non-proxied src carries over + if element['data-canonical-src'] + video['data-canonical-src'] = element['data-canonical-src'] + link['data-canonical-src'] = element['data-canonical-src'] + end + download_paragraph = doc.document.create_element('p') download_paragraph.children = link diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb index d25b74b23b2..82b99d3de4a 100644 --- a/lib/banzai/pipeline/ascii_doc_pipeline.rb +++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb @@ -6,12 +6,17 @@ module Banzai def self.filters FilterArray[ Filter::AsciiDocSanitizationFilter, + Filter::AssetProxyFilter, Filter::SyntaxHighlightFilter, Filter::ExternalLinkFilter, Filter::PlantumlFilter, Filter::AsciiDocPostProcessingFilter ] end + + def self.transform_context(context) + Filter::AssetProxyFilter.transform_context(context) + end end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 2c1006f708a..f419e54c264 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -17,6 +17,7 @@ module Banzai Filter::SpacedLinkFilter, Filter::SanitizationFilter, + Filter::AssetProxyFilter, Filter::SyntaxHighlightFilter, Filter::MathFilter, @@ -60,7 +61,7 @@ module Banzai def self.transform_context(context) context[:only_path] = true unless context.key?(:only_path) - context + Filter::AssetProxyFilter.transform_context(context) end end end diff --git a/lib/banzai/pipeline/markup_pipeline.rb b/lib/banzai/pipeline/markup_pipeline.rb index ceba082cd4f..c86d5f08ded 100644 --- a/lib/banzai/pipeline/markup_pipeline.rb +++ b/lib/banzai/pipeline/markup_pipeline.rb @@ -6,11 +6,16 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::SanitizationFilter, + Filter::AssetProxyFilter, Filter::ExternalLinkFilter, Filter::PlantumlFilter, Filter::SyntaxHighlightFilter ] end + + def self.transform_context(context) + Filter::AssetProxyFilter.transform_context(context) + end end end end diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 72374207a8f..9aff6880f56 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -7,6 +7,7 @@ module Banzai @filters ||= FilterArray[ Filter::HtmlEntityFilter, Filter::SanitizationFilter, + Filter::AssetProxyFilter, Filter::EmojiFilter, Filter::AutolinkFilter, @@ -29,6 +30,8 @@ module Banzai end def self.transform_context(context) + context = Filter::AssetProxyFilter.transform_context(context) + super(context).merge( no_sourcepos: true ) diff --git a/lib/gitlab/anonymous_session.rb b/lib/gitlab/anonymous_session.rb new file mode 100644 index 00000000000..148b6d3310d --- /dev/null +++ b/lib/gitlab/anonymous_session.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + class AnonymousSession + def initialize(remote_ip, session_id: nil) + @remote_ip = remote_ip + @session_id = session_id + end + + def store_session_id_per_ip + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do + redis.sadd(session_lookup_name, session_id) + redis.expire(session_lookup_name, 24.hours) + end + end + end + + def stored_sessions + Gitlab::Redis::SharedState.with do |redis| + redis.scard(session_lookup_name) + end + end + + def cleanup_session_per_ip_entries + Gitlab::Redis::SharedState.with do |redis| + redis.srem(session_lookup_name, session_id) + end + end + + private + + attr_reader :remote_ip, :session_id + + def session_lookup_name + @session_lookup_name ||= "#{Gitlab::Redis::SharedState::IP_SESSIONS_LOOKUP_NAMESPACE}:#{remote_ip}" + end + end +end diff --git a/lib/gitlab/authorized_keys.rb b/lib/gitlab/authorized_keys.rb index 3fe72f5fd43..820a78b653c 100644 --- a/lib/gitlab/authorized_keys.rb +++ b/lib/gitlab/authorized_keys.rb @@ -13,6 +13,24 @@ module Gitlab @logger = logger end + # Checks if the file is accessible or not + # + # @return [Boolean] + def accessible? + open_authorized_keys_file('r') { true } + rescue Errno::ENOENT, Errno::EACCES + false + end + + # Creates the authorized_keys file if it doesn't exist + # + # @return [Boolean] + def create + open_authorized_keys_file(File::CREAT) { true } + rescue Errno::EACCES + false + end + # Add id and its key to the authorized_keys file # # @param [String] id identifier of key prefixed by `key-` @@ -102,10 +120,14 @@ module Gitlab [] end + def file + @file ||= Gitlab.config.gitlab_shell.authorized_keys_file + end + private def lock(timeout = 10) - File.open("#{authorized_keys_file}.lock", "w+") do |f| + File.open("#{file}.lock", "w+") do |f| f.flock File::LOCK_EX Timeout.timeout(timeout) { yield } ensure @@ -114,7 +136,7 @@ module Gitlab end def open_authorized_keys_file(mode) - File.open(authorized_keys_file, mode, 0o600) do |file| + File.open(file, mode, 0o600) do |file| file.chmod(0o600) yield file end @@ -141,9 +163,5 @@ module Gitlab def strip(key) key.split(/[ ]+/)[0, 2].join(' ') end - - def authorized_keys_file - Gitlab.config.gitlab_shell.authorized_keys_file - end end end diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb index 6ab4fca3854..f448d55f00a 100644 --- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -43,7 +43,7 @@ module Gitlab end def create_namespace - Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + Clusters::Kubernetes::CreateOrUpdateNamespaceService.new( cluster: deployment_cluster, kubernetes_namespace: kubernetes_namespace || build_namespace_record ).execute diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb index 2ffbb214a92..c56d33544ba 100644 --- a/lib/gitlab/ci/config/external/file/base.rb +++ b/lib/gitlab/ci/config/external/file/base.rb @@ -26,6 +26,10 @@ module Gitlab location.present? end + def invalid_location_type? + !location.is_a?(String) + end + def invalid_extension? location.nil? || !::File.basename(location).match?(YAML_WHITELIST_EXTENSION) end @@ -71,7 +75,9 @@ module Gitlab end def validate_location! - if invalid_extension? + if invalid_location_type? + errors.push("Included file `#{location}` needs to be a string") + elsif invalid_extension? errors.push("Included file `#{location}` does not have YAML extension!") end end diff --git a/lib/gitlab/ci/pipeline/chain/limit/job_activity.rb b/lib/gitlab/ci/pipeline/chain/limit/job_activity.rb new file mode 100644 index 00000000000..31c218bf954 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/limit/job_activity.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Limit + class JobActivity < Chain::Base + def perform! + # to be overridden in EE + end + + def break? + false # to be overridden in EE + end + end + end + end + end + end +end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index a12bbededc4..6ecd506d55b 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -13,6 +13,10 @@ module Gitlab # FIXME: this should just be the max value of timestampz MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze + # The maximum number of characters for text fields, to avoid DoS attacks via parsing huge text fields + # https://gitlab.com/gitlab-org/gitlab-ce/issues/61974 + MAX_TEXT_SIZE_LIMIT = 1_000_000 + # Minimum schema version from which migrations are supported # Migrations before this version may have been removed MIN_SCHEMA_VERSION = 20190506135400 diff --git a/lib/gitlab/jira/http_client.rb b/lib/gitlab/jira/http_client.rb new file mode 100644 index 00000000000..11a33a7b358 --- /dev/null +++ b/lib/gitlab/jira/http_client.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Gitlab + module Jira + # Gitlab JIRA HTTP client to be used with jira-ruby gem, this subclasses JIRA::HTTPClient. + # Uses Gitlab::HTTP to make requests to JIRA REST API. + # The parent class implementation can be found at: https://github.com/sumoheavy/jira-ruby/blob/v1.4.0/lib/jira/http_client.rb + class HttpClient < JIRA::HttpClient + extend ::Gitlab::Utils::Override + + override :request + def request(*args) + result = make_request(*args) + + raise JIRA::HTTPError.new(result) unless result.response.is_a?(Net::HTTPSuccess) + + result + end + + override :make_cookie_auth_request + def make_cookie_auth_request + body = { + username: @options.delete(:username), + password: @options.delete(:password) + }.to_json + + make_request(:post, @options[:context_path] + '/rest/auth/1/session', body, { 'Content-Type' => 'application/json' }) + end + + override :make_request + def make_request(http_method, path, body = '', headers = {}) + request_params = { headers: headers } + request_params[:body] = body if body.present? + request_params[:headers][:Cookie] = get_cookies if options[:use_cookies] + request_params[:timeout] = options[:read_timeout] if options[:read_timeout] + request_params[:base_uri] = uri.to_s + request_params.merge!(auth_params) + + result = Gitlab::HTTP.public_send(http_method, path, **request_params) # rubocop:disable GitlabSecurity/PublicSend + @authenticated = result.response.is_a?(Net::HTTPOK) + store_cookies(result) if options[:use_cookies] + + result + end + + def auth_params + return {} unless @options[:username] && @options[:password] + + { + basic_auth: { + username: @options[:username], + password: @options[:password] + } + } + end + + private + + def get_cookies + cookie_array = @cookies.values.map { |cookie| "#{cookie.name}=#{cookie.value[0]}" } + cookie_array += Array(@options[:additional_cookies]) if @options.key?(:additional_cookies) + cookie_array.join('; ') if cookie_array.any? + end + end + end +end diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb index 0354c710a3f..03a2f62cbd9 100644 --- a/lib/gitlab/markdown_cache.rb +++ b/lib/gitlab/markdown_cache.rb @@ -3,8 +3,8 @@ module Gitlab module MarkdownCache # Increment this number every time the renderer changes its output + CACHE_COMMONMARK_VERSION = 17 CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 16 BaseError = Class.new(StandardError) UnsupportedClassError = Class.new(BaseError) diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index f96466b2b00..d9c28ff1181 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -132,7 +132,7 @@ module Gitlab NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze - FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/)*#{NAMESPACE_FORMAT_REGEX}}.freeze + FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/){,#{Namespace::NUMBER_OF_ANCESTORS_ALLOWED}}#{NAMESPACE_FORMAT_REGEX}}.freeze def root_namespace_route_regex @root_namespace_route_regex ||= begin diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb index 772d743c9b0..f3cbe1db901 100644 --- a/lib/gitlab/recaptcha.rb +++ b/lib/gitlab/recaptcha.rb @@ -3,7 +3,7 @@ module Gitlab module Recaptcha def self.load_configurations! - if Gitlab::CurrentSettings.recaptcha_enabled + if Gitlab::CurrentSettings.recaptcha_enabled || enabled_on_login? ::Recaptcha.configure do |config| config.site_key = Gitlab::CurrentSettings.recaptcha_site_key config.secret_key = Gitlab::CurrentSettings.recaptcha_private_key @@ -16,5 +16,9 @@ module Gitlab def self.enabled? Gitlab::CurrentSettings.recaptcha_enabled end + + def self.enabled_on_login? + Gitlab::CurrentSettings.login_recaptcha_protection_enabled + end end end diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb index 9066606ca21..270a19e780c 100644 --- a/lib/gitlab/redis/shared_state.rb +++ b/lib/gitlab/redis/shared_state.rb @@ -9,6 +9,7 @@ module Gitlab SESSION_NAMESPACE = 'session:gitlab'.freeze USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze + IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab'.freeze DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb index 75503ee1789..e40c366ed02 100644 --- a/lib/gitlab/repository_cache_adapter.rb +++ b/lib/gitlab/repository_cache_adapter.rb @@ -23,37 +23,6 @@ module Gitlab end end - # Caches and strongly memoizes the method as a Redis Set. - # - # This only works for methods that do not take any arguments. The method - # should return an Array of Strings to be cached. - # - # In addition to overriding the named method, a "name_include?" method is - # defined. This uses the "SISMEMBER" query to efficiently check membership - # without needing to load the entire set into memory. - # - # name - The name of the method to be cached. - # fallback - A value to fall back to if the repository does not exist, or - # in case of a Git error. Defaults to nil. - def cache_method_as_redis_set(name, fallback: nil) - uncached_name = alias_uncached_method(name) - - define_method(name) do - cache_method_output_as_redis_set(name, fallback: fallback) do - __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend - end - end - - define_method("#{name}_include?") do |value| - # If the cache isn't populated, we can't rely on it - return redis_set_cache.include?(name, value) if redis_set_cache.exist?(name) - - # Since we have to pull all branch names to populate the cache, use - # the data we already have to answer the query just this once - __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend - end - end - # Caches truthy values from the method. All values are strongly memoized, # and cached in RequestStore. # @@ -115,11 +84,6 @@ module Gitlab raise NotImplementedError end - # RepositorySetCache to be used. Should be overridden by the including class - def redis_set_cache - raise NotImplementedError - end - # List of cached methods. Should be overridden by the including class def cached_methods raise NotImplementedError @@ -136,18 +100,6 @@ module Gitlab end end - # Caches and strongly memoizes the supplied block as a Redis Set. The result - # will be provided as a sorted array. - # - # name - The name of the method to be cached. - # fallback - A value to fall back to if the repository does not exist, or - # in case of a Git error. Defaults to nil. - def cache_method_output_as_redis_set(name, fallback: nil, &block) - memoize_method_output(name, fallback: fallback) do - redis_set_cache.fetch(name, &block).sort - end - end - # Caches truthy values from the supplied block. All values are strongly # memoized, and cached in RequestStore. # @@ -202,7 +154,6 @@ module Gitlab clear_memoization(memoizable_name(name)) end - expire_redis_set_method_caches(methods) expire_request_store_method_caches(methods) end @@ -218,10 +169,6 @@ module Gitlab end end - def expire_redis_set_method_caches(methods) - methods.each { |name| redis_set_cache.expire(name) } - end - # All cached repository methods depend on the existence of a Git repository, # so if the repository doesn't exist, we already know not to call it. def fallback_early?(method_name) diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb deleted file mode 100644 index fb634328a95..00000000000 --- a/lib/gitlab/repository_set_cache.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -# Interface to the Redis-backed cache store for keys that use a Redis set -module Gitlab - class RepositorySetCache - attr_reader :repository, :namespace, :expires_in - - def initialize(repository, extra_namespace: nil, expires_in: 2.weeks) - @repository = repository - @namespace = "#{repository.full_path}:#{repository.project.id}" - @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace - @expires_in = expires_in - end - - def cache_key(type) - [type, namespace, 'set'].join(':') - end - - def expire(key) - with { |redis| redis.del(cache_key(key)) } - end - - def exist?(key) - with { |redis| redis.exists(cache_key(key)) } - end - - def read(key) - with { |redis| redis.smembers(cache_key(key)) } - end - - def write(key, value) - full_key = cache_key(key) - - with do |redis| - redis.multi do - redis.del(full_key) - - # Splitting into groups of 1000 prevents us from creating a too-long - # Redis command - value.in_groups_of(1000, false) { |subset| redis.sadd(full_key, subset) } - - redis.expire(full_key, expires_in) - end - end - - value - end - - def fetch(key, &block) - if exist?(key) - read(key) - else - write(key, yield) - end - end - - def include?(key, value) - with { |redis| redis.sismember(cache_key(key), value) } - end - - private - - def with(&blk) - Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord - end - end -end diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb index bb4e4ce7bbc..2f3d14ecebd 100644 --- a/lib/gitlab/sanitizers/exif.rb +++ b/lib/gitlab/sanitizers/exif.rb @@ -53,15 +53,18 @@ module Gitlab end # rubocop: disable CodeReuse/ActiveRecord - def batch_clean(start_id: nil, stop_id: nil, dry_run: true, sleep_time: nil) + def batch_clean(start_id: nil, stop_id: nil, dry_run: true, sleep_time: nil, uploader: nil, since: nil) relation = Upload.where('lower(path) like ? or lower(path) like ? or lower(path) like ?', '%.jpg', '%.jpeg', '%.tiff') + relation = relation.where(uploader: uploader) if uploader + relation = relation.where('created_at > ?', since) if since logger.info "running in dry run mode, no images will be rewritten" if dry_run find_params = { start: start_id.present? ? start_id.to_i : nil, - finish: stop_id.present? ? stop_id.to_i : Upload.last&.id + finish: stop_id.present? ? stop_id.to_i : Upload.last&.id, + batch_size: 1000 } relation.find_each(find_params) do |upload| diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 0fa17b3f559..9e813968093 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -165,16 +165,7 @@ module Gitlab def add_key(key_id, key_content) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - gitlab_shell_fast_execute([ - gitlab_shell_keys_path, - 'add-key', - key_id, - strip_key(key_content) - ]) - else - gitlab_authorized_keys.add_key(key_id, key_content) - end + gitlab_authorized_keys.add_key(key_id, key_content) end # Batch-add keys to authorized_keys @@ -184,19 +175,7 @@ module Gitlab def batch_add_keys(keys) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - begin - IO.popen("#{gitlab_shell_keys_path} batch-add-keys", 'w') do |io| - add_keys_to_io(keys, io) - end - - $?.success? - rescue Error - false - end - else - gitlab_authorized_keys.batch_add_keys(keys) - end + gitlab_authorized_keys.batch_add_keys(keys) end # Remove ssh key from authorized_keys @@ -207,11 +186,7 @@ module Gitlab def remove_key(id, _ = nil) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - gitlab_shell_fast_execute([gitlab_shell_keys_path, 'rm-key', id]) - else - gitlab_authorized_keys.rm_key(id) - end + gitlab_authorized_keys.rm_key(id) end # Remove all ssh keys from gitlab shell @@ -222,11 +197,7 @@ module Gitlab def remove_all_keys return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) - else - gitlab_authorized_keys.clear - end + gitlab_authorized_keys.clear end # Remove ssh keys from gitlab shell that are not in the DB @@ -341,14 +312,6 @@ module Gitlab File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, dir_name) end - def gitlab_shell_projects_path - File.join(gitlab_shell_path, 'bin', 'gitlab-projects') - end - - def gitlab_shell_keys_path - File.join(gitlab_shell_path, 'bin', 'gitlab-keys') - end - def authorized_keys_enabled? # Return true if nil to ensure the authorized_keys methods work while # fixing the authorized_keys file during migration. @@ -359,35 +322,6 @@ module Gitlab private - def shell_out_for_gitlab_keys? - Gitlab.config.gitlab_shell.authorized_keys_file.blank? - end - - def gitlab_shell_fast_execute(cmd) - output, status = gitlab_shell_fast_execute_helper(cmd) - - return true if status.zero? - - Rails.logger.error("gitlab-shell failed with error #{status}: #{output}") # rubocop:disable Gitlab/RailsLogger - false - end - - def gitlab_shell_fast_execute_raise_error(cmd, vars = {}) - output, status = gitlab_shell_fast_execute_helper(cmd, vars) - - raise Error, output unless status.zero? - - true - end - - def gitlab_shell_fast_execute_helper(cmd, vars = {}) - vars.merge!(ENV.to_h.slice(*GITLAB_SHELL_ENV_VARS)) - - # Don't pass along the entire parent environment to prevent gitlab-shell - # from wasting I/O by searching through GEM_PATH - Bundler.with_original_env { Popen.popen(cmd, nil, vars) } - end - def git_timeout Gitlab.config.gitlab_shell.git_timeout end @@ -407,16 +341,8 @@ module Gitlab def batch_read_key_ids(batch_size: 100, &block) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - IO.popen("#{gitlab_shell_keys_path} list-key-ids") do |key_id_stream| - key_id_stream.lazy.each_slice(batch_size) do |lines| - yield(lines.map { |l| l.chomp.to_i }) - end - end - else - gitlab_authorized_keys.list_key_ids.lazy.each_slice(batch_size) do |key_ids| - yield(key_ids) - end + gitlab_authorized_keys.list_key_ids.lazy.each_slice(batch_size) do |key_ids| + yield(key_ids) end end diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb index 3dc9521ee8b..368f37a5d8c 100644 --- a/lib/gitlab/sidekiq_middleware/metrics.rb +++ b/lib/gitlab/sidekiq_middleware/metrics.rb @@ -35,7 +35,7 @@ module Gitlab def init_metrics { - sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', buckets: SIDEKIQ_LATENCY_BUCKETS), + sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS), sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'), sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'), sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :livesum) diff --git a/lib/gitlab/slash_commands/application_help.rb b/lib/gitlab/slash_commands/application_help.rb index 0ea7554ba64..1a92346be15 100644 --- a/lib/gitlab/slash_commands/application_help.rb +++ b/lib/gitlab/slash_commands/application_help.rb @@ -3,12 +3,15 @@ module Gitlab module SlashCommands class ApplicationHelp < BaseCommand - def initialize(params) + def initialize(project, params) + @project = project @params = params end def execute - Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, params[:text]) + Gitlab::SlashCommands::Presenters::Help + .new(project, commands) + .present(trigger, params[:text]) end private diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index 7c963fcf38a..079b5916566 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -9,6 +9,7 @@ module Gitlab Gitlab::SlashCommands::IssueNew, Gitlab::SlashCommands::IssueSearch, Gitlab::SlashCommands::IssueMove, + Gitlab::SlashCommands::IssueClose, Gitlab::SlashCommands::Deploy, Gitlab::SlashCommands::Run ] @@ -21,7 +22,7 @@ module Gitlab if command.allowed?(project, current_user) command.new(project, chat_name, params).execute(match) else - Gitlab::SlashCommands::Presenters::Access.new.access_denied + Gitlab::SlashCommands::Presenters::Access.new.access_denied(project) end else Gitlab::SlashCommands::Help.new(project, chat_name, params) diff --git a/lib/gitlab/slash_commands/help.rb b/lib/gitlab/slash_commands/help.rb index dbe15baa3d7..3eff64192ab 100644 --- a/lib/gitlab/slash_commands/help.rb +++ b/lib/gitlab/slash_commands/help.rb @@ -19,7 +19,9 @@ module Gitlab end def execute(commands, text) - Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, text) + Gitlab::SlashCommands::Presenters::Help + .new(project, commands) + .present(trigger, text) end def trigger diff --git a/lib/gitlab/slash_commands/issue_close.rb b/lib/gitlab/slash_commands/issue_close.rb new file mode 100644 index 00000000000..5fcc86e91c4 --- /dev/null +++ b/lib/gitlab/slash_commands/issue_close.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + class IssueClose < IssueCommand + def self.match(text) + /\Aissue\s+close\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text) + end + + def self.help_message + "issue close <id>" + end + + def self.allowed?(project, user) + can?(user, :update_issue, project) + end + + def execute(match) + issue = find_by_iid(match[:iid]) + + return not_found unless issue + return presenter(issue).already_closed if issue.closed? + + close_issue(issue: issue) + + presenter(issue).present + end + + private + + def close_issue(issue:) + Issues::CloseService.new(project, current_user).execute(issue) + end + + def presenter(issue) + Gitlab::SlashCommands::Presenters::IssueClose.new(issue) + end + + def not_found + Gitlab::SlashCommands::Presenters::Access.new.not_found + end + end + end +end diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb index fa163cb098e..b1bfaa6cb59 100644 --- a/lib/gitlab/slash_commands/presenters/access.rb +++ b/lib/gitlab/slash_commands/presenters/access.rb @@ -4,8 +4,15 @@ module Gitlab module SlashCommands module Presenters class Access < Presenters::Base - def access_denied - ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + def access_denied(project) + ephemeral_response(text: <<~MESSAGE) + You are not allowed to perform the given chatops command. Most + likely you do not have access to the GitLab project for this chatops + integration. + + The GitLab project for this chatops integration can be found at + #{url_for(project)}. + MESSAGE end def not_found @@ -22,20 +29,6 @@ module Gitlab ephemeral_response(text: message) end - - def unknown_command(commands) - ephemeral_response(text: help_message(trigger)) - end - - private - - def help_message(trigger) - header_with_list("Command not found, these are the commands you can use", full_commands(trigger)) - end - - def full_commands(trigger) - @resource.map { |command| "#{trigger} #{command.help_message}" } - end end end end diff --git a/lib/gitlab/slash_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb index 480d7aa6a30..5421b0b9a84 100644 --- a/lib/gitlab/slash_commands/presenters/help.rb +++ b/lib/gitlab/slash_commands/presenters/help.rb @@ -4,6 +4,11 @@ module Gitlab module SlashCommands module Presenters class Help < Presenters::Base + def initialize(project, commands) + @project = project + @commands = commands + end + def present(trigger, text) ephemeral_response(text: help_message(trigger, text)) end @@ -11,17 +16,64 @@ module Gitlab private def help_message(trigger, text) - return "No commands available :thinking_face:" unless @resource.present? + unless @commands.present? + return <<~MESSAGE + This chatops integration does not have any commands that can be + executed. + + #{footer} + MESSAGE + end if text.start_with?('help') - header_with_list("Available commands", full_commands(trigger)) + <<~MESSAGE + #{full_commands_message(trigger)} + + #{help_footer} + MESSAGE else - header_with_list("Unknown command, these commands are available", full_commands(trigger)) + <<~MESSAGE + The specified command is not valid. + + #{full_commands_message(trigger)} + + #{help_footer} + MESSAGE end end - def full_commands(trigger) - @resource.map { |command| "#{trigger} #{command.help_message}" } + def help_footer + <<~MESSAGE + *Project* + + The GitLab project for this chatops integration can be found at + #{url_for(@project)}. + + *Documentation* + + For more information about GitLab chatops, refer to its + documentation: https://docs.gitlab.com/ce/ci/chatops/README.html. + MESSAGE + end + + def full_commands_message(trigger) + list = @commands + .map { |command| "#{trigger} #{command.help_message}" } + .join("\n") + + <<~MESSAGE + *Available commands* + + The following commands are available for this chatops integration: + + #{list} + + If available, the `run` command is used for running GitLab CI jobs + defined in this project's `.gitlab-ci.yml` file. For example, if a + job called "help" is defined you can run it like so: + + `#{trigger} run help` + MESSAGE end end end diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb index b6db103b82b..08cb82274fd 100644 --- a/lib/gitlab/slash_commands/presenters/issue_base.rb +++ b/lib/gitlab/slash_commands/presenters/issue_base.rb @@ -40,6 +40,14 @@ module Gitlab ] end + def project_link + "[#{project.full_name}](#{project.web_url})" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + private attr_reader :resource diff --git a/lib/gitlab/slash_commands/presenters/issue_close.rb b/lib/gitlab/slash_commands/presenters/issue_close.rb new file mode 100644 index 00000000000..b3f24f4296a --- /dev/null +++ b/lib/gitlab/slash_commands/presenters/issue_close.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + module Presenters + class IssueClose < Presenters::Base + include Presenters::IssueBase + + def present + if @resource.confidential? + ephemeral_response(close_issue) + else + in_channel_response(close_issue) + end + end + + def already_closed + ephemeral_response(text: "Issue #{@resource.to_reference} is already closed.") + end + + private + + def close_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "Closed issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :pretext, + :fields + ] + } + ] + } + end + + def pretext + "I closed an issue on #{author_profile_link}'s behalf: *#{@resource.to_reference}* in #{project_link}" + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/presenters/issue_new.rb b/lib/gitlab/slash_commands/presenters/issue_new.rb index ac78745ae70..1424a4ac381 100644 --- a/lib/gitlab/slash_commands/presenters/issue_new.rb +++ b/lib/gitlab/slash_commands/presenters/issue_new.rb @@ -36,15 +36,7 @@ module Gitlab end def pretext - "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}" - end - - def project_link - "[#{project.full_name}](#{project.web_url})" - end - - def author_profile_link - "[#{author.to_reference}](#{url_for(author)})" + "I created an issue on #{author_profile_link}'s behalf: *#{@resource.to_reference}* in #{project_link}" end end end diff --git a/lib/gitlab/visibility_level_checker.rb b/lib/gitlab/visibility_level_checker.rb new file mode 100644 index 00000000000..f15f1486a4e --- /dev/null +++ b/lib/gitlab/visibility_level_checker.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Gitlab::VisibilityLevelChecker verifies that: +# - Current @project.visibility_level is not restricted +# - Override visibility param is not restricted +# - @see https://docs.gitlab.com/ce/api/project_import_export.html#import-a-file +# +# @param current_user [User] Current user object to verify visibility level against +# @param project [Project] Current project that is being created/imported +# @param project_params [Hash] Supplementary project params (e.g. import +# params containing visibility override) +# +# @example +# user = User.find(2) +# project = Project.last +# project_params = {:import_data=>{:data=>{:override_params=>{"visibility"=>"public"}}}} +# level_checker = Gitlab::VisibilityLevelChecker.new(user, project, project_params: project_params) +# +# project_visibility = level_checker.level_restricted? +# => #<Gitlab::VisibilityEvaluationResult:0x00007fbe16ee33c0 @restricted=true, @visibility_level=20> +# +# if project_visibility.restricted? +# deny_visibility_level(project, project_visibility.visibility_level) +# end +# +# @return [VisibilityEvaluationResult] Visibility evaluation result. Responds to: +# #restricted - boolean indicating if level is restricted +# #visibility_level - integer of restricted visibility level +# +module Gitlab + class VisibilityLevelChecker + def initialize(current_user, project, project_params: {}) + @current_user = current_user + @project = project + @project_params = project_params + end + + def level_restricted? + return VisibilityEvaluationResult.new(true, override_visibility_level_value) if override_visibility_restricted? + return VisibilityEvaluationResult.new(true, project.visibility_level) if project_visibility_restricted? + + VisibilityEvaluationResult.new(false, nil) + end + + private + + attr_reader :current_user, :project, :project_params + + def override_visibility_restricted? + return unless import_data + return unless override_visibility_level + return if Gitlab::VisibilityLevel.allowed_for?(current_user, override_visibility_level_value) + + true + end + + def project_visibility_restricted? + return if Gitlab::VisibilityLevel.allowed_for?(current_user, project.visibility_level) + + true + end + + def import_data + @import_data ||= project_params[:import_data] + end + + def override_visibility_level + @override_visibility_level ||= import_data.deep_symbolize_keys.dig(:data, :override_params, :visibility) + end + + def override_visibility_level_value + @override_visibility_level_value ||= Gitlab::VisibilityLevel.level_value(override_visibility_level) + end + end + + class VisibilityEvaluationResult + attr_reader :visibility_level + + def initialize(restricted, visibility_level) + @restricted = restricted + @visibility_level = visibility_level + end + + def restricted? + @restricted + end + end +end diff --git a/lib/system_check/app/authorized_keys_permission_check.rb b/lib/system_check/app/authorized_keys_permission_check.rb new file mode 100644 index 00000000000..1246a6875a3 --- /dev/null +++ b/lib/system_check/app/authorized_keys_permission_check.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module SystemCheck + module App + class AuthorizedKeysPermissionCheck < SystemCheck::BaseCheck + set_name 'Is authorized keys file accessible?' + set_skip_reason 'skipped (authorized keys not enabled)' + + def skip? + !authorized_keys_enabled? + end + + def check? + authorized_keys.accessible? + end + + def repair! + authorized_keys.create + end + + def show_error + try_fixing_it([ + "sudo chmod 700 #{File.dirname(authorized_keys.file)}", + "touch #{authorized_keys.file}", + "sudo chmod 600 #{authorized_keys.file}" + ]) + fix_and_rerun + end + + private + + def authorized_keys_enabled? + Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled + end + + def authorized_keys + @authorized_keys ||= Gitlab::AuthorizedKeys.new + end + end + end +end diff --git a/lib/system_check/rake_task/app_task.rb b/lib/system_check/rake_task/app_task.rb index cc32feb8604..e98cee510ff 100644 --- a/lib/system_check/rake_task/app_task.rb +++ b/lib/system_check/rake_task/app_task.rb @@ -30,7 +30,8 @@ module SystemCheck SystemCheck::App::RubyVersionCheck, SystemCheck::App::GitVersionCheck, SystemCheck::App::GitUserDefaultSSHConfigCheck, - SystemCheck::App::ActiveUsersCheck + SystemCheck::App::ActiveUsersCheck, + SystemCheck::App::AuthorizedKeysPermissionCheck ] end end diff --git a/lib/tasks/gitlab/uploads/sanitize.rake b/lib/tasks/gitlab/uploads/sanitize.rake index 12cf5302555..4f23a0a5d82 100644 --- a/lib/tasks/gitlab/uploads/sanitize.rake +++ b/lib/tasks/gitlab/uploads/sanitize.rake @@ -2,7 +2,7 @@ namespace :gitlab do namespace :uploads do namespace :sanitize do desc 'GitLab | Uploads | Remove EXIF from images.' - task :remove_exif, [:start_id, :stop_id, :dry_run, :sleep_time] => :environment do |task, args| + task :remove_exif, [:start_id, :stop_id, :dry_run, :sleep_time, :uploader, :since] => :environment do |task, args| args.with_defaults(dry_run: 'true') args.with_defaults(sleep_time: 0.3) @@ -11,7 +11,9 @@ namespace :gitlab do sanitizer = Gitlab::Sanitizers::Exif.new(logger: logger) sanitizer.batch_clean(start_id: args.start_id, stop_id: args.stop_id, dry_run: args.dry_run != 'false', - sleep_time: args.sleep_time.to_f) + sleep_time: args.sleep_time.to_f, + uploader: args.uploader, + since: args.since) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 12138d2db3a..c0c2bd3aab7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1104,9 +1104,6 @@ msgstr "" msgid "An error occurred while parsing recent searches" msgstr "" -msgid "An error occurred while rendering KaTeX" -msgstr "" - msgid "An error occurred while rendering preview broadcast message" msgstr "" @@ -1516,6 +1513,18 @@ msgstr "" msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}" msgstr "" +msgid "Autocomplete" +msgstr "" + +msgid "Autocomplete description" +msgstr "" + +msgid "Autocomplete hint" +msgstr "" + +msgid "Autocomplete usage hint" +msgstr "" + msgid "Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}" msgstr "" @@ -2974,6 +2983,9 @@ msgstr "" msgid "ComboSearch is not defined" msgstr "" +msgid "Command" +msgstr "" + msgid "Command line instructions" msgstr "" @@ -3611,9 +3623,15 @@ msgstr "" msgid "Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import." msgstr "" +msgid "Customize icon" +msgstr "" + msgid "Customize language and region related settings." msgstr "" +msgid "Customize name" +msgstr "" + msgid "Customize your pipeline configuration, view your pipeline status and coverage report." msgstr "" @@ -3964,6 +3982,9 @@ msgstr "" msgid "Description:" msgstr "" +msgid "Descriptive label" +msgstr "" + msgid "Deselect all" msgstr "" @@ -4075,6 +4096,9 @@ msgstr "" msgid "Dismiss Cycle Analytics introduction box" msgstr "" +msgid "Display name" +msgstr "" + msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?" msgstr "" @@ -4120,6 +4144,9 @@ msgstr "" msgid "Download export" msgstr "" +msgid "Download image" +msgstr "" + msgid "Download source code" msgstr "" @@ -4330,7 +4357,7 @@ msgstr "" msgid "Enable or disable version check and usage ping." msgstr "" -msgid "Enable reCAPTCHA or Akismet and set IP limits." +msgid "Enable reCAPTCHA or Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}" msgstr "" msgid "Enable shared Runners" @@ -5700,7 +5727,10 @@ msgstr "" msgid "Help page text and support page url." msgstr "" -msgid "Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}" +msgid "Helps prevent bots from brute-force attacks." +msgstr "" + +msgid "Helps prevent bots from creating accounts." msgstr "" msgid "Hide archived projects" @@ -6110,6 +6140,9 @@ msgstr "" msgid "Invalid Login or password" msgstr "" +msgid "Invalid URL" +msgstr "" + msgid "Invalid date" msgstr "" @@ -6158,6 +6191,9 @@ msgstr "" msgid "IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})" msgstr "" +msgid "IssuableStatus|moved" +msgstr "" + msgid "Issue" msgstr "" @@ -6834,6 +6870,36 @@ msgstr "" msgid "Marks this issue as a duplicate of %{duplicate_reference}." msgstr "" +msgid "MattermostService|Add to Mattermost" +msgstr "" + +msgid "MattermostService|Command trigger word" +msgstr "" + +msgid "MattermostService|Fill in the word that works best for your team." +msgstr "" + +msgid "MattermostService|Request URL" +msgstr "" + +msgid "MattermostService|Request method" +msgstr "" + +msgid "MattermostService|Response icon" +msgstr "" + +msgid "MattermostService|Response username" +msgstr "" + +msgid "MattermostService|See list of available commands in Mattermost after setting up this service, by entering" +msgstr "" + +msgid "MattermostService|Suggestions:" +msgstr "" + +msgid "MattermostService|This service allows users to perform common operations on this project by entering slash commands in Mattermost." +msgstr "" + msgid "Max access level" msgstr "" @@ -7011,6 +7077,9 @@ msgstr "" msgid "Messages" msgstr "" +msgid "Method" +msgstr "" + msgid "Metrics" msgstr "" @@ -7574,6 +7643,9 @@ msgstr "" msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token." msgstr "" +msgid "Note: the container registry is always visible when a project is public" +msgstr "" + msgid "NoteForm|Note" msgstr "" @@ -7930,6 +8002,9 @@ msgstr "" msgid "Perform advanced options such as changing path, transferring, or removing the group." msgstr "" +msgid "Perform common operations on GitLab project" +msgstr "" + msgid "Performance optimization" msgstr "" @@ -8887,6 +8962,39 @@ msgstr "" msgid "ProjectSelect|Search for project" msgstr "" +msgid "ProjectService|%{service_title}: status off" +msgstr "" + +msgid "ProjectService|%{service_title}: status on" +msgstr "" + +msgid "ProjectService|Integrations" +msgstr "" + +msgid "ProjectService|Last edit" +msgstr "" + +msgid "ProjectService|Perform common operations on GitLab project: %{project_name}" +msgstr "" + +msgid "ProjectService|Project services" +msgstr "" + +msgid "ProjectService|Project services allow you to integrate GitLab with other applications" +msgstr "" + +msgid "ProjectService|Service" +msgstr "" + +msgid "ProjectService|Services" +msgstr "" + +msgid "ProjectService|Settings" +msgstr "" + +msgid "ProjectService|To set up this service:" +msgstr "" + msgid "ProjectSettings|Additional merge request capabilities that influence how and when merges will be performed" msgstr "" @@ -9992,9 +10100,57 @@ msgstr "" msgid "SearchCodeResults|of %{link_to_project}" msgstr "" +msgid "SearchResults|Showing %{count} %{scope} for \"%{term}\"" +msgstr "" + msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"" msgstr "" +msgid "SearchResults|comment" +msgid_plural "SearchResults|comments" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|commit" +msgid_plural "SearchResults|commits" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|issue" +msgid_plural "SearchResults|issues" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|merge request" +msgid_plural "SearchResults|merge requests" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|milestone" +msgid_plural "SearchResults|milestones" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|project" +msgid_plural "SearchResults|projects" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|result" +msgid_plural "SearchResults|results" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|snippet" +msgid_plural "SearchResults|snippets" +msgstr[0] "" +msgstr[1] "" + +msgid "SearchResults|user" +msgid_plural "SearchResults|users" +msgstr[0] "" +msgstr[1] "" + msgid "Secret" msgstr "" @@ -10414,6 +10570,21 @@ msgstr "" msgid "Size and domain settings for static websites" msgstr "" +msgid "SlackService|2. Paste the <strong>Token</strong> into the field below" +msgstr "" + +msgid "SlackService|3. Select the <strong>Active</strong> checkbox, press <strong>Save changes</strong> and start using GitLab inside Slack!" +msgstr "" + +msgid "SlackService|Fill in the word that works best for your team." +msgstr "" + +msgid "SlackService|See list of available commands in Slack after setting up this service, by entering" +msgstr "" + +msgid "SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack." +msgstr "" + msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job" msgstr "" @@ -10987,6 +11158,9 @@ msgstr "" msgid "SuggestedColors|Very pale orange" msgstr "" +msgid "Suggestions:" +msgstr "" + msgid "Sunday" msgstr "" @@ -11589,6 +11763,9 @@ msgstr "" msgid "This domain is not verified. You will need to verify ownership before access is enabled." msgstr "" +msgid "This feature is in development. Please disable the `job_log_json` feature flag" +msgstr "" + msgid "This feature requires local storage to be enabled" msgstr "" @@ -12043,6 +12220,9 @@ msgstr "" msgid "To see all the user's personal access tokens you must impersonate them first." msgstr "" +msgid "To set up this service:" +msgstr "" + msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there." msgstr "" @@ -13676,6 +13856,12 @@ msgstr "" msgid "manual" msgstr "" +msgid "math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead." +msgstr "" + +msgid "math|There was an error rendering this math block" +msgstr "" + msgid "merge request" msgid_plural "merge requests" msgstr[0] "" diff --git a/package.json b/package.json index 50cd5f36d20..3d9e0838893 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.5.5", - "@gitlab/svgs": "^1.70.0", - "@gitlab/ui": "5.19.0", - "@gitlab/visual-review-tools": "^1.0.0", + "@gitlab/svgs": "^1.71.0", + "@gitlab/ui": "5.20.2", + "@gitlab/visual-review-tools": "1.0.1", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", @@ -73,6 +73,7 @@ module QA end module Repository + autoload :Commit, 'qa/resource/repository/commit' autoload :Push, 'qa/resource/repository/push' autoload :ProjectPush, 'qa/resource/repository/project_push' autoload :WikiPush, 'qa/resource/repository/wiki_push' @@ -346,6 +347,10 @@ module QA module Issuable autoload :Common, 'qa/page/component/issuable/common' end + + module WebIDE + autoload :Alert, 'qa/page/component/web_ide/alert' + end end end diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index a520fb546c8..7c214da8486 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -83,3 +83,5 @@ module QA end end end + +QA::Page::Admin::Menu.prepend_if_ee('QA::EE::Page::Admin::Menu') diff --git a/qa/qa/page/component/web_ide/alert.rb b/qa/qa/page/component/web_ide/alert.rb new file mode 100644 index 00000000000..0f0623d5ebf --- /dev/null +++ b/qa/qa/page/component/web_ide/alert.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module WebIDE + module Alert + def self.prepended(page) + page.module_eval do + view 'app/assets/javascripts/ide/components/error_message.vue' do + element :flash_alert + end + end + end + + def has_no_alert?(message = nil) + return has_no_element?(:flash_alert) if message.nil? + + within_element(:flash_alert) do + has_no_text?(message) + end + end + end + end + end + end +end diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 0c23d7cffbb..378ac793f7b 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -29,3 +29,5 @@ module QA end end end + +QA::Page::Dashboard::Projects.prepend_if_ee('QA::EE::Page::Dashboard::Projects') diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb index 92f9181f99d..f5f44909f25 100644 --- a/qa/qa/page/file/show.rb +++ b/qa/qa/page/file/show.rb @@ -32,3 +32,5 @@ module QA end end end + +QA::Page::File::Show.prepend_if_ee('QA::EE::Page::File::Show') diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 6a415b56e50..72f8e1c3ef0 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -187,3 +187,5 @@ module QA end end end + +QA::Page::MergeRequest::Show.prepend_if_ee('QA::EE::Page::MergeRequest::Show') diff --git a/qa/qa/page/profile/menu.rb b/qa/qa/page/profile/menu.rb index 2d503499e13..99a795a23ef 100644 --- a/qa/qa/page/profile/menu.rb +++ b/qa/qa/page/profile/menu.rb @@ -34,3 +34,5 @@ module QA end end end + +QA::Page::Profile::Menu.prepend_if_ee('QA::EE::Page::Profile::Menu') diff --git a/qa/qa/page/project/commit/show.rb b/qa/qa/page/project/commit/show.rb index 9770b8a657c..ba09dd1b92a 100644 --- a/qa/qa/page/project/commit/show.rb +++ b/qa/qa/page/project/commit/show.rb @@ -9,6 +9,7 @@ module QA element :options_button element :email_patches element :plain_diff + element :commit_sha_content end def select_email_patches @@ -20,6 +21,10 @@ module QA click_element :options_button click_element :plain_diff end + + def commit_sha + find_element(:commit_sha_content).text + end end end end diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index c4383951ec4..f74366f6967 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -9,11 +9,21 @@ module QA element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern end + view 'app/views/shared/issuable/_nav.html.haml' do + element :closed_issues_link + end + def click_issue_link(title) click_link(title) end + + def click_closed_issues_link + click_element :closed_issues_link + end end end end end end + +QA::Page::Project::Issue::Index.prepend_if_ee('QA::EE::Page::Project::Issue::Index') diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 45dad9bc0ae..52929ece9ed 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -37,6 +37,10 @@ module QA element :dropdown_menu_labels end + view 'app/views/shared/issuable/_close_reopen_button.html.haml' do + element :reopen_issue_button + end + # Adds a comment to an issue # attachment option should be an absolute path def comment(text, attachment: nil, filter: :all_activities) @@ -108,3 +112,5 @@ module QA end end end + +QA::Page::Project::Issue::Show.prepend_if_ee('QA::EE::Page::Project::Issue::Show') diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 838d59b59cb..a9226927741 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -39,3 +39,5 @@ module QA end end end + +QA::Page::Project::Menu.prepend_if_ee('QA::EE::Page::Project::SubMenus::SecurityCompliance') diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 64aab9be056..d0e8011d82d 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -73,3 +73,5 @@ module QA end end end + +QA::Page::Project::New.prepend_if_ee('QA::EE::Page::Project::New') diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index eb30e0ea02a..fa276f15b8a 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -53,3 +53,5 @@ module QA end end end + +QA::Page::Project::Operations::Kubernetes::Show.prepend_if_ee('QA::EE::Page::Project::Operations::Kubernetes::Show') diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index 284d0957eb8..3dca47a57e9 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -60,3 +60,5 @@ module QA::Page end end end + +QA::Page::Project::Pipeline::Show.prepend_if_ee('QA::EE::Page::Project::Pipeline::Show') diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index dbbe62e3b1d..a196fc0123a 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -41,3 +41,5 @@ module QA end end end + +QA::Page::Project::Settings::Main.prepend_if_ee('QA::EE::Page::Project::Settings::Main') diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb index 831166f6373..e3afaceda80 100644 --- a/qa/qa/page/project/settings/mirroring_repositories.rb +++ b/qa/qa/page/project/settings/mirroring_repositories.rb @@ -89,3 +89,5 @@ module QA end end end + +QA::Page::Project::Settings::MirroringRepositories.prepend_if_ee('QA::EE::Page::Project::Settings::MirroringRepositories') diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index 903b0979614..1e707f1d315 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -73,3 +73,5 @@ module QA end end end + +QA::Page::Project::Settings::ProtectedBranches.prepend_if_ee('QA::EE::Page::Project::Settings::ProtectedBranches') diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 9fd668f812b..549992f271b 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -131,3 +131,5 @@ module QA end end end + +QA::Page::Project::Show.prepend_if_ee('QA::EE::Page::Project::Show') diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 7541baed467..4d26cadcdfe 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -5,6 +5,7 @@ module QA module Project module WebIDE class Edit < Page::Base + prepend Page::Component::WebIDE::Alert include Page::Component::DropdownFilter view 'app/assets/javascripts/ide/components/activity_bar.vue' do @@ -123,3 +124,5 @@ module QA end end end + +QA::Page::Project::WebIDE::Edit.prepend_if_ee('QA::EE::Page::Component::WebIDE::WebTerminalPanel') diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 93a82094776..4a29a14c5c2 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -15,6 +15,7 @@ module QA attribute :add_name_uuid attribute :description attribute :standalone + attribute :runners_token attribute :group do Group.fabricate! diff --git a/qa/qa/resource/repository/commit.rb b/qa/qa/resource/repository/commit.rb new file mode 100644 index 00000000000..61c2ad6bfc0 --- /dev/null +++ b/qa/qa/resource/repository/commit.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module QA + module Resource + module Repository + class Commit < Base + attr_accessor :author_email, + :author_name, + :branch, + :commit_message, + :file_path, + :sha + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-commit' + end + end + + def initialize + @commit_message = 'QA Test - Commit message' + end + + def files=(files) + if !files.is_a?(Array) || + files.empty? || + files.any? { |file| !file.has_key?(:file_path) || !file.has_key?(:content) } + raise ArgumentError, "Please provide an array of hashes e.g.: [{file_path: 'file1', content: 'foo'}]" + end + + @files = files + end + + def resource_web_url(resource) + super + rescue ResourceURLMissingError + # this particular resource does not expose a web_url property + end + + def api_get_path + "#{api_post_path}/#{@sha}" + end + + def api_post_path + "/projects/#{CGI.escape(project.path_with_namespace)}/repository/commits" + end + + def api_post_body + { + branch: @branch || "master", + author_email: @author_email || Runtime::User.default_email, + author_name: @author_name || Runtime::User.username, + commit_message: commit_message, + actions: actions + } + end + + def actions + @files.map do |file| + file.merge({ action: "create" }) + end + end + end + end + end +end diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index 3344ad3360a..9c2e138bade 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -6,9 +6,10 @@ module QA module Resource class Runner < Base attr_writer :name, :tags, :image + attr_accessor :config attribute :project do - Project.fabricate! do |resource| + Project.fabricate_via_api! do |resource| resource.name = 'project-with-ci-cd' resource.description = 'Project with CI/CD Pipelines' end @@ -26,24 +27,27 @@ module QA @image || 'gitlab/gitlab-runner:alpine' end - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:go_to_ci_cd_settings) - + def fabricate_via_api! Service::Runner.new(name).tap do |runner| - Page::Project::Settings::CICD.perform do |settings| - settings.expand_runners_settings do |runners| - runner.pull - runner.token = runners.registration_token - runner.address = runners.coordinator_address - runner.tags = tags - runner.image = image - runner.register! - end - end + runner.pull + runner.token = project.runners_token + runner.address = Runtime::Scenario.gitlab_address + runner.tags = tags + runner.image = image + runner.config = config if config + runner.run_untagged = true + runner.register! end end + + def api_get_path + end + + def api_post_path + end + + def api_post_body + end end end end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 2987bb1a213..197008ed8bf 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -68,7 +68,7 @@ module QA options = Selenium::WebDriver.const_get(QA::Runtime::Env.browser.capitalize)::Options.new if QA::Runtime::Env.browser == :chrome - options.add_argument("window-size=1240,1680") + options.add_argument("window-size=1480,2200") # Chrome won't work properly in a Docker container in sandbox mode options.add_argument("no-sandbox") diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index b184eeb1701..594e5712ab2 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -233,3 +233,5 @@ module QA end end end + +QA::Runtime::Env.extend_if_ee('QA::EE::Runtime::Env') diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb index 632a0f5f2a9..99497cbe0ad 100644 --- a/qa/qa/scenario/test/sanity/selectors.rb +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -56,3 +56,5 @@ module QA end end end + +QA::Scenario::Test::Sanity::Selectors.prepend_if_ee('QA::EE::Scenario::Test::Sanity::Selectors') diff --git a/qa/qa/service/runner.rb b/qa/qa/service/runner.rb index 03b234f7a56..6fc5984b12a 100644 --- a/qa/qa/service/runner.rb +++ b/qa/qa/service/runner.rb @@ -7,13 +7,25 @@ module QA class Runner include Service::Shellout - attr_accessor :token, :address, :tags, :image + attr_accessor :token, :address, :tags, :image, :run_untagged + attr_writer :config def initialize(name) @image = 'gitlab/gitlab-runner:alpine' @name = name || "qa-runner-#{SecureRandom.hex(4)}" @network = Runtime::Scenario.attributes[:network] || 'test' @tags = %w[qa test] + @run_untagged = false + end + + def config + @config ||= <<~END + concurrent = 1 + check_interval = 0 + + [session_server] + session_timeout = 1800 + END end def network @@ -32,19 +44,30 @@ module QA shell <<~CMD.tr("\n", ' ') docker run -d --rm --entrypoint=/bin/sh --network #{network} --name #{@name} + -p 8093:8093 -e CI_SERVER_URL=#{@address} -e REGISTER_NON_INTERACTIVE=true -e REGISTRATION_TOKEN=#{@token} -e RUNNER_EXECUTOR=shell -e RUNNER_TAG_LIST=#{@tags.join(',')} -e RUNNER_NAME=#{@name} - #{@image} -c 'gitlab-runner register && gitlab-runner run' + #{@image} -c "#{register_command}" CMD end def remove! shell "docker rm -f #{@name}" end + + private + + def register_command + <<~CMD + printf '#{config.chomp.gsub(/\n/, "\\n").gsub('"', '\"')}' > /etc/gitlab-runner/config.toml && + gitlab-runner register --run-untagged=#{@run_untagged} && + gitlab-runner run + CMD + end end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb new file mode 100644 index 00000000000..7b6a3579af0 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module QA + context 'Plan' do + describe 'Close issue' do + let(:issue_title) { 'issue title' } + let(:commit_message) { 'Closes' } + + before do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + issue = Resource::Issue.fabricate_via_api! do |issue| + issue.title = issue_title + end + + @project = issue.project + @issue_id = issue.api_response[:iid] + + # Initial commit should be pushed because + # the very first commit to the project doesn't close the issue + # https://gitlab.com/gitlab-org/gitlab-ce/issues/38965 + push_commit('Initial commit') + end + + it 'user closes an issue by pushing commit' do + push_commit("#{commit_message} ##{@issue_id}", false) + + @project.visit! + Page::Project::Show.perform do |show| + show.click_commit(commit_message) + end + commit_sha = Page::Project::Commit::Show.perform(&:commit_sha) + + Page::Project::Menu.perform(&:click_issues) + Page::Project::Issue::Index.perform do |index| + index.click_closed_issues_link + index.click_issue_link(issue_title) + end + + Page::Project::Issue::Show.perform do |show| + expect(show).to have_element(:reopen_issue_button) + expect(show).to have_content("closed via commit #{commit_sha}") + end + end + + def push_commit(commit_message, new_branch = true) + Resource::Repository::ProjectPush.fabricate! do |push| + push.commit_message = commit_message + push.new_branch = new_branch + push.file_content = commit_message + push.project = @project + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb index 33744236dd4..900ddcb7f59 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -15,11 +15,11 @@ module QA Resource::Runner.fabricate! do |runner| runner.name = executor - end + end.project.visit! + Page::Project::Menu.perform(&:go_to_ci_cd_settings) Page::Project::Settings::CICD.perform do |settings| sleep 5 # Runner should register within 5 seconds - settings.refresh settings.expand_runners_settings do |page| expect(page).to have_content(executor) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index f6411d8c5ad..141166f6971 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -17,7 +17,7 @@ module QA @repository_location = @project.repository_ssh_location - Resource::Runner.fabricate_via_browser_ui! do |resource| + Resource::Runner.fabricate_via_api! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb index e0e1b2d6c7d..6f007e667f2 100644 --- a/rubocop/cop/inject_enterprise_edition_module.rb +++ b/rubocop/cop/inject_enterprise_edition_module.rb @@ -24,7 +24,7 @@ module RuboCop # We use `match?` here instead of RuboCop's AST matching, as this makes # it far easier to handle nested constants such as `EE::Foo::Bar::Baz`. - line.match?(/(\s|\()('|")?(::)?EE::/) + line.match?(/(\s|\()('|")?(::)?(QA::)?EE::/) end def on_send(node) diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb index 91f9e2c7832..14b0cf959b3 100644 --- a/spec/controllers/groups/runners_controller_spec.rb +++ b/spec/controllers/groups/runners_controller_spec.rb @@ -3,73 +3,202 @@ require 'spec_helper' describe Groups::RunnersController do - let(:user) { create(:user) } - let(:group) { create(:group) } + let(:user) { create(:user) } + let(:group) { create(:group) } let(:runner) { create(:ci_runner, :group, groups: [group]) } - - let(:params) do - { - group_id: group, - id: runner - } - end + let(:params) { { group_id: group, id: runner } } before do sign_in(user) - group.add_maintainer(user) + end + + describe '#show' do + context 'when user is owner' do + before do + group.add_owner(user) + end + + it 'renders show with 200 status code' do + get :show, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(200) + expect(response).to render_template(:show) + end + end + + context 'when user is not owner' do + before do + group.add_maintainer(user) + end + + it 'renders a 404' do + get :show, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(404) + end + end + end + + describe '#edit' do + context 'when user is owner' do + before do + group.add_owner(user) + end + + it 'renders show with 200 status code' do + get :edit, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(200) + expect(response).to render_template(:edit) + end + end + + context 'when user is not owner' do + before do + group.add_maintainer(user) + end + + it 'renders a 404' do + get :edit, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(404) + end + end end describe '#update' do - it 'updates the runner and ticks the queue' do - new_desc = runner.description.swapcase + context 'when user is an owner' do + before do + group.add_owner(user) + end - expect do - post :update, params: params.merge(runner: { description: new_desc } ) - end.to change { runner.ensure_runner_queue_value } + it 'updates the runner, ticks the queue, and redirects' do + new_desc = runner.description.swapcase - runner.reload + expect do + post :update, params: params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } - expect(response).to have_gitlab_http_status(302) - expect(runner.description).to eq(new_desc) + expect(response).to have_gitlab_http_status(302) + expect(runner.reload.description).to eq(new_desc) + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'rejects the update and responds 404' do + old_desc = runner.description + + expect do + post :update, params: params.merge(runner: { description: old_desc.swapcase } ) + end.not_to change { runner.ensure_runner_queue_value } + + expect(response).to have_gitlab_http_status(404) + expect(runner.reload.description).to eq(old_desc) + end end end describe '#destroy' do - it 'destroys the runner' do - delete :destroy, params: params + context 'when user is an owner' do + before do + group.add_owner(user) + end + + it 'destroys the runner and redirects' do + delete :destroy, params: params + + expect(response).to have_gitlab_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'responds 404 and does not destroy the runner' do + delete :destroy, params: params - expect(response).to have_gitlab_http_status(302) - expect(Ci::Runner.find_by(id: runner.id)).to be_nil + expect(response).to have_gitlab_http_status(404) + expect(Ci::Runner.find_by(id: runner.id)).to be_present + end end end describe '#resume' do - it 'marks the runner as active and ticks the queue' do - runner.update(active: false) + context 'when user is an owner' do + before do + group.add_owner(user) + end - expect do - post :resume, params: params - end.to change { runner.ensure_runner_queue_value } + it 'marks the runner as active, ticks the queue, and redirects' do + runner.update(active: false) - runner.reload + expect do + post :resume, params: params + end.to change { runner.ensure_runner_queue_value } - expect(response).to have_gitlab_http_status(302) - expect(runner.active).to eq(true) + expect(response).to have_gitlab_http_status(302) + expect(runner.reload.active).to eq(true) + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'responds 404 and does not activate the runner' do + runner.update(active: false) + + expect do + post :resume, params: params + end.not_to change { runner.ensure_runner_queue_value } + + expect(response).to have_gitlab_http_status(404) + expect(runner.reload.active).to eq(false) + end end end describe '#pause' do - it 'marks the runner as inactive and ticks the queue' do - runner.update(active: true) + context 'when user is an owner' do + before do + group.add_owner(user) + end + + it 'marks the runner as inactive, ticks the queue, and redirects' do + runner.update(active: true) + + expect do + post :pause, params: params + end.to change { runner.ensure_runner_queue_value } + + expect(response).to have_gitlab_http_status(302) + expect(runner.reload.active).to eq(false) + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end - expect do - post :pause, params: params - end.to change { runner.ensure_runner_queue_value } + it 'responds 404 and does not update the runner or queue' do + runner.update(active: true) - runner.reload + expect do + post :pause, params: params + end.not_to change { runner.ensure_runner_queue_value } - expect(response).to have_gitlab_http_status(302) - expect(runner.active).to eq(false) + expect(response).to have_gitlab_http_status(404) + expect(runner.reload.active).to eq(true) + end end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 187c7864ad7..608131dcbc8 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1084,16 +1084,41 @@ describe Projects::IssuesController do end it "deletes the issue" do - delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, destroy_confirm: true } expect(response).to have_gitlab_http_status(302) expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./) end + it "deletes the issue" do + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, destroy_confirm: true } + + expect(response).to have_gitlab_http_status(302) + expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./) + end + + it "prevents deletion if destroy_confirm is not set" do + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } + + expect(response).to have_gitlab_http_status(302) + expect(controller).to set_flash[:notice].to('Destroy confirmation not provided for issue') + end + + it "prevents deletion in JSON format if destroy_confirm is not set" do + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, format: 'json' } + + expect(response).to have_gitlab_http_status(422) + expect(json_response).to eq({ 'errors' => 'Destroy confirmation not provided for issue' }) + end + it 'delegates the update of the todos count cache to TodoService' do expect_any_instance_of(TodoService).to receive(:destroy_target).with(issue).once - delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, destroy_confirm: true } end end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index f076a5e769f..bd3e66efd58 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -12,6 +12,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do before do stub_feature_flags(ci_enable_live_trace: true) + stub_feature_flags(job_log_json: false) stub_not_protect_default_branch end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 11b1eaf11b7..d0370dfaeee 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -573,16 +573,34 @@ describe Projects::MergeRequestsController do end it "deletes the merge request" do - delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, destroy_confirm: true } expect(response).to have_gitlab_http_status(302) expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./) end + it "prevents deletion if destroy_confirm is not set" do + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + + expect(response).to have_gitlab_http_status(302) + expect(controller).to set_flash[:notice].to('Destroy confirmation not provided for merge request') + end + + it "prevents deletion in JSON format if destroy_confirm is not set" do + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, format: 'json' } + + expect(response).to have_gitlab_http_status(422) + expect(json_response).to eq({ 'errors' => 'Destroy confirmation not provided for merge request' }) + end + it 'delegates the update of the todos count cache to TodoService' do expect_any_instance_of(TodoService).to receive(:destroy_target).with(merge_request).once - delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, destroy_confirm: true } end end end @@ -719,19 +737,63 @@ describe Projects::MergeRequestsController do end describe 'GET test_reports' do + let(:merge_request) do + create(:merge_request, + :with_diffs, + :with_merge_request_pipeline, + target_project: project, + source_project: project + ) + end + subject do - get :test_reports, - params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :test_reports, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end before do allow_any_instance_of(MergeRequest) - .to receive(:compare_test_reports).and_return(comparison_status) + .to receive(:compare_test_reports) + .and_return(comparison_status) + + allow_any_instance_of(MergeRequest) + .to receive(:actual_head_pipeline) + .and_return(merge_request.all_pipelines.take) + end + + describe 'permissions on a public project with private CI/CD' do + let(:project) { create :project, :repository, :public, :builds_private } + let(:comparison_status) { { status: :parsed, data: { summary: 1 } } } + + context 'while signed out' do + before do + sign_out(user) + end + + it 'responds with a 404' do + subject + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to be_blank + end + end + + context 'while signed in as an unrelated user' do + before do + sign_in(create(:user)) + end + + it 'responds with a 404' do + subject + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to be_blank + end + end end context 'when comparison is being processed' do @@ -1052,17 +1114,39 @@ describe Projects::MergeRequestsController do let(:status) { pipeline.detailed_status(double('user')) } - before do + it 'returns a detailed head_pipeline status in json' do get_pipeline_status - end - it 'return a detailed head_pipeline status in json' do expect(response).to have_gitlab_http_status(:ok) expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png" end + + context 'with project member visibility on a public project' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public, :builds_private) } + + it 'returns pipeline data to project members' do + project.add_developer(user) + + get_pipeline_status + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png" + end + + it 'returns blank OK response to non-project-members' do + get_pipeline_status + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + end + end end context 'when head_pipeline does not exist' do @@ -1070,7 +1154,7 @@ describe Projects::MergeRequestsController do get_pipeline_status end - it 'return empty' do + it 'returns blank OK response' do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_empty end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 4500c412521..4db77921f24 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -212,40 +212,232 @@ describe Projects::NotesController do describe 'POST create' do let(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } + let(:note_text) { 'some note' } let(:request_params) do { - note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, + note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, namespace_id: project.namespace, project_id: project, merge_request_diff_head_sha: 'sha', target_type: 'merge_request', target_id: merge_request.id - } + }.merge(extra_request_params) + end + let(:extra_request_params) { {} } + + let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC } + let(:merge_requests_access_level) { ProjectFeature::ENABLED } + + def create! + post :create, params: request_params end before do + project.update_attribute(:visibility_level, project_visibility) + project.project_feature.update(merge_requests_access_level: merge_requests_access_level) sign_in(user) - project.add_developer(user) end - it "returns status 302 for html" do - post :create, params: request_params + describe 'making the creation request' do + before do + create! + end + + context 'the project is publically available' do + context 'for HTML' do + it "returns status 302" do + expect(response).to have_gitlab_http_status(302) + end + end + + context 'for JSON' do + let(:extra_request_params) { { format: :json } } + + it "returns status 200 for json" do + expect(response).to have_gitlab_http_status(200) + end + end + end - expect(response).to have_gitlab_http_status(302) + context 'the project is a private project' do + let(:project_visibility) { Gitlab::VisibilityLevel::PRIVATE } + + [{}, { format: :json }].each do |extra| + context "format is #{extra[:format]}" do + let(:extra_request_params) { extra } + + it "returns status 404" do + expect(response).to have_gitlab_http_status(404) + end + end + end + end end - it "returns status 200 for json" do - post :create, params: request_params.merge(format: :json) + context 'the user is a developer on a private project' do + let(:project_visibility) { Gitlab::VisibilityLevel::PRIVATE } - expect(response).to have_gitlab_http_status(200) + before do + project.add_developer(user) + end + + context 'HTML requests' do + it "returns status 302 (redirect)" do + create! + + expect(response).to have_gitlab_http_status(302) + end + end + + context 'JSON requests' do + let(:extra_request_params) { { format: :json } } + + it "returns status 200" do + create! + + expect(response).to have_gitlab_http_status(200) + end + end + + context 'the return_discussion param is set' do + let(:extra_request_params) { { format: :json, return_discussion: 'true' } } + + it 'returns discussion JSON when the return_discussion param is set' do + create! + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to have_key 'discussion' + expect(json_response.dig('discussion', 'notes', 0, 'note')).to eq(request_params[:note][:note]) + end + end + + context 'when creating a note with quick actions' do + context 'with commands that return changes' do + let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" } + let(:extra_request_params) { { format: :json } } + + it 'includes changes in commands_changes ' do + create! + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time') + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + + context 'with commands that do not return changes' do + let(:issue) { create(:issue, project: project) } + let(:other_project) { create(:project) } + let(:note_text) { "/move #{other_project.full_path}\n/title AAA" } + let(:extra_request_params) { { format: :json, target_id: issue.id, target_type: 'issue' } } + + before do + other_project.add_developer(user) + end + + it 'does not include changes in commands_changes' do + create! + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + end end - it 'returns discussion JSON when the return_discussion param is set' do - post :create, params: request_params.merge(format: :json, return_discussion: 'true') + context 'when the internal project prohibits non-members from accessing merge requests' do + let(:project_visibility) { Gitlab::VisibilityLevel::INTERNAL } + let(:merge_requests_access_level) { ProjectFeature::PRIVATE } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to have_key 'discussion' - expect(json_response['discussion']['notes'][0]['note']).to eq(request_params[:note][:note]) + it "prevents a non-member user from creating a note on one of the project's merge requests" do + create! + + expect(response).to have_gitlab_http_status(404) + end + + context 'when the user is a team member' do + before do + project.add_developer(user) + end + + it 'can add comments' do + expect { create! }.to change { project.notes.count }.by(1) + end + end + + # Illustration of the attack vector for posting comments to discussions that should + # be inaccessible. + # + # This relies on posting a note to a commit that is not necessarily even in the + # merge request, with a value of :in_reply_to_discussion_id that points to a + # discussion on a merge_request that should not be accessible. + context 'when the request includes a :in_reply_to_discussion_id designed to fool us' do + let(:commit) { create(:commit, project: project) } + + let(:existing_comment) do + create(:note_on_commit, + note: 'first', + project: project, + commit_id: merge_request.commit_shas.first) + end + + let(:discussion) { existing_comment.discussion } + + # see !60465 for details of the structure of this request + let(:request_params) do + { "utf8" => "✓", + "authenticity_token" => "1", + "view" => "inline", + "line_type" => "", + "merge_request_diff_head_sha" => "", + "in_reply_to_discussion_id" => discussion.id, + "note_project_id" => project.id, + "project_id" => project.id, + "namespace_id" => project.namespace, + "target_type" => "commit", + "target_id" => commit.id, + "note" => { + "noteable_type" => "", + "noteable_id" => "", + "commit_id" => "", + "type" => "", + "line_code" => "", + "position" => "", + "note" => "ThisReplyWillGoToMergeRequest" + } } + end + + it 'prevents the request from adding notes to the spoofed discussion' do + expect { create! }.not_to change { discussion.notes.count } + end + + it 'returns an error to the user' do + create! + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when the public project prohibits non-members from accessing merge requests' do + let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC } + let(:merge_requests_access_level) { ProjectFeature::PRIVATE } + + it "prevents a non-member user from creating a note on one of the project's merge requests" do + create! + + expect(response).to have_gitlab_http_status(404) + end + + context 'when the user is a team member' do + before do + project.add_developer(user) + create! + end + + it 'can add comments' do + expect(response).to be_redirect + end + end end context 'when merge_request_diff_head_sha present' do @@ -262,7 +454,7 @@ describe Projects::NotesController do end it "returns status 302 for html" do - post :create, params: request_params + create! expect(response).to have_gitlab_http_status(302) end @@ -285,7 +477,7 @@ describe Projects::NotesController do end context 'when creating a commit comment from an MR fork' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :public) } let(:forked_project) do fork_project(project, nil, repository: true) @@ -299,45 +491,59 @@ describe Projects::NotesController do create(:note_on_commit, note: 'a note', project: forked_project, commit_id: merge_request.commit_shas.first) end - def post_create(extra_params = {}) - post :create, params: { + let(:note_project_id) do + forked_project.id + end + + let(:request_params) do + { note: { note: 'some other note', noteable_id: merge_request.id }, namespace_id: project.namespace, project_id: project, target_type: 'merge_request', target_id: merge_request.id, - note_project_id: forked_project.id, + note_project_id: note_project_id, in_reply_to_discussion_id: existing_comment.discussion_id - }.merge(extra_params) + } + end + + let(:fork_visibility) { Gitlab::VisibilityLevel::PUBLIC } + + before do + forked_project.update_attribute(:visibility_level, fork_visibility) end context 'when the note_project_id is not correct' do - it 'returns a 404' do - post_create(note_project_id: Project.maximum(:id).succ) + let(:note_project_id) do + project.id && Project.maximum(:id).succ + end + it 'returns a 404' do + create! expect(response).to have_gitlab_http_status(404) end end context 'when the user has no access to the fork' do - it 'returns a 404' do - post_create + let(:fork_visibility) { Gitlab::VisibilityLevel::PRIVATE } + it 'returns a 404' do + create! expect(response).to have_gitlab_http_status(404) end end context 'when the user has access to the fork' do - let(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) } + let!(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) } + let(:fork_visibility) { Gitlab::VisibilityLevel::PUBLIC } - before do - forked_project.add_developer(user) - - existing_comment + it 'is successful' do + create! + expect(response).to have_gitlab_http_status(302) end it 'creates the note' do - expect { post_create }.to change { forked_project.notes.count }.by(1) + expect { create! }.to change { forked_project.notes.count }.by(1) end end end @@ -346,11 +552,6 @@ describe Projects::NotesController do let(:locked_issue) { create(:issue, :locked, project: project) } let(:issue) {create(:issue, project: project)} - before do - project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) - project.project_member(user).destroy - end - it 'uses target_id and ignores noteable_id' do request_params = { note: { note: 'some note', noteable_type: 'Issue', noteable_id: locked_issue.id }, @@ -368,7 +569,6 @@ describe Projects::NotesController do context 'when the merge request discussion is locked' do before do - project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) merge_request.update_attribute(:discussion_locked, true) end @@ -382,6 +582,10 @@ describe Projects::NotesController do end context 'when a user is a team member' do + before do + project.add_developer(user) + end + it 'returns 302 status for html' do post :create, params: request_params @@ -400,10 +604,6 @@ describe Projects::NotesController do end context 'when a user is not a team member' do - before do - project.project_member(user).destroy - end - it 'returns 404 status' do post :create, params: request_params @@ -415,37 +615,6 @@ describe Projects::NotesController do end end end - - context 'when creating a note with quick actions' do - context 'with commands that return changes' do - let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" } - - it 'includes changes in commands_changes ' do - post :create, params: request_params.merge(note: { note: note_text }, format: :json) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time') - expect(json_response['commands_changes']).not_to include('target_project', 'title') - end - end - - context 'with commands that do not return changes' do - let(:issue) { create(:issue, project: project) } - let(:other_project) { create(:project) } - let(:note_text) { "/move #{other_project.full_path}\n/title AAA" } - - before do - other_project.add_developer(user) - end - - it 'does not include changes in commands_changes' do - post :create, params: request_params.merge(note: { note: note_text }, target_type: 'issue', target_id: issue.id, format: :json) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['commands_changes']).not_to include('target_project', 'title') - end - end - end end describe 'PUT update' do diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 563b61962cf..180d997a8e8 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -11,6 +11,7 @@ describe Projects::ServicesController do before do sign_in(user) project.add_maintainer(user) + allow(Gitlab::UrlBlocker).to receive(:validate!).and_return([URI.parse('http://example.com'), nil]) end describe '#test' do @@ -56,6 +57,8 @@ describe Projects::ServicesController do stub_request(:get, 'http://example.com/rest/api/2/serverInfo') .to_return(status: 200, body: '{}') + expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original + put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params } expect(response.status).to eq(200) @@ -66,6 +69,8 @@ describe Projects::ServicesController do stub_request(:get, 'http://example.com/rest/api/2/serverInfo') .to_return(status: 200, body: '{}') + expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original + put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params } expect(response.status).to eq(200) diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 9c4ddce5409..68b7bf61231 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -100,16 +100,8 @@ describe SessionsController do end end - context 'when reCAPTCHA is enabled' do - let(:user) { create(:user) } - let(:user_params) { { login: user.username, password: user.password } } - - before do - stub_application_setting(recaptcha_enabled: true) - request.headers[described_class::CAPTCHA_HEADER] = 1 - end - - it 'displays an error when the reCAPTCHA is not solved' do + context 'with reCAPTCHA' do + def unsuccesful_login(user_params, sesion_params: {}) # Without this, `verify_recaptcha` arbitrarily returns true in test env Recaptcha.configuration.skip_verify_env.delete('test') counter = double(:counter) @@ -119,14 +111,10 @@ describe SessionsController do .with(:failed_login_captcha_total, anything) .and_return(counter) - post(:create, params: { user: user_params }) - - expect(response).to render_template(:new) - expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' - expect(subject.current_user).to be_nil + post(:create, params: { user: user_params }, session: sesion_params) end - it 'successfully logs in a user when reCAPTCHA is solved' do + def succesful_login(user_params, sesion_params: {}) # Avoid test ordering issue and ensure `verify_recaptcha` returns true Recaptcha.configuration.skip_verify_env << 'test' counter = double(:counter) @@ -137,9 +125,80 @@ describe SessionsController do .and_return(counter) expect(Gitlab::Metrics).to receive(:counter).and_call_original - post(:create, params: { user: user_params }) + post(:create, params: { user: user_params }, session: sesion_params) + end - expect(subject.current_user).to eq user + context 'when reCAPTCHA is enabled' do + let(:user) { create(:user) } + let(:user_params) { { login: user.username, password: user.password } } + + before do + stub_application_setting(recaptcha_enabled: true) + request.headers[described_class::CAPTCHA_HEADER] = 1 + end + + it 'displays an error when the reCAPTCHA is not solved' do + # Without this, `verify_recaptcha` arbitrarily returns true in test env + + unsuccesful_login(user_params) + + expect(response).to render_template(:new) + expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + expect(subject.current_user).to be_nil + end + + it 'successfully logs in a user when reCAPTCHA is solved' do + succesful_login(user_params) + + expect(subject.current_user).to eq user + end + end + + context 'when reCAPTCHA login protection is enabled' do + let(:user) { create(:user) } + let(:user_params) { { login: user.username, password: user.password } } + + before do + stub_application_setting(login_recaptcha_protection_enabled: true) + end + + context 'when user tried to login 5 times' do + it 'displays an error when the reCAPTCHA is not solved' do + unsuccesful_login(user_params, sesion_params: { failed_login_attempts: 6 }) + + expect(response).to render_template(:new) + expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + expect(subject.current_user).to be_nil + end + + it 'successfully logs in a user when reCAPTCHA is solved' do + succesful_login(user_params, sesion_params: { failed_login_attempts: 6 }) + + expect(subject.current_user).to eq user + end + end + + context 'when there are more than 5 anonymous session with the same IP' do + before do + allow(Gitlab::AnonymousSession).to receive_message_chain(:new, :stored_sessions).and_return(6) + end + + it 'displays an error when the reCAPTCHA is not solved' do + unsuccesful_login(user_params) + + expect(response).to render_template(:new) + expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + expect(subject.current_user).to be_nil + end + + it 'successfully logs in a user when reCAPTCHA is solved' do + expect(Gitlab::AnonymousSession).to receive_message_chain(:new, :cleanup_session_per_ip_entries) + + succesful_login(user_params) + + expect(subject.current_user).to eq user + end + end end end end @@ -348,4 +407,17 @@ describe SessionsController do expect(controller.stored_location_for(:redirect)).to eq(search_path) end end + + context 'when login fails' do + before do + set_devise_mapping(context: @request) + @request.env["warden.options"] = { action: 'unauthenticated' } + end + + it 'does increment failed login counts for session' do + get(:new, params: { user: { login: 'failed' } }) + + expect(session[:failed_login_attempts]).to eq(1) + end + end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 0876502a899..5f4a6bf8ee7 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -21,8 +21,20 @@ shared_examples 'content publicly cached' do end describe UploadsController do + include WorkhorseHelpers + let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } + describe 'POST #authorize' do + it_behaves_like 'handle uploads authorize' do + let(:uploader_class) { PersonalFileUploader } + let(:model) { create(:personal_snippet, :public) } + let(:params) do + { model: 'personal_snippet', id: model.id } + end + end + end + describe 'POST create' do let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } @@ -636,4 +648,10 @@ describe UploadsController do end end end + + def post_authorize(verified: true) + request.headers.merge!(workhorse_internal_api_request_header) if verified + + post :authorize, params: { model: 'personal_snippet', id: model.id }, format: :json + end end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 232890b1bba..52af470efac 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -30,7 +30,7 @@ describe 'Database schema' do draft_notes: %w[discussion_id commit_id], emails: %w[user_id], events: %w[target_id], - epics: %w[updated_by_id last_edited_by_id start_date_sourcing_milestone_id due_date_sourcing_milestone_id], + epics: %w[updated_by_id last_edited_by_id start_date_sourcing_milestone_id due_date_sourcing_milestone_id state_id], forked_project_links: %w[forked_from_project_id], geo_event_log: %w[hashed_storage_attachments_event_id], geo_job_artifact_deleted_events: %w[job_artifact_id], diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb index 4c875935d82..a93f13395a2 100644 --- a/spec/factories/group_members.rb +++ b/spec/factories/group_members.rb @@ -24,5 +24,9 @@ FactoryBot.define do trait(:ldap) do ldap true end + + trait :blocked do + after(:build) { |group_member, _| group_member.user.block! } + end end end diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb index 6dcac0400ca..723fa6058fe 100644 --- a/spec/factories/project_members.rb +++ b/spec/factories/project_members.rb @@ -17,5 +17,9 @@ FactoryBot.define do invite_token 'xxx' invite_email 'email@email.com' end + + trait :blocked do + after(:build) { |project_member, _| project_member.user.block! } + end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index b2c8bdab013..57e58513529 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -39,6 +39,14 @@ FactoryBot.define do avatar { fixture_file_upload('spec/fixtures/dk.png') } end + trait :with_sign_ins do + sign_in_count 3 + current_sign_in_at { Time.now } + last_sign_in_at { FFaker::Time.between(10.days.ago, 1.day.ago) } + current_sign_in_ip '127.0.0.1' + last_sign_in_ip '127.0.0.1' + end + trait :two_factor_via_otp do before(:create) do |user| user.otp_required_for_login = true diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index ddd87404003..eb59de2e132 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -263,6 +263,7 @@ describe 'Admin updates settings' do page.within('.as-spam') do check 'Enable reCAPTCHA' + check 'Enable reCAPTCHA for login' fill_in 'reCAPTCHA Site Key', with: 'key' fill_in 'reCAPTCHA Private Key', with: 'key' fill_in 'IPs per user', with: 15 @@ -271,6 +272,7 @@ describe 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" expect(current_settings.recaptcha_enabled).to be true + expect(current_settings.login_recaptcha_protection_enabled).to be true expect(current_settings.unique_ips_limit_per_user).to eq(15) end end diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb index a7ccc6f7d7b..00fa85930b1 100644 --- a/spec/features/global_search_spec.rb +++ b/spec/features/global_search_spec.rb @@ -16,8 +16,7 @@ describe 'Global search' do it 'increases usage ping searches counter' do expect(Gitlab::UsageDataCounters::SearchCounter).to receive(:increment_navbar_searches_count) - fill_in "search", with: "foobar" - click_button "Go" + submit_search('foobar') end describe 'I search through the issues and I see pagination' do @@ -27,10 +26,9 @@ describe 'Global search' do end it "has a pagination" do - fill_in "search", with: "initial" - click_button "Go" + submit_search('initial') + select_search_scope('Issues') - select_filter("Issues") expect(page).to have_selector('.gl-pagination .next') end end diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb index 84a8691a7f2..8891866c1f8 100644 --- a/spec/features/groups/clusters/user_spec.rb +++ b/spec/features/groups/clusters/user_spec.rb @@ -13,7 +13,7 @@ describe 'User Cluster', :js do gitlab_sign_in(user) allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 } - allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) + allow_any_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected) end diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 68d99b4241a..76eef66c517 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -34,7 +34,9 @@ describe 'Math rendering', :js do visit project_issue_path(project, issue) - expect(page).to have_selector('.katex-error', text: "\href{javascript:alert('xss');}{xss}") - expect(page).to have_selector('.katex-html a', text: 'Gitlab') + page.within '.description > .md' do + expect(page).to have_selector('.katex-error') + expect(page).to have_selector('.katex-html a', text: 'Gitlab') + end end end diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index 1ab7742b36e..0905ab0aef8 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -49,6 +49,23 @@ describe 'User edit profile' do end end + describe 'when I change my email' do + before do + user.send_reset_password_instructions + end + + it 'clears the reset password token' do + expect(user.reset_password_token?).to be true + + fill_in 'user_email', with: 'new-email@example.com' + submit_settings + + user.reload + expect(user.confirmation_token).not_to be_nil + expect(user.reset_password_token?).to be false + end + end + context 'user avatar' do before do attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif')) diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 3899aab8170..84f2e3e09ae 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -13,7 +13,7 @@ describe 'User Cluster', :js do gitlab_sign_in(user) allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 } - allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) + allow_any_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute) allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected) end diff --git a/spec/features/projects/files/user_searches_for_files_spec.rb b/spec/features/projects/files/user_searches_for_files_spec.rb index e82f54fbe50..ff7547bce83 100644 --- a/spec/features/projects/files/user_searches_for_files_spec.rb +++ b/spec/features/projects/files/user_searches_for_files_spec.rb @@ -18,8 +18,7 @@ describe 'Projects > Files > User searches for files' do end it 'does not show any result' do - fill_in('search', with: 'coffee') - click_button('Go') + submit_search('coffee') expect(page).to have_content("We couldn't find any") end @@ -50,8 +49,7 @@ describe 'Projects > Files > User searches for files' do it 'shows found files' do expect(page).to have_selector('.tree-controls .shortcuts-find-file') - fill_in('search', with: 'coffee') - click_button('Go') + submit_search('coffee') expect(page).to have_content('coffee') expect(page).to have_content('CONTRIBUTING.md') diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 1b277e17b0c..4d8a4812123 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -10,6 +10,8 @@ describe 'User browses a job', :js do let!(:build) { create(:ci_build, :success, :trace_artifact, :coverage, pipeline: pipeline) } before do + stub_feature_flags(job_log_json: false) + project.add_maintainer(user) project.enable_ci diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 8ed420300af..d1783de0330 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -20,6 +20,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do before do project.add_role(user, user_access_level) sign_in(user) + stub_feature_flags(job_log_json: false) end describe "GET /:project/jobs" do @@ -609,6 +610,14 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}") expect(find('.js-job-deployment-link')['href']).to include(second_deployment.deployable.project.path, second_deployment.deployable_id.to_s) end + + context 'when deployment does not have a deployable' do + let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: nil) } + + it 'has an empty href' do + expect(find('.js-job-deployment-link')['href']).to be_empty + end + end end context 'job failed to deploy' do diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 4ca79ccaea8..9451ee6eb15 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -6,21 +6,6 @@ describe 'User searches for code' do let(:user) { create(:user) } let(:project) { create(:project, :repository, namespace: user.namespace) } - def submit_search(search, with_send_keys: false) - page.within('.search') do - field = find_field('search') - field.fill_in(with: search) - - if with_send_keys - field.send_keys(:enter) - else - click_button("Go") - end - end - - click_link('Code') - end - context 'when signed in' do before do project.add_maintainer(user) @@ -31,7 +16,9 @@ describe 'User searches for code' do visit(project_path(project)) submit_search('application.js') + select_search_scope('Code') + expect(page).to have_selector('.results', text: 'application.js') expect(page).to have_selector('.file-content .code') expect(page).to have_selector("span.line[lang='javascript']") end @@ -52,9 +39,7 @@ describe 'User searches for code' do fill_in('dashboard_search', with: 'rspec') find('.btn-search').click - page.within('.results') do - expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') - end + expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions') end it 'search mutiple words with refs switching' do @@ -64,16 +49,12 @@ describe 'User searches for code' do fill_in('dashboard_search', with: search) find('.btn-search').click - page.within('.results') do - expect(find('.search-results')).to have_content(expected_result) - end + expect(page).to have_selector('.results', text: expected_result) find('.js-project-refs-dropdown').click find('.dropdown-page-one .dropdown-content').click_link('v1.0.0') - page.within('.results') do - expect(find(:css, '.search-results')).to have_content(expected_result) - end + expect(page).to have_selector('.results', text: expected_result) expect(find_field('dashboard_search').value).to eq(search) end @@ -84,7 +65,9 @@ describe 'User searches for code' do before do visit(project_tree_path(project, ref_name)) - submit_search('gitlab-grack', with_send_keys: true) + + submit_search('gitlab-grack') + select_search_scope('Code') end it 'shows ref switcher in code result summary' do @@ -104,22 +87,27 @@ describe 'User searches for code' do end it 'search result changes when refs switched' do - expect(find('.search-results')).not_to have_content('path = gitlab-grack') + expect(find('.results')).not_to have_content('path = gitlab-grack') + find('.js-project-refs-dropdown').click find('.dropdown-page-one .dropdown-content').click_link('master') - expect(find('.search-results')).to have_content('path = gitlab-grack') + + expect(page).to have_selector('.results', text: 'path = gitlab-grack') end end it 'no ref switcher shown in issue result summary', :js do issue = create(:issue, title: 'test', project: project) visit(project_tree_path(project)) - submit_search('test', with_send_keys: true) + + submit_search('test') + select_search_scope('Code') + expect(page).to have_selector('.js-project-refs-dropdown') - page.within('.search-filter') do - click_link('Issues') - end - expect(find(:css, '.search-results')).to have_link(issue.title) + + select_search_scope('Issues') + + expect(find(:css, '.results')).to have_link(issue.title) expect(page).not_to have_selector('.js-project-refs-dropdown') end end @@ -133,10 +121,9 @@ describe 'User searches for code' do it 'finds code' do submit_search('rspec') + select_search_scope('Code') - page.within('.results') do - expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') - end + expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions') end end end diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb index 2ce3fa4735f..0a203a5bf2d 100644 --- a/spec/features/search/user_searches_for_comments_spec.rb +++ b/spec/features/search/user_searches_for_comments_spec.rb @@ -18,15 +18,13 @@ describe 'User searches for comments' do let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') } it 'finds a commit' do - page.within('.search') do - fill_in('search', with: comment.note) - click_button('Go') - end - - click_link('Comments') + submit_search(comment.note) + select_search_scope('Comments') - expect(page).to have_text('Commit deleted') - expect(page).to have_text('12345678') + page.within('.results') do + expect(page).to have_content('Commit deleted') + expect(page).to have_content('12345678') + end end end end @@ -36,14 +34,10 @@ describe 'User searches for comments' do let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) } it 'finds a snippet' do - page.within('.search') do - fill_in('search', with: comment.note) - click_button('Go') - end - - click_link('Comments') + submit_search(comment.note) + select_search_scope('Comments') - expect(page).to have_link(snippet.title) + expect(page).to have_selector('.results', text: snippet.title) end end end diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb index 81c299752ea..958f12d3b84 100644 --- a/spec/features/search/user_searches_for_commits_spec.rb +++ b/spec/features/search/user_searches_for_commits_spec.rb @@ -16,15 +16,13 @@ describe 'User searches for commits' do context 'when searching by SHA' do it 'finds a commit and redirects to its page' do - fill_in('search', with: sha) - click_button('Search') + submit_search(sha) expect(page).to have_current_path(project_commit_path(project, sha)) end it 'finds a commit in uppercase and redirects to its page' do - fill_in('search', with: sha.upcase) - click_button('Search') + submit_search(sha.upcase) expect(page).to have_current_path(project_commit_path(project, sha)) end @@ -34,16 +32,14 @@ describe 'User searches for commits' do it 'finds a commit and holds on /search page' do create_commit('Message referencing another sha: "deadbeef"', project, user, 'master') - fill_in('search', with: 'deadbeef') - click_button('Search') + submit_search('deadbeef') expect(page).to have_current_path('/search', ignore_query: true) end it 'finds multiple commits' do - fill_in('search', with: 'See merge request') - click_button('Search') - click_link('Commits') + submit_search('See merge request') + select_search_scope('Commits') expect(page).to have_selector('.commit-row-description', count: 9) end diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index f0fcf6df70c..ae718cec7af 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -21,13 +21,11 @@ describe 'User searches for issues', :js do it 'finds an issue' do fill_in('dashboard_search', with: issue1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Issues') - end + select_search_scope('Issues') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + expect(page).to have_link(issue1.title) + expect(page).not_to have_link(issue2.title) end end @@ -41,13 +39,11 @@ describe 'User searches for issues', :js do fill_in('dashboard_search', with: issue1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Issues') - end + select_search_scope('Issues') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + expect(page).to have_link(issue1.title) + expect(page).not_to have_link(issue2.title) end end end @@ -65,13 +61,11 @@ describe 'User searches for issues', :js do it 'finds an issue' do fill_in('dashboard_search', with: issue1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Issues') - end + select_search_scope('Issues') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + expect(page).to have_link(issue1.title) + expect(page).not_to have_link(issue2.title) end end end diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index d005b87cdfe..0139ac26816 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -20,13 +20,11 @@ describe 'User searches for merge requests', :js do it 'finds a merge request' do fill_in('dashboard_search', with: merge_request1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Merge requests') - end + select_search_scope('Merge requests') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title) + expect(page).to have_link(merge_request1.title) + expect(page).not_to have_link(merge_request2.title) end end @@ -40,13 +38,11 @@ describe 'User searches for merge requests', :js do fill_in('dashboard_search', with: merge_request1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Merge requests') - end + select_search_scope('Merge requests') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title) + expect(page).to have_link(merge_request1.title) + expect(page).not_to have_link(merge_request2.title) end end end diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 00964ab4f1d..0714cfcc309 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -20,13 +20,11 @@ describe 'User searches for milestones', :js do it 'finds a milestone' do fill_in('dashboard_search', with: milestone1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Milestones') - end + select_search_scope('Milestones') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title) + expect(page).to have_link(milestone1.title) + expect(page).not_to have_link(milestone2.title) end end @@ -40,13 +38,11 @@ describe 'User searches for milestones', :js do fill_in('dashboard_search', with: milestone1.title) find('.btn-search').click - - page.within('.search-filter') do - click_link('Milestones') - end + select_search_scope('Milestones') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title) + expect(page).to have_link(milestone1.title) + expect(page).not_to have_link(milestone2.title) end end end diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb index 082c1ae8e4a..b194ac32ff6 100644 --- a/spec/features/search/user_searches_for_projects_spec.rb +++ b/spec/features/search/user_searches_for_projects_spec.rb @@ -20,8 +20,7 @@ describe 'User searches for projects' do it 'preserves the group being searched in' do visit(search_path(group_id: project.namespace.id)) - fill_in('search', with: 'foo') - click_button('Search') + submit_search('foo') expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) end @@ -29,8 +28,7 @@ describe 'User searches for projects' do it 'preserves the project being searched in' do visit(search_path(project_id: project.id)) - fill_in('search', with: 'foo') - click_button('Search') + submit_search('foo') expect(find('#project_id', visible: false).value).to eq(project.id.to_s) end diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb index e10c1afc0b8..6f2c5d48018 100644 --- a/spec/features/search/user_searches_for_users_spec.rb +++ b/spec/features/search/user_searches_for_users_spec.rb @@ -3,83 +3,81 @@ require 'spec_helper' describe 'User searches for users' do - context 'when on the dashboard' do - it 'finds the user', :js do - create(:user, username: 'gob_bluth', name: 'Gob Bluth') + let(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') } + let(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') } + let(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') } - sign_in(create(:user)) + before do + sign_in(user1) + end + context 'when on the dashboard' do + it 'finds the user', :js do visit dashboard_projects_path - fill_in 'search', with: 'gob' - find('#search').send_keys(:enter) + submit_search('gob') + select_search_scope('Users') - expect(page).to have_content('Users 1') - - click_on('Users 1') - - expect(page).to have_content('Gob Bluth') - expect(page).to have_content('@gob_bluth') + page.within('.results') do + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + end end end context 'when on the project page' do - it 'finds the user belonging to the project' do - project = create(:project) + let(:project) { create(:project) } - user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + before do create(:project_member, :developer, user: user1, project: project) - - user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') create(:project_member, :developer, user: user2, project: project) + user3 + end - create(:user, username: 'gob_2018', name: 'George Oscar Bluth') - - sign_in(user1) - - visit projects_path(project) + it 'finds the user belonging to the project' do + visit project_path(project) - fill_in 'search', with: 'gob' - click_button 'Go' + submit_search('gob') + select_search_scope('Users') - expect(page).to have_content('Gob Bluth') - expect(page).to have_content('@gob_bluth') + page.within('.results') do + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') - expect(page).not_to have_content('Michael Bluth') - expect(page).not_to have_content('@michael_bluth') + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') - expect(page).not_to have_content('George Oscar Bluth') - expect(page).not_to have_content('@gob_2018') + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end end end context 'when on the group page' do - it 'finds the user belonging to the group' do - group = create(:group) + let(:group) { create(:group) } - user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + before do create(:group_member, :developer, user: user1, group: group) - - user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') create(:group_member, :developer, user: user2, group: group) + user3 + end - create(:user, username: 'gob_2018', name: 'George Oscar Bluth') - - sign_in(user1) - + it 'finds the user belonging to the group' do visit group_path(group) - fill_in 'search', with: 'gob' - click_button 'Go' + submit_search('gob') + select_search_scope('Users') - expect(page).to have_content('Gob Bluth') - expect(page).to have_content('@gob_bluth') + page.within('.results') do + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') - expect(page).not_to have_content('Michael Bluth') - expect(page).not_to have_content('@michael_bluth') + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') - expect(page).not_to have_content('George Oscar Bluth') - expect(page).not_to have_content('@gob_2018') + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end end end end diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 0a5abfbf46a..1ae37447bdc 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -26,13 +26,10 @@ describe 'User searches for wiki pages', :js do fill_in('dashboard_search', with: search_term) find('.btn-search').click - - page.within('.search-filter') do - click_link('Wiki') - end + select_search_scope('Wiki') page.within('.results') do - expect(find(:css, '.search-results')).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug)) + expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug)) end end end diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 5006631cc14..7e7c09e4a13 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -19,8 +19,7 @@ describe 'User uses header search field', :js do end it 'starts searching by pressing the enter key' do - fill_in('search', with: 'gitlab') - find('#search').native.send_keys(:enter) + submit_search('gitlab') page.within('.page-title') do expect(page).to have_content('Search') @@ -101,8 +100,7 @@ describe 'User uses header search field', :js do before do create(:issue, project: project, title: 'project issue') - fill_in('search', with: 'project') - find('#search').send_keys(:enter) + submit_search('project') end it 'displays result counts for all categories' do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 42c747c674f..d089fa718d2 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -7,6 +7,10 @@ describe "Internal Project Access" do set(:project) { create(:project, :internal, :repository) } + before do + stub_feature_flags(job_log_json: false) + end + describe "Project should be internal" do describe '#internal?' do subject { project.internal? } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index a86d240b7d6..b868cd595cb 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -7,6 +7,10 @@ describe "Private Project Access" do set(:project) { create(:project, :private, :repository, public_builds: false) } + before do + stub_feature_flags(job_log_json: false) + end + describe "Project should be private" do describe '#private?' do subject { project.private? } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 8d7f8c84358..8db2f2d69e5 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -7,6 +7,10 @@ describe "Public Project Access" do set(:project) { create(:project, :public, :repository) } + before do + stub_feature_flags(job_log_json: false) + end + describe "Project should be public" do describe '#public?' do subject { project.public? } diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb index 4a8c5f9b1fe..bbdf544bd0c 100644 --- a/spec/features/snippets/search_snippets_spec.rb +++ b/spec/features/snippets/search_snippets_spec.rb @@ -10,12 +10,8 @@ describe 'Search Snippets' do sign_in private_snippet.author visit dashboard_snippets_path - page.within '.search' do - fill_in 'search', with: 'Middle' - click_button 'Go' - end - - click_link 'Titles and Filenames' + submit_search('Middle') + select_search_scope('Titles and Filenames') expect(page).to have_link(public_snippet.title) expect(page).to have_link(private_snippet.title) @@ -45,11 +41,7 @@ describe 'Search Snippets' do sign_in create(:user) visit dashboard_snippets_path - - page.within '.search' do - fill_in 'search', with: 'line seven' - click_button 'Go' - end + submit_search('line seven') expect(page).to have_content('line seven') diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js index 9f127ccb690..41da4125a20 100644 --- a/spec/frontend/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -371,7 +371,7 @@ describe('Application Row', () => { it('contains a link to the chart repo if application has been updated', () => { const version = '0.1.45'; - const chartRepo = 'https://gitlab.com/charts/gitlab-runner'; + const chartRepo = 'https://gitlab.com/gitlab-org/charts/gitlab-runner'; vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.INSTALLED, diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index f2cc413512d..c168bce7a4e 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -86,7 +86,7 @@ describe('Clusters Store', () => { requestReason: null, version: mockResponseData.applications[2].version, updateAvailable: mockResponseData.applications[2].update_available, - chartRepo: 'https://gitlab.com/charts/gitlab-runner', + chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner', installed: false, installFailed: false, updateFailed: false, diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js index ff833d2c899..02fd30d5a15 100644 --- a/spec/frontend/notes/components/note_app_spec.js +++ b/spec/frontend/notes/components/note_app_spec.js @@ -133,32 +133,31 @@ describe('note_app', () => { ); }); - it('should not render form when commenting is disabled', () => { - wrapper.destroy(); + it('should render form comment button as disabled', () => { + expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); + }); - store.state.commentsDisabled = true; - wrapper = mountComponent(); - return waitForDiscussionsRequest().then(() => { - expect(wrapper.find('.js-main-target-form').exists()).toBe(false); - }); + it('updates discussions badge', () => { + expect(document.querySelector('.js-discussions-count').textContent).toEqual('2'); }); + }); - it('should render discussion filter note `commentsDisabled` is true', () => { - wrapper.destroy(); + describe('render with comments disabled', () => { + beforeEach(() => { + setFixtures('<div class="js-discussions-count"></div>'); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); store.state.commentsDisabled = true; wrapper = mountComponent(); - return waitForDiscussionsRequest().then(() => { - expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true); - }); + return waitForDiscussionsRequest(); }); - it('should render form comment button as disabled', () => { - expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); + it('should not render form when commenting is disabled', () => { + expect(wrapper.find('.js-main-target-form').exists()).toBe(false); }); - it('updates discussions badge', () => { - expect(document.querySelector('.js-discussions-count').textContent).toEqual('2'); + it('should render discussion filter note `commentsDisabled` is true', () => { + expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true); }); }); diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js new file mode 100644 index 00000000000..8102033139f --- /dev/null +++ b/spec/frontend/project_find_file_spec.js @@ -0,0 +1,77 @@ +import MockAdapter from 'axios-mock-adapter'; +import $ from 'jquery'; +import ProjectFindFile from '~/project_find_file'; +import axios from '~/lib/utils/axios_utils'; +import { TEST_HOST } from 'helpers/test_constants'; + +const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`; +const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`; +const FIND_TREE_URL = `${TEST_HOST}/namespace/project/tree/master`; +const TEMPLATE = `<div class="file-finder-holder tree-holder js-file-finder" data-blob-url-template="${BLOB_URL_TEMPLATE}" data-file-find-url="${FILE_FIND_URL}" data-find-tree-url="${FIND_TREE_URL}"> + <input class="file-finder-input" id="file_find" /> + <div class="tree-content-holder"> + <div class="table-holder"> + <table class="files-slider tree-table"> + <tbody /> + </table> + </div> + </div> +</div>`; + +describe('ProjectFindFile', () => { + let element; + let mock; + + const getProjectFindFileInstance = () => + new ProjectFindFile(element, { + url: FILE_FIND_URL, + treeUrl: FIND_TREE_URL, + blobUrlTemplate: BLOB_URL_TEMPLATE, + }); + + const findFiles = () => + element + .find('.tree-table tr') + .toArray() + .map(el => ({ + text: el.textContent, + href: el.querySelector('a').href, + })); + + beforeEach(() => { + // Create a mock adapter for stubbing axios API requests + mock = new MockAdapter(axios); + + element = $(TEMPLATE); + }); + + afterEach(() => { + // Reset the mock adapter + mock.restore(); + }); + + it('loads and renders elements from remote server', done => { + const files = [ + 'fileA.txt', + 'fileB.txt', + 'fi#leC.txt', + 'folderA/fileD.txt', + 'folder#B/fileE.txt', + 'folde?rC/fil#F.txt', + ]; + mock.onGet(FILE_FIND_URL).replyOnce(200, files); + + getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor + + setImmediate(() => { + expect(findFiles()).toEqual( + files.map(text => ({ + text, + href: `${BLOB_URL_TEMPLATE}/${encodeURIComponent(text)}`, + })), + ); + + done(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js index 806602877ef..d0586f9e63f 100644 --- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -106,12 +106,10 @@ describe('Changed file icon', () => { expect(findIcon().props('size')).toBe(size); }); - // NOTE: It looks like 'showStagedIcon' behavior is backwards to what the name suggests - // https://gitlab.com/gitlab-org/gitlab-ce/issues/66071 it.each` showStagedIcon | iconName | desc - ${false} | ${'file-modified-solid'} | ${'with showStagedIcon false, renders staged icon'} - ${true} | ${'file-modified'} | ${'with showStagedIcon true, renders regular icon'} + ${true} | ${'file-modified-solid'} | ${'with showStagedIcon true, renders staged icon'} + ${false} | ${'file-modified'} | ${'with showStagedIcon false, renders regular icon'} `('$desc', ({ showStagedIcon, iconName }) => { factory({ file: stagedFile(), diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 94998d302f9..6fbb6147d84 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -324,5 +324,47 @@ describe AvatarsHelper do ) end end + + context 'with only_path parameter set to false' do + let(:user_with_avatar) { create(:user, :with_avatar, username: 'foobar') } + + context 'with user parameter' do + let(:options) { { user: user_with_avatar, only_path: false } } + + it 'will return avatar with a full path' do + is_expected.to eq tag( + :img, + alt: "#{user_with_avatar.name}'s avatar", + src: avatar_icon_for_user(user_with_avatar, 16, only_path: false), + data: { container: 'body' }, + class: "avatar s16 has-tooltip", + title: user_with_avatar.name + ) + end + end + + context 'with user_name and user_email' do + let(:options) { { user_email: user_with_avatar.email, user_name: user_with_avatar.username, only_path: false } } + + it 'will return avatar with a full path' do + is_expected.to eq tag( + :img, + alt: "#{user_with_avatar.username}'s avatar", + src: avatar_icon_for_email(user_with_avatar.email, 16, only_path: false), + data: { container: 'body' }, + class: "avatar s16 has-tooltip", + title: user_with_avatar.username + ) + end + end + end + + context 'with unregistered email address' do + let(:options) { { user_email: "unregistered_email@example.com" } } + + it 'will return default alt text for avatar' do + expect(subject).to include("default avatar") + end + end end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index d25f0c6de4a..a14ae2cde4b 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -6,30 +6,62 @@ describe EmailsHelper do let(:merge_request) { create(:merge_request) } let(:merge_request_presenter) { merge_request.present } - context "and format is text" do - it "returns plain text" do - expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + context 'when user can read merge request' do + let(:user) { create(:user) } + + before do + merge_request.project.add_developer(user) + self.instance_variable_set(:@recipient, user) + self.instance_variable_set(:@project, merge_request.project) + end + + context "and format is text" do + it "returns plain text" do + expect(helper.closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + end end - end - context "and format is HTML" do - it "returns HTML" do - expect(closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") + context "and format is HTML" do + it "returns HTML" do + expect(helper.closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") + end + end + + context "and format is unknown" do + it "returns plain text" do + expect(helper.closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + end end end - context "and format is unknown" do - it "returns plain text" do - expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + context 'when user cannot read merge request' do + it "does not have link to merge request" do + expect(helper.closure_reason_text(merge_request)).to be_empty end end end context 'when given a String' do + let(:user) { create(:user) } + let(:project) { create(:project) } let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" } - it "returns plain text" do - expect(closure_reason_text(closed_via)).to eq("via #{closed_via}") + context 'when user can read commits' do + before do + project.add_developer(user) + self.instance_variable_set(:@recipient, user) + self.instance_variable_set(:@project, project) + end + + it "returns plain text" do + expect(closure_reason_text(closed_via)).to eq("via #{closed_via}") + end + end + + context 'when user cannot read commits' do + it "returns plain text" do + expect(closure_reason_text(closed_via)).to be_empty + end end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 4f1cab38f34..1d57aaa0da5 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -278,4 +278,14 @@ describe LabelsHelper do it { is_expected.to eq('Subscribe at group level') } end end + + describe '#label_tooltip_title' do + let(:html) { '<img src="example.png">This is an image</img>' } + let(:label_with_html_content) { create(:label, title: 'test', description: html) } + + it 'removes HTML' do + tooltip = label_tooltip_title(label_with_html_content) + expect(tooltip).to eq('This is an image') + end + end end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index f6e1720e113..1757ec8fa4d 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -65,6 +65,9 @@ describe MarkupHelper do describe 'inside a group' do before do + # Ensure the generated reference links aren't redacted + group.add_maintainer(user) + helper.instance_variable_set(:@group, group) helper.instance_variable_set(:@project, nil) end @@ -78,6 +81,9 @@ describe MarkupHelper do let(:project_in_group) { create(:project, group: group) } before do + # Ensure the generated reference links aren't redacted + project_in_group.add_maintainer(user) + helper.instance_variable_set(:@group, group) helper.instance_variable_set(:@project, project_in_group) end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index a70bfc2adc7..d2a4ce6540d 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -503,7 +503,7 @@ describe ProjectsHelper do allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'ssh' } allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return('git@localhost:') - expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream git@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)') + expect(helper.push_to_create_project_command(user)).to eq("git push --set-upstream #{Gitlab.config.gitlab.user}@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)") end end @@ -549,6 +549,42 @@ describe ProjectsHelper do end end + describe '#git_user_email' do + context 'not logged-in' do + before do + allow(helper).to receive(:current_user).and_return(nil) + end + + it 'returns your@email.com' do + expect(helper.send(:git_user_email)).to eq('your@email.com') + end + end + + context 'user logged in' do + let(:user) { create(:user) } + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'user has no configured commit email' do + it 'returns the primary email' do + expect(helper.send(:git_user_email)).to eq(user.email) + end + end + + context 'user has a configured commit email' do + before do + confirmed_email = create(:email, :confirmed, user: user) + user.update(commit_email: confirmed_email) + end + + it 'returns the commit email' do + expect(helper.send(:git_user_email)).to eq(user.commit_email) + end + end + end + end + describe 'show_xcode_link' do let!(:project) { create(:project) } let(:mac_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36' } diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 2ab72679ee7..e1dc589236b 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -6,7 +6,7 @@ describe SearchHelper do str end - describe 'search_autocomplete_source' do + describe 'search_autocomplete_opts' do context "with no current user" do before do allow(self).to receive(:current_user).and_return(nil) @@ -99,6 +99,47 @@ describe SearchHelper do end end + describe 'search_entries_info' do + using RSpec::Parameterized::TableSyntax + + where(:scope, :label) do + 'commits' | 'commit' + 'issues' | 'issue' + 'merge_requests' | 'merge request' + 'milestones' | 'milestone' + 'projects' | 'project' + 'snippet_titles' | 'snippet' + 'users' | 'user' + + 'blobs' | 'result' + 'snippet_blobs' | 'result' + 'wiki_blobs' | 'result' + + 'notes' | 'comment' + end + + with_them do + it 'uses the correct singular label' do + collection = Kaminari.paginate_array([:foo]).page(1).per(10) + + expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for \"foo\"") + end + + it 'uses the correct plural label' do + collection = Kaminari.paginate_array([:foo] * 23).page(1).per(10) + + expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for \"foo\"") + end + end + + it 'raises an error for unrecognized scopes' do + expect do + collection = Kaminari.paginate_array([:foo]).page(1).per(10) + search_entries_info(collection, 'unknown', 'foo') + end.to raise_error(RuntimeError) + end + end + describe 'search_filter_input_options' do context 'project' do before do diff --git a/spec/initializers/asset_proxy_setting_spec.rb b/spec/initializers/asset_proxy_setting_spec.rb new file mode 100644 index 00000000000..42e4d4aa594 --- /dev/null +++ b/spec/initializers/asset_proxy_setting_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe 'Asset proxy settings initialization' do + describe '#asset_proxy' do + it 'defaults to disabled' do + expect(Banzai::Filter::AssetProxyFilter).to receive(:initialize_settings) + + require_relative '../../config/initializers/asset_proxy_settings' + + expect(Gitlab.config.asset_proxy.enabled).to be_falsey + end + end +end diff --git a/spec/initializers/rest-client-hostname_override_spec.rb b/spec/initializers/rest-client-hostname_override_spec.rb new file mode 100644 index 00000000000..3707e001d41 --- /dev/null +++ b/spec/initializers/rest-client-hostname_override_spec.rb @@ -0,0 +1,147 @@ +require 'spec_helper' + +describe 'rest-client dns rebinding protection' do + include StubRequests + + context 'when local requests are not allowed' do + it 'allows an external request with http' do + request_stub = stub_full_request('http://example.com', ip_address: '93.184.216.34') + + RestClient.get('http://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows an external request with https' do + request_stub = stub_full_request('https://example.com', ip_address: '93.184.216.34') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'raises error when it is a request that resolves to a local address' do + stub_full_request('https://example.com', ip_address: '172.16.0.0') + + expect { RestClient.get('https://example.com') } + .to raise_error(ArgumentError, + "URL 'https://example.com' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request that resolves to a localhost address' do + stub_full_request('https://example.com', ip_address: '127.0.0.1') + + expect { RestClient.get('https://example.com') } + .to raise_error(ArgumentError, + "URL 'https://example.com' is blocked: Requests to localhost are not allowed") + end + + it 'raises error when it is a request to local address' do + expect { RestClient.get('http://172.16.0.0') } + .to raise_error(ArgumentError, + "URL 'http://172.16.0.0' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request to localhost address' do + expect { RestClient.get('http://127.0.0.1') } + .to raise_error(ArgumentError, + "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed") + end + end + + context 'when port different from URL scheme is used' do + it 'allows the request' do + request_stub = stub_full_request('https://example.com:8080', ip_address: '93.184.216.34') + + RestClient.get('https://example.com:8080/') + + expect(request_stub).to have_been_requested + end + + it 'raises error when it is a request to local address' do + expect { RestClient.get('https://172.16.0.0:8080') } + .to raise_error(ArgumentError, + "URL 'https://172.16.0.0:8080' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request to localhost address' do + expect { RestClient.get('https://127.0.0.1:8080') } + .to raise_error(ArgumentError, + "URL 'https://127.0.0.1:8080' is blocked: Requests to localhost are not allowed") + end + end + + context 'when DNS rebinding protection is disabled' do + before do + stub_application_setting(dns_rebinding_protection_enabled: false) + end + + it 'allows the request' do + request_stub = stub_request(:get, 'https://example.com') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + end + + context 'when http(s) proxy environment variable is set' do + before do + stub_env('https_proxy' => 'https://my.proxy') + end + + it 'allows the request' do + request_stub = stub_request(:get, 'https://example.com') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + it 'allows an external request' do + request_stub = stub_full_request('https://example.com', ip_address: '93.184.216.34') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows an external request that resolves to a local address' do + request_stub = stub_full_request('https://example.com', ip_address: '172.16.0.0') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows an external request that resolves to a localhost address' do + request_stub = stub_full_request('https://example.com', ip_address: '127.0.0.1') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows a local address request' do + request_stub = stub_request(:get, 'http://172.16.0.0') + + RestClient.get('http://172.16.0.0') + + expect(request_stub).to have_been_requested + end + + it 'allows a localhost address request' do + request_stub = stub_request(:get, 'http://127.0.0.1') + + RestClient.get('http://127.0.0.1') + + expect(request_stub).to have_been_requested + end + end +end diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js index d92c54ea83f..2ab74ae4e10 100644 --- a/spec/javascripts/issue_show/components/edit_actions_spec.js +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -104,7 +104,7 @@ describe('Edit Actions components', () => { spyOn(window, 'confirm').and.returnValue(true); vm.$el.querySelector('.btn-danger').click(); - expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable'); + expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true }); }); it('shows loading icon after clicking delete button', done => { diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js index f761a18e326..fdf23f3f69d 100644 --- a/spec/javascripts/releases/components/release_block_spec.js +++ b/spec/javascripts/releases/components/release_block_spec.js @@ -88,6 +88,10 @@ describe('Release block', () => { vm.$destroy(); }); + it("renders the block with an id equal to the release's tag name", () => { + expect(vm.$el.id).toBe('18.04'); + }); + it('renders release name', () => { expect(vm.$el.textContent).toContain(release.name); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 53e1f077610..2bb2319cc60 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -6,7 +6,7 @@ import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { MWPS_MERGE_STRATEGY, ATMTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; +import { MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; const commitMessage = 'This is the commit message'; const squashCommitMessage = 'This is the squash commit message'; @@ -164,7 +164,7 @@ describe('ReadyToMerge', () => { }); it('returns info class for pending status', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); + Vue.set(vm.mr, 'availableAutoMergeStrategies', [MTWPS_MERGE_STRATEGY]); expect(vm.mergeButtonClass).toEqual(inActionClass); }); diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb new file mode 100644 index 00000000000..b7f45421b2a --- /dev/null +++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +describe Banzai::Filter::AssetProxyFilter do + include FilterSpecHelper + + def image(path) + %(<img src="#{path}" />) + end + + it 'does not replace if disabled' do + stub_asset_proxy_setting(enabled: false) + + context = described_class.transform_context({}) + src = 'http://example.com/test.png' + doc = filter(image(src), context) + + expect(doc.at_css('img')['src']).to eq src + end + + context 'during initialization' do + after do + Gitlab.config.asset_proxy['enabled'] = false + end + + it '#initialize_settings' do + stub_application_setting(asset_proxy_enabled: true) + stub_application_setting(asset_proxy_secret_key: 'shared-secret') + stub_application_setting(asset_proxy_url: 'https://assets.example.com') + stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com)) + + described_class.initialize_settings + + expect(Gitlab.config.asset_proxy.enabled).to be_truthy + expect(Gitlab.config.asset_proxy.secret_key).to eq 'shared-secret' + expect(Gitlab.config.asset_proxy.url).to eq 'https://assets.example.com' + expect(Gitlab.config.asset_proxy.whitelist).to eq %w(gitlab.com *.mydomain.com) + expect(Gitlab.config.asset_proxy.domain_regexp).to eq /^(gitlab\.com|.*?\.mydomain\.com)$/i + end + end + + context 'when properly configured' do + before do + stub_asset_proxy_setting(enabled: true) + stub_asset_proxy_setting(secret_key: 'shared-secret') + stub_asset_proxy_setting(url: 'https://assets.example.com') + stub_asset_proxy_setting(whitelist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host})) + stub_asset_proxy_setting(domain_regexp: described_class.compile_whitelist(Gitlab.config.asset_proxy.whitelist)) + @context = described_class.transform_context({}) + end + + it 'replaces img src' do + src = 'http://example.com/test.png' + new_src = 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67' + doc = filter(image(src), @context) + + expect(doc.at_css('img')['src']).to eq new_src + expect(doc.at_css('img')['data-canonical-src']).to eq src + end + + it 'skips internal images' do + src = "#{Gitlab.config.gitlab.url}/test.png" + doc = filter(image(src), @context) + + expect(doc.at_css('img')['src']).to eq src + end + + it 'skip relative urls' do + src = "/test.png" + doc = filter(image(src), @context) + + expect(doc.at_css('img')['src']).to eq src + end + + it 'skips single domain' do + src = "http://gitlab.com/test.png" + doc = filter(image(src), @context) + + expect(doc.at_css('img')['src']).to eq src + end + + it 'skips single domain and ignores url in query string' do + src = "http://gitlab.com/test.png?url=http://example.com/test.png" + doc = filter(image(src), @context) + + expect(doc.at_css('img')['src']).to eq src + end + + it 'skips wildcarded domain' do + src = "http://images.mydomain.com/test.png" + doc = filter(image(src), @context) + + expect(doc.at_css('img')['src']).to eq src + end + end +end diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb index bcb74be1034..192d00805e0 100644 --- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb @@ -189,5 +189,26 @@ describe Banzai::Filter::CommitTrailersFilter do expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) expect(doc.text).to include(commit_body) end + + context 'with Gitlab-hosted avatars in commit trailers' do + # Because commit trailers are contained within markdown, + # any path-only link will automatically be prefixed + # with the path of its repository. + # See: "build_relative_path" in "lib/banzai/filter/relative_link_filter.rb" + let(:user_with_avatar) { create(:user, :with_avatar, username: 'foobar') } + + it 'returns a full path for avatar urls' do + _, message_html = build_commit_message( + trailer: trailer, + name: user_with_avatar.name, + email: user_with_avatar.email + ) + + doc = filter(message_html) + expected = "#{Gitlab.config.gitlab.url}#{user_with_avatar.avatar_url}" + + expect(doc.css('img')[0].attr('src')).to start_with expected + end + end end end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 59fea5766ee..4b2500b31f7 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -156,6 +156,18 @@ describe Banzai::Filter::ExternalLinkFilter do expect(doc_email.to_html).to include('http://xn--example-6p25f.com/</a>') end end + + context 'autolinked image' do + let(:html) { %q(<a href="https://assets.example.com/6d8b/634c" data-canonical-src="http://exa%F0%9F%98%84mple.com/test.png"><img src="http://exa%F0%9F%98%84mple.com/test.png" data-canonical-src="http://exa%F0%9F%98%84mple.com/test.png"></a>) } + let(:doc) { filter(html) } + + it_behaves_like 'an external link with rel attribute' + + it 'adds a toolip with punycode' do + expect(doc.to_html).to include('class="has-tooltip"') + expect(doc.to_html).to include('title="http://xn--example-6p25f.com/test.png"') + end + end end context 'for links that look malicious' do diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index 7b0cb675551..011e3a1e2da 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -28,4 +28,11 @@ describe Banzai::Filter::ImageLinkFilter do doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>)) expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$} end + + it 'keep the data-canonical-src' do + doc = filter(%q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="http://example.com/test.png" />)) + + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + expect(doc.at_css('img')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src'] + end end diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb index 9f6dcded56f..cb431df7551 100644 --- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -131,6 +131,14 @@ describe Banzai::Filter::IssuableStateFilter do expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (closed)") end + + it 'appends state to moved issue references' do + moved_issue = create(:issue, :closed, project: project, moved_to: create_issue(:opened)) + link = create_link(moved_issue.to_reference, issue: moved_issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)") + end end context 'for merge request references' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 213a5459118..35e99d2586e 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -10,6 +10,11 @@ describe Banzai::Filter::LabelReferenceFilter do let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } + it_behaves_like 'HTML text with references' do + let(:resource) { label } + let(:resource_text) { resource.title } + end + it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 3f021adc756..ab0c2c383c5 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -329,6 +329,10 @@ describe Banzai::Filter::MilestoneReferenceFilter do it_behaves_like 'cross-project / same-namespace complete reference' it_behaves_like 'cross project shorthand reference' it_behaves_like 'references with HTML entities' + it_behaves_like 'HTML text with references' do + let(:resource) { milestone } + let(:resource_text) { "#{resource.class.reference_prefix}#{resource.title}" } + end end shared_context 'group milestones' do @@ -340,6 +344,10 @@ describe Banzai::Filter::MilestoneReferenceFilter do it_behaves_like 'String-based multi-word references in quotes' it_behaves_like 'referencing a milestone in a link href' it_behaves_like 'references with HTML entities' + it_behaves_like 'HTML text with references' do + let(:resource) { milestone } + let(:resource_text) { "#{resource.class.reference_prefix}#{resource.title}" } + end it 'does not support references by IID' do doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}") diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb index 69f9c1ae829..927d226c400 100644 --- a/spec/lib/banzai/filter/project_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb @@ -26,10 +26,18 @@ describe Banzai::Filter::ProjectReferenceFilter do expect(reference_filter(act).to_html).to eq(CGI.escapeHTML(exp)) end - it 'fails fast for long invalid string' do - expect do - Timeout.timeout(5.seconds) { reference_filter("A" * 50000).to_html } - end.not_to raise_error + context 'when invalid reference strings are very long' do + shared_examples_for 'fails fast' do |ref_string| + it 'fails fast for long strings' do + # took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172824 + expect do + Timeout.timeout(3.seconds) { reference_filter(ref_string).to_html } + end.not_to raise_error + end + end + + it_behaves_like 'fails fast', 'A' * 50000 + it_behaves_like 'fails fast', '/a' * 50000 end it 'allows references with text after the > character' do diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index ecb83b6cb66..789530fbc56 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -7,6 +7,7 @@ describe Banzai::Filter::RelativeLinkFilter do contexts.reverse_merge!({ commit: commit, project: project, + current_user: user, group: group, project_wiki: project_wiki, ref: ref, @@ -33,7 +34,8 @@ describe Banzai::Filter::RelativeLinkFilter do %(<div>#{element}</div>) end - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :public) } + let(:user) { create(:user) } let(:group) { nil } let(:project_path) { project.full_path } let(:ref) { 'markdown' } @@ -75,6 +77,11 @@ describe Banzai::Filter::RelativeLinkFilter do include_examples :preserve_unchanged end + context 'without project repository access' do + let(:project) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) } + include_examples :preserve_unchanged + end + it 'does not raise an exception on invalid URIs' do act = link("://foo") expect { filter(act) }.not_to raise_error @@ -282,6 +289,37 @@ describe Banzai::Filter::RelativeLinkFilter do let(:relative_path) { "/#{project.full_path}#{upload_path}" } context 'to a project upload' do + context 'without project repository access' do + let(:project) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) } + + it 'does not rebuild relative URL for a link' do + doc = filter(link(upload_path)) + expect(doc.at_css('a')['href']).to eq(upload_path) + + doc = filter(nested(link(upload_path))) + expect(doc.at_css('a')['href']).to eq(upload_path) + end + + it 'does not rebuild relative URL for an image' do + doc = filter(image(upload_path)) + expect(doc.at_css('img')['src']).to eq(upload_path) + + doc = filter(nested(image(upload_path))) + expect(doc.at_css('img')['src']).to eq(upload_path) + end + + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'does not rewrite the link' do + doc = filter(link(upload_path)) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + end + end + context 'with an absolute URL' do let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } let(:only_path) { false } @@ -331,11 +369,41 @@ describe Banzai::Filter::RelativeLinkFilter do end context 'to a group upload' do - let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } + let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' } + let(:upload_link) { link(upload_path) } let(:group) { create(:group) } let(:project) { nil } let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } + context 'without group read access' do + let(:group) { create(:group, :private) } + + it 'does not rewrite the link' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + + it 'does not rewrite the link for subgroup' do + group.update!(parent: create(:group)) + + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'does not rewrite the link' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + end + end + context 'with an absolute URL' do let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } let(:only_path) { false } diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb index 483e806624c..cd932f502f3 100644 --- a/spec/lib/banzai/filter/video_link_filter_spec.rb +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -49,4 +49,26 @@ describe Banzai::Filter::VideoLinkFilter do expect(element['src']).to eq '/path/my_image.jpg' end end + + context 'when asset proxy is enabled' do + it 'uses the correct src' do + stub_asset_proxy_setting(enabled: true) + + proxy_src = 'https://assets.example.com/6d8b63' + canonical_src = 'http://example.com/test.mp4' + image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}" />) + container = filter(image, asset_proxy_enabled: true).children.first + + expect(container['class']).to eq 'video-container' + + video, paragraph = container.children + + expect(video['src']).to eq proxy_src + expect(video['data-canonical-src']).to eq canonical_src + + link = paragraph.children.first + + expect(link['href']).to eq proxy_src + end + end end diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index 0a3e0962452..3a9ecd2fb81 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -142,4 +142,48 @@ describe Banzai::Pipeline::GfmPipeline do expect(output).to include(Gitlab::Routing.url_helpers.milestone_path(milestone)) end end + + describe 'asset proxy' do + let(:project) { create(:project, :public) } + let(:image) { '![proxy](http://example.com/test.png)' } + let(:proxy) { 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67' } + let(:version) { Gitlab::CurrentSettings.current_application_settings.local_markdown_version } + + before do + stub_asset_proxy_setting(enabled: true) + stub_asset_proxy_setting(secret_key: 'shared-secret') + stub_asset_proxy_setting(url: 'https://assets.example.com') + stub_asset_proxy_setting(whitelist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host})) + stub_asset_proxy_setting(domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist)) + end + + it 'replaces a lazy loaded img src' do + output = described_class.to_html(image, project: project) + doc = Nokogiri::HTML.fragment(output) + result = doc.css('img').first + + expect(result['data-src']).to eq(proxy) + end + + it 'autolinks images to the proxy' do + output = described_class.to_html(image, project: project) + doc = Nokogiri::HTML.fragment(output) + result = doc.css('a').first + + expect(result['href']).to eq(proxy) + expect(result['data-canonical-src']).to eq('http://example.com/test.png') + end + + it 'properly adds tooltips to link for IDN images' do + image = '![proxy](http://exa😄mple.com/test.png)' + proxy = 'https://assets.example.com/6d8b634c412a23c6bfe1b2963f174febf5635ddd/687474703a2f2f6578612546302539462539382538346d706c652e636f6d2f746573742e706e67' + output = described_class.to_html(image, project: project) + doc = Nokogiri::HTML.fragment(output) + result = doc.css('a').first + + expect(result['href']).to eq(proxy) + expect(result['data-canonical-src']).to eq('http://exa%F0%9F%98%84mple.com/test.png') + expect(result['title']).to eq 'http://xn--example-6p25f.com/test.png' + end + end end diff --git a/spec/lib/gitlab/anonymous_session_spec.rb b/spec/lib/gitlab/anonymous_session_spec.rb new file mode 100644 index 00000000000..628aae81ada --- /dev/null +++ b/spec/lib/gitlab/anonymous_session_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do + let(:default_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' } + let(:additional_session_id) { '7919a6f1bb119dd7396fadc38fd18d0d' } + + subject { new_anonymous_session } + + def new_anonymous_session(session_id = default_session_id) + described_class.new('127.0.0.1', session_id: session_id) + end + + describe '#store_session_id_per_ip' do + it 'adds session id to proper key' do + subject.store_session_id_per_ip + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:ip:gitlab:127.0.0.1")).to eq [default_session_id] + end + end + + it 'adds expiration time to key' do + Timecop.freeze do + subject.store_session_id_per_ip + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.ttl("session:lookup:ip:gitlab:127.0.0.1")).to eq(24.hours.to_i) + end + end + end + + it 'adds id only once' do + subject.store_session_id_per_ip + subject.store_session_id_per_ip + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:ip:gitlab:127.0.0.1")).to eq [default_session_id] + end + end + + context 'when there is already one session' do + it 'adds session id to proper key' do + subject.store_session_id_per_ip + new_anonymous_session(additional_session_id).store_session_id_per_ip + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:ip:gitlab:127.0.0.1")).to contain_exactly(default_session_id, additional_session_id) + end + end + end + end + + describe '#stored_sessions' do + it 'returns all anonymous sessions per ip' do + Gitlab::Redis::SharedState.with do |redis| + redis.sadd("session:lookup:ip:gitlab:127.0.0.1", default_session_id) + redis.sadd("session:lookup:ip:gitlab:127.0.0.1", additional_session_id) + end + + expect(subject.stored_sessions).to eq(2) + end + end + + it 'removes obsolete lookup through ip entries' do + Gitlab::Redis::SharedState.with do |redis| + redis.sadd("session:lookup:ip:gitlab:127.0.0.1", default_session_id) + redis.sadd("session:lookup:ip:gitlab:127.0.0.1", additional_session_id) + end + + subject.cleanup_session_per_ip_entries + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:ip:gitlab:127.0.0.1")).to eq [additional_session_id] + end + end +end diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb index 42bc509eeef..adf36cf1050 100644 --- a/spec/lib/gitlab/authorized_keys_spec.rb +++ b/spec/lib/gitlab/authorized_keys_spec.rb @@ -5,10 +5,81 @@ require 'spec_helper' describe Gitlab::AuthorizedKeys do let(:logger) { double('logger').as_null_object } - subject { described_class.new(logger) } + subject(:authorized_keys) { described_class.new(logger) } + + describe '#accessible?' do + subject { authorized_keys.accessible? } + + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + context 'can open file' do + it { is_expected.to be_truthy } + end + + context 'cannot open file' do + before do + allow(File).to receive(:open).and_raise(Errno::EACCES) + end + + it { is_expected.to be_falsey } + end + end + + context 'authorized_keys file does not exist' do + it { is_expected.to be_falsey } + end + end + + describe '#create' do + subject { authorized_keys.create } + + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it { is_expected.to be_truthy } + end + + context 'authorized_keys file does not exist' do + after do + delete_authorized_keys_file + end + + it 'creates authorized_keys file' do + expect(subject).to be_truthy + expect(File.exist?(tmp_authorized_keys_path)).to be_truthy + end + end + + context 'cannot create file' do + before do + allow(File).to receive(:open).and_raise(Errno::EACCES) + end + + it { is_expected.to be_falsey } + end + end describe '#add_key' do + let(:id) { 'key-741' } + + subject { authorized_keys.add_key(id, key) } + context 'authorized_keys file exists' do + let(:key) { 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage' } + before do create_authorized_keys_fixture end @@ -21,19 +92,20 @@ describe Gitlab::AuthorizedKeys do auth_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" expect(logger).to receive(:info).with('Adding key (key-741): ssh-rsa AAAAB3NzaDAxx2E') - expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage')) - .to be_truthy + expect(subject).to be_truthy expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n") end end context 'authorized_keys file does not exist' do + let(:key) { 'ssh-rsa AAAAB3NzaDAxx2E' } + before do delete_authorized_keys_file end it 'creates the file' do - expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to be_truthy + expect(subject).to be_truthy expect(File.exist?(tmp_authorized_keys_path)).to be_truthy end end @@ -47,6 +119,8 @@ describe Gitlab::AuthorizedKeys do ] end + subject { authorized_keys.batch_add_keys(keys) } + context 'authorized_keys file exists' do before do create_authorized_keys_fixture @@ -62,7 +136,7 @@ describe Gitlab::AuthorizedKeys do expect(logger).to receive(:info).with('Adding key (key-12): ssh-dsa ASDFASGADG') expect(logger).to receive(:info).with('Adding key (key-123): ssh-rsa GFDGDFSGSDFG') - expect(subject.batch_add_keys(keys)).to be_truthy + expect(subject).to be_truthy expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n") end @@ -70,7 +144,7 @@ describe Gitlab::AuthorizedKeys do let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } it "doesn't add keys" do - expect(subject.batch_add_keys(keys)).to be_falsey + expect(subject).to be_falsey expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n") end end @@ -82,16 +156,28 @@ describe Gitlab::AuthorizedKeys do end it 'creates the file' do - expect(subject.batch_add_keys(keys)).to be_truthy + expect(subject).to be_truthy expect(File.exist?(tmp_authorized_keys_path)).to be_truthy end end end describe '#rm_key' do + let(:key) { 'key-741' } + + subject { authorized_keys.rm_key(key) } + context 'authorized_keys file exists' do + let(:other_line) { "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" } + let(:delete_line) { "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" } + before do create_authorized_keys_fixture + + File.open(tmp_authorized_keys_path, 'a') do |auth_file| + auth_file.puts delete_line + auth_file.puts other_line + end end after do @@ -99,16 +185,10 @@ describe Gitlab::AuthorizedKeys do end it "removes the right line" do - other_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" - delete_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" erased_line = delete_line.gsub(/./, '#') - File.open(tmp_authorized_keys_path, 'a') do |auth_file| - auth_file.puts delete_line - auth_file.puts other_line - end expect(logger).to receive(:info).with('Removing key (key-741)') - expect(subject.rm_key('key-741')).to be_truthy + expect(subject).to be_truthy expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") end end @@ -118,13 +198,13 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it 'returns false' do - expect(subject.rm_key('key-741')).to be_falsey - end + it { is_expected.to be_falsey } end end describe '#clear' do + subject { authorized_keys.clear } + context 'authorized_keys file exists' do before do create_authorized_keys_fixture @@ -134,9 +214,7 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it "returns true" do - expect(subject.clear).to be_truthy - end + it { is_expected.to be_truthy } end context 'authorized_keys file does not exist' do @@ -144,13 +222,13 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it "still returns true" do - expect(subject.clear).to be_truthy - end + it { is_expected.to be_truthy } end end describe '#list_key_ids' do + subject { authorized_keys.list_key_ids } + context 'authorized_keys file exists' do before do create_authorized_keys_fixture( @@ -163,9 +241,7 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it 'returns array of key IDs' do - expect(subject.list_key_ids).to eq([1, 2, 3, 9000]) - end + it { is_expected.to eq([1, 2, 3, 9000]) } end context 'authorized_keys file does not exist' do @@ -173,9 +249,7 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it 'returns an empty array' do - expect(subject.list_key_ids).to be_empty - end + it { is_expected.to be_empty } end end diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb index 775550f2acc..c7a5ac783b3 100644 --- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -87,7 +87,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do .with(cluster, environment: deployment.environment) .and_return(namespace_builder) - expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService) + expect(Clusters::Kubernetes::CreateOrUpdateNamespaceService) .to receive(:new) .with(cluster: cluster, kubernetes_namespace: kubernetes_namespace) .and_return(service) @@ -107,7 +107,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do it 'creates a namespace using the tokenless record' do expect(Clusters::BuildKubernetesNamespaceService).not_to receive(:new) - expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService) + expect(Clusters::Kubernetes::CreateOrUpdateNamespaceService) .to receive(:new) .with(cluster: cluster, kubernetes_namespace: kubernetes_namespace) .and_return(service) @@ -123,7 +123,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do end it 'does not create a namespace' do - expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new) + expect(Clusters::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new) subject end diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index dd536a241bd..af995f4869a 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -41,6 +41,12 @@ describe Gitlab::Ci::Config::External::File::Base do end describe '#valid?' do + context 'when location is not a string' do + let(:location) { %w(some/file.txt other/file.txt) } + + it { is_expected.not_to be_valid } + end + context 'when location is not a YAML file' do let(:location) { 'some/file.txt' } diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb index b25f0f6b887..b3dedfe1f77 100644 --- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb +++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb @@ -130,7 +130,7 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do end it 'returns error when saving project ID fails' do - allow(application_setting).to receive(:update) { false } + allow(application_setting).to receive(:save) { false } expect { result }.to raise_error(StandardError, 'Could not save project ID') end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index d6e1fbaa979..0aef4887c75 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -396,6 +396,27 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(project.lfs_enabled).to be_falsey end + + it 'overrides project feature access levels' do + access_level_keys = project.project_feature.attributes.keys.select { |a| a =~ /_access_level/ } + + # `pages_access_level` is not included, since it is not available in the public API + # and has a dependency on project's visibility level + # see ProjectFeature model + access_level_keys.delete('pages_access_level') + + disabled_access_levels = Hash[access_level_keys.collect { |item| [item, 'disabled'] }] + + project.create_import_data(data: { override_params: disabled_access_levels }) + + restored_project_json + + aggregate_failures do + access_level_keys.each do |key| + expect(project.public_send(key)).to eq(ProjectFeature::DISABLED) + end + end + end end context 'with a project that has a group' do diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index f49d4e23e39..e5d688aa391 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::Kubernetes::KubeClient do + include StubRequests include KubernetesHelpers let(:api_url) { 'https://kubernetes.example.com/prefix' } @@ -14,6 +15,17 @@ describe Gitlab::Kubernetes::KubeClient do stub_kubeclient_discover(api_url) end + def method_call(client, method_name) + case method_name + when /\A(get_|delete_)/ + client.public_send(method_name) + when /\A(create_|update_)/ + client.public_send(method_name, {}) + else + raise "Unknown method name #{method_name}" + end + end + shared_examples 'a Kubeclient' do it 'is a Kubeclient::Client' do is_expected.to be_an_instance_of Kubeclient::Client @@ -25,28 +37,30 @@ describe Gitlab::Kubernetes::KubeClient do end shared_examples 'redirection not allowed' do |method_name| - before do - redirect_url = 'https://not-under-our-control.example.com/api/v1/pods' + context 'api_url is redirected' do + before do + redirect_url = 'https://not-under-our-control.example.com/api/v1/pods' - stub_request(:get, %r{\A#{api_url}/}) - .to_return(status: 302, headers: { location: redirect_url }) + stub_request(:get, %r{\A#{api_url}/}) + .to_return(status: 302, headers: { location: redirect_url }) - stub_request(:get, redirect_url) - .to_return(status: 200, body: '{}') - end + stub_request(:get, redirect_url) + .to_return(status: 200, body: '{}') + end - it 'does not follow redirects' do - method_call = -> do - case method_name - when /\A(get_|delete_)/ - client.public_send(method_name) - when /\A(create_|update_)/ - client.public_send(method_name, {}) - else - raise "Unknown method name #{method_name}" - end + it 'does not follow redirects' do + expect { method_call(client, method_name) }.to raise_error(Kubeclient::HttpError) end - expect { method_call.call }.to raise_error(Kubeclient::HttpError) + end + end + + shared_examples 'dns rebinding not allowed' do |method_name| + it 'does not allow DNS rebinding' do + stub_dns(api_url, ip_address: '8.8.8.8') + client + + stub_dns(api_url, ip_address: '192.168.2.120') + expect { method_call(client, method_name) }.to raise_error(ArgumentError, /is blocked/) end end @@ -160,6 +174,7 @@ describe Gitlab::Kubernetes::KubeClient do ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method + include_examples 'dns rebinding not allowed', method it 'delegates to the core client' do expect(client).to delegate_method(method).to(:core_client) @@ -185,6 +200,7 @@ describe Gitlab::Kubernetes::KubeClient do ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method + include_examples 'dns rebinding not allowed', method it 'delegates to the rbac client' do expect(client).to delegate_method(method).to(:rbac_client) @@ -203,6 +219,7 @@ describe Gitlab::Kubernetes::KubeClient do describe '#get_deployments' do include_examples 'redirection not allowed', 'get_deployments' + include_examples 'dns rebinding not allowed', 'get_deployments' it 'delegates to the extensions client' do expect(client).to delegate_method(:get_deployments).to(:extensions_client) diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index f52095bf633..16595102375 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -202,7 +202,7 @@ describe Gitlab::Middleware::Go do def expect_response_with_path(response, protocol, path) repository_url = case protocol when :ssh - "ssh://git@#{Gitlab.config.gitlab.host}/#{path}.git" + "ssh://#{Gitlab.config.gitlab.user}@#{Gitlab.config.gitlab.host}/#{path}.git" when :http, nil "http://#{Gitlab.config.gitlab.host}/#{path}.git" end diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index fd1338b55a6..808eb865a21 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -6,7 +6,6 @@ describe Gitlab::RepositoryCacheAdapter do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:cache) { repository.send(:cache) } - let(:redis_set_cache) { repository.send(:redis_set_cache) } describe '#cache_method_output', :use_clean_rails_memory_store_caching do let(:fallback) { 10 } @@ -209,11 +208,9 @@ describe Gitlab::RepositoryCacheAdapter do describe '#expire_method_caches' do it 'expires the caches of the given methods' do expect(cache).to receive(:expire).with(:rendered_readme) - expect(cache).to receive(:expire).with(:branch_names) - expect(redis_set_cache).to receive(:expire).with(:rendered_readme) - expect(redis_set_cache).to receive(:expire).with(:branch_names) + expect(cache).to receive(:expire).with(:gitignore) - repository.expire_method_caches(%i(rendered_readme branch_names)) + repository.expire_method_caches(%i(rendered_readme gitignore)) end it 'does not expire caches for non-existent methods' do diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb deleted file mode 100644 index 87e51f801e5..00000000000 --- a/spec/lib/gitlab/repository_set_cache_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do - let(:project) { create(:project) } - let(:repository) { project.repository } - let(:namespace) { "#{repository.full_path}:#{project.id}" } - let(:cache) { described_class.new(repository) } - - describe '#cache_key' do - subject { cache.cache_key(:foo) } - - it 'includes the namespace' do - is_expected.to eq("foo:#{namespace}:set") - end - - context 'with a given namespace' do - let(:extra_namespace) { 'my:data' } - let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) } - - it 'includes the full namespace' do - is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set") - end - end - end - - describe '#expire' do - it 'expires the given key from the cache' do - cache.write(:foo, ['value']) - - expect(cache.read(:foo)).to contain_exactly('value') - expect(cache.expire(:foo)).to eq(1) - expect(cache.read(:foo)).to be_empty - end - end - - describe '#exist?' do - it 'checks whether the key exists' do - expect(cache.exist?(:foo)).to be(false) - - cache.write(:foo, ['value']) - - expect(cache.exist?(:foo)).to be(true) - end - end - - describe '#fetch' do - let(:blk) { -> { ['block value'] } } - - subject { cache.fetch(:foo, &blk) } - - it 'fetches the key from the cache when filled' do - cache.write(:foo, ['value']) - - is_expected.to contain_exactly('value') - end - - it 'writes the value of the provided block when empty' do - cache.expire(:foo) - - is_expected.to contain_exactly('block value') - expect(cache.read(:foo)).to contain_exactly('block value') - end - end - - describe '#include?' do - it 'checks inclusion in the Redis set' do - cache.write(:foo, ['value']) - - expect(cache.include?(:foo, 'value')).to be(true) - expect(cache.include?(:foo, 'bar')).to be(false) - end - end -end diff --git a/spec/lib/gitlab/sanitizers/exif_spec.rb b/spec/lib/gitlab/sanitizers/exif_spec.rb index 22c5f27dc6d..f882dbbdb5c 100644 --- a/spec/lib/gitlab/sanitizers/exif_spec.rb +++ b/spec/lib/gitlab/sanitizers/exif_spec.rb @@ -7,7 +7,9 @@ describe Gitlab::Sanitizers::Exif do describe '#batch_clean' do context 'with image uploads' do - let!(:uploads) { create_list(:upload, 3, :with_file, :issuable_upload) } + set(:upload1) { create(:upload, :with_file, :issuable_upload) } + set(:upload2) { create(:upload, :with_file, :personal_snippet_upload) } + set(:upload3) { create(:upload, :with_file, created_at: 3.days.ago) } it 'processes all uploads if range ID is not set' do expect(sanitizer).to receive(:clean).exactly(3).times @@ -18,7 +20,19 @@ describe Gitlab::Sanitizers::Exif do it 'processes only uploads in the selected range' do expect(sanitizer).to receive(:clean).once - sanitizer.batch_clean(start_id: uploads[1].id, stop_id: uploads[1].id) + sanitizer.batch_clean(start_id: upload1.id, stop_id: upload1.id) + end + + it 'processes only uploads for the selected uploader' do + expect(sanitizer).to receive(:clean).once + + sanitizer.batch_clean(uploader: 'PersonalFileUploader') + end + + it 'processes only uploads created since specified date' do + expect(sanitizer).to receive(:clean).exactly(2).times + + sanitizer.batch_clean(since: 2.days.ago) end it 'pauses if sleep_time is set' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 0ba16b93ee7..fe4853fd819 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -52,38 +52,14 @@ describe Gitlab::Shell do describe '#add_key' do context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with add-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'add-key', - 'key-123', - 'ssh-rsa foobar' - ]) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys) - .to receive(:add_key) - .with('key-123', 'ssh-rsa foobar') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') - gitlab_shell.add_key('key-123', 'ssh-rsa foobar') - end + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') end end @@ -92,24 +68,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') end end @@ -118,38 +80,14 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with add-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'add-key', - 'key-123', - 'ssh-rsa foobar' - ]) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys) - .to receive(:add_key) - .with('key-123', 'ssh-rsa foobar') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') - gitlab_shell.add_key('key-123', 'ssh-rsa foobar') - end + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') end end end @@ -158,50 +96,14 @@ describe Gitlab::Shell do let(:keys) { [double(shell_id: 'key-123', key: 'ssh-rsa foobar')] } context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - let(:io) { double } - - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - context 'valid keys' do - before do - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls gitlab-keys with batch-add-keys command' do - expect(IO) - .to receive(:popen) - .with("gitlab_shell_keys_path batch-add-keys", 'w') - .and_yield(io) - - expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") - expect(gitlab_shell.batch_add_keys(keys)).to be_truthy - end - end - - context 'invalid keys' do - let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } - - it 'catches failure and returns false' do - expect(gitlab_shell.batch_add_keys(keys)).to be_falsey - end - end - end + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) - expect(gitlab_authorized_keys) - .to receive(:batch_add_keys) - .with(keys) - - gitlab_shell.batch_add_keys(keys) - end + gitlab_shell.batch_add_keys(keys) end end @@ -210,24 +112,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - it 'does nothing' do - expect(IO).not_to receive(:popen) - - gitlab_shell.batch_add_keys(keys) - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.batch_add_keys(keys) - end + gitlab_shell.batch_add_keys(keys) end end @@ -236,72 +124,25 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - let(:io) { double } + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls gitlab-keys with batch-add-keys command' do - expect(IO) - .to receive(:popen) - .with("gitlab_shell_keys_path batch-add-keys", 'w') - .and_yield(io) + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) - expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") - - gitlab_shell.batch_add_keys(keys) - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - - expect(gitlab_authorized_keys) - .to receive(:batch_add_keys) - .with(keys) - - gitlab_shell.batch_add_keys(keys) - end + gitlab_shell.batch_add_keys(keys) end end end describe '#remove_key' do context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with rm-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'rm-key', - 'key-123' - ]) - - gitlab_shell.remove_key('key-123') - end - end + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - context 'authorized_keys_file not set' do - it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - - gitlab_shell.remove_key('key-123') - end + gitlab_shell.remove_key('key-123') end end @@ -310,24 +151,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.remove_key('key-123') - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) - - gitlab_shell.remove_key('key-123') - end + gitlab_shell.remove_key('key-123') end end @@ -336,64 +163,22 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with rm-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'rm-key', - 'key-123' - ]) - - gitlab_shell.remove_key('key-123') - end - end - - context 'authorized_keys_file not set' do - it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - gitlab_shell.remove_key('key-123') - end + gitlab_shell.remove_key('key-123') end end end describe '#remove_all_keys' do context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - it 'calls #gitlab_shell_fast_execute with clear command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([:gitlab_shell_keys_path, 'clear']) - - gitlab_shell.remove_all_keys - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#clear' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:clear) - - gitlab_shell.remove_all_keys - end + gitlab_shell.remove_all_keys end end @@ -402,24 +187,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.remove_all_keys - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) - - gitlab_shell.remove_all_keys - end + gitlab_shell.remove_all_keys end end @@ -428,163 +199,73 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with clear command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([:gitlab_shell_keys_path, 'clear']) + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - gitlab_shell.remove_all_keys - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#clear' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:clear) - - gitlab_shell.remove_all_keys - end + gitlab_shell.remove_all_keys end end end describe '#remove_keys_not_found_in_db' do context 'when keys are in the file that are not in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(gitlab_shell).to receive(:remove_key).with('key-9876') - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(gitlab_shell).to receive(:remove_key).with('key-9876') - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end context 'when keys there are duplicate keys in the file that are not in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end context 'when keys there are duplicate keys in the file that ARE in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) - end - - it 'does not remove the key' do - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) - end - - it 'does not remove the key' do - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end unless ENV['CI'] # Skip in CI, it takes 1 minute context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys not in the DB' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys not in the DB' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb index e430599bd94..ac97a5ebd15 100644 --- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::SidekiqMiddleware::Metrics do let(:running_jobs_metric) { double('running jobs metric') } before do - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything).and_return(completion_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :livesum).and_return(running_jobs_metric) diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb index b203a1ee79c..afa63c21584 100644 --- a/spec/lib/gitlab/slash_commands/application_help_spec.rb +++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb @@ -4,10 +4,11 @@ require 'spec_helper' describe Gitlab::SlashCommands::ApplicationHelp do let(:params) { { command: '/gitlab', text: 'help' } } + let(:project) { build(:project) } describe '#execute' do subject do - described_class.new(params).execute + described_class.new(project, params).execute end it 'displays the help section' do diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb index c4ea8cbf2b1..dc412c80e68 100644 --- a/spec/lib/gitlab/slash_commands/command_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::SlashCommands::Command do it 'displays the help message' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Unknown command') + expect(subject[:text]).to start_with('The specified command is not valid') expect(subject[:text]).to match('/gitlab issue show') end end @@ -37,7 +37,7 @@ describe Gitlab::SlashCommands::Command do it 'rejects the actions' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! This action is not allowed') + expect(subject[:text]).to start_with('You are not allowed') end end @@ -57,7 +57,7 @@ describe Gitlab::SlashCommands::Command do context 'and user can not create deployment' do it 'returns action' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! This action is not allowed') + expect(subject[:text]).to start_with('You are not allowed') end end diff --git a/spec/lib/gitlab/slash_commands/issue_close_spec.rb b/spec/lib/gitlab/slash_commands/issue_close_spec.rb new file mode 100644 index 00000000000..c0760ce0ba6 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/issue_close_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::IssueClose do + describe '#execute' do + let(:issue) { create(:issue, project: project) } + let(:project) { create(:project) } + let(:user) { issue.author } + let(:chat_name) { double(:chat_name, user: user) } + let(:regex_match) { described_class.match("issue close #{issue.iid}") } + + subject do + described_class.new(project, chat_name).execute(regex_match) + end + + context 'when the user does not have permission' do + let(:chat_name) { double(:chat_name, user: create(:user)) } + + it 'does not allow the user to close the issue' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") + expect(issue.reload).to be_open + end + end + + context 'the issue exists' do + let(:title) { subject[:attachments].first[:title] } + + it 'closes and returns the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(issue.reload).to be_closed + expect(title).to start_with(issue.title) + end + + context 'when its reference is given' do + let(:regex_match) { described_class.match("issue close #{issue.to_reference}") } + + it 'closes and returns the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(issue.reload).to be_closed + expect(title).to start_with(issue.title) + end + end + end + + context 'the issue does not exist' do + let(:regex_match) { described_class.match("issue close 2343242") } + + it "returns not found" do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") + end + end + + context 'when the issue is already closed' do + let(:issue) { create(:issue, :closed, project: project) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:ephemeral) + expect(issue.reload).to be_closed + expect(subject[:text]).to match("already closed") + end + end + end + + describe '.match' do + it 'matches the iid' do + match = described_class.match("issue close 123") + + expect(match[:iid]).to eq("123") + end + + it 'accepts a reference' do + match = described_class.match("issue close #{Issue.reference_prefix}123") + + expect(match[:iid]).to eq("123") + end + end +end diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb index 286fec892e6..f00039c634f 100644 --- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb @@ -4,12 +4,14 @@ require 'spec_helper' describe Gitlab::SlashCommands::Presenters::Access do describe '#access_denied' do - subject { described_class.new.access_denied } + let(:project) { build(:project) } + + subject { described_class.new.access_denied(project) } it { is_expected.to be_a(Hash) } it 'displays an error message' do - expect(subject[:text]).to match("is not allowed") + expect(subject[:text]).to match('are not allowed') expect(subject[:response_type]).to be(:ephemeral) end end diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb new file mode 100644 index 00000000000..adc13b4ee56 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Presenters::IssueClose do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end + + context 'confidential issue' do + let(:issue) { create(:issue, :confidential, project: project) } + + it 'shows an ephemeral response' do + expect(subject[:response_type]).to be(:ephemeral) + end + end +end diff --git a/spec/lib/gitlab/visibility_level_checker_spec.rb b/spec/lib/gitlab/visibility_level_checker_spec.rb new file mode 100644 index 00000000000..325ac3c6f31 --- /dev/null +++ b/spec/lib/gitlab/visibility_level_checker_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Gitlab::VisibilityLevelChecker do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:visibility_level_checker) { } + let(:override_params) { {} } + + subject { described_class.new(user, project, project_params: override_params) } + + describe '#level_restricted?' do + context 'when visibility level is allowed' do + it 'returns false with nil for visibility level' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end + + context 'when visibility level is restricted' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns true and visibility name' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + result = subject.level_restricted? + + expect(result.restricted?).to eq(true) + expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + context 'overridden visibility' do + let(:override_params) do + { + import_data: { + data: { + override_params: { + visibility: override_visibility + } + } + } + } + end + + context 'when restricted' do + let(:override_visibility) { 'public' } + + it 'returns true and visibility name' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(true) + expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context 'when misspelled' do + let(:override_visibility) { 'publik' } + + it 'returns false with nil for visibility level' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end + + context 'when import_data is missing' do + let(:override_params) { {} } + + it 'returns false with nil for visibility level' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end + end + end + end +end diff --git a/spec/lib/system_check/app/authorized_keys_permission_check_spec.rb b/spec/lib/system_check/app/authorized_keys_permission_check_spec.rb new file mode 100644 index 00000000000..1a8123c3f0a --- /dev/null +++ b/spec/lib/system_check/app/authorized_keys_permission_check_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SystemCheck::App::AuthorizedKeysPermissionCheck do + subject(:system_check) { described_class.new } + + describe '#skip?' do + subject { system_check.skip? } + + context 'authorized keys enabled' do + it { is_expected.to eq(false) } + end + + context 'authorized keys not enabled' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it { is_expected.to eq(true) } + end + end + + describe '#check?' do + subject { system_check.check? } + + before do + expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance| + allow(instance).to receive(:accessible?) { accessible? } + end + end + + context 'authorized keys is accessible' do + let(:accessible?) { true } + + it { is_expected.to eq(true) } + end + + context 'authorized keys is not accessible' do + let(:accessible?) { false } + + it { is_expected.to eq(false) } + end + end + + describe '#repair!' do + subject { system_check.repair! } + + before do + expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance| + allow(instance).to receive(:create) { created } + end + end + + context 'authorized_keys file created' do + let(:created) { true } + + it { is_expected.to eq(true) } + end + + context 'authorized_keys file is not created' do + let(:created) { false } + + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 6cba7df114c..56fa26d5f23 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -187,6 +187,22 @@ describe Notify do end end + describe 'that are due soon' do + subject { described_class.issue_due_email(recipient.id, issue.id) } + + before do + issue.update(due_date: Date.tomorrow) + end + + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' + it_behaves_like 'appearance header and footer enabled' + it_behaves_like 'appearance header and footer not enabled' + end + describe 'status changed' do let(:status) { 'closed' } subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index db80b85360f..4f7a6d102b8 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -355,6 +355,71 @@ describe ApplicationSetting do end end end + + context 'asset proxy settings' do + before do + subject.asset_proxy_enabled = true + end + + describe '#asset_proxy_url' do + it { is_expected.not_to allow_value('').for(:asset_proxy_url) } + it { is_expected.to allow_value(http).for(:asset_proxy_url) } + it { is_expected.to allow_value(https).for(:asset_proxy_url) } + it { is_expected.not_to allow_value(ftp).for(:asset_proxy_url) } + + it 'is not required when asset proxy is disabled' do + subject.asset_proxy_enabled = false + subject.asset_proxy_url = '' + + expect(subject).to be_valid + end + end + + describe '#asset_proxy_secret_key' do + it { is_expected.not_to allow_value('').for(:asset_proxy_secret_key) } + it { is_expected.to allow_value('anything').for(:asset_proxy_secret_key) } + + it 'is not required when asset proxy is disabled' do + subject.asset_proxy_enabled = false + subject.asset_proxy_secret_key = '' + + expect(subject).to be_valid + end + + it 'is encrypted' do + subject.asset_proxy_secret_key = 'shared secret' + + expect(subject.encrypted_asset_proxy_secret_key).to be_present + expect(subject.encrypted_asset_proxy_secret_key).not_to eq(subject.asset_proxy_secret_key) + end + end + + describe '#asset_proxy_whitelist' do + context 'when given an Array' do + it 'sets the domains and adds current running host' do + setting.asset_proxy_whitelist = ['example.com', 'assets.example.com'] + expect(setting.asset_proxy_whitelist).to eq(['example.com', 'assets.example.com', 'localhost']) + end + end + + context 'when given a String' do + it 'sets multiple domains with spaces' do + setting.asset_proxy_whitelist = 'example.com *.example.com' + expect(setting.asset_proxy_whitelist).to eq(['example.com', '*.example.com', 'localhost']) + end + + it 'sets multiple domains with newlines and a space' do + setting.asset_proxy_whitelist = "example.com\n *.example.com" + expect(setting.asset_proxy_whitelist).to eq(['example.com', '*.example.com', 'localhost']) + end + + it 'sets multiple domains with commas' do + setting.asset_proxy_whitelist = "example.com, *.example.com" + expect(setting.asset_proxy_whitelist).to eq(['example.com', '*.example.com', 'localhost']) + end + end + end + end end context 'restrict creating duplicates' do diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb index 93050e80b07..f6d5d05e4a0 100644 --- a/spec/models/clusters/applications/cert_manager_spec.rb +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -44,11 +44,18 @@ describe Clusters::Applications::CertManager do it 'is initialized with cert_manager arguments' do expect(subject.name).to eq('certmanager') - expect(subject.chart).to eq('stable/cert-manager') - expect(subject.version).to eq('v0.5.2') + expect(subject.chart).to eq('certmanager/cert-manager') + expect(subject.repository).to eq('https://charts.jetstack.io') + expect(subject.version).to eq('v0.9.1') expect(subject).to be_rbac expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file)) - expect(subject.postinstall).to eq(['kubectl create -f /data/helm/certmanager/config/cluster_issuer.yaml']) + expect(subject.preinstall).to eq([ + 'kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.9/deploy/manifests/00-crds.yaml', + 'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true' + ]) + expect(subject.postinstall).to eq([ + 'for i in $(seq 1 30); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && break; sleep 1s; echo "Retrying ($i)..."; done' + ]) end context 'for a specific user' do @@ -75,7 +82,7 @@ describe Clusters::Applications::CertManager do let(:cert_manager) { create(:clusters_applications_cert_manager, :errored, version: '0.0.1') } it 'is initialized with the locked version' do - expect(subject.version).to eq('v0.5.2') + expect(subject.version).to eq('v0.9.1') end end end @@ -93,10 +100,13 @@ describe Clusters::Applications::CertManager do it 'specifies a post delete command to remove custom resource definitions' do expect(subject.postdelete).to eq([ - "kubectl delete secret -n gitlab-managed-apps letsencrypt-prod --ignore-not-found", + 'kubectl delete secret -n gitlab-managed-apps letsencrypt-prod --ignore-not-found', 'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found', + 'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found', + 'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found', 'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found', - 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found' + 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found', + 'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found' ]) end @@ -111,8 +121,11 @@ describe Clusters::Applications::CertManager do it 'does not try and delete the secret' do expect(subject.postdelete).to eq([ 'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found', + 'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found', + 'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found', 'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found', - 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found' + 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found', + 'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found' ]) end end diff --git a/spec/models/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb deleted file mode 100644 index 6b82825d2cc..00000000000 --- a/spec/models/concerns/ignorable_column_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe IgnorableColumn do - let :base_class do - Class.new do - def self.columns - # This method does not have access to "double" - [ - Struct.new(:name).new('id'), - Struct.new(:name).new('title'), - Struct.new(:name).new('date') - ] - end - end - end - - let :model do - Class.new(base_class) do - include IgnorableColumn - end - end - - describe '.columns' do - it 'returns the columns, excluding the ignored ones' do - model.ignore_column(:title, :date) - - expect(model.columns.map(&:name)).to eq(%w(id)) - end - end - - describe '.ignored_columns' do - it 'returns a Set' do - expect(model.ignored_columns).to be_an_instance_of(Set) - end - - it 'returns the names of the ignored columns' do - model.ignore_column(:title, :date) - - expect(model.ignored_columns).to eq(Set.new(%w(title date))) - end - end -end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 39680c0e51a..65d41edc035 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -46,6 +46,7 @@ describe Issuable do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_length_of(:title).is_at_most(255) } + it { is_expected.to validate_length_of(:description).is_at_most(1_000_000) } end describe 'milestone' do @@ -795,4 +796,29 @@ describe Issuable do end end end + + describe '#matches_cross_reference_regex?' do + context "issue description with long path string" do + let(:mentionable) { build(:issue, description: "/a" * 50000) } + + it_behaves_like 'matches_cross_reference_regex? fails fast' + end + + context "note with long path string" do + let(:mentionable) { build(:note, note: "/a" * 50000) } + + it_behaves_like 'matches_cross_reference_regex? fails fast' + end + + context "note with long path string" do + let(:project) { create(:project, :public, :repository) } + let(:mentionable) { project.commit } + + before do + expect(mentionable.raw).to receive(:message).and_return("/a" * 50000) + end + + it_behaves_like 'matches_cross_reference_regex? fails fast' + end + end end diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 31163a5bb5c..cff86afe768 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -58,7 +58,7 @@ describe Group, 'Routable' do end end - describe '.find_by_full_path' do + shared_examples_for '.find_by_full_path' do let!(:nested_group) { create(:group, parent: group) } context 'without any redirect routes' do @@ -110,6 +110,24 @@ describe Group, 'Routable' do end end + describe '.find_by_full_path' do + context 'with routable_two_step_lookup feature' do + before do + stub_feature_flags(routable_two_step_lookup: true) + end + + it_behaves_like '.find_by_full_path' + end + + context 'without routable_two_step_lookup feature' do + before do + stub_feature_flags(routable_two_step_lookup: false) + end + + it_behaves_like '.find_by_full_path' + end + end + describe '.where_full_path_in' do context 'without any paths' do it 'returns an empty relation' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1c41ceb7deb..796b6917fb2 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1039,4 +1039,23 @@ describe Group do .to eq(Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS) end end + + describe '#access_request_approvers_to_be_notified' do + it 'returns a maximum of ten, active, non_requested owners of the group in recent_sign_in descending order' do + group = create(:group, :public) + + users = create_list(:user, 12, :with_sign_ins) + active_owners = users.map do |user| + create(:group_member, :owner, group: group, user: user) + end + + create(:group_member, :owner, :blocked, group: group) + create(:group_member, :maintainer, group: group) + create(:group_member, :access_request, :owner, group: group) + + active_owners_in_recent_sign_in_desc_order = group.members_and_requesters.where(id: active_owners).order_recent_sign_in.limit(10) + + expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order) + end + end end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index c2e2298823e..baf2cfeab0c 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -84,6 +84,13 @@ describe Label do end end + describe '#description' do + it 'sanitizes description' do + label = described_class.new(description: '<b>foo & bar?</b>') + expect(label.description).to eq('foo & bar?') + end + end + describe 'priorization' do subject(:label) { create(:label) } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index bfd0e5f0558..927fbdb93d8 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -22,6 +22,7 @@ describe Note do end describe 'validation' do + it { is_expected.to validate_length_of(:note).is_at_most(1_000_000) } it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:project) } diff --git a/spec/models/project_services/discord_service_spec.rb b/spec/models/project_services/discord_service_spec.rb index be82f223478..96ac532dcd1 100644 --- a/spec/models/project_services/discord_service_spec.rb +++ b/spec/models/project_services/discord_service_spec.rb @@ -8,4 +8,37 @@ describe DiscordService do let(:client_arguments) { { url: webhook_url } } let(:content_key) { :content } end + + describe '#execute' do + include StubRequests + + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:webhook_url) { "https://example.gitlab.com/" } + + let(:sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + before do + allow(subject).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + context 'DNS rebind to local address' do + before do + stub_dns(webhook_url, ip_address: '192.168.2.120') + end + + it 'does not allow DNS rebinding' do + expect { subject.execute(sample_data) }.to raise_error(ArgumentError, /is blocked/) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bd352db2236..bfbcac60fea 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -4991,6 +4991,26 @@ describe Project do end end + describe '#access_request_approvers_to_be_notified' do + it 'returns a maximum of ten, active, non_requested maintainers of the project in recent_sign_in descending order' do + group = create(:group, :public) + project = create(:project, group: group) + + users = create_list(:user, 12, :with_sign_ins) + active_maintainers = users.map do |user| + create(:project_member, :maintainer, user: user) + end + + create(:project_member, :maintainer, :blocked, project: project) + create(:project_member, :developer, project: project) + create(:project_member, :access_request, :maintainer, project: project) + + active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters.where(id: active_maintainers).order_recent_sign_in.limit(10) + + expect(project.access_request_approvers_to_be_notified).to eq(active_maintainers_in_recent_sign_in_desc_order) + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 7edeb56efe2..f8d6e500e10 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -40,6 +40,13 @@ describe RemoteMirror, :mailer do expect(remote_mirror).to be_invalid expect(remote_mirror.errors[:url].first).to include('Requests to the local network are not allowed') end + + it 'returns a nil safe_url' do + remote_mirror = build(:remote_mirror, url: 'http://[0:0:0:0:ffff:123.123.123.123]/foo.git') + + expect(remote_mirror.url).to eq('http://[0:0:0:0:ffff:123.123.123.123]/foo.git') + expect(remote_mirror.safe_url).to be_nil + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 79395fcc994..419e1dc2459 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1223,66 +1223,36 @@ describe Repository do end describe '#branch_exists?' do - let(:branch) { repository.root_ref } + it 'uses branch_names' do + allow(repository).to receive(:branch_names).and_return(['foobar']) - subject { repository.branch_exists?(branch) } - - it 'delegates to branch_names when the cache is empty' do - repository.expire_branches_cache - - expect(repository).to receive(:branch_names).and_call_original - is_expected.to eq(true) - end - - it 'uses redis set caching when the cache is filled' do - repository.branch_names # ensure the branch name cache is filled - - expect(repository) - .to receive(:branch_names_include?) - .with(branch) - .and_call_original - - is_expected.to eq(true) + expect(repository.branch_exists?('foobar')).to eq(true) + expect(repository.branch_exists?('master')).to eq(false) end end describe '#tag_exists?' do - let(:tag) { repository.tags.first.name } - - subject { repository.tag_exists?(tag) } - - it 'delegates to tag_names when the cache is empty' do - repository.expire_tags_cache - - expect(repository).to receive(:tag_names).and_call_original - is_expected.to eq(true) - end - - it 'uses redis set caching when the cache is filled' do - repository.tag_names # ensure the tag name cache is filled - - expect(repository) - .to receive(:tag_names_include?) - .with(tag) - .and_call_original + it 'uses tag_names' do + allow(repository).to receive(:tag_names).and_return(['foobar']) - is_expected.to eq(true) + expect(repository.tag_exists?('foobar')).to eq(true) + expect(repository.tag_exists?('master')).to eq(false) end end - describe '#branch_names', :clean_gitlab_redis_cache do + describe '#branch_names', :use_clean_rails_memory_store_caching do let(:fake_branch_names) { ['foobar'] } it 'gets cached across Repository instances' do allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names) - expect(repository.branch_names).to match_array(fake_branch_names) + expect(repository.branch_names).to eq(fake_branch_names) fresh_repository = Project.find(project.id).repository expect(fresh_repository.object_id).not_to eq(repository.object_id) expect(fresh_repository.raw_repository).not_to receive(:branch_names) - expect(fresh_repository.branch_names).to match_array(fake_branch_names) + expect(fresh_repository.branch_names).to eq(fake_branch_names) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f1408194250..b8c323904b8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3065,6 +3065,47 @@ describe User do end end + describe '#will_save_change_to_login?' do + let(:user) { create(:user, username: 'old-username', email: 'old-email@example.org') } + let(:new_username) { 'new-name' } + let(:new_email) { 'new-email@example.org' } + + subject { user.will_save_change_to_login? } + + context 'when the username is changed' do + before do + user.username = new_username + end + + it { is_expected.to be true } + end + + context 'when the email is changed' do + before do + user.email = new_email + end + + it { is_expected.to be true } + end + + context 'when both email and username are changed' do + before do + user.username = new_username + user.email = new_email + end + + it { is_expected.to be true } + end + + context 'when email and username aren\'t changed' do + before do + user.name = 'new_name' + end + + it { is_expected.to be_falsy } + end + end + describe '#sync_attribute?' do let(:user) { described_class.new } diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index b149dbcf871..25267d36ab8 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -172,6 +172,34 @@ describe IssuePolicy do expect(permissions(assignee, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue) end + context 'when issues are private' do + before do + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) + end + let(:issue) { create(:issue, project: project, author: author) } + let(:visitor) { create(:user) } + let(:admin) { create(:user, :admin) } + + it 'forbids visitors from viewing issues' do + expect(permissions(visitor, issue)).to be_disallowed(:read_issue) + end + it 'forbids visitors from commenting' do + expect(permissions(visitor, issue)).to be_disallowed(:create_note) + end + it 'allows guests to view' do + expect(permissions(guest, issue)).to be_allowed(:read_issue) + end + it 'allows guests to comment' do + expect(permissions(guest, issue)).to be_allowed(:create_note) + end + it 'allows admins to view' do + expect(permissions(admin, issue)).to be_allowed(:read_issue) + end + it 'allows admins to comment' do + expect(permissions(admin, issue)).to be_allowed(:create_note) + end + end + context 'with confidential issues' do let(:confidential_issue) { create(:issue, :confidential, project: project, assignees: [assignee], author: author) } let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb index 81279225d61..87205f56589 100644 --- a/spec/policies/merge_request_policy_spec.rb +++ b/spec/policies/merge_request_policy_spec.rb @@ -6,6 +6,7 @@ describe MergeRequestPolicy do let(:guest) { create(:user) } let(:author) { create(:user) } let(:developer) { create(:user) } + let(:non_team_member) { create(:user) } let(:project) { create(:project, :public) } def permissions(user, merge_request) @@ -18,6 +19,78 @@ describe MergeRequestPolicy do project.add_developer(developer) end + MR_PERMS = %i[create_merge_request_in + create_merge_request_from + read_merge_request + create_note].freeze + + shared_examples_for 'a denied user' do + let(:perms) { permissions(subject, merge_request) } + + MR_PERMS.each do |thing| + it "cannot #{thing}" do + expect(perms).to be_disallowed(thing) + end + end + end + + shared_examples_for 'a user with access' do + let(:perms) { permissions(subject, merge_request) } + + MR_PERMS.each do |thing| + it "can #{thing}" do + expect(perms).to be_allowed(thing) + end + end + end + + context 'when merge requests have been disabled' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } + + before do + project.project_feature.update(merge_requests_access_level: ProjectFeature::DISABLED) + end + + describe 'the author' do + subject { author } + it_behaves_like 'a denied user' + end + + describe 'a guest' do + subject { guest } + it_behaves_like 'a denied user' + end + + describe 'a developer' do + subject { developer } + it_behaves_like 'a denied user' + end + + describe 'any other user' do + subject { non_team_member } + it_behaves_like 'a denied user' + end + end + + context 'when merge requests are private' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) } + + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + describe 'a non-team-member' do + subject { non_team_member } + it_behaves_like 'a denied user' + end + + describe 'a developer' do + subject { developer } + it_behaves_like 'a user with access' + end + end + context 'when merge request is unlocked' do let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) } @@ -48,6 +121,22 @@ describe MergeRequestPolicy do it 'prevents guests from reopening merge request' do expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request) end + + context 'when the user is not a project member' do + let(:user) { create(:user) } + + it 'cannot create a note' do + expect(permissions(user, merge_request_locked)).to be_disallowed(:create_note) + end + end + + context 'when the user is project member, with at least guest access' do + let(:user) { guest } + + it 'can create a note' do + expect(permissions(user, merge_request_locked)).to be_allowed(:create_note) + end + end end context 'with external authorization enabled' do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 29f69b6ce20..58a28e636f1 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -96,6 +96,28 @@ describe API::ProjectSnippets do } end + context 'with a regular user' do + let(:user) { create(:user) } + + before do + project.add_developer(user) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) + params['visibility'] = 'internal' + end + + it 'creates a new snippet' do + post api("/projects/#{project.id}/snippets/", user), params: params + + expect(response).to have_gitlab_http_status(201) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(params[:code]) + expect(snippet.description).to eq(params[:description]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.visibility_level).to eq(Snippet::INTERNAL) + end + end + it 'creates a new snippet' do post api("/projects/#{project.id}/snippets/", admin), params: params @@ -108,6 +130,29 @@ describe API::ProjectSnippets do expect(snippet.visibility_level).to eq(Snippet::PUBLIC) end + it 'creates a new snippet with content parameter' do + params[:content] = params.delete(:code) + + post api("/projects/#{project.id}/snippets/", admin), params: params + + expect(response).to have_gitlab_http_status(201) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(params[:content]) + expect(snippet.description).to eq(params[:description]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.visibility_level).to eq(Snippet::PUBLIC) + end + + it 'returns 400 when both code and content parameters specified' do + params[:content] = params[:code] + + post api("/projects/#{project.id}/snippets/", admin), params: params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('code, content are mutually exclusive') + end + it 'returns 400 for missing parameters' do params.delete(:title) @@ -167,7 +212,20 @@ describe API::ProjectSnippets do new_content = 'New content' new_description = 'New description' - put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { code: new_content, description: new_description } + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { code: new_content, description: new_description, visibility: 'private' } + + expect(response).to have_gitlab_http_status(200) + snippet.reload + expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) + expect(snippet.visibility).to eq('private') + end + + it 'updates snippet with content parameter' do + new_content = 'New content' + new_description = 'New description' + + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { content: new_content, description: new_description } expect(response).to have_gitlab_http_status(200) snippet.reload @@ -175,6 +233,13 @@ describe API::ProjectSnippets do expect(snippet.description).to eq(new_description) end + it 'returns 400 when both code and content parameters specified' do + put api("/projects/#{snippet.project.id}/snippets/1234", admin), params: { code: 'some content', content: 'other content' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('code, content are mutually exclusive') + end + it 'returns 404 for invalid snippet id' do put api("/projects/#{snippet.project.id}/snippets/1234", admin), params: { title: 'foo' } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 590107d5161..048d04cdefd 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -224,5 +224,33 @@ describe API::Settings, 'Settings' do expect(json_response['error']).to eq('plantuml_url is missing') end end + + context 'asset_proxy settings' do + it 'updates application settings' do + put api('/application/settings', admin), + params: { + asset_proxy_enabled: true, + asset_proxy_url: 'http://assets.example.com', + asset_proxy_secret_key: 'shared secret', + asset_proxy_whitelist: ['example.com', '*.example.com'] + } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['asset_proxy_enabled']).to be(true) + expect(json_response['asset_proxy_url']).to eq('http://assets.example.com') + expect(json_response['asset_proxy_secret_key']).to be_nil + expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost']) + end + + it 'allows a string for asset_proxy_whitelist' do + put api('/application/settings', admin), + params: { + asset_proxy_whitelist: 'example.com, *.example.com' + } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost']) + end + end end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index d600076e9fb..cc05b8d5b45 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -193,18 +193,32 @@ describe API::Snippets do } end - it 'creates a new snippet' do - expect do - post api("/snippets/", user), params: params - end.to change { PersonalSnippet.count }.by(1) + shared_examples 'snippet creation' do + it 'creates a new snippet' do + expect do + post api("/snippets/", user), params: params + end.to change { PersonalSnippet.count }.by(1) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq(params[:title]) + expect(json_response['description']).to eq(params[:description]) + expect(json_response['file_name']).to eq(params[:file_name]) + expect(json_response['visibility']).to eq(params[:visibility]) + end + end + + context 'with restricted visibility settings' do + before do + stub_application_setting(restricted_visibility_levels: + [Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PRIVATE]) + end - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(params[:title]) - expect(json_response['description']).to eq(params[:description]) - expect(json_response['file_name']).to eq(params[:file_name]) - expect(json_response['visibility']).to eq(params[:visibility]) + it_behaves_like 'snippet creation' end + it_behaves_like 'snippet creation' + it 'returns 400 for missing parameters' do params.delete(:title) @@ -253,18 +267,33 @@ describe API::Snippets do create(:personal_snippet, author: user, visibility_level: visibility_level) end - it 'updates snippet' do - new_content = 'New content' - new_description = 'New description' + shared_examples 'snippet updates' do + it 'updates a snippet' do + new_content = 'New content' + new_description = 'New description' - put api("/snippets/#{snippet.id}", user), params: { content: new_content, description: new_description } + put api("/snippets/#{snippet.id}", user), params: { content: new_content, description: new_description, visibility: 'internal' } - expect(response).to have_gitlab_http_status(200) - snippet.reload - expect(snippet.content).to eq(new_content) - expect(snippet.description).to eq(new_description) + expect(response).to have_gitlab_http_status(200) + snippet.reload + expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) + expect(snippet.visibility).to eq('internal') + end end + context 'with restricted visibility settings' do + before do + stub_application_setting(restricted_visibility_levels: + [Gitlab::VisibilityLevel::PUBLIC, + Gitlab::VisibilityLevel::PRIVATE]) + end + + it_behaves_like 'snippet updates' + end + + it_behaves_like 'snippet updates' + it 'returns 404 for invalid snippet id' do put api("/snippets/1234", user), params: { title: 'foo' } diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb index 27df42c0aee..ce20d494542 100644 --- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb +++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb @@ -19,6 +19,15 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do SOURCE end + it 'flags the use of `prepend_if_ee QA::EE` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend_if_ee 'QA::EE::Foo' + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + it 'does not flag the use of `prepend_if_ee EEFoo` in the middle of a file' do expect_no_offenses(<<~SOURCE) class Foo @@ -176,6 +185,16 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do SOURCE end + it 'disallows the use of prepend to inject a QA::EE module' do + expect_offense(<<~SOURCE) + class Foo + end + + Foo.prepend(QA::EE::Foo) + ^^^^^^^^^^^^^^^^^^^^^^^^ EE modules must be injected using `include_if_ee`, `extend_if_ee`, or `prepend_if_ee` + SOURCE + end + it 'disallows the use of extend to inject an EE module' do expect_offense(<<~SOURCE) class Foo diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 1b19eac9a97..79f89dc1a9c 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -36,6 +36,15 @@ describe DeploymentEntity do expect(subject).to include(:deployed_at) end + context 'when deployable is nil' do + let(:entity) { described_class.new(deployment, request: request, deployment_details: false) } + let(:deployment) { create(:deployment, deployable: nil, project: project) } + + it 'does not expose deployable entry' do + expect(subject).not_to include(:deployable) + end + end + context 'when the pipeline has another manual action' do let(:other_build) { create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline) } let!(:other_deployment) { create(:deployment, deployable: other_build) } diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb index ab06c1a1209..51fb43907a6 100644 --- a/spec/services/application_settings/update_service_spec.rb +++ b/spec/services/application_settings/update_service_spec.rb @@ -110,6 +110,39 @@ describe ApplicationSettings::UpdateService do end end + describe 'markdown cache invalidators' do + shared_examples 'invalidates markdown cache' do |attribute| + let(:params) { attribute } + + it 'increments cache' do + expect { subject.execute }.to change(application_settings, :local_markdown_version).by(1) + end + end + + it_behaves_like 'invalidates markdown cache', { asset_proxy_enabled: true } + it_behaves_like 'invalidates markdown cache', { asset_proxy_url: 'http://test.com' } + it_behaves_like 'invalidates markdown cache', { asset_proxy_secret_key: 'another secret' } + it_behaves_like 'invalidates markdown cache', { asset_proxy_whitelist: ['domain.com'] } + + context 'when also setting the local_markdown_version' do + let(:params) { { asset_proxy_enabled: true, local_markdown_version: 12 } } + + it 'does not increment' do + expect { subject.execute }.to change(application_settings, :local_markdown_version).to(12) + end + end + + context 'do not invalidate if value does not change' do + let(:params) { { asset_proxy_enabled: true, asset_proxy_secret_key: 'secret', asset_proxy_url: 'http://test.com' } } + + it 'does not increment' do + described_class.new(application_settings, admin, params).execute + + expect { described_class.new(application_settings, admin, params).execute }.not_to change(application_settings, :local_markdown_version) + end + end + end + describe 'performance bar settings' do using RSpec::Parameterized::TableSyntax diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb index 41cbac4e8e9..7f32948daad 100644 --- a/spec/services/chat_names/authorize_user_service_spec.rb +++ b/spec/services/chat_names/authorize_user_service_spec.rb @@ -4,23 +4,36 @@ require 'spec_helper' describe ChatNames::AuthorizeUserService do describe '#execute' do - let(:service) { create(:service) } + subject { described_class.new(service, params) } - subject { described_class.new(service, params).execute } + let(:result) { subject.execute } + let(:service) { create(:service) } context 'when all parameters are valid' do let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } } + it 'produces a valid HTTP URL' do + expect(result).to be_http_url + end + it 'requests a new token' do - is_expected.to be_url + expect(subject).to receive(:request_token).once.and_call_original + + subject.execute end end context 'when there are missing parameters' do let(:params) { {} } + it 'does not produce a URL' do + expect(result).to be_nil + end + it 'does not request a new token' do - is_expected.to be_nil + expect(subject).not_to receive(:request_token) + + subject.execute end end end diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index a54bd85a11a..464a67649ff 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -14,7 +14,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do let(:phase) { a_phase } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end context "when phase is #{a_phase}" do @@ -44,7 +44,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do before do application.update!(cluster: cluster) - expect(service).to receive(:installation_phase).and_raise(error) + expect(service).to receive(:pod_phase).and_raise(error) end include_examples 'logs kubernetes errors' do @@ -77,7 +77,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do context 'when installation POD succeeded' do let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'removes the installation POD' do @@ -101,7 +101,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do let(:errors) { 'test installation failed' } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'make the application errored' do @@ -116,7 +116,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do let(:application) { create(:clusters_applications_helm, :timed_out, :updating) } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'make the application errored' do @@ -138,7 +138,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do context 'when installation POD succeeded' do let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'removes the installation POD' do @@ -162,7 +162,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do let(:errors) { 'test installation failed' } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'make the application errored' do @@ -177,7 +177,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do let(:application) { create(:clusters_applications_helm, :timed_out) } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'make the application errored' do diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index a948b442441..1a9f7089c3d 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -20,7 +20,7 @@ describe Clusters::Applications::CheckUninstallProgressService do let(:phase) { a_phase } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end context "when phase is #{a_phase}" do @@ -47,7 +47,7 @@ describe Clusters::Applications::CheckUninstallProgressService do context 'when installation POD succeeded' do let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'removes the installation POD' do @@ -95,7 +95,7 @@ describe Clusters::Applications::CheckUninstallProgressService do let(:errors) { 'test installation failed' } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'make the application errored' do @@ -110,7 +110,7 @@ describe Clusters::Applications::CheckUninstallProgressService do let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) } before do - expect(service).to receive(:installation_phase).once.and_return(phase) + expect(service).to receive(:pod_phase).once.and_return(phase) end it 'make the application errored' do @@ -131,7 +131,7 @@ describe Clusters::Applications::CheckUninstallProgressService do before do application.update!(cluster: cluster) - expect(service).to receive(:installation_phase).and_raise(error) + expect(service).to receive(:pod_phase).and_raise(error) end include_examples 'logs kubernetes errors' do diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb index e44cc3f5a78..5a3b1cd6cfb 100644 --- a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do +describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do include KubernetesHelpers let(:cluster) { create(:cluster, :project, :provided_by_gcp) } @@ -35,8 +35,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d stub_kubeclient_create_service_account(api_url, namespace: namespace) stub_kubeclient_create_secret(api_url, namespace: namespace) stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace) - stub_kubeclient_put_role(api_url, Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) - stub_kubeclient_put_role_binding(api_url, Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) + stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) + stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) stub_kubeclient_get_secret( api_url, @@ -56,7 +56,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d end it 'creates project service account' do - expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once + expect_any_instance_of(Clusters::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once subject end @@ -123,7 +123,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d end it 'creates project service account' do - expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once + expect_any_instance_of(Clusters::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once subject end diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb index 8b874989758..10dbfc800ff 100644 --- a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do +describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do include KubernetesHelpers let(:api_url) { 'http://111.111.111.111' } @@ -143,8 +143,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace) stub_kubeclient_create_role_binding(api_url, namespace: namespace) - stub_kubeclient_put_role(api_url, Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) - stub_kubeclient_put_role_binding(api_url, Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) + stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) + stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) end it_behaves_like 'creates service account and token' @@ -175,10 +175,10 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do it 'creates a role and role binding granting knative serving permissions to the service account' do subject - expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/roles/#{Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME}").with( + expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/roles/#{Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME}").with( body: hash_including( metadata: { - name: Clusters::Gcp::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, + name: Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace }, rules: [{ diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb index 93c0dc37ade..145528616ee 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do +describe Clusters::Kubernetes::FetchKubernetesTokenService do include KubernetesHelpers describe '#execute' do diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb index 9b83f65a17e..7d2491b3a49 100644 --- a/spec/services/create_snippet_service_spec.rb +++ b/spec/services/create_snippet_service_spec.rb @@ -34,6 +34,19 @@ describe CreateSnippetService do expect(snippet.errors.any?).to be_falsey expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) end + + describe "when visibility level is passed as a string" do + before do + @opts[:visibility] = 'internal' + @opts.delete(:visibility_level) + end + + it "assigns the correct visibility level" do + snippet = create_snippet(nil, @user, @opts) + expect(snippet.errors.any?).to be_falsey + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end end describe 'usage counter' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 6874a8a0929..642a49d57d5 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -60,35 +60,63 @@ describe Issues::CloseService do describe '#close_issue' do context "closed by a merge request" do - before do + it 'mentions closure via a merge request' do perform_enqueued_jobs do described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request) end - end - it 'mentions closure via a merge request' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(issue.title) expect(email.body.parts.map(&:body)).to all(include(closing_merge_request.to_reference)) end + + context 'when user cannot read merge request' do + it 'does not mention merge request' do + project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + perform_enqueued_jobs do + described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request) + end + + email = ActionMailer::Base.deliveries.last + body_text = email.body.parts.map(&:body).join(" ") + + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + expect(body_text).not_to include(closing_merge_request.to_reference) + end + end end context "closed by a commit" do - before do + it 'mentions closure via a commit' do perform_enqueued_jobs do described_class.new(project, user).close_issue(issue, closed_via: closing_commit) end - end - it 'mentions closure via a commit' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(issue.title) expect(email.body.parts.map(&:body)).to all(include(closing_commit.id)) end + + context 'when user cannot read the commit' do + it 'does not mention the commit id' do + project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + perform_enqueued_jobs do + described_class.new(project, user).close_issue(issue, closed_via: closing_commit) + end + + email = ActionMailer::Base.deliveries.last + body_text = email.body.parts.map(&:body).join(" ") + + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + expect(body_text).not_to include(closing_commit.id) + end + end end context "valid params" do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d925aa2b6c3..ab0e01e27d7 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1932,31 +1932,39 @@ describe NotificationService, :mailer do let(:added_user) { create(:user) } describe '#new_access_request' do - let(:maintainer) { create(:user) } - let(:owner) { create(:user) } - let(:developer) { create(:user) } - let!(:group) do - create(:group, :public, :access_requestable) do |group| - group.add_owner(owner) - group.add_maintainer(maintainer) - group.add_developer(developer) + context 'recipients' do + let(:maintainer) { create(:user) } + let(:owner) { create(:user) } + let(:developer) { create(:user) } + + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_owner(owner) + group.add_maintainer(maintainer) + group.add_developer(developer) + end end - end - before do - reset_delivered_emails! - end + before do + reset_delivered_emails! + end - it 'sends notification to group owners_and_maintainers' do - group.request_access(added_user) + it 'sends notification only to group owners' do + group.request_access(added_user) + + should_email(owner) + should_not_email(maintainer) + should_not_email(developer) + end - should_email(owner) - should_email(maintainer) - should_not_email(developer) + it_behaves_like 'group emails are disabled' do + let(:notification_target) { group } + let(:notification_trigger) { group.request_access(added_user) } + end end - it_behaves_like 'group emails are disabled' do - let(:notification_target) { group } + it_behaves_like 'sends notification only to a maximum of ten, most recently active group owners' do + let(:group) { create(:group, :public, :access_requestable) } let(:notification_trigger) { group.request_access(added_user) } end end @@ -2012,20 +2020,36 @@ describe NotificationService, :mailer do describe '#new_access_request' do context 'for a project in a user namespace' do - let(:project) do - create(:project, :public, :access_requestable) do |project| - project.add_maintainer(project.owner) + context 'recipients' do + let(:developer) { create(:user) } + let(:maintainer) { create(:user) } + + let!(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_developer(developer) + project.add_maintainer(maintainer) + end end - end - it 'sends notification to project owners_and_maintainers' do - project.request_access(added_user) + before do + reset_delivered_emails! + end + + it 'sends notification only to project maintainers' do + project.request_access(added_user) + + should_email(maintainer) + should_not_email(developer) + end - should_only_email(project.owner) + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { project.request_access(added_user) } + end end - it_behaves_like 'project emails are disabled' do - let(:notification_target) { project } + it_behaves_like 'sends notification only to a maximum of ten, most recently active project maintainers' do + let(:project) { create(:project, :public, :access_requestable) } let(:notification_trigger) { project.request_access(added_user) } end end @@ -2033,16 +2057,76 @@ describe NotificationService, :mailer do context 'for a project in a group' do let(:group_owner) { create(:user) } let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } - let!(:project) { create(:project, :public, :access_requestable, namespace: group) } - before do - reset_delivered_emails! + context 'when the project has no maintainers' do + context 'when the group has at least one owner' do + let!(:project) { create(:project, :public, :access_requestable, namespace: group) } + + before do + reset_delivered_emails! + end + + context 'recipients' do + it 'sends notifications to the group owners' do + project.request_access(added_user) + + should_only_email(group_owner) + end + end + + it_behaves_like 'sends notification only to a maximum of ten, most recently active group owners' do + let(:group) { create(:group, :public, :access_requestable) } + let(:notification_trigger) { project.request_access(added_user) } + end + end + + context 'when the group does not have any owners' do + let(:group) { create(:group) } + let!(:project) { create(:project, :public, :access_requestable, namespace: group) } + + context 'recipients' do + before do + reset_delivered_emails! + end + + it 'does not send any notifications' do + project.request_access(added_user) + + should_not_email_anyone + end + end + end end - it 'sends notification to group owners_and_maintainers' do - project.request_access(added_user) + context 'when the project has maintainers' do + let(:maintainer) { create(:user) } + let(:developer) { create(:user) } + + let!(:project) do + create(:project, :public, :access_requestable, namespace: group) do |project| + project.add_maintainer(maintainer) + project.add_developer(developer) + end + end + + before do + reset_delivered_emails! + end + + context 'recipients' do + it 'sends notifications only to project maintainers' do + project.request_access(added_user) - should_only_email(group_owner) + should_email(maintainer) + should_not_email(developer) + should_not_email(group_owner) + end + end + + it_behaves_like 'sends notification only to a maximum of ten, most recently active project maintainers' do + let(:project) { create(:project, :public, :access_requestable, namespace: group) } + let(:notification_trigger) { project.request_access(added_user) } + end end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index d4fa62fa85d..8178b7d2ba2 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -183,27 +183,65 @@ describe Projects::CreateService, '#execute' do context 'restricted visibility level' do before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end - opts.merge!( - visibility_level: Gitlab::VisibilityLevel::PUBLIC - ) + shared_examples 'restricted visibility' do + it 'does not allow a restricted visibility level for non-admins' do + project = create_project(user, opts) + + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:visibility_level) + expect(project.errors.messages[:visibility_level].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'allows a restricted visibility level for admins' do + admin = create(:admin) + project = create_project(admin, opts) + + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) + end end - it 'does not allow a restricted visibility level for non-admins' do - project = create_project(user, opts) - expect(project).to respond_to(:errors) - expect(project.errors.messages).to have_key(:visibility_level) - expect(project.errors.messages[:visibility_level].first).to( - match('restricted by your GitLab administrator') - ) + context 'when visibility is project based' do + before do + opts.merge!( + visibility_level: Gitlab::VisibilityLevel::PUBLIC + ) + end + + include_examples 'restricted visibility' end - it 'allows a restricted visibility level for admins' do - admin = create(:admin) - project = create_project(admin, opts) + context 'when visibility is overridden' do + let(:visibility) { 'public' } - expect(project.errors.any?).to be(false) - expect(project.saved?).to be(true) + before do + opts.merge!( + import_data: { + data: { + override_params: { + visibility: visibility + } + } + } + ) + end + + include_examples 'restricted visibility' + + context 'when visibility is misspelled' do + let(:visibility) { 'publik' } + + it 'does not restrict project creation' do + project = create_project(user, opts) + + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) + end + end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 9ee23f3eb48..bdf2f59704c 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -436,25 +436,114 @@ describe TodoService do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) end - context 'on commit' do - let(:project) { create(:project, :repository) } - - it 'creates a todo for each valid mentioned user when leaving a note on commit' do - service.new_note(note_on_commit, john_doe) - - should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + context 'commits' do + let(:base_commit_todo_attrs) { { target_id: nil, target_type: 'Commit', author: john_doe } } + + context 'leaving a note on a commit in a public project' do + let(:project) { create(:project, :repository, :public) } + it 'creates a todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::MENTIONED, + note: note_on_commit, + commit_id: note_on_commit.commit_id + ) + + service.new_note(note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_create_todo(expected_todo.merge(user: non_member)) + end + + it 'creates a directly addressed todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::DIRECTLY_ADDRESSED, + note: addressed_note_on_commit, + commit_id: addressed_note_on_commit.commit_id + ) + + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_create_todo(expected_todo.merge(user: non_member)) + end end - it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do - service.new_note(addressed_note_on_commit, john_doe) + context 'leaving a note on a commit in a public project with private code' do + let(:project) { create(:project, :repository, :public, :repository_private) } + + it 'creates a todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::MENTIONED, + note: note_on_commit, + commit_id: note_on_commit.commit_id + ) + + service.new_note(note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end + + it 'creates a directly addressed todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::DIRECTLY_ADDRESSED, + note: addressed_note_on_commit, + commit_id: addressed_note_on_commit.commit_id + ) + + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end + end - should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) - should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) - should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) - should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + context 'leaving a note on a commit in a private project' do + let(:project) { create(:project, :repository, :private) } + + it 'creates a todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::MENTIONED, + note: note_on_commit, + commit_id: note_on_commit.commit_id + ) + + service.new_note(note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_not_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end + + it 'creates a directly addressed todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::DIRECTLY_ADDRESSED, + note: addressed_note_on_commit, + commit_id: addressed_note_on_commit.commit_id + ) + + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_not_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end end end diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index 0678f54c195..19b35dca6a7 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -32,12 +32,25 @@ describe UpdateSnippetService do expect(@snippet.visibility_level).to eq(old_visibility) end - it 'admins should be able to update to pubic visibility' do + it 'admins should be able to update to public visibility' do old_visibility = @snippet.visibility_level update_snippet(@project, @admin, @snippet, @opts) expect(@snippet.visibility_level).not_to eq(old_visibility) expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) end + + describe "when visibility level is passed as a string" do + before do + @opts[:visibility] = 'internal' + @opts.delete(:visibility_level) + end + + it "assigns the correct visibility level" do + update_snippet(@project, @user, @snippet, @opts) + expect(@snippet.errors.any?).to be_falsey + expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end end describe 'usage counter' do diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 5590bf0fb7e..f070243f111 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -73,7 +73,7 @@ shared_examples 'thread comments' do |resource_name| expect(page).not_to have_selector menu_selector find(toggle_selector).click - execute_script("document.querySelector('body').click()") + find("#{form_selector} .note-textarea").click expect(page).not_to have_selector menu_selector end diff --git a/spec/support/helpers/capybara_helpers.rb b/spec/support/helpers/capybara_helpers.rb index 5abbc1e2951..a7baa7042c9 100644 --- a/spec/support/helpers/capybara_helpers.rb +++ b/spec/support/helpers/capybara_helpers.rb @@ -42,4 +42,8 @@ module CapybaraHelpers def clear_browser_session page.driver.browser.manage.delete_cookie('_gitlab_session') end + + def javascript_test? + Capybara.current_driver == Capybara.javascript_driver + end end diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index 815337f8615..2cf3f4b83c4 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -1,7 +1,22 @@ # frozen_string_literal: true module SearchHelpers - def select_filter(name) - find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click + def submit_search(query, scope: nil) + page.within('.search-form, .search-page-form') do + field = find_field('search') + field.fill_in(with: query) + + if javascript_test? + field.send_keys(:enter) + else + click_button('Search') + end + end + end + + def select_search_scope(scope) + page.within '.search-filter' do + click_link scope + end end end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index dec7898d8d2..f364e4fd158 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -105,6 +105,10 @@ module StubConfiguration allow(Gitlab.config.gitlab_shell).to receive_messages(to_settings(messages)) end + def stub_asset_proxy_setting(messages) + allow(Gitlab.config.asset_proxy).to receive_messages(to_settings(messages)) + end + def stub_rack_attack_setting(messages) allow(Gitlab.config.rack_attack).to receive(:git_basic_auth).and_return(messages) allow(Gitlab.config.rack_attack.git_basic_auth).to receive_messages(to_settings(messages)) diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb index 3bb2f7c5b51..30dff1063b5 100644 --- a/spec/support/helpers/wait_for_requests.rb +++ b/spec/support/helpers/wait_for_requests.rb @@ -61,8 +61,4 @@ module WaitForRequests Capybara.page.evaluate_script('jQuery.active').zero? end - - def javascript_test? - Capybara.current_driver == Capybara.javascript_driver - end end diff --git a/spec/support/matchers/be_url.rb b/spec/support/matchers/be_url.rb index 69171f53891..388c1b384c7 100644 --- a/spec/support/matchers/be_url.rb +++ b/spec/support/matchers/be_url.rb @@ -1,11 +1,29 @@ # frozen_string_literal: true -RSpec::Matchers.define :be_url do |_| +# Assert that this value is a valid URL of at least one type. +# +# By default, this checks that the URL is either a HTTP or HTTPS URI, +# but you can check other URI schemes by passing the type, eg: +# +# ``` +# expect(value).to be_url(URI::FTP) +# ``` +# +# Pass an empty array of types if you want to match any URI scheme (be +# aware that this might not do what you think it does! `foo` is a valid +# URI, for instance). +RSpec::Matchers.define :be_url do |types = [URI::HTTP, URI::HTTPS]| match do |actual| - URI.parse(actual) rescue false + next false unless actual.present? + + uri = URI.parse(actual) + Array.wrap(types).any? { |t| uri.is_a?(t) } + rescue URI::InvalidURIError + false end end # looks better when used like: # expect(thing).to receive(:method).with(a_valid_url) RSpec::Matchers.alias_matcher :a_valid_url, :be_url +RSpec::Matchers.alias_matcher :be_http_url, :be_url diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb index 27f6d0570b6..468f25bfffe 100644 --- a/spec/support/services/clusters/create_service_shared.rb +++ b/spec/support/services/clusters/create_service_shared.rb @@ -32,56 +32,24 @@ shared_context 'invalid cluster create params' do end shared_examples 'create cluster service success' do - context 'namespace per environment feature is enabled' do - before do - stub_feature_flags(kubernetes_namespace_per_environment: true) - end - - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) - - expect { subject } - .to change { Clusters::Cluster.count }.by(1) - .and change { Clusters::Providers::Gcp.count }.by(1) - - expect(subject.name).to eq('test-cluster') - expect(subject.user).to eq(user) - expect(subject.project).to eq(project) - expect(subject.provider.gcp_project_id).to eq('gcp-project') - expect(subject.provider.zone).to eq('us-central1-a') - expect(subject.provider.num_nodes).to eq(1) - expect(subject.provider.machine_type).to eq('machine_type-a') - expect(subject.provider.access_token).to eq(access_token) - expect(subject.provider).to be_legacy_abac - expect(subject.platform).to be_nil - expect(subject.namespace_per_environment).to eq true - end - end - - context 'namespace per environment feature is disabled' do - before do - stub_feature_flags(kubernetes_namespace_per_environment: false) - end - - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) - - expect { subject } - .to change { Clusters::Cluster.count }.by(1) - .and change { Clusters::Providers::Gcp.count }.by(1) - - expect(subject.name).to eq('test-cluster') - expect(subject.user).to eq(user) - expect(subject.project).to eq(project) - expect(subject.provider.gcp_project_id).to eq('gcp-project') - expect(subject.provider.zone).to eq('us-central1-a') - expect(subject.provider.num_nodes).to eq(1) - expect(subject.provider.machine_type).to eq('machine_type-a') - expect(subject.provider.access_token).to eq(access_token) - expect(subject.provider).to be_legacy_abac - expect(subject.platform).to be_nil - expect(subject.namespace_per_environment).to eq false - end + it 'creates a cluster object and performs a worker' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Providers::Gcp.count }.by(1) + + expect(subject.name).to eq('test-cluster') + expect(subject.user).to eq(user) + expect(subject.project).to eq(project) + expect(subject.provider.gcp_project_id).to eq('gcp-project') + expect(subject.provider.zone).to eq('us-central1-a') + expect(subject.provider.num_nodes).to eq(1) + expect(subject.provider.machine_type).to eq('machine_type-a') + expect(subject.provider.access_token).to eq(access_token) + expect(subject.provider).to be_legacy_abac + expect(subject.platform).to be_nil + expect(subject.namespace_per_environment).to eq true end end diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb index dcc92dda950..a99068ab678 100644 --- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb @@ -103,7 +103,7 @@ RSpec.shared_examples 'chat slash commands service' do expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute) result = subject.trigger(params) - expect(result).to include(text: /^Whoops! This action is not allowed/) + expect(result).to include(text: /^You are not allowed/) end end end diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index 39d13cccb13..4bc22861d58 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -7,6 +7,8 @@ shared_examples 'handle uploads' do let(:secret) { FileUploader.generate_secret } let(:uploader_class) { FileUploader } + it_behaves_like 'handle uploads authorize' + describe "POST #create" do context 'when a user is not authorized to upload a file' do it 'returns 404 status' do @@ -271,7 +273,9 @@ shared_examples 'handle uploads' do end end end +end +shared_examples 'handle uploads authorize' do describe "POST #authorize" do context 'when a user is not authorized to upload a file' do it 'returns 404 status' do @@ -284,7 +288,12 @@ shared_examples 'handle uploads' do context 'when a user can upload a file' do before do sign_in(user) - model.add_developer(user) + + if model.is_a?(PersonalSnippet) + model.update!(author: user) + else + model.add_developer(user) + end end context 'and the request bypassed workhorse' do diff --git a/spec/support/shared_examples/lib/banzai/filters/reference_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/reference_filter_shared_examples.rb new file mode 100644 index 00000000000..b1ecd4fd007 --- /dev/null +++ b/spec/support/shared_examples/lib/banzai/filters/reference_filter_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'HTML text with references' do + let(:markdown_prepend) { "<img src=\"\" onerror=alert(`bug`)>" } + + it 'preserves escaped HTML text and adds valid references' do + reference = resource.to_reference(format: :name) + + doc = reference_filter("#{markdown_prepend}#{reference}") + + expect(doc.to_html).to start_with(markdown_prepend) + expect(doc.text).to eq %(<img src="" onerror=alert(`bug`)>#{resource_text}) + end + + it 'preserves escaped HTML text if there are no valid references' do + reference = "#{resource.class.reference_prefix}invalid" + text = "#{markdown_prepend}#{reference}" + + doc = reference_filter(text) + + expect(doc.to_html).to eq text + end +end diff --git a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb new file mode 100644 index 00000000000..9604555c57d --- /dev/null +++ b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb @@ -0,0 +1,8 @@ +shared_examples_for 'matches_cross_reference_regex? fails fast' do + it 'fails fast for long strings' do + # took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823 + expect do + Timeout.timeout(3.seconds) { mentionable.matches_cross_reference_regex? } + end.not_to raise_error + end +end diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb index dd338ea47c7..ad580b581d6 100644 --- a/spec/support/shared_examples/services/notification_service_shared_examples.rb +++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb @@ -52,3 +52,47 @@ shared_examples 'group emails are disabled' do should_email_anyone end end + +shared_examples 'sends notification only to a maximum of ten, most recently active group owners' do + let(:owners) { create_list(:user, 12, :with_sign_ins) } + + before do + owners.each do |owner| + group.add_owner(owner) + end + + reset_delivered_emails! + end + + context 'limit notification emails' do + it 'sends notification only to a maximum of ten, most recently active group owners' do + ten_most_recently_active_group_owners = owners.sort_by(&:last_sign_in_at).last(10) + + notification_trigger + + should_only_email(*ten_most_recently_active_group_owners) + end + end +end + +shared_examples 'sends notification only to a maximum of ten, most recently active project maintainers' do + let(:maintainers) { create_list(:user, 12, :with_sign_ins) } + + before do + maintainers.each do |maintainer| + project.add_maintainer(maintainer) + end + + reset_delivered_emails! + end + + context 'limit notification emails' do + it 'sends notification only to a maximum of ten, most recently active project maintainers' do + ten_most_recently_active_project_maintainers = maintainers.sort_by(&:last_sign_in_at).last(10) + + notification_trigger + + should_only_email(*ten_most_recently_active_project_maintainers) + end + end +end diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb index 66c064e3fba..5d521d18c70 100644 --- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb +++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb @@ -7,6 +7,7 @@ describe 'devise/shared/_signin_box' do assign(:ldap_servers, []) allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) allow(view).to receive(:captcha_enabled?).and_return(false) + allow(view).to receive(:captcha_on_login_required?).and_return(false) end it 'is shown when Crowd is enabled' do diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb index 47804411b9d..0da3470433c 100644 --- a/spec/views/groups/edit.html.haml_spec.rb +++ b/spec/views/groups/edit.html.haml_spec.rb @@ -23,7 +23,7 @@ describe 'groups/edit.html.haml' do render expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups") - expect(rendered).to have_css('.descr', text: 'help text here') + expect(rendered).to have_css('.js-descr', text: 'help text here') expect(rendered).to have_field('group_share_with_group_lock', checkbox_options) end end diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb new file mode 100644 index 00000000000..177ade3b700 --- /dev/null +++ b/spec/views/search/_results.html.haml_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'search/_results' do + before do + controller.params[:action] = 'show' + + 3.times { create(:issue) } + + @search_objects = Issue.page(1).per(2) + @scope = 'issues' + @search_term = 'foo' + end + + it 'displays the page size' do + render + + expect(rendered).to have_content('Showing 1 - 2 of 3 issues for "foo"') + end + + context 'when search results do not have a count' do + before do + @search_objects = @search_objects.without_count + end + + it 'does not display the page size' do + render + + expect(rendered).not_to have_content(/Showing .* of .*/) + end + end +end diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 20aacff75fd..41dd9eb5256 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -152,6 +152,7 @@ assert,1.4.1,MIT assign-symbols,1.0.0,MIT async-each,1.0.1,MIT async-limiter,1.0.0,MIT +atlassian-jwt,0.2.0,Apache 2.0 atob,2.1.2,(MIT OR Apache-2.0) atomic,1.1.99,Apache 2.0 attr_encrypted,3.1.0,MIT diff --git a/yarn.lock b/yarn.lock index c4fe112d833..6ab2aa24685 100644 --- a/yarn.lock +++ b/yarn.lock @@ -991,15 +991,15 @@ dependencies: vue-eslint-parser "^6.0.4" -"@gitlab/svgs@^1.70.0": - version "1.70.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.70.0.tgz#bdae478148c15d955fc06e69fd5d5ecae8298943" - integrity sha512-0uV9fgTwe17Fyy0hTcrsGX2jJuCrz3uRIe8yffuqc6pbQrSfYJyN66mfCCB45wq8lKTgOB5q0qcUyJx3RQEcKg== +"@gitlab/svgs@^1.71.0": + version "1.71.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.71.0.tgz#c8e6e8f500ea91e5cbba4ac08df533fb2e622a00" + integrity sha512-kkeNic/FFwaqKnzwio4NE7whBOZ/toRJ8cS0587DBotajAzSYhph5ij4TCY2GTjPa33zIJ5OUr/k90C0Kr71hQ== -"@gitlab/ui@5.19.0": - version "5.19.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.19.0.tgz#ef5431e0e91eb4009a30edcf49a40716cc2570d9" - integrity sha512-gCeTymtVzzO2uOWZGIweLM5JcHq3TN1QwP61CXw7b9Lp5A2MysRb8upehtR5d4JLOf/GQg8rHQB26uRvUc+AXQ== +"@gitlab/ui@5.20.2": + version "5.20.2" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.20.2.tgz#a51270d5a521e71059c5fd05f86cfc835f5e28ae" + integrity sha512-TSaD5Cz0YXBTsRtQwsa7LbS2O5h0CL3YkdYmBKrMZkphL76xQaN08ZImkQ5Xl8cD1ZiWN2CsTvoUbF19UP2V1w== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" @@ -1014,10 +1014,10 @@ vue "^2.6.10" vue-loader "^15.4.2" -"@gitlab/visual-review-tools@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.0.0.tgz#6012e0a19797c1f5dad34ccf4dacdaf38e400a73" - integrity sha512-xMvz9IwrXisQ1MH+Tj6lfbQcQSiQy88nTPuQV6OTLBGuV+vIQeVwXeIkQeTKuSpd0GqZvigPdRqxyQCa3blpIg== +"@gitlab/visual-review-tools@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.0.1.tgz#7e588328ed018d91560633d56865d65b72c3a11b" + integrity sha512-vNqpui0khtPi3crrrFtfuT+nw0SdD/nMyb+aurbJzc3RXuVJGCdgYwosvTLPcJkdMOVfTijizV5+ys75s8INBw== "@gitlab/vue-toasted@^1.2.1": version "1.2.1" |