Commit 8c759580 authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'master' into 'doc_api_settings'

# Conflicts:
#   doc/api/settings.md
parents 0020cb5f b76bc276
...@@ -2,6 +2,38 @@ ...@@ -2,6 +2,38 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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 ## 12.2.1
### Fixed (3 changes) ### Fixed (3 changes)
...@@ -591,6 +623,34 @@ entry. ...@@ -591,6 +623,34 @@ entry.
- Removes EE differences for app/views/admin/users/show.html.haml. - 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 ## 12.0.6
- No changes. - No changes.
......
import $ from 'jquery';
import { __ } from '~/locale';
import flash from '~/flash'; import flash from '~/flash';
import { s__, sprintf } from '~/locale';
// Renders math using KaTeX in any element with the // Renders math using KaTeX in any element with the
// `js-render-math` class // `js-render-math` class
...@@ -10,21 +9,131 @@ import flash from '~/flash'; ...@@ -10,21 +9,131 @@ import flash from '~/flash';
// <code class="js-render-math"></div> // <code class="js-render-math"></div>
// //
// Loop over all math elements and render math const MAX_MATH_CHARS = 1000;
function renderWithKaTeX(elements, katex) { const MAX_RENDER_TIME_MS = 2000;
elements.each(function katexElementsLoop() {
const mathNode = $('<span></span>'); // These messages might be used with inline errors in the future. Keep them around. For now, we will
const $this = $(this); // display a single error message using flash().
const display = $this.attr('data-math-style') === 'display'; // const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
try { // s__(
katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false }); // '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.',
mathNode.insertAfter($this); // ),
$this.remove(); // { maxChars: MAX_MATH_CHARS },
} catch (err) { // );
throw err; // 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 {
// 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) { export default function renderMath($els) {
...@@ -34,7 +143,8 @@ export default function renderMath($els) { ...@@ -34,7 +143,8 @@ export default function renderMath($els) {
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'), import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]) ])
.then(([katex]) => { .then(([katex]) => {
renderWithKaTeX($els, katex); const renderer = new SafeMathRenderer($els.get(), katex);
renderer.render();
}) })
.catch(() => flash(__('An error occurred while rendering KaTeX'))); .catch(() => {});
} }
<script> <script>
import Vue from 'vue'; import axios from '~/lib/utils/axios_utils';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store'; import boardsStore from '../../stores/boards_store';
export default Vue.extend({ export default {
props: { props: {
issue: { issue: {
type: Object, type: Object,
...@@ -35,7 +35,7 @@ export default Vue.extend({ ...@@ -35,7 +35,7 @@ export default Vue.extend({
} }
// Post the remove data // 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.')); Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => { lists.forEach(list => {
...@@ -71,7 +71,7 @@ export default Vue.extend({ ...@@ -71,7 +71,7 @@ export default Vue.extend({
return req; return req;
}, },
}, },
}); };
</script> </script>
<template> <template>
<div class="block list"> <div class="block list">
......
...@@ -55,7 +55,7 @@ export default class ClusterStore { ...@@ -55,7 +55,7 @@ export default class ClusterStore {
...applicationInitialState, ...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'), title: s__('ClusterIntegration|GitLab Runner'),
version: null, version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner', chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
updateAvailable: null, updateAvailable: null,
updateSuccessful: false, updateSuccessful: false,
updateFailed: false, updateFailed: false,
......
...@@ -60,7 +60,7 @@ class DropLab { ...@@ -60,7 +60,7 @@ class DropLab {
addEvents() { addEvents() {
this.eventWrapper.documentClicked = this.documentClicked.bind(this); this.eventWrapper.documentClicked = this.documentClicked.bind(this);
document.addEventListener('click', this.eventWrapper.documentClicked); document.addEventListener('mousedown', this.eventWrapper.documentClicked);
} }
documentClicked(e) { documentClicked(e) {
...@@ -74,7 +74,7 @@ class DropLab { ...@@ -74,7 +74,7 @@ class DropLab {
} }
removeEvents() { removeEvents() {
document.removeEventListener('click', this.eventWrapper.documentClicked); document.removeEventListener('mousedown', this.eventWrapper.documentClicked);
} }
changeHookList(trigger, list, plugins, config) { changeHookList(trigger, list, plugins, config) {
......
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
<template> <template>
<div class="flash-container flash-container-page" @click="clickFlash"> <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> <span v-html="message.text"> </span>
<button <button
v-if="message.action" v-if="message.action"
......
...@@ -89,7 +89,7 @@ export default { ...@@ -89,7 +89,7 @@ export default {
</script> </script>
<template> <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 <resizable-panel
v-show="isOpen" v-show="isOpen"
:collapsible="false" :collapsible="false"
...@@ -120,6 +120,7 @@ export default { ...@@ -120,6 +120,7 @@ export default {
}" }"
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
class="ide-sidebar-link is-right" class="ide-sidebar-link is-right"
type="button" type="button"
@click="clickTab($event, tab)" @click="clickTab($event, tab)"
......
...@@ -300,9 +300,9 @@ export default { ...@@ -300,9 +300,9 @@ export default {
this.closeRecaptcha(); this.closeRecaptcha();
}, },
deleteIssuable() { deleteIssuable(payload) {
this.service this.service
.deleteIssuable() .deleteIssuable(payload)
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
// Stop the poll so we don't get 404's with the issuable not existing // Stop the poll so we don't get 404's with the issuable not existing
......
...@@ -55,7 +55,7 @@ export default { ...@@ -55,7 +55,7 @@ export default {
if (window.confirm(confirmMessage)) { if (window.confirm(confirmMessage)) {
this.deleteLoading = true; this.deleteLoading = true;
eventHub.$emit('delete.issuable'); eventHub.$emit('delete.issuable', { destroy_confirm: true });
} }
}, },
}, },
......
...@@ -10,8 +10,8 @@ export default class Service { ...@@ -10,8 +10,8 @@ export default class Service {
return axios.get(this.realtimeEndpoint); return axios.get(this.realtimeEndpoint);
} }
deleteIssuable() { deleteIssuable(payload) {
return axios.delete(this.endpoint); return axios.delete(this.endpoint, { params: payload });
} }
updateIssuable(data) { updateIssuable(data) {
......
...@@ -113,7 +113,7 @@ export default class ProjectFindFile { ...@@ -113,7 +113,7 @@ export default class ProjectFindFile {
if (searchText) { if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText); matches = fuzzaldrinPlus.match(filePath, searchText);
} }
blobItemUrl = this.options.blobUrlTemplate + '/' + filePath; blobItemUrl = this.options.blobUrlTemplate + '/' + encodeURIComponent(filePath);
html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html)); results.push(this.element.find('.tree-table > tbody').append(html));
} }
......
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="card"> <div :id="release.tag_name" class="card">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mt-0"> <h2 class="card-title mt-0">
{{ release.name }} {{ release.name }}
......
...@@ -5,11 +5,7 @@ export const WARNING_MESSAGE_CLASS = 'warning_message'; ...@@ -5,11 +5,7 @@ export const WARNING_MESSAGE_CLASS = 'warning_message';
export const DANGER_MESSAGE_CLASS = 'danger_message'; export const DANGER_MESSAGE_CLASS = 'danger_message';
export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds'; 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 MT_MERGE_STRATEGY = 'merge_train';
export const AUTO_MERGE_STRATEGIES = [ export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY];
MWPS_MERGE_STRATEGY,
ATMTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
];
...@@ -3,7 +3,7 @@ import _ from 'underscore'; ...@@ -3,7 +3,7 @@ import _ from 'underscore';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps'; import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; 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 { export default class MergeRequestStore {
constructor(data) { constructor(data) {
...@@ -217,8 +217,8 @@ export default class MergeRequestStore { ...@@ -217,8 +217,8 @@ export default class MergeRequestStore {
} }
static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) { static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {
if (_.includes(availableAutoMergeStrategies, ATMTWPS_MERGE_STRATEGY)) { if (_.includes(availableAutoMergeStrategies, MTWPS_MERGE_STRATEGY)) {
return ATMTWPS_MERGE_STRATEGY; return MTWPS_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) { } else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) {
return MT_MERGE_STRATEGY; return MT_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) { } else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) {
......
...@@ -45,8 +45,7 @@ input[type='checkbox']:hover { ...@@ -45,8 +45,7 @@ input[type='checkbox']:hover {
border: 0; border: 0;
border-radius: $border-radius-default; border-radius: $border-radius-default;
transition: border-color ease-in-out $default-transition-duration, transition: border-color ease-in-out $default-transition-duration,
background-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration;
width ease-in-out $default-transition-duration;
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
width: $search-input-xl-width; width: $search-input-xl-width;
......
...@@ -6,6 +6,7 @@ module IssuableActions ...@@ -6,6 +6,7 @@ module IssuableActions
included do included do
before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_destroy_issuable!, only: :destroy
before_action :check_destroy_confirmation!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
before_action only: :show do before_action only: :show do
push_frontend_feature_flag(:scoped_labels, default_enabled: true) push_frontend_feature_flag(:scoped_labels, default_enabled: true)
...@@ -91,6 +92,33 @@ module IssuableActions ...@@ -91,6 +92,33 @@ module IssuableActions
end end
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 def bulk_update
result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name) result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name)
quantity = result[:count] quantity = result[:count]
...@@ -110,7 +138,7 @@ module IssuableActions ...@@ -110,7 +138,7 @@ module IssuableActions
end end
notes = prepare_notes_for_rendering(notes) 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) discussions = Discussion.build_collection(notes, issuable)
......
...@@ -29,7 +29,7 @@ module NotesActions ...@@ -29,7 +29,7 @@ module NotesActions
end end
notes = prepare_notes_for_rendering(notes) 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] = notes_json[:notes] =
if use_note_serializer? if use_note_serializer?
......
...@@ -127,4 +127,8 @@ module UploadsActions ...@@ -127,4 +127,8 @@ module UploadsActions
def model def model
strong_memoize(:model) { find_model } strong_memoize(:model) { find_model }
end end
def workhorse_authorize_request?
action_name == 'authorize'
end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class Groups::RunnersController < Groups::ApplicationController class Groups::RunnersController < Groups::ApplicationController
# Proper policies should be implemented per # Proper policies should be implemented per
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 # 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] before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
...@@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController ...@@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController
@runner ||= @group.runners.find(params[:id]) @runner ||= @group.runners.find(params[:id])
end end
def authorize_admin_pipeline!
return render_404 unless can?(current_user, :admin_pipeline, group)
end
def runner_params def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end end
......
...@@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :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_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 :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action :check_user_can_push_to_source_branch!, only: [:rebase]
...@@ -189,7 +190,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -189,7 +190,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def pipeline_status def pipeline_status
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.represent_status(@merge_request.head_pipeline) .represent_status(head_pipeline)
end end
def ci_environments_status def ci_environments_status
...@@ -239,6 +240,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -239,6 +240,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private 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? def ci_environments_status_on_merge_result?
params[:environment_target] == 'merge_commit' params[:environment_target] == 'merge_commit'
end end
...@@ -337,4 +345,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -337,4 +345,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: { status_reason: 'Unknown error' }, status: :internal_server_error render json: { status_reason: 'Unknown error' }, status: :internal_server_error
end end
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 end
...@@ -21,10 +21,13 @@ class SessionsController < Devise::SessionsController ...@@ -21,10 +21,13 @@ class SessionsController < Devise::SessionsController
prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? } 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 :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 before_action :load_recaptcha
after_action :log_failed_login, if: -> { action_name == 'new' && failed_login? } after_action :log_failed_login, if: :action_new_and_failed_login?
helper_method :captcha_enabled?
helper_method :captcha_enabled?, :captcha_on_login_required?
# protect_from_forgery is already prepended in ApplicationController but # protect_from_forgery is already prepended in ApplicationController but
# authenticate_with_two_factor which signs in the user is prepended before # authenticate_with_two_factor which signs in the user is prepended before
...@@ -38,6 +41,7 @@ class SessionsController < Devise::SessionsController ...@@ -38,6 +41,7 @@ class SessionsController < Devise::SessionsController
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
MAX_FAILED_LOGIN_ATTEMPTS = 5
def new def new
set_minimum_password_length set_minimum_password_length
...@@ -81,10 +85,14 @@ class SessionsController < Devise::SessionsController ...@@ -81,10 +85,14 @@ class SessionsController < Devise::SessionsController
request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled? request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
end 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 # From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
def check_captcha def check_captcha
return unless user_params[:password].present? return unless user_params[:password].present?
return unless captcha_enabled? return unless captcha_enabled? || captcha_on_login_required?
return unless Gitlab::Recaptcha.load_configurations! return unless Gitlab::Recaptcha.load_configurations!
if verify_recaptcha if verify_recaptcha
...@@ -126,10 +134,28 @@ class SessionsController < Devise::SessionsController ...@@ -126,10 +134,28 @@ class SessionsController < Devise::SessionsController
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}") Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end 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? def failed_login?
(options = request.env["warden.options"]) && options[:action] == "unauthenticated" (options = request.env["warden.options"]) && options[:action] == "unauthenticated"
end 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, # Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change. # and they require a password change.
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -240,6 +266,18 @@ class SessionsController < Devise::SessionsController ...@@ -240,6 +266,18 @@ class SessionsController < Devise::SessionsController
@ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers
end 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 def authentication_method
if user_params[:otp_attempt] if user_params[:otp_attempt]
"two-factor" "two-factor"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class UploadsController < ApplicationController class UploadsController < ApplicationController
include UploadsActions include UploadsActions
include WorkhorseRequest
UnknownUploadModelError = Class.new(StandardError) UnknownUploadModelError = Class.new(StandardError)
...@@ -21,7 +22,8 @@ class UploadsController < ApplicationController ...@@ -21,7 +22,8 @@ class UploadsController < ApplicationController
before_action :upload_mount_satisfied? before_action :upload_mount_satisfied?
before_action :find_model before_action :find_model
before_action :authorize_access!, only: [:show] 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 def uploader_class
PersonalFileUploader PersonalFileUploader
...@@ -72,7 +74,7 @@ class UploadsController < ApplicationController ...@@ -72,7 +74,7 @@ class UploadsController < ApplicationController
end end
def render_unauthorized def render_unauthorized
if current_user if current_user || workhorse_authorize_request?
render_404 render_404
else else
authenticate_user! authenticate_user!
......
...@@ -164,6 +164,10 @@ module ApplicationSettingsHelper ...@@ -164,6 +164,10 @@ module ApplicationSettingsHelper
:allow_local_requests_from_system_hooks, :allow_local_requests_from_system_hooks,
:dns_rebinding_protection_enabled, :dns_rebinding_protection_enabled,
:archive_builds_in_human_readable, :archive_builds_in_human_readable,
:asset_proxy_enabled,
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
...@@ -231,6 +235,7 @@ module ApplicationSettingsHelper ...@@ -231,6 +235,7 @@ module ApplicationSettingsHelper
:recaptcha_enabled, :recaptcha_enabled,
:recaptcha_private_key, :recaptcha_private_key,
:recaptcha_site_key, :recaptcha_site_key,
:login_recaptcha_protection_enabled,
:receive_max_input_size, :receive_max_input_size,
:repository_checks_enabled, :repository_checks_enabled,
:repository_storages, :repository_storages,
......
...@@ -90,6 +90,8 @@ module EmailsHelper ...@@ -90,6 +90,8 @@ module EmailsHelper
when MergeRequest when MergeRequest
merge_request = MergeRequest.find(closed_via[:id]).present merge_request = MergeRequest.find(closed_via[:id]).present
return "" unless Ability.allowed?(@recipient, :read_merge_request, merge_request)
case format case format
when :html when :html
merge_request_link = link_to(merge_request.to_reference, merge_request.web_url) merge_request_link = link_to(merge_request.to_reference, merge_request.web_url)
...@@ -102,6 +104,8 @@ module EmailsHelper ...@@ -102,6 +104,8 @@ module EmailsHelper
# Technically speaking this should be Commit but per # Technically speaking this should be Commit but per
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339 # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
# we can't deserialize Commit without custom serializer for ActiveJob # 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 } _("via %{closed_via}") % { closed_via: closed_via }
else else
"" ""
......
...@@ -71,7 +71,7 @@ module LabelsHelper ...@@ -71,7 +71,7 @@ module LabelsHelper
end end
def label_tooltip_title(label) def label_tooltip_title(label)
label.description Sanitize.clean(label.description)
end end
def suggested_colors def suggested_colors
......
...@@ -448,7 +448,7 @@ module ProjectsHelper ...@@ -448,7 +448,7 @@ module ProjectsHelper
def git_user_email def git_user_email
if current_user if current_user
current_user.email current_user.commit_email
else else
"your@email.com" "your@email.com"
end end
......
...@@ -34,6 +34,8 @@ module Emails ...@@ -34,6 +34,8 @@ module Emails
setup_issue_mail(issue_id, recipient_id, closed_via: closed_via) setup_issue_mail(issue_id, recipient_id, closed_via: closed_via)
@updated_by = User.find(updated_by_user_id) @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)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
......
...@@ -18,12 +18,19 @@ class ApplicationSetting < ApplicationRecord ...@@ -18,12 +18,19 @@ class ApplicationSetting < ApplicationRecord
# fix a lot of tests using allow_any_instance_of # fix a lot of tests using allow_any_instance_of
include ApplicationSettingImplementation 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 :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # 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_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # 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_url
ignore_column :koding_enabled ignore_column :koding_enabled
...@@ -75,11 +82,11 @@ class ApplicationSetting < ApplicationRecord ...@@ -75,11 +82,11 @@ class ApplicationSetting < ApplicationRecord
validates :recaptcha_site_key, validates :recaptcha_site_key,
presence: true, presence: true,
if: :recaptcha_enabled if: :recaptcha_or_login_protection_enabled
validates :recaptcha_private_key, validates :recaptcha_private_key,
presence: true, presence: true,
if: :recaptcha_enabled if: :recaptcha_or_login_protection_enabled
validates :akismet_api_key, validates :akismet_api_key,
presence: true, presence: true,
...@@ -192,6 +199,17 @@ class ApplicationSetting < ApplicationRecord ...@@ -192,6 +199,17 @@ class ApplicationSetting < ApplicationRecord
allow_nil: true, allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } 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| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
...@@ -292,4 +310,8 @@ class ApplicationSetting < ApplicationRecord ...@@ -292,4 +310,8 @@ class ApplicationSetting < ApplicationRecord
def self.cache_backend def self.cache_backend
Gitlab::ThreadMemoryCache.cache_backend Gitlab::ThreadMemoryCache.cache_backend
end end
def recaptcha_or_login_protection_enabled
recaptcha_enabled || login_recaptcha_protection_enabled
end
end end
...@@ -23,8 +23,9 @@ module ApplicationSettingImplementation ...@@ -23,8 +23,9 @@ module ApplicationSettingImplementation
akismet_enabled: false, akismet_enabled: false,
allow_local_requests_from_web_hooks_and_services: false, allow_local_requests_from_web_hooks_and_services: false,
allow_local_requests_from_system_hooks: true, 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 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, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days', default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
...@@ -33,7 +34,9 @@ module ApplicationSettingImplementation ...@@ -33,7 +34,9 @@ module ApplicationSettingImplementation
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'], default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], 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: [], disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
dsa_key_restriction: 0, dsa_key_restriction: 0,
ecdsa_key_restriction: 0, ecdsa_key_restriction: 0,
...@@ -52,9 +55,11 @@ module ApplicationSettingImplementation ...@@ -52,9 +55,11 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200, housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'], import_sources: Settings.gitlab['import_sources'],
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
mirror_available: true, mirror_available: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true, password_authentication_enabled_for_git: true,
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil, performance_bar_allowed_group_id: nil,
...@@ -63,7 +68,10 @@ module ApplicationSettingImplementation ...@@ -63,7 +68,10 @@ module ApplicationSettingImplementation
plantuml_url: nil, plantuml_url: nil,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
project_export_enabled: true, project_export_enabled: true,
protected_ci_variables: false,
raw_blob_request_limit: 300,
recaptcha_enabled: false, recaptcha_enabled: false,
login_recaptcha_protection_enabled: false,
repository_checks_enabled: true, repository_checks_enabled: true,
repository_storages: ['default'], repository_storages: ['default'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
...@@ -95,16 +103,10 @@ module ApplicationSettingImplementation ...@@ -95,16 +103,10 @@ module ApplicationSettingImplementation
user_default_internal_regex: nil, user_default_internal_regex: nil,
user_show_add_ssh_key_message: true, user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil, 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_collector_hostname: nil,
snowplow_cookie_domain: nil, snowplow_cookie_domain: nil,
snowplow_enabled: false, snowplow_enabled: false,
snowplow_site_id: nil, snowplow_site_id: nil
protected_ci_variables: false,
local_markdown_version: 0,
outbound_local_requests_whitelist: [],
raw_blob_request_limit: 300
} }
end end
...@@ -198,6 +200,15 @@ module ApplicationSettingImplementation ...@@ -198,6 +200,15 @@ module ApplicationSettingImplementation
end end
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 def repository_storages
Array(read_attribute(:repository_storages)) Array(read_attribute(:repository_storages))
end end
...@@ -306,6 +317,7 @@ module ApplicationSettingImplementation ...@@ -306,6 +317,7 @@ module ApplicationSettingImplementation
values values
.split(DOMAIN_LIST_SEPARATOR) .split(DOMAIN_LIST_SEPARATOR)
.map(&:strip)
.reject(&:empty?) .reject(&:empty?)
.uniq .uniq
end end
......
...@@ -203,6 +203,7 @@ module Ci ...@@ -203,6 +203,7 @@ module Ci
scope :for_sha, -> (sha) { where(sha: sha) } scope :for_sha, -> (sha) { where(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_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 :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 scope :triggered_by_merge_request, -> (merge_request) do
where(source: :merge_request_event, merge_request: merge_request) where(source: :merge_request_event, merge_request: merge_request)
......
...@@ -73,6 +73,7 @@ module Issuable ...@@ -73,6 +73,7 @@ module Issuable
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: 255 }
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true
validate :milestone_is_valid validate :milestone_is_valid
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
......
...@@ -365,6 +365,8 @@ class Group < Namespace ...@@ -365,6 +365,8 @@ class Group < Namespace
end end
def max_member_access_for_user(user) def max_member_access_for_user(user)
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.admin? return GroupMember::OWNER if user.admin?
members_with_parents members_with_parents
......
...@@ -178,7 +178,7 @@ class Issue < ApplicationRecord ...@@ -178,7 +178,7 @@ class Issue < ApplicationRecord
end end
def moved? def moved?
!moved_to.nil? !moved_to_id.nil?
end end
def can_move?(user, to_project = nil) def can_move?(user, to_project = nil)
......
...@@ -199,7 +199,11 @@ class Label < ApplicationRecord ...@@ -199,7 +199,11 @@ class Label < ApplicationRecord
end end
def title=(value) 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 end
## ##
...@@ -260,7 +264,7 @@ class Label < ApplicationRecord ...@@ -260,7 +264,7 @@ class Label < ApplicationRecord
end end
end end
def sanitize_title(value) def sanitize_value(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s)) CGI.unescapeHTML(Sanitize.clean(value.to_s))
end end
......
...@@ -89,6 +89,7 @@ class Note < ApplicationRecord ...@@ -89,6 +89,7 @@ class Note < ApplicationRecord
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true validates :note, presence: true
validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }
validates :project, presence: true, if: :for_project_noteable? validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
...@@ -331,6 +332,10 @@ class Note < ApplicationRecord ...@@ -331,6 +332,10 @@ class Note < ApplicationRecord
cross_reference? && !all_referenced_mentionables_allowed?(user) cross_reference? && !all_referenced_mentionables_allowed?(user)
end end
def visible_for?(user)
!cross_reference_not_visible_for?(user)
end
def award_emoji? def award_emoji?
can_be_award_emoji? && contains_emoji_only? can_be_award_emoji? && contains_emoji_only?
end end
......
...@@ -61,6 +61,8 @@ class Project < ApplicationRecord ...@@ -61,6 +61,8 @@ class Project < ApplicationRecord
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
:merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?, :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 to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
......
...@@ -64,7 +64,12 @@ class JiraService < IssueTrackerService ...@@ -64,7 +64,12 @@ class JiraService < IssueTrackerService
end end
def client 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 end
def help def help
......
...@@ -200,6 +200,7 @@ class RemoteMirror < ApplicationRecord ...@@ -200,6 +200,7 @@ class RemoteMirror < ApplicationRecord
result.password = '*****' if result.password result.password = '*****' if result.password
result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user
result.to_s result.to_s
rescue URI::Error
end end
def ensure_remote! def ensure_remote!
......
...@@ -239,13 +239,13 @@ class Repository ...@@ -239,13 +239,13 @@ class Repository
def branch_exists?(branch_name) def branch_exists?(branch_name)
return false unless raw_repository return false unless raw_repository
branch_names_include?(branch_name) branch_names.include?(branch_name)
end end
def tag_exists?(tag_name) def tag_exists?(tag_name)
return false unless raw_repository return false unless raw_repository
tag_names_include?(tag_name) tag_names.include?(tag_name)
end end
def ref_exists?(ref) def ref_exists?(ref)
...@@ -565,10 +565,10 @@ class Repository ...@@ -565,10 +565,10 @@ class Repository
end end
delegate :branch_names, to: :raw_repository 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 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 delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0 cache_method :branch_count, fallback: 0
...@@ -1130,10 +1130,6 @@ class Repository ...@@ -1130,10 +1130,6 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self) @cache ||= Gitlab::RepositoryCache.new(self)
end end
def redis_set_cache
@redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
end
def request_store_cache def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end end
......
...@@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord ...@@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord
TYPES_WITH_CROSS_REFERENCES = %w[ TYPES_WITH_CROSS_REFERENCES = %w[
commit cross_reference commit cross_reference
close duplicate close duplicate
moved moved merge
].freeze ].freeze
ICON_TYPES = %w[ ICON_TYPES = %w[
......
...@@ -645,6 +645,13 @@ class User < ApplicationRecord ...@@ -645,6 +645,13 @@ class User < ApplicationRecord
end end
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 def unique_email
if !emails.exists?(email: email) && Email.exists?(email: email) if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, _('has already been taken')) errors.add(:email, _('has already been taken'))
......
...@@ -5,6 +5,8 @@ class IssuePolicy < IssuablePolicy ...@@ -5,6 +5,8 @@ class IssuePolicy < IssuablePolicy
# Make sure to sync this class checks with issue.rb to avoid security problems. # Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
extend ProjectPolicy::ClassMethods
desc "User can read confidential issues" desc "User can read confidential issues"
condition(:can_read_confidential) do condition(:can_read_confidential) do
@user && IssueCollection.new([@subject]).visible_to(@user).any? @user && IssueCollection.new([@subject]).visible_to(@user).any?
...@@ -14,13 +16,12 @@ class IssuePolicy < IssuablePolicy ...@@ -14,13 +16,12 @@ class IssuePolicy < IssuablePolicy
condition(:confidential, scope: :subject) { @subject.confidential? } condition(:confidential, scope: :subject) { @subject.confidential? }
rule { confidential & ~can_read_confidential }.policy do rule { confidential & ~can_read_confidential }.policy do
prevent :read_issue prevent(*create_read_update_admin_destroy(:issue))
prevent :read_issue_iid prevent :read_issue_iid
prevent :update_issue
prevent :admin_issue
prevent :create_note
end end
rule { ~can?(:read_issue) }.prevent :create_note
rule { locked }.policy do rule { locked }.policy do
prevent :reopen_issue prevent :reopen_issue
end end
......
...@@ -4,4 +4,10 @@ class MergeRequestPolicy < IssuablePolicy ...@@ -4,4 +4,10 @@ class MergeRequestPolicy < IssuablePolicy
rule { locked }.policy do rule { locked }.policy do
prevent :reopen_merge_request prevent :reopen_merge_request
end 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 end
...@@ -6,6 +6,8 @@ module ApplicationSettings ...@@ -6,6 +6,8 @@ module ApplicationSettings
attr_reader :params, :application_setting 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 def execute
validate_classification_label(application_setting, :external_authorization_service_default_label) unless bypass_external_auth? validate_classification_label(application_setting, :external_authorization_service_default_label) unless bypass_external_auth?
...@@ -25,7 +27,13 @@ module ApplicationSettings ...@@ -25,7 +27,13 @@ module ApplicationSettings
params[:usage_stats_set_by_user_id] = current_user.id params[:usage_stats_set_by_user_id] = current_user.id
end 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 end
private private
...@@ -41,6 +49,11 @@ module ApplicationSettings ...@@ -41,6 +49,11 @@ module ApplicationSettings
@application_setting.add_to_outbound_local_requests_whitelist(values_array) @application_setting.add_to_outbound_local_requests_whitelist(values_array)
end end
def invalidate_markdown_cache?
!params.key?(:local_markdown_version) &&
(@application_setting.changes.keys & MARKDOWN_CACHE_INVALIDATING_PARAMS).any?
end
def update_terms(terms) def update_terms(terms)
return unless terms.present? return unless terms.present?
......
...@@ -44,6 +44,10 @@ class BaseService ...@@ -44,6 +44,10 @@ class BaseService
model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator") model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
end end
def visibility_level
params[:visibility].is_a?(String) ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level]
end
private private
def error(message, http_status = nil) def error(message, http_status = nil)
......
...@@ -24,16 +24,16 @@ module ChatNames ...@@ -24,16 +24,16 @@ module ChatNames
end end
def chat_name_token def chat_name_token
Gitlab::ChatNameToken.new @chat_name_token ||= Gitlab::ChatNameToken.new
end end
def chat_name_params def chat_name_params
{ {
service_id: @service.id, service_id: @service.id,
team_id: @params[:team_id], team_id: @params[:team_id],
team_domain: @params[:team_domain], team_domain: @params[:team_domain],
chat_id: @params[:user_id], chat_id: @params[:user_id],
chat_name: @params[:user_name] chat_name: @params[:user_name]
} }
end end
end end
......
...@@ -15,7 +15,8 @@ module Ci ...@@ -15,7 +15,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create, 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) 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 @pipeline = Ci::Pipeline.new
......
...@@ -2,24 +2,7 @@ ...@@ -2,24 +2,7 @@
module Clusters module Clusters
module Applications module Applications
class CheckInstallationProgressService < BaseHelmService class CheckInstallationProgressService < CheckProgressService
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
private private
def operation_in_progress? def operation_in_progress?
...@@ -32,10 +15,6 @@ module Clusters ...@@ -32,10 +15,6 @@ module Clusters
remove_installation_pod remove_installation_pod
end end
def on_failed
app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.")
end
def check_timeout def check_timeout
if timed_out? if timed_out?
begin begin
...@@ -54,18 +33,6 @@ module Clusters ...@@ -54,18 +33,6 @@ module Clusters
def timed_out? def timed_out?
Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end 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 end
end end
# 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
...@@ -2,26 +2,13 @@ ...@@ -2,26 +2,13 @@
module Clusters module Clusters
module Applications module Applications
class CheckUninstallProgressService < BaseHelmService class CheckUninstallProgressService < CheckProgressService
def execute private
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)
app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code }) def operation_in_progress?
app.uninstalling?
end end
private
def on_success def on_success
app.post_uninstall app.post_uninstall
app.destroy! app.destroy!
...@@ -31,10 +18,6 @@ module Clusters ...@@ -31,10 +18,6 @@ module Clusters
remove_installation_pod remove_installation_pod
end 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 def check_timeout
if timed_out? if timed_out?
app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) 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 ...@@ -50,14 +33,6 @@ module Clusters
def timed_out? def timed_out?
Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
end end
def remove_installation_pod
helm_api.delete_pod!(pod_name)
end
def installation_phase
helm_api.status(pod_name)
end
end end
end end
end end
...@@ -12,7 +12,7 @@ class CreateSnippetService < BaseService ...@@ -12,7 +12,7 @@ class CreateSnippetService < BaseService
PersonalSnippet.new(params) PersonalSnippet.new(params)
end 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) deny_visibility_level(snippet)
return snippet return snippet
end end
......
...@@ -68,9 +68,5 @@ module Groups ...@@ -68,9 +68,5 @@ module Groups
true true
end end
def visibility_level
params[:visibility].present? ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level]
end
end end
end end
...@@ -5,9 +5,11 @@ module Projects ...@@ -5,9 +5,11 @@ module Projects
include ValidatesClassificationLabel include ValidatesClassificationLabel
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki) @skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme)) @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 end
def execute def execute
...@@ -15,14 +17,11 @@ module Projects ...@@ -15,14 +17,11 @@ module Projects
return ::Projects::CreateFromTemplateService.new(current_user, params).execute return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end end
import_data = params.delete(:import_data)
relations_block = params.delete(:relations_block)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level # Make sure that the user is allowed to use the specified visibility level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level) if project_visibility.restricted?
deny_visibility_level(@project) deny_visibility_level(@project, project_visibility.visibility_level)
return @project return @project
end end
...@@ -44,7 +43,7 @@ module Projects ...@@ -44,7 +43,7 @@ module Projects
@project.namespace_id = current_user.namespace_id @project.namespace_id = current_user.namespace_id
end end
relations_block&.call(@project) @relations_block&.call(@project)
yield(@project) if block_given? yield(@project) if block_given?
validate_classification_label(@project, :external_authorization_classification_label) validate_classification_label(@project, :external_authorization_classification_label)
...@@ -54,7 +53,7 @@ module Projects ...@@ -54,7 +53,7 @@ module Projects
@project.creator = current_user @project.creator = current_user
save_project_and_import_data(import_data) save_project_and_import_data
after_create_actions if @project.persisted? after_create_actions if @project.persisted?
...@@ -129,9 +128,9 @@ module Projects ...@@ -129,9 +128,9 @@ module Projects
!@project.feature_available?(:wiki, current_user) || @skip_wiki !@project.feature_available?(:wiki, current_user) || @skip_wiki
end end
def save_project_and_import_data(import_data) def save_project_and_import_data
Project.transaction do 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 if @project.save
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
...@@ -192,5 +191,11 @@ module Projects ...@@ -192,5 +191,11 @@ module Projects
fail(error: @project.errors.full_messages.join(', ')) fail(error: @project.errors.full_messages.join(', '))
end end
end end
def project_visibility
@project_visibility ||= Gitlab::VisibilityLevelChecker
.new(current_user, @project, project_params: { import_data: @import_data })
.level_restricted?
end
end end
end end
...@@ -314,11 +314,9 @@ class TodoService ...@@ -314,11 +314,9 @@ class TodoService
end end
def reject_users_without_access(users, parent, target) def reject_users_without_access(users, parent, target)
if target.is_a?(Note) && target.for_issuable? target = target.noteable if target.is_a?(Note)
target = target.noteable
end
if target.is_a?(Issuable) if target.respond_to?(:to_ability_name)
select_users(users, :"read_#{target.to_ability_name}", target) select_users(users, :"read_#{target.to_ability_name}", target)
else else
select_users(users, :read_project, parent) select_users(users, :read_project, parent)
......
...@@ -12,7 +12,7 @@ class UpdateSnippetService < BaseService ...@@ -12,7 +12,7 @@ class UpdateSnippetService < BaseService
def execute def execute
# check that user is allowed to set specified visibility_level # 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 if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
......
...@@ -6,6 +6,10 @@ class PersonalFileUploader < FileUploader ...@@ -6,6 +6,10 @@ class PersonalFileUploader < FileUploader
options.storage_path options.storage_path
end end
def self.workhorse_local_upload_path
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
end
def self.base_dir(model, _store = nil) def self.base_dir(model, _store = nil)
# base_dir is the path seen by the user when rendering Markdown, so # 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 # it should be the same for both local and object storage. It is
......
...@@ -7,11 +7,15 @@ ...@@ -7,11 +7,15 @@
= f.check_box :recaptcha_enabled, class: 'form-check-input' = f.check_box :recaptcha_enabled, class: 'form-check-input'
= f.label :recaptcha_enabled, class: 'form-check-label' do = f.label :recaptcha_enabled, class: 'form-check-label' do
Enable reCAPTCHA 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 %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 .form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold' = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold'
= f.text_field :recaptcha_site_key, class: 'form-control' = f.text_field :recaptcha_site_key, class: 'form-control'
...@@ -21,6 +25,7 @@ ...@@ -21,6 +25,7 @@
.form-group .form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold' = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold'
.form-group
= f.text_field :recaptcha_private_key, class: 'form-control' = f.text_field :recaptcha_private_key, class: 'form-control'
.form-group .form-group
......
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' } %button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand') = expanded_by_default? ? _('Collapse') : _('Expand')
%p %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 .settings-content
= render 'spam' = render 'spam'
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
- else - else
= link_to _('Forgot your password?'), new_password_path(:user) = link_to _('Forgot your password?'), new_password_path(:user)
%div %div
- if captcha_enabled? - if captcha_enabled? || captcha_on_login_required?
= recaptcha_tags = recaptcha_tags
.submit-container.move-submit-down .submit-container.move-submit-down
......
<%= 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) if @issue.assignees.any? %>
<%= assignees_label(@issue) %>
<% end %>
<% if @issue.description -%> <%= @issue.description %>
<%= @issue.description %>
<% end %>
<%= @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') %> <%= merge_path_description(@merge_request, 'to') %>
<%= 'Author:' %> <%= @merge_request.author_name %> <%= '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 %> <%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
<%= @merge_request.description %> <%= @merge_request.description %>
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
- @project.remote_mirrors.each_with_index do |mirror, index| - @project.remote_mirrors.each_with_index do |mirror, index|
- next if mirror.new_record? - next if mirror.new_record?
%tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } %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= _('Push')
%td %td
= mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.footer-block.row-content-block .footer-block.row-content-block
= service_save_button(@service) = service_save_button(@service)
&nbsp; &nbsp;
= 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) - if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr %hr
......
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-4 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
Project services = s_("ProjectService|Project services")
%p Project services allow you to integrate GitLab with other applications %p= s_("ProjectService|Project services allow you to integrate GitLab with other applications")
.col-lg-8 .col-lg-8
%table.table %table.table
%colgroup %colgroup
...@@ -13,12 +13,12 @@ ...@@ -13,12 +13,12 @@
%thead %thead
%tr %tr
%th %th
%th Service %th= s_("ProjectService|Service")
%th.d-none.d-sm-block Description %th.d-none.d-sm-block= _("Description")
%th Last edit %th= s_("ProjectService|Last edit")
- @services.sort_by(&:title).each do |service| - @services.sort_by(&:title).each do |service|
%tr %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? = boolean_to_icon service.activated?
%td %td
= link_to edit_project_service_path(@project, service.to_param) do = link_to edit_project_service_path(@project, service.to_param) do
......
- breadcrumb_title "Integrations" - breadcrumb_title s_("ProjectService|Integrations")
- page_title @service.title, "Services" - page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs("Settings", edit_project_path(@project)) - add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
= render 'deprecated_message' if @service.deprecation_message = render 'deprecated_message' if @service.deprecation_message
......
- 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 %ul.list-unstyled.indent-list
%li %li
1. 1.
...@@ -18,67 +18,67 @@ ...@@ -18,67 +18,67 @@
.help-form .help-form
.form-group .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 .col-12.input-group
= text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#display_name', class: 'input-group-text') = clipboard_button(target: '#display_name', class: 'input-group-text')
.form-group .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 .col-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#description', class: 'input-group-text') = clipboard_button(target: '#description', class: 'input-group-text')
.form-group .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 .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 %p
Suggestions: = s_('MattermostService|Suggestions:')
%code= 'gitlab' %code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes %code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path %code= @project.full_path
.form-group .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 .col-12.input-group
= text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#request_url', class: 'input-group-text') = clipboard_button(target: '#request_url', class: 'input-group-text')
.form-group .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 .col-12 POST
.form-group .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 .col-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#response_username', class: 'input-group-text') = clipboard_button(target: '#response_username', class: 'input-group-text')
.form-group .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 .col-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#response_icon', class: 'input-group-text') = clipboard_button(target: '#response_icon', class: 'input-group-text')
.form-group .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 .col-12 Yes
.form-group .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 .col-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#autocomplete_hint', class: 'input-group-text') = clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
.form-group .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 .col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
......
...@@ -3,14 +3,12 @@ ...@@ -3,14 +3,12 @@
.info-well .info-well
.well-segment .well-segment
%p %p
This service allows users to perform common operations on this = s_("MattermostService|This service allows users to perform common operations on this project by entering slash commands in Mattermost.")
project by entering slash commands in Mattermost.
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do = 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) = sprite_icon('external-link', size: 16)
%p.inline %p.inline
See list of available commands in Mattermost after setting up this service, = s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering")
by entering
%kbd.inline /&lt;trigger&gt; help %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template? - unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
......
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
.col-sm-9.offset-sm-3 .col-sm-9.offset-sm-3
= link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do = link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15) = custom_icon('mattermost_logo', size: 15)
Add to Mattermost = s_("MattermostService|Add to Mattermost")
...@@ -4,17 +4,15 @@ ...@@ -4,17 +4,15 @@
.info-well .info-well
.well-segment .well-segment
%p %p
This service allows users to perform common operations on this = s_("SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack.")
project by entering slash commands in Slack.
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do = 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) = sprite_icon('external-link', size: 16)
%p.inline %p.inline
See list of available commands in Slack after setting up this service, = s_("SlackService|See list of available commands in Slack after setting up this service, by entering")
by entering
%kbd.inline /&lt;command&gt; help %kbd.inline /&lt;command&gt; help
- unless @service.template? - unless @service.template?
%p To set up this service: %p= _("To set up this service:")
%ul.list-unstyled.indent-list %ul.list-unstyled.indent-list
%li %li
1. 1.
...@@ -27,11 +25,11 @@ ...@@ -27,11 +25,11 @@
.help-form .help-form
.form-group .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 .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 %p
Suggestions: = _("Suggestions:")
%code= 'gitlab' %code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes %code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path %code= @project.full_path
...@@ -44,44 +42,44 @@ ...@@ -44,44 +42,44 @@
= clipboard_button(target: '#url', class: 'input-group-text') = clipboard_button(target: '#url', class: 'input-group-text')
.form-group .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 .col-12 POST
.form-group .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 .col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text') = clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group .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 .col-12
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36, class: 'mr-3') = 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 .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 .col-12 Show this command in the autocomplete list
.form-group .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 .col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text') = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group .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 .col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group .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 .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 .input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text') = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
...@@ -89,12 +87,6 @@ ...@@ -89,12 +87,6 @@
%ul.list-unstyled.indent-list %ul.list-unstyled.indent-list
%li %li
2. Paste the = s_("SlackService|2. Paste the <strong>Token</strong> into the field below").html_safe
%strong Token
into the field below
%li %li
3. Select the = s_("SlackService|3. Select the <strong>Active</strong> checkbox, press <strong>Save changes</strong> and start using GitLab inside Slack!").html_safe
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
%p= current_user_empty_message_description %p= current_user_empty_message_description
- if secondary_button_link.present? - 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' = link_to primary_button_label, primary_button_link, class: 'btn btn-success'
- else - else
......
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else - else
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) - 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' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
%span.append-right-10 %span.append-right-10
......
---
title: Fix encoding of special characters in "Find File"
merge_request: 31311
author: Jan Beckmann
type: fixed
---
title: make test of note app with comments disabled dry
merge_request: 32383
author: Romain Maneschi
type: other
---
title: Cache branch and tag names as Redis sets
merge_request: 30476
author:
type: performance
---
title: Default clusters namespace_per_environment column to true
merge_request: 32139
author:
type: other
---
title: Use moved instead of closed in issue references
merge_request: 32277
author: juliette-derancourt
type: changed
---
title: delete animation width on global search input
merge_request: 32399
author: Romain Maneschi
type: other
---
title: Ensure only authorised users can create notes on Merge Requests and Issues
type: security
---
title: Add a close issue slack slash command
merge_request: 32150
author:
type: added
---
title: Fix style of secondary profile tab buttons.
merge_request: 32010
author: Wolfgang Faust
type: fixed
---
title: Fix dropdowns closing when click is released outside the dropdown
merge_request: 32084
author:
type: fixed
---
title: Allow project feature permissions to be overridden during import with override_params
merge_request: 32348
author:
type: fixed
---
title: Handle invalid mirror url
merge_request: 32353
author: Lee Tickett
type: fixed
---
title: Remove vue resource from remove issue
merge_request: 32425
author: Lee Tickett
type: other
---
title: Use new location for gitlab-runner helm charts
merge_request: 32384
author:
type: other
---
title: Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
merge_request:
author:
type: security
---
title: Speed up regexp in namespace format by failing fast after reaching maximum namespace depth
merge_request:
author:
type: security
---
title: Limit the size of issuable description and comments
merge_request:
author:
type: security
---
title: Send TODOs for comments on commits correctly
merge_request:
author:
type: security
---
title: Restrict MergeRequests#test_reports to authenticated users with read-access
on Builds
merge_request:
author:
type: security
---
title: Added image proxy to mitigate potential stealing of IP addresses
merge_request:
author:
type: security
---
title: Filter out old system notes for epics in notes api endpoint response
merge_request:
author:
type: security
---
title: Avoid exposing unaccessible repo data upon GFM post processing
merge_request:
author:
type: security
---
title: Fix HTML injection for label description
merge_request:
author:
type: security
---
title: Make sure HTML text is always escaped when replacing label/milestone references.
merge_request:
author:
type: security
---
title: Prevent DNS rebind on JIRA service integration
merge_request:
author:
type: security
---
title: "Gitaly: ignore git redirects"
merge_request:
author:
type: security
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment