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 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 12.2.3
### Security (22 changes)
- Ensure only authorised users can create notes on Merge Requests and Issues.
- Gitaly: ignore git redirects.
- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth.
- Limit the size of issuable description and comments.
- Send TODOs for comments on commits correctly.
- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds.
- Added image proxy to mitigate potential stealing of IP addresses.
- Filter out old system notes for epics in notes api endpoint response.
- Avoid exposing unaccessible repo data upon GFM post processing.
- Fix HTML injection for label description.
- Make sure HTML text is always escaped when replacing label/milestone references.
- Prevent DNS rebind on JIRA service integration.
- Use admin_group authorization in Groups::RunnersController.
- Prevent disclosure of merge request ID via email.
- Show cross-referenced MR-id in issues' activities only to authorized users.
- Enforce max chars and max render time in markdown math.
- Check permissions before responding in MergeController#pipeline_status.
- Remove EXIF from users/personal snippet uploads.
- Fix project import restricted visibility bypass via API.
- Fix weak session management by clearing password reset tokens after login (username/email) are updated.
- Fix SSRF via DNS rebinding in Kubernetes Integration.
## 12.2.2
- Unreleased due to QA failure.
## 12.2.1
### Fixed (3 changes)
......@@ -591,6 +623,34 @@ entry.
- Removes EE differences for app/views/admin/users/show.html.haml.
## 12.0.7
### Security (22 changes)
- Ensure only authorised users can create notes on Merge Requests and Issues.
- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
- Queries for Upload should be scoped by model.
- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth.
- Limit the size of issuable description and comments.
- Send TODOs for comments on commits correctly.
- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds.
- Added image proxy to mitigate potential stealing of IP addresses.
- Filter out old system notes for epics in notes api endpoint response.
- Avoid exposing unaccessible repo data upon GFM post processing.
- Fix HTML injection for label description.
- Make sure HTML text is always escaped when replacing label/milestone references.
- Prevent DNS rebind on JIRA service integration.
- Use admin_group authorization in Groups::RunnersController.
- Prevent disclosure of merge request ID via email.
- Show cross-referenced MR-id in issues' activities only to authorized users.
- Enforce max chars and max render time in markdown math.
- Check permissions before responding in MergeController#pipeline_status.
- Remove EXIF from users/personal snippet uploads.
- Fix project import restricted visibility bypass via API.
- Fix weak session management by clearing password reset tokens after login (username/email) are updated.
- Fix SSRF via DNS rebinding in Kubernetes Integration.
## 12.0.6
- No changes.
......
import $ from 'jquery';
import { __ } from '~/locale';
import flash from '~/flash';
import { s__, sprintf } from '~/locale';
// Renders math using KaTeX in any element with the
// `js-render-math` class
......@@ -10,21 +9,131 @@ import flash from '~/flash';
// <code class="js-render-math"></div>
//
// Loop over all math elements and render math
function renderWithKaTeX(elements, katex) {
elements.each(function katexElementsLoop() {
const mathNode = $('<span></span>');
const $this = $(this);
const MAX_MATH_CHARS = 1000;
const MAX_RENDER_TIME_MS = 2000;
// These messages might be used with inline errors in the future. Keep them around. For now, we will
// display a single error message using flash().
// const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
// s__(
// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.',
// ),
// { maxChars: MAX_MATH_CHARS },
// );
// const RENDER_TIME_EXCEEDED_MSG = s__(
// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.",
// );
const RENDER_FLASH_MSG = sprintf(
s__(
'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.',
),
{ maxChars: MAX_MATH_CHARS },
);
// Wait for the browser to reflow the layout. Reflowing SVG takes time.
// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object".
const waitForReflow = fn => {
window.requestAnimationFrame(fn);
};
/**
* Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
*/
class SafeMathRenderer {
/*
How this works:
The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG.
During this time, the JS is blocked and the page becomes unresponsive.
We want to render math blocks one by one until a certain time is exceeded, after which we stop
rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an
asynchronous task, so we can't time it synchronously.
SafeMathRenderer essentially does the following:
1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice.
2. Places each placeholder element in a queue.
3. Renders the element at the head of the queue and waits for reflow.
4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty.
*/
queue = [];
totalMS = 0;
constructor(elements, katex) {
this.elements = elements;
this.katex = katex;
this.renderElement = this.renderElement.bind(this);
this.render = this.render.bind(this);
}
renderElement() {
if (!this.queue.length) {
return;
}
const el = this.queue.shift();
const text = el.textContent;
el.removeAttribute('style');
if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) {
if (!this.flashShown) {
flash(RENDER_FLASH_MSG);
this.flashShown = true;
}
// Show unrendered math code
const codeElement = document.createElement('pre');
codeElement.className = 'code';
codeElement.textContent = el.textContent;
el.parentNode.replaceChild(codeElement, el);
// Render the next math
this.renderElement();
} else {
this.startTime = Date.now();
const display = $this.attr('data-math-style') === 'display';
try {
katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
throw err;
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) {
......@@ -34,7 +143,8 @@ export default function renderMath($els) {
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
])
.then(([katex]) => {
renderWithKaTeX($els, katex);
const renderer = new SafeMathRenderer($els.get(), katex);
renderer.render();
})
.catch(() => flash(__('An error occurred while rendering KaTeX')));
.catch(() => {});
}
<script>
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store';
export default Vue.extend({
export default {
props: {
issue: {
type: Object,
......@@ -35,7 +35,7 @@ export default Vue.extend({
}
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
axios.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => {
......@@ -71,7 +71,7 @@ export default Vue.extend({
return req;
},
},
});
};
</script>
<template>
<div class="block list">
......
......@@ -55,7 +55,7 @@ export default class ClusterStore {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
updateAvailable: null,
updateSuccessful: false,
updateFailed: false,
......
......@@ -60,7 +60,7 @@ class DropLab {
addEvents() {
this.eventWrapper.documentClicked = this.documentClicked.bind(this);
document.addEventListener('click', this.eventWrapper.documentClicked);
document.addEventListener('mousedown', this.eventWrapper.documentClicked);
}
documentClicked(e) {
......@@ -74,7 +74,7 @@ class DropLab {
}
removeEvents() {
document.removeEventListener('click', this.eventWrapper.documentClicked);
document.removeEventListener('mousedown', this.eventWrapper.documentClicked);
}
changeHookList(trigger, list, plugins, config) {
......
......@@ -44,7 +44,7 @@ export default {
<template>
<div class="flash-container flash-container-page" @click="clickFlash">
<div class="flash-alert">
<div class="flash-alert" data-qa-selector="flash_alert">
<span v-html="message.text"> </span>
<button
v-if="message.action"
......
......@@ -89,7 +89,7 @@ export default {
</script>
<template>
<div class="multi-file-commit-panel ide-right-sidebar">
<div class="multi-file-commit-panel ide-right-sidebar" data-qa-selector="ide_right_sidebar">
<resizable-panel
v-show="isOpen"
:collapsible="false"
......@@ -120,6 +120,7 @@ export default {
}"
data-container="body"
data-placement="left"
:data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
class="ide-sidebar-link is-right"
type="button"
@click="clickTab($event, tab)"
......
......@@ -300,9 +300,9 @@ export default {
this.closeRecaptcha();
},
deleteIssuable() {
deleteIssuable(payload) {
this.service
.deleteIssuable()
.deleteIssuable(payload)
.then(res => res.data)
.then(data => {
// Stop the poll so we don't get 404's with the issuable not existing
......
......@@ -55,7 +55,7 @@ export default {
if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
eventHub.$emit('delete.issuable', { destroy_confirm: true });
}
},
},
......
......@@ -10,8 +10,8 @@ export default class Service {
return axios.get(this.realtimeEndpoint);
}
deleteIssuable() {
return axios.delete(this.endpoint);
deleteIssuable(payload) {
return axios.delete(this.endpoint, { params: payload });
}
updateIssuable(data) {
......
......@@ -113,7 +113,7 @@ export default class ProjectFindFile {
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
blobItemUrl = this.options.blobUrlTemplate + '/' + filePath;
blobItemUrl = this.options.blobUrlTemplate + '/' + encodeURIComponent(filePath);
html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
......
......@@ -53,7 +53,7 @@ export default {
};
</script>
<template>
<div class="card">
<div :id="release.tag_name" class="card">
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
......
......@@ -5,11 +5,7 @@ export const WARNING_MESSAGE_CLASS = 'warning_message';
export const DANGER_MESSAGE_CLASS = 'danger_message';
export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
export const ATMTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MT_MERGE_STRATEGY = 'merge_train';
export const AUTO_MERGE_STRATEGIES = [
MWPS_MERGE_STRATEGY,
ATMTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
];
export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY];
......@@ -3,7 +3,7 @@ import _ from 'underscore';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
import { ATMTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
export default class MergeRequestStore {
constructor(data) {
......@@ -217,8 +217,8 @@ export default class MergeRequestStore {
}
static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {
if (_.includes(availableAutoMergeStrategies, ATMTWPS_MERGE_STRATEGY)) {
return ATMTWPS_MERGE_STRATEGY;
if (_.includes(availableAutoMergeStrategies, MTWPS_MERGE_STRATEGY)) {
return MTWPS_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) {
return MT_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) {
......
......@@ -45,8 +45,7 @@ input[type='checkbox']:hover {
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out $default-transition-duration,
background-color ease-in-out $default-transition-duration,
width ease-in-out $default-transition-duration;
background-color ease-in-out $default-transition-duration;
@include media-breakpoint-up(xl) {
width: $search-input-xl-width;
......
......@@ -6,6 +6,7 @@ module IssuableActions
included do
before_action :authorize_destroy_issuable!, only: :destroy
before_action :check_destroy_confirmation!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
before_action only: :show do
push_frontend_feature_flag(:scoped_labels, default_enabled: true)
......@@ -91,6 +92,33 @@ module IssuableActions
end
end
def check_destroy_confirmation!
return true if params[:destroy_confirm]
error_message = "Destroy confirmation not provided for #{issuable.human_class_name}"
exception = RuntimeError.new(error_message)
Gitlab::Sentry.track_acceptable_exception(
exception,
extra: {
project_path: issuable.project.full_path,
issuable_type: issuable.class.name,
issuable_id: issuable.id
}
)
index_path = polymorphic_path([parent, issuable.class])
respond_to do |format|
format.html do
flash[:notice] = error_message
redirect_to index_path
end
format.json do
render json: { errors: error_message }, status: :unprocessable_entity
end
end
end
def bulk_update
result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name)
quantity = result[:count]
......@@ -110,7 +138,7 @@ module IssuableActions
end
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = notes.select { |n| n.visible_for?(current_user) }
discussions = Discussion.build_collection(notes, issuable)
......
......@@ -29,7 +29,7 @@ module NotesActions
end
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = notes.select { |n| n.visible_for?(current_user) }
notes_json[:notes] =
if use_note_serializer?
......
......@@ -127,4 +127,8 @@ module UploadsActions
def model
strong_memoize(:model) { find_model }
end
def workhorse_authorize_request?
action_name == 'authorize'
end
end
......@@ -3,7 +3,7 @@
class Groups::RunnersController < Groups::ApplicationController
# Proper policies should be implemented per
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
before_action :authorize_admin_pipeline!
before_action :authorize_admin_group!
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
......@@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController
@runner ||= @group.runners.find(params[:id])
end
def authorize_admin_pipeline!
return render_404 unless can?(current_user, :admin_pipeline, group)
end
def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
......
......@@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authorize_test_reports!, only: [:test_reports]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
......@@ -189,7 +190,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def pipeline_status
render json: PipelineSerializer
.new(project: @project, current_user: @current_user)
.represent_status(@merge_request.head_pipeline)
.represent_status(head_pipeline)
end
def ci_environments_status
......@@ -239,6 +240,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
def head_pipeline
strong_memoize(:head_pipeline) do
pipeline = @merge_request.head_pipeline
pipeline if can?(current_user, :read_pipeline, pipeline)
end
end
def ci_environments_status_on_merge_result?
params[:environment_target] == 'merge_commit'
end
......@@ -337,4 +345,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: { status_reason: 'Unknown error' }, status: :internal_server_error
end
end
def authorize_test_reports!
# MergeRequest#actual_head_pipeline is the pipeline accessed in MergeRequest#compare_reports.
return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline)
end
end
......@@ -21,10 +21,13 @@ class SessionsController < Devise::SessionsController
prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? }
before_action :auto_sign_in_with_provider, only: [:new]
before_action :store_unauthenticated_sessions, only: [:new]
before_action :save_failed_login, if: :action_new_and_failed_login?
before_action :load_recaptcha
after_action :log_failed_login, if: -> { action_name == 'new' && failed_login? }
helper_method :captcha_enabled?
after_action :log_failed_login, if: :action_new_and_failed_login?
helper_method :captcha_enabled?, :captcha_on_login_required?
# protect_from_forgery is already prepended in ApplicationController but
# authenticate_with_two_factor which signs in the user is prepended before
......@@ -38,6 +41,7 @@ class SessionsController < Devise::SessionsController
protect_from_forgery with: :exception, prepend: true
CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
MAX_FAILED_LOGIN_ATTEMPTS = 5
def new
set_minimum_password_length
......@@ -81,10 +85,14 @@ class SessionsController < Devise::SessionsController
request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
end
def captcha_on_login_required?
Gitlab::Recaptcha.enabled_on_login? && unverified_anonymous_user?
end
# From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
def check_captcha
return unless user_params[:password].present?
return unless captcha_enabled?
return unless captcha_enabled? || captcha_on_login_required?
return unless Gitlab::Recaptcha.load_configurations!
if verify_recaptcha
......@@ -126,10 +134,28 @@ class SessionsController < Devise::SessionsController
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
def action_new_and_failed_login?
action_name == 'new' && failed_login?
end
def save_failed_login
session[:failed_login_attempts] ||= 0
session[:failed_login_attempts] += 1
end
def failed_login?
(options = request.env["warden.options"]) && options[:action] == "unauthenticated"
end
# storing sessions per IP lets us check if there are associated multiple
# anonymous sessions with one IP and prevent situations when there are
# multiple attempts of logging in
def store_unauthenticated_sessions
return if current_user
Gitlab::AnonymousSession.new(request.remote_ip, session_id: request.session.id).store_session_id_per_ip
end
# Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change.
# rubocop: disable CodeReuse/ActiveRecord
......@@ -240,6 +266,18 @@ class SessionsController < Devise::SessionsController
@ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers
end
def unverified_anonymous_user?
exceeded_failed_login_attempts? || exceeded_anonymous_sessions?
end
def exceeded_failed_login_attempts?
session.fetch(:failed_login_attempts, 0) > MAX_FAILED_LOGIN_ATTEMPTS
end
def exceeded_anonymous_sessions?
Gitlab::AnonymousSession.new(request.remote_ip).stored_sessions >= MAX_FAILED_LOGIN_ATTEMPTS
end
def authentication_method
if user_params[:otp_attempt]
"two-factor"
......
......@@ -2,6 +2,7 @@
class UploadsController < ApplicationController
include UploadsActions
include WorkhorseRequest
UnknownUploadModelError = Class.new(StandardError)
......@@ -21,7 +22,8 @@ class UploadsController < ApplicationController
before_action :upload_mount_satisfied?
before_action :find_model
before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
before_action :authorize_create_access!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
def uploader_class
PersonalFileUploader
......@@ -72,7 +74,7 @@ class UploadsController < ApplicationController
end
def render_unauthorized
if current_user
if current_user || workhorse_authorize_request?
render_404
else
authenticate_user!
......
......@@ -164,6 +164,10 @@ module ApplicationSettingsHelper
:allow_local_requests_from_system_hooks,
:dns_rebinding_protection_enabled,
:archive_builds_in_human_readable,
:asset_proxy_enabled,
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
......@@ -231,6 +235,7 @@ module ApplicationSettingsHelper
:recaptcha_enabled,
:recaptcha_private_key,
:recaptcha_site_key,
:login_recaptcha_protection_enabled,
:receive_max_input_size,
:repository_checks_enabled,
:repository_storages,
......
......@@ -90,6 +90,8 @@ module EmailsHelper
when MergeRequest
merge_request = MergeRequest.find(closed_via[:id]).present
return "" unless Ability.allowed?(@recipient, :read_merge_request, merge_request)
case format
when :html
merge_request_link = link_to(merge_request.to_reference, merge_request.web_url)
......@@ -102,6 +104,8 @@ module EmailsHelper
# Technically speaking this should be Commit but per
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
# we can't deserialize Commit without custom serializer for ActiveJob
return "" unless Ability.allowed?(@recipient, :download_code, @project)
_("via %{closed_via}") % { closed_via: closed_via }
else
""
......
......@@ -71,7 +71,7 @@ module LabelsHelper
end
def label_tooltip_title(label)
label.description
Sanitize.clean(label.description)
end
def suggested_colors
......
......@@ -448,7 +448,7 @@ module ProjectsHelper
def git_user_email
if current_user
current_user.email
current_user.commit_email
else
"your@email.com"
end
......
......@@ -34,6 +34,8 @@ module Emails
setup_issue_mail(issue_id, recipient_id, closed_via: closed_via)
@updated_by = User.find(updated_by_user_id)
@recipient = User.find(recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end
......
......@@ -18,12 +18,19 @@ class ApplicationSetting < ApplicationRecord
# fix a lot of tests using allow_any_instance_of
include ApplicationSettingImplementation
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
insecure_mode: true,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
ignore_column :koding_url
ignore_column :koding_enabled
......@@ -75,11 +82,11 @@ class ApplicationSetting < ApplicationRecord
validates :recaptcha_site_key,
presence: true,
if: :recaptcha_enabled
if: :recaptcha_or_login_protection_enabled
validates :recaptcha_private_key,
presence: true,
if: :recaptcha_enabled
if: :recaptcha_or_login_protection_enabled
validates :akismet_api_key,
presence: true,
......@@ -192,6 +199,17 @@ class ApplicationSetting < ApplicationRecord
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 }
validates :asset_proxy_url,
presence: true,
allow_blank: false,
url: true,
if: :asset_proxy_enabled?
validates :asset_proxy_secret_key,
presence: true,
allow_blank: false,
if: :asset_proxy_enabled?
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
......@@ -292,4 +310,8 @@ class ApplicationSetting < ApplicationRecord
def self.cache_backend
Gitlab::ThreadMemoryCache.cache_backend
end
def recaptcha_or_login_protection_enabled
recaptcha_enabled || login_recaptcha_protection_enabled
end
end
......@@ -23,8 +23,9 @@ module ApplicationSettingImplementation
akismet_enabled: false,
allow_local_requests_from_web_hooks_and_services: false,
allow_local_requests_from_system_hooks: true,
dns_rebinding_protection_enabled: true,
asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
......@@ -33,7 +34,9 @@ module ApplicationSettingImplementation
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
domain_whitelist: Settings.gitlab['domain_whitelist'],
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
......@@ -52,9 +55,11 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
mirror_available: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil,
......@@ -63,7 +68,10 @@ module ApplicationSettingImplementation
plantuml_url: nil,
polling_interval_multiplier: 1,
project_export_enabled: true,
protected_ci_variables: false,
raw_blob_request_limit: 300,
recaptcha_enabled: false,
login_recaptcha_protection_enabled: false,
repository_checks_enabled: true,
repository_storages: ['default'],
require_two_factor_authentication: false,
......@@ -95,16 +103,10 @@ module ApplicationSettingImplementation
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname,
snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil,
snowplow_enabled: false,
snowplow_site_id: nil,
protected_ci_variables: false,
local_markdown_version: 0,
outbound_local_requests_whitelist: [],
raw_blob_request_limit: 300
snowplow_site_id: nil
}
end
......@@ -198,6 +200,15 @@ module ApplicationSettingImplementation
end
end
def asset_proxy_whitelist=(values)
values = domain_strings_to_array(values) if values.is_a?(String)
# make sure we always whitelist the running host
values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host)
self[:asset_proxy_whitelist] = values
end
def repository_storages
Array(read_attribute(:repository_storages))
end
......@@ -306,6 +317,7 @@ module ApplicationSettingImplementation
values
.split(DOMAIN_LIST_SEPARATOR)
.map(&:strip)
.reject(&:empty?)
.uniq
end
......
......@@ -203,6 +203,7 @@ module Ci
scope :for_sha, -> (sha) { where(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
scope :triggered_by_merge_request, -> (merge_request) do
where(source: :merge_request_event, merge_request: merge_request)
......
......@@ -73,6 +73,7 @@ module Issuable
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true
validate :milestone_is_valid
scope :authored, ->(user) { where(author_id: user) }
......
......@@ -365,6 +365,8 @@ class Group < Namespace
end
def max_member_access_for_user(user)
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.admin?
members_with_parents
......
......@@ -178,7 +178,7 @@ class Issue < ApplicationRecord
end
def moved?
!moved_to.nil?
!moved_to_id.nil?
end
def can_move?(user, to_project = nil)
......
......@@ -199,7 +199,11 @@ class Label < ApplicationRecord
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
write_attribute(:title, sanitize_value(value)) if value.present?
end
def description=(value)
write_attribute(:description, sanitize_value(value)) if value.present?
end
##
......@@ -260,7 +264,7 @@ class Label < ApplicationRecord
end
end
def sanitize_title(value)
def sanitize_value(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end
......
......@@ -89,6 +89,7 @@ class Note < ApplicationRecord
delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true
validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }
validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader
......@@ -331,6 +332,10 @@ class Note < ApplicationRecord
cross_reference? && !all_referenced_mentionables_allowed?(user)
end
def visible_for?(user)
!cross_reference_not_visible_for?(user)
end
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
......
......@@ -61,6 +61,8 @@ class Project < ApplicationRecord
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
:merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
:merge_requests_access_level, :issues_access_level, :wiki_access_level,
:snippets_access_level, :builds_access_level, :repository_access_level,
to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
......
......@@ -64,7 +64,12 @@ class JiraService < IssueTrackerService
end
def client
@client ||= JIRA::Client.new(options)
@client ||= begin
JIRA::Client.new(options).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
end
def help
......
......@@ -200,6 +200,7 @@ class RemoteMirror < ApplicationRecord
result.password = '*****' if result.password
result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user
result.to_s
rescue URI::Error
end
def ensure_remote!
......
......@@ -239,13 +239,13 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
branch_names_include?(branch_name)
branch_names.include?(branch_name)
end
def tag_exists?(tag_name)
return false unless raw_repository
tag_names_include?(tag_name)
tag_names.include?(tag_name)
end
def ref_exists?(ref)
......@@ -565,10 +565,10 @@ class Repository
end
delegate :branch_names, to: :raw_repository
cache_method_as_redis_set :branch_names, fallback: []
cache_method :branch_names, fallback: []
delegate :tag_names, to: :raw_repository
cache_method_as_redis_set :tag_names, fallback: []
cache_method :tag_names, fallback: []
delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0
......@@ -1130,10 +1130,6 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self)
end
def redis_set_cache
@redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
end
def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
......
......@@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord
TYPES_WITH_CROSS_REFERENCES = %w[
commit cross_reference
close duplicate
moved
moved merge
].freeze
ICON_TYPES = %w[
......
......@@ -645,6 +645,13 @@ class User < ApplicationRecord
end
end
# will_save_change_to_attribute? is used by Devise to check if it is necessary
# to clear any existing reset_password_tokens before updating an authentication_key
# and login in our case is a virtual attribute to allow login by username or email.
def will_save_change_to_login?
will_save_change_to_username? || will_save_change_to_email?
end
def unique_email
if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, _('has already been taken'))
......
......@@ -5,6 +5,8 @@ class IssuePolicy < IssuablePolicy
# Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
extend ProjectPolicy::ClassMethods
desc "User can read confidential issues"
condition(:can_read_confidential) do
@user && IssueCollection.new([@subject]).visible_to(@user).any?
......@@ -14,13 +16,12 @@ class IssuePolicy < IssuablePolicy
condition(:confidential, scope: :subject) { @subject.confidential? }
rule { confidential & ~can_read_confidential }.policy do
prevent :read_issue
prevent(*create_read_update_admin_destroy(:issue))
prevent :read_issue_iid
prevent :update_issue
prevent :admin_issue
prevent :create_note
end
rule { ~can?(:read_issue) }.prevent :create_note
rule { locked }.policy do
prevent :reopen_issue
end
......
......@@ -4,4 +4,10 @@ class MergeRequestPolicy < IssuablePolicy
rule { locked }.policy do
prevent :reopen_merge_request
end
# Only users who can read the merge request can comment.
# Although :read_merge_request is computed in the policy context,
# it would not be safe to prevent :create_note there, since
# note permissions are shared, and this would apply too broadly.
rule { ~can?(:read_merge_request) }.prevent :create_note
end
......@@ -6,6 +6,8 @@ module ApplicationSettings
attr_reader :params, :application_setting
MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist).freeze
def execute
validate_classification_label(application_setting, :external_authorization_service_default_label) unless bypass_external_auth?
......@@ -25,7 +27,13 @@ module ApplicationSettings
params[:usage_stats_set_by_user_id] = current_user.id
end
@application_setting.update(@params)
@application_setting.assign_attributes(params)
if invalidate_markdown_cache?
@application_setting[:local_markdown_version] = @application_setting.local_markdown_version + 1
end
@application_setting.save
end
private
......@@ -41,6 +49,11 @@ module ApplicationSettings
@application_setting.add_to_outbound_local_requests_whitelist(values_array)
end
def invalidate_markdown_cache?
!params.key?(:local_markdown_version) &&
(@application_setting.changes.keys & MARKDOWN_CACHE_INVALIDATING_PARAMS).any?
end
def update_terms(terms)
return unless terms.present?
......
......@@ -44,6 +44,10 @@ class BaseService
model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
end
def visibility_level
params[:visibility].is_a?(String) ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level]
end
private
def error(message, http_status = nil)
......
......@@ -24,7 +24,7 @@ module ChatNames
end
def chat_name_token
Gitlab::ChatNameToken.new
@chat_name_token ||= Gitlab::ChatNameToken.new
end
def chat_name_params
......
......@@ -15,7 +15,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create,
Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new
......
......@@ -2,24 +2,7 @@
module Clusters
module Applications
class CheckInstallationProgressService < BaseHelmService
def execute
return unless operation_in_progress?
case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED
on_success
when Gitlab::Kubernetes::Pod::FAILED
on_failed
else
check_timeout
end
rescue Kubeclient::HttpError => e
log_error(e)
app.make_errored!("Kubernetes error: #{e.error_code}")
end
class CheckInstallationProgressService < CheckProgressService
private
def operation_in_progress?
......@@ -32,10 +15,6 @@ module Clusters
remove_installation_pod
end
def on_failed
app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.")
end
def check_timeout
if timed_out?
begin
......@@ -54,18 +33,6 @@ module Clusters
def timed_out?
Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
helm_api.delete_pod!(pod_name)
end
def installation_phase
helm_api.status(pod_name)
end
def installation_errors
helm_api.log(pod_name)
end
end
end
end
# 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 @@
module Clusters
module Applications
class CheckUninstallProgressService < BaseHelmService
def execute
return unless app.uninstalling?
case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED
on_success
when Gitlab::Kubernetes::Pod::FAILED
on_failed
else
check_timeout
end
rescue Kubeclient::HttpError => e
log_error(e)
class CheckUninstallProgressService < CheckProgressService
private
app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
def operation_in_progress?
app.uninstalling?
end
private
def on_success
app.post_uninstall
app.destroy!
......@@ -31,10 +18,6 @@ module Clusters
remove_installation_pod
end
def on_failed
app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
end
def check_timeout
if timed_out?
app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
......@@ -50,14 +33,6 @@ module Clusters
def timed_out?
Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
end
def remove_installation_pod
helm_api.delete_pod!(pod_name)
end
def installation_phase
helm_api.status(pod_name)
end
end
end
end
......@@ -12,7 +12,7 @@ class CreateSnippetService < BaseService
PersonalSnippet.new(params)
end
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
unless Gitlab::VisibilityLevel.allowed_for?(current_user, snippet.visibility_level)
deny_visibility_level(snippet)
return snippet
end
......
......@@ -68,9 +68,5 @@ module Groups
true
end
def visibility_level
params[:visibility].present? ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level]
end
end
end
......@@ -8,6 +8,8 @@ module Projects
@current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
@import_data = @params.delete(:import_data)
@relations_block = @params.delete(:relations_block)
end
def execute
......@@ -15,14 +17,11 @@ module Projects
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
import_data = params.delete(:import_data)
relations_block = params.delete(:relations_block)
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level)
deny_visibility_level(@project)
if project_visibility.restricted?
deny_visibility_level(@project, project_visibility.visibility_level)
return @project
end
......@@ -44,7 +43,7 @@ module Projects
@project.namespace_id = current_user.namespace_id
end
relations_block&.call(@project)
@relations_block&.call(@project)
yield(@project) if block_given?
validate_classification_label(@project, :external_authorization_classification_label)
......@@ -54,7 +53,7 @@ module Projects
@project.creator = current_user
save_project_and_import_data(import_data)
save_project_and_import_data
after_create_actions if @project.persisted?
......@@ -129,9 +128,9 @@ module Projects
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
def save_project_and_import_data(import_data)
def save_project_and_import_data
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
@project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
if @project.save
unless @project.gitlab_project_import?
......@@ -192,5 +191,11 @@ module Projects
fail(error: @project.errors.full_messages.join(', '))
end
end
def project_visibility
@project_visibility ||= Gitlab::VisibilityLevelChecker
.new(current_user, @project, project_params: { import_data: @import_data })
.level_restricted?
end
end
end
......@@ -314,11 +314,9 @@ class TodoService
end
def reject_users_without_access(users, parent, target)
if target.is_a?(Note) && target.for_issuable?
target = target.noteable
end
target = target.noteable if target.is_a?(Note)
if target.is_a?(Issuable)
if target.respond_to?(:to_ability_name)
select_users(users, :"read_#{target.to_ability_name}", target)
else
select_users(users, :read_project, parent)
......
......@@ -12,7 +12,7 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
new_visibility = visibility_level
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
......
......@@ -6,6 +6,10 @@ class PersonalFileUploader < FileUploader
options.storage_path
end
def self.workhorse_local_upload_path
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
end
def self.base_dir(model, _store = nil)
# base_dir is the path seen by the user when rendering Markdown, so
# it should be the same for both local and object storage. It is
......
......@@ -7,11 +7,15 @@
= f.check_box :recaptcha_enabled, class: 'form-check-input'
= f.label :recaptcha_enabled, class: 'form-check-label' do
Enable reCAPTCHA
- recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
- recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url }
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe }
= _('Helps prevent bots from creating accounts.')
.form-group
.form-check
= f.check_box :login_recaptcha_protection_enabled, class: 'form-check-input'
= f.label :login_recaptcha_protection_enabled, class: 'form-check-label' do
Enable reCAPTCHA for login
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from brute-force attacks.')
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold'
= f.text_field :recaptcha_site_key, class: 'form-control'
......@@ -21,6 +25,7 @@
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold'
.form-group
= f.text_field :recaptcha_private_key, class: 'form-control'
.form-group
......
......@@ -9,7 +9,9 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable reCAPTCHA or Akismet and set IP limits.')
- recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
- recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url }
= _('Enable reCAPTCHA or Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe }
.settings-content
= render 'spam'
......
......@@ -16,7 +16,7 @@
- else
= link_to _('Forgot your password?'), new_password_path(:user)
%div
- if captcha_enabled?
- if captcha_enabled? || captcha_on_login_required?
= recaptcha_tags
.submit-container.move-submit-down
......
<%= sanitize_name(@issue.author_name) %> <%= 'created an issue:' %>
<%= sanitize_name(@issue.author_name) %> <%= 'created an issue:' %> <%= url_for(project_issue_url(@issue.project, @issue)) %>
<% if @issue.assignees.any? -%>
<%= assignees_label(@issue) %>
<% end %>
<%= assignees_label(@issue) if @issue.assignees.any? %>
<% if @issue.description -%>
<%= @issue.description %>
<% end %>
<%= @issue.description %>
<%= @merge_request.author_name %> <%= 'created a merge request:' %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= sanitize_name(@merge_request.author_name) %> <%= 'created a merge request:' %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= merge_path_description(@merge_request, 'to') %>
<%= 'Author:' %> <%= @merge_request.author_name %>
<%= assignees_label(@merge_request) %>
<%= assignees_label(@merge_request) if @merge_request.assignees.any? %>
<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
<%= @merge_request.description %>
......@@ -50,7 +50,7 @@
- @project.remote_mirrors.each_with_index do |mirror, index|
- next if mirror.new_record?
%tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) }
%td.qa-mirror-repository-url= mirror.safe_url
%td.qa-mirror-repository-url= mirror.safe_url || _('Invalid URL')
%td= _('Push')
%td
= mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')
......
......@@ -15,7 +15,7 @@
.footer-block.row-content-block
= service_save_button(@service)
&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)
%hr
......
.row.prepend-top-default.append-bottom-default
.col-lg-4
%h4.prepend-top-0
Project services
%p Project services allow you to integrate GitLab with other applications
= s_("ProjectService|Project services")
%p= s_("ProjectService|Project services allow you to integrate GitLab with other applications")
.col-lg-8
%table.table
%colgroup
......@@ -13,12 +13,12 @@
%thead
%tr
%th
%th Service
%th.d-none.d-sm-block Description
%th Last edit
%th= s_("ProjectService|Service")
%th.d-none.d-sm-block= _("Description")
%th= s_("ProjectService|Last edit")
- @services.sort_by(&:title).each do |service|
%tr
%td{ "aria-label" => "#{service.title}: status " + (service.activated? ? "on" : "off") }
%td{ "aria-label" => (service.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: service.title } }
= boolean_to_icon service.activated?
%td
= link_to edit_project_service_path(@project, service.to_param) do
......
- breadcrumb_title "Integrations"
- page_title @service.title, "Services"
- add_to_breadcrumbs("Settings", edit_project_path(@project))
- breadcrumb_title s_("ProjectService|Integrations")
- page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
= render 'deprecated_message' if @service.deprecation_message
......
- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}"
- run_actions_text = s_("ProjectService|Perform common operations on GitLab project: %{project_name}") % { project_name: @project.full_name }
%p To set up this service:
%p= s_("ProjectService|To set up this service:")
%ul.list-unstyled.indent-list
%li
1.
......@@ -18,67 +18,67 @@
.help-form
.form-group
= label_tag :display_name, 'Display name', class: 'col-12 col-form-label label-bold'
= label_tag :display_name, _('Display name'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#display_name', class: 'input-group-text')
.form-group
= label_tag :description, 'Description', class: 'col-12 col-form-label label-bold'
= label_tag :description, _('Description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#description', class: 'input-group-text')
.form-group
= label_tag nil, 'Command trigger word', class: 'col-12 col-form-label label-bold'
= label_tag nil, s_('MattermostService|Command trigger word'), class: 'col-12 col-form-label label-bold'
.col-12
%p Fill in the word that works best for your team.
%p= s_('MattermostService|Fill in the word that works best for your team.')
%p
Suggestions:
= s_('MattermostService|Suggestions:')
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path
.form-group
= label_tag :request_url, 'Request URL', class: 'col-12 col-form-label label-bold'
= label_tag :request_url, s_('MattermostService|Request URL'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#request_url', class: 'input-group-text')
.form-group
= label_tag nil, 'Request method', class: 'col-12 col-form-label label-bold'
= label_tag nil, s_('MattermostService|Request method'), class: 'col-12 col-form-label label-bold'
.col-12 POST
.form-group
= label_tag :response_username, 'Response username', class: 'col-12 col-form-label label-bold'
= label_tag :response_username, s_('MattermostService|Response username'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#response_username', class: 'input-group-text')
.form-group
= label_tag :response_icon, 'Response icon', class: 'col-12 col-form-label label-bold'
= label_tag :response_icon, s_('MattermostService|Response icon'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#response_icon', class: 'input-group-text')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold'
= label_tag nil, _('Autocomplete'), class: 'col-12 col-form-label label-bold'
.col-12 Yes
.form-group
= label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-12 col-12 col-form-label label-bold'
= label_tag :autocomplete_hint, _('Autocomplete hint'), class: 'col-12 col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold'
= label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
......
......@@ -3,14 +3,12 @@
.info-well
.well-segment
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
= s_("MattermostService|This service allows users to perform common operations on this project by entering slash commands in Mattermost.")
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
= _("View documentation")
= sprite_icon('external-link', size: 16)
%p.inline
See list of available commands in Mattermost after setting up this service,
by entering
= s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering")
%kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
......
......@@ -4,4 +4,4 @@
.col-sm-9.offset-sm-3
= link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15)
Add to Mattermost
= s_("MattermostService|Add to Mattermost")
......@@ -4,17 +4,15 @@
.info-well
.well-segment
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
= s_("SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack.")
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
= _("View documentation")
= sprite_icon('external-link', size: 16)
%p.inline
See list of available commands in Slack after setting up this service,
by entering
= s_("SlackService|See list of available commands in Slack after setting up this service, by entering")
%kbd.inline /&lt;command&gt; help
- unless @service.template?
%p To set up this service:
%p= _("To set up this service:")
%ul.list-unstyled.indent-list
%li
1.
......@@ -27,11 +25,11 @@
.help-form
.form-group
= label_tag nil, 'Command', class: 'col-12 col-form-label label-bold'
= label_tag nil, _('Command'), class: 'col-12 col-form-label label-bold'
.col-12
%p Fill in the word that works best for your team.
%p= s_('SlackService|Fill in the word that works best for your team.')
%p
Suggestions:
= _("Suggestions:")
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path
......@@ -44,44 +42,44 @@
= clipboard_button(target: '#url', class: 'input-group-text')
.form-group
= label_tag nil, 'Method', class: 'col-12 col-form-label label-bold'
= label_tag nil, _('Method'), class: 'col-12 col-form-label label-bold'
.col-12 POST
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-12 col-form-label label-bold'
= label_tag :customize_name, _('Customize name'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group
= label_tag nil, 'Customize icon', class: 'col-12 col-form-label label-bold'
= label_tag nil, _('Customize icon'), class: 'col-12 col-form-label label-bold'
.col-12
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36, class: 'mr-3')
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
= link_to(_('Download image'), asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold'
= label_tag nil, _('Autocomplete'), class: 'col-12 col-form-label label-bold'
.col-12 Show this command in the autocomplete list
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold'
= label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-12 col-form-label label-bold'
= label_tag :autocomplete_usage_hint, _('Autocomplete usage hint'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-12 col-form-label label-bold'
= label_tag :descriptive_label, _('Descriptive label'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
= text_field_tag :descriptive_label, _('Perform common operations on GitLab project'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text')
......@@ -89,12 +87,6 @@
%ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
into the field below
= s_("SlackService|2. Paste the <strong>Token</strong> into the field below").html_safe
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
= s_("SlackService|3. Select the <strong>Active</strong> checkbox, press <strong>Save changes</strong> and start using GitLab inside Slack!").html_safe
......@@ -12,7 +12,7 @@
%p= current_user_empty_message_description
- if secondary_button_link.present?
= link_to secondary_button_label, secondary_button_link, class: 'btn btn-create btn-inverted'
= link_to secondary_button_label, secondary_button_link, class: 'btn btn-success btn-inverted'
= link_to primary_button_label, primary_button_link, class: 'btn btn-success'
- else
......
......@@ -66,7 +66,7 @@
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable], params: { destroy_confirm: true }), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
%span.append-right-10
......
---
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.
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