Commit 9d9960bd authored by Robert Speicher's avatar Robert Speicher

Merge branch 'master' of dev.gitlab.org:gitlab/gitlab-ee

parents 8f20404c e6d838eb
Please view this file on the master branch, on stable branches it's out of date.
## 12.2.3
### Security (2 changes)
- Limit number of jobs in running pipelines for the past hour on per plan basis. !1182
- Filter out old system notes for epics in notes api endpoint response.
## 12.2.2
### Security (2 changes)
- Limit number of jobs in running pipelines for the past hour on per plan basis. !1182
- Filter out old system notes for epics in notes api endpoint response.
## 12.2.1
- No changes.
......@@ -274,6 +290,15 @@ Please view this file on the master branch, on stable branches it's out of date.
- Don't send CI usage email notifications for self-hosted instances. !14809
## 12.0.7
### Security (3 changes)
- Limit number of jobs in running pipelines for the past hour on per plan basis. !1182
- Queries for Upload should be scoped by model.
- Filter out old system notes for epics in notes api endpoint response.
## 12.0.6
- No changes.
......
......@@ -2,6 +2,38 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 12.2.3
- No changes.
## 12.2.2
### 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.1
### Fixed (2 changes)
......@@ -600,6 +632,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 display = $this.attr('data-math-style') === 'display';
try {
katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
throw err;
const MAX_MATH_CHARS = 1000;
const MAX_RENDER_TIME_MS = 2000;
// These messages might be used with inline errors in the future. Keep them around. For now, we will
// display a single error message using flash().
// const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
// s__(
// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.',
// ),
// { maxChars: MAX_MATH_CHARS },
// );
// const RENDER_TIME_EXCEEDED_MSG = s__(
// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.",
// );
const RENDER_FLASH_MSG = sprintf(
s__(
'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.',
),
{ maxChars: MAX_MATH_CHARS },
);
// Wait for the browser to reflow the layout. Reflowing SVG takes time.
// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object".
const waitForReflow = fn => {
window.requestAnimationFrame(fn);
};
/**
* Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
*/
class SafeMathRenderer {
/*
How this works:
The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG.
During this time, the JS is blocked and the page becomes unresponsive.
We want to render math blocks one by one until a certain time is exceeded, after which we stop
rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an
asynchronous task, so we can't time it synchronously.
SafeMathRenderer essentially does the following:
1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice.
2. Places each placeholder element in a queue.
3. Renders the element at the head of the queue and waits for reflow.
4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty.
*/
queue = [];
totalMS = 0;
constructor(elements, katex) {
this.elements = elements;
this.katex = katex;
this.renderElement = this.renderElement.bind(this);
this.render = this.render.bind(this);
}
renderElement() {
if (!this.queue.length) {
return;
}
});
const el = this.queue.shift();
const text = el.textContent;
el.removeAttribute('style');
if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) {
if (!this.flashShown) {
flash(RENDER_FLASH_MSG);
this.flashShown = true;
}
// Show unrendered math code
const codeElement = document.createElement('pre');
codeElement.className = 'code';
codeElement.textContent = el.textContent;
el.parentNode.replaceChild(codeElement, el);
// Render the next math
this.renderElement();
} else {
this.startTime = Date.now();
try {
el.innerHTML = this.katex.renderToString(text, {
displayMode: el.getAttribute('data-math-style') === 'display',
throwOnError: true,
maxSize: 20,
maxExpand: 20,
});
} catch {
// 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(() => {});
}
......@@ -138,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,6 +345,11 @@ 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
Projects::MergeRequestsController.prepend_if_ee('EE::Projects::MergeRequestsController')
......@@ -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
......
......@@ -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,6 +310,10 @@ 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
ApplicationSetting.prepend_if_ee('EE::ApplicationSetting')
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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'))
......
......@@ -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?
......
......@@ -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
......
......@@ -5,9 +5,11 @@ module Projects
include ValidatesClassificationLabel
def initialize(user, params)
@current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
@import_data = @params.delete(:import_data)
@relations_block = @params.delete(:relations_block)
end
def execute
......@@ -15,14 +17,11 @@ module Projects
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
import_data = params.delete(:import_data)
relations_block = params.delete(:relations_block)
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level)
deny_visibility_level(@project)
if project_visibility.restricted?
deny_visibility_level(@project, project_visibility.visibility_level)
return @project
end
......@@ -44,7 +43,7 @@ module Projects
@project.namespace_id = current_user.namespace_id
end
relations_block&.call(@project)
@relations_block&.call(@project)
yield(@project) if block_given?
validate_classification_label(@project, :external_authorization_classification_label)
......@@ -54,7 +53,7 @@ module Projects
@project.creator = current_user
save_project_and_import_data(import_data)
save_project_and_import_data
after_create_actions if @project.persisted?
......@@ -129,9 +128,9 @@ module Projects
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
def save_project_and_import_data(import_data)
def save_project_and_import_data
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
@project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
if @project.save
unless @project.gitlab_project_import?
......@@ -192,6 +191,12 @@ 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)
......
......@@ -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
......
---
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: 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
---
title: Use admin_group authorization in Groups::RunnersController
merge_request:
author:
type: security
---
title: Prevent disclosure of merge request ID via email
merge_request:
author:
type: security
---
title: Enforce max chars and max render time in markdown math
merge_request:
author:
type: security
---
title: Check permissions before responding in MergeController#pipeline_status
merge_request:
author:
type: security
---
title: Remove EXIF from users/personal snippet uploads.
merge_request:
author:
type: security
---
title: Fix project import restricted visibility bypass via API
merge_request:
author:
type: security
---
title: Fix weak session management by clearing password reset tokens after login (username/email)
are updated
merge_request:
author:
type: security
---
title: Fix SSRF via DNS rebinding in Kubernetes Integration
merge_request:
author:
type: security
#
# Asset proxy settings
#
ActiveSupport.on_load(:active_record) do
Banzai::Filter::AssetProxyFilter.initialize_settings
end
if Shard.connected? && !Gitlab::Database.read_only?
# The `table_exists?` check is needed because during our migration rollback testing,
# `Shard.connected?` could be cached and return true even though the table doesn't exist
if Shard.connected? && Shard.table_exists? && !Gitlab::Database.read_only?
Shard.populate!
end
# frozen_string_literal: true
module RestClient
class Request
attr_accessor :hostname_override
module UrlBlocker
def transmit(uri, req, payload, &block)
begin
ip, hostname_override = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_settings_local_requests?,
allow_localhost: allow_settings_local_requests?,
dns_rebind_protection: dns_rebind_protection?)
self.hostname_override = hostname_override
rescue Gitlab::UrlBlocker::BlockedUrlError => e
raise ArgumentError, "URL '#{uri}' is blocked: #{e.message}"
end
# Gitlab::UrlBlocker returns a Addressable::URI which we need to coerce
# to URI so that rest-client can use it to determine if it's a
# URI::HTTPS or not. It uses it to set `net.use_ssl` to true or not:
#
# https://github.com/rest-client/rest-client/blob/f450a0f086f1cd1049abbef2a2c66166a1a9ba71/lib/restclient/request.rb#L656
ip_as_uri = URI.parse(ip)
super(ip_as_uri, req, payload, &block)
end
def net_http_object(hostname, port)
super.tap do |http|
http.hostname_override = hostname_override if hostname_override
end
end
private
def dns_rebind_protection?
return false if Gitlab.http_proxy_env?
Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
end
def allow_settings_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
end
prepend UrlBlocker
end
end
......@@ -19,6 +19,7 @@ Rails.application.configure do |config|
Warden::Manager.after_authentication(scope: :user) do |user, auth, opts|
ActiveSession.cleanup(user)
Gitlab::AnonymousSession.new(auth.request.remote_ip, session_id: auth.request.session.id).cleanup_session_per_ip_entries
end
Warden::Manager.after_set_user(scope: :user, only: :fetch) do |user, auth, opts|
......
......@@ -30,6 +30,10 @@ scope path: :uploads do
to: 'uploads#create',
constraints: { model: /personal_snippet|user/, id: /\d+/ },
as: 'upload'
post ':model/authorize',
to: 'uploads#authorize',
constraints: { model: /personal_snippet|user/ }
end
# Redirect old note attachments path to new uploads path.
......
# frozen_string_literal: true
class AddAssetProxySettings < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :application_settings, :asset_proxy_enabled, :boolean, default: false, null: false
add_column :application_settings, :asset_proxy_url, :string # rubocop:disable Migration/AddLimitToStringColumns
add_column :application_settings, :asset_proxy_whitelist, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key_iv, :string # rubocop:disable Migration/AddLimitToStringColumns
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddLoginRecaptchaProtectionEnabledToApplicationSettings < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
add_column :application_settings, :login_recaptcha_protection_enabled, :boolean, default: false, null: false
end
end
# frozen_string_literal: true
class AddActiveJobsLimitToPlans < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :plans, :active_jobs_limit, :integer, default: 0
end
def down
remove_column :plans, :active_jobs_limit
end
end
......@@ -273,11 +273,17 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do
t.boolean "lock_memberships_to_ldap", default: false, null: false
t.boolean "time_tracking_limit_to_hours", default: false, null: false
t.string "grafana_url", default: "/-/grafana", null: false
t.boolean "login_recaptcha_protection_enabled", default: false, null: false
t.string "outbound_local_requests_whitelist", limit: 255, default: [], null: false, array: true
t.integer "raw_blob_request_limit", default: 300, null: false
t.boolean "allow_local_requests_from_web_hooks_and_services", default: false, null: false
t.boolean "allow_local_requests_from_system_hooks", default: true, null: false
t.bigint "instance_administration_project_id"
t.boolean "asset_proxy_enabled", default: false, null: false
t.string "asset_proxy_url"
t.text "asset_proxy_whitelist"
t.text "encrypted_asset_proxy_secret_key"
t.string "encrypted_asset_proxy_secret_key_iv"
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......@@ -2514,6 +2520,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do
t.string "title"
t.integer "active_pipelines_limit"
t.integer "pipeline_size_limit"
t.integer "active_jobs_limit", default: 0
t.index ["name"], name: "index_plans_on_name"
end
......
......@@ -37,6 +37,8 @@ Parameter | Type | Description
`stop_id` | integer | Only uploads with equal or smaller ID will be processed
`dry_run` | boolean | Do not remove EXIF data, only check if EXIF data are present or not, default: true
`sleep_time` | float | Pause for number of seconds after processing each image, default: 0.3 seconds
`uploader` | string | Run sanitization only for uploads of the given uploader (`FileUploader`, `PersonalFileUploader`, `NamespaceFileUploader`)
`since` | date | Run sanitization only for uploads newer than given date (e.g. `2019-05-01`)
If you have too many uploads, you can speed up sanitization by setting
`sleep_time` to a lower value or by running multiple rake tasks in parallel,
......
......@@ -165,7 +165,7 @@ POST /groups/:id/epics
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `title` | string | yes | The title of the epic |
| `labels` | string | no | The comma separated list of labels |
| `description` | string | no | The description of the epic |
| `description` | string | no | The description of the epic. Limited to 1 000 000 characters. |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
......@@ -231,7 +231,7 @@ PUT /groups/:id/epics/:epic_iid
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `epic_iid` | integer/string | yes | The internal ID of the epic |
| `title` | string | no | The title of an epic |
| `description` | string | no | The description of an epic |
| `description` | string | no | The description of an epic. Limited to 1 000 000 characters. |
| `labels` | string | no | The comma separated list of labels |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
......
......@@ -590,7 +590,7 @@ POST /projects/:id/issues
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iid` | integer/string | no | The internal ID of the project's issue (requires admin or project owner rights) |
| `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue |
| `description` | string | no | The description of an issue. Limited to 1 000 000 characters. |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_ids` | integer array | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
......@@ -691,7 +691,7 @@ PUT /projects/:id/issues/:issue_iid
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue |
| `description` | string | no | The description of an issue. Limited to 1 000 000 characters. |
| `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
......
......@@ -837,7 +837,7 @@ POST /projects/:id/merge_requests
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR |
| `description` | string | no | Description of MR. Limited to 1 000 000 characters. |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The global ID of a milestone |
......@@ -990,7 +990,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
| `description` | string | no | Description of MR. Limited to 1 000 000 characters. |
| `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `squash` | boolean | no | Squash commits into a single commit when merging |
......
......@@ -113,7 +113,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `issue_iid` (required) - The IID of an issue
- `body` (required) - The content of a note
- `body` (required) - The content of a note. Limited to 1 000 000 characters.
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights)
```bash
......@@ -133,7 +133,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `issue_iid` (required) - The IID of an issue
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
- `body` (required) - The content of a note. Limited to 1 000 000 characters.
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note
......@@ -231,7 +231,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `snippet_id` (required) - The ID of a snippet
- `body` (required) - The content of a note
- `body` (required) - The content of a note. Limited to 1 000 000 characters.
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
```bash
......@@ -251,7 +251,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `snippet_id` (required) - The ID of a snippet
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
- `body` (required) - The content of a note. Limited to 1 000 000 characters.
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/notes?body=note
......@@ -354,7 +354,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `merge_request_iid` (required) - The IID of a merge request
- `body` (required) - The content of a note
- `body` (required) - The content of a note. Limited to 1 000 000 characters.
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
### Modify existing merge request note
......@@ -370,7 +370,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `merge_request_iid` (required) - The IID of a merge request
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
- `body` (required) - The content of a note. Limited to 1 000 000 characters.
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/notes?body=note
......@@ -472,7 +472,7 @@ Parameters:
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `epic_id` | integer | yes | The ID of an epic |
| `body` | string | yes | The content of a note |
| `body` | string | yes | The content of a note. Limited to 1 000 000 characters. |
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
......@@ -493,7 +493,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `epic_id` | integer | yes | The ID of an epic |
| `note_id` | integer | yes | The ID of a note |
| `body` | string | yes | The content of a note |
| `body` | string | yes | The content of a note. Limited to 1 000 000 characters. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
......
......@@ -67,7 +67,10 @@ Example response:
"local_markdown_version": 0,
"allow_local_requests_from_hooks_and_services": true,
"allow_local_requests_from_web_hooks_and_services": true,
"allow_local_requests_from_system_hooks": false
"allow_local_requests_from_system_hooks": false,
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"]
}
```
......@@ -141,6 +144,9 @@ Example response:
"user_show_add_ssh_key_message": true,
"file_template_project_id": 1,
"local_markdown_version": 0,
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
"geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"allow_local_requests_from_hooks_and_services": true,
"allow_local_requests_from_web_hooks_and_services": true,
......@@ -186,6 +192,10 @@ are listed in the descriptions of the relevant settings.
| `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from hooks and services. |
| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. |
| `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. |
| `asset_proxy_enabled` | boolean | no | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. GitLab restart is required to apply changes. |
| `asset_proxy_secret_key` | string | no | Shared secret with the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_url` | string | no | URL of the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_whitelist` | string or array of strings | no | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. GitLab restart is required to apply changes. |
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. |
......
......@@ -18,3 +18,4 @@ type: index
- [Enforce Two-factor authentication](two_factor_authentication.md)
- [Send email confirmation on sign-up](user_email_confirmation.md)
- [Security of running jobs](https://docs.gitlab.com/runner/security/)
- [Proxying images](asset_proxy.md)
A possible security concern when managing a public facing GitLab instance is
the ability to steal a users IP address by referencing images in issues, comments, etc.
For example, adding `![Example image](http://example.com/example.png)` to
an issue description will cause the image to be loaded from the external
server in order to be displayed. However this also allows the external server
to log the IP address of the user.
One way to mitigate this is by proxying any external images to a server you
control. GitLab handles this by allowing you to run the "Camo" server
[cactus/go-camo](https://github.com/cactus/go-camo#how-it-works).
The image request is sent to the Camo server, which then makes the request for
the original image. This way an attacker only ever seems the IP address
of your Camo server.
Once you have your Camo server up and running, you can configure GitLab to
proxy image requests to it. The following settings are supported:
| Attribute | Description |
| ------------------------- | ----------- |
| `asset_proxy_enabled` | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. |
| `asset_proxy_secret_key` | Shared secret with the asset proxy server. |
| `asset_proxy_url` | URL of the asset proxy server. |
| `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. |
These can be set via the [Application setting API](../api/settings.md)
Note that a GitLab restart is required to apply any changes.
......@@ -71,6 +71,10 @@ module DesignManagement
filename
end
def to_ability_name
'design'
end
def description
''
end
......
......@@ -10,7 +10,11 @@ module EE
override :failure_reasons
def failure_reasons
super.merge(activity_limit_exceeded: 20, size_limit_exceeded: 21)
super.merge(
activity_limit_exceeded: 20,
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22
)
end
override :sources
......
......@@ -223,6 +223,10 @@ module EE
actual_plan&.pipeline_size_limit.to_i
end
def max_active_jobs
actual_plan&.active_jobs_limit.to_i
end
def memoized_plans=(plans)
@plans = plans # rubocop: disable Gitlab/ModuleWithInstanceVariables
end
......
......@@ -67,8 +67,29 @@ module EE
noteable&.after_note_destroyed(self)
end
override :visible_for?
def visible_for?(user)
return false unless super
return true unless system_note_for_epic? && created_before_noteable?
group_reporter?(user, noteable.group)
end
private
def system_note_for_epic?
for_epic? && system?
end
def created_before_noteable?
created_at.to_i < noteable.created_at.to_i
end
def group_reporter?(user, group)
group.max_member_access_for_user(user) >= ::Gitlab::Access::REPORTER
end
def banzai_context_params
{ group: noteable.group, label_url_method: :group_epics_url }
end
......
---
title: Limit number of jobs in running pipelines for the past hour on per plan basis
merge_request: 1182
author:
type: security
---
title: Filter out old system notes for epics in notes api endpoint response
merge_request:
author:
type: security
......@@ -83,7 +83,7 @@ module API
# They're not presented on Jira Dev Panel ATM. A comments count with a
# redirect link is presented.
notes = paginate(noteable.notes.user.reorder(nil))
notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes.select { |n| n.visible_for?(current_user) }
end
# rubocop: enable CodeReuse/ActiveRecord
......
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Pipeline
module Chain
module Limit
module JobActivity
extend ::Gitlab::Utils::Override
include ::Gitlab::Ci::Pipeline::Chain::Helpers
include ::Gitlab::OptimisticLocking
attr_reader :limit
private :limit
def initialize(*)
super
@limit = Pipeline::Quota::JobActivity
.new(project.namespace, pipeline.project)
end
override :perform!
def perform!
return unless limit.exceeded?
retry_optimistic_lock(pipeline) do
pipeline.drop!(:job_activity_limit_exceeded)
end
end
override :break?
def break?
limit.exceeded?
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Pipeline
module Quota
class JobActivity < Ci::Limit
include ::Gitlab::Utils::StrongMemoize
include ActionView::Helpers::TextHelper
def initialize(namespace, project)
@namespace = namespace
@project = project
end
def enabled?
strong_memoize(:enabled) do
@namespace.max_active_jobs > 0
end
end
def exceeded?
return false unless enabled?
excessive_jobs_count > 0
end
def message
return unless exceeded?
'Active jobs limit exceeded by ' \
"#{pluralize(excessive_jobs_count, 'job')} in the past 24 hours!"
end
private
def excessive_jobs_count
@excessive ||= jobs_in_alive_pipelines_count - max_active_jobs_count
end
# rubocop: disable CodeReuse/ActiveRecord
def jobs_in_alive_pipelines_count
@project.all_pipelines.created_after(24.hours.ago).alive.joins(:builds).count
end
# rubocop: enable CodeReuse/ActiveRecord
def max_active_jobs_count
@namespace.max_active_jobs
end
end
end
end
end
end
end
require 'spec_helper'
describe 'NotesHelpers' do
describe '#find_noteable' do
let!(:group) { create(:group, :public) }
let!(:other_group) { create(:group, :public) }
let!(:project) { create(:project, :public, namespace: group) }
let!(:user) { create(:group_member, :owner, group: group, user: create(:user)).user }
let!(:epic) { create(:epic, author: user, group: group) }
let!(:parent_id) { group.id }
let!(:noteable_type) { Epic }
let(:klazz) do
klazz = Class.new do
def initialize(user)
@user = user
end
def current_user
@user
end
def can?(user, ability, noteable)
user == @user && ability == :read_epic
end
end
klazz.prepend(API::Helpers::NotesHelpers)
end
let(:subject) { klazz.new(user) }
before do
stub_licensed_features(epics: true)
end
it 'returns the expected epic' do
expect(subject.find_noteable(Group, parent_id, noteable_type, epic.id)).to eq(epic)
end
it 'raises not found exception when epic does not belong to group' do
expect { subject.find_noteable(Group, other_group.id, noteable_type, epic.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
require 'spec_helper'
describe EE::Gitlab::Ci::Pipeline::Quota::JobActivity do
set(:namespace) { create(:namespace) }
set(:project) { create(:project, namespace: namespace) }
let(:active_jobs_limit) { 0 }
let(:gold_plan) { create(:gold_plan, active_jobs_limit: active_jobs_limit) }
let(:limit) { described_class.new(namespace, project) }
before do
create(:gitlab_subscription, namespace: namespace, hosted_plan: gold_plan)
end
describe '#enabled?' do
context 'when limit is enabled in plan' do
let(:active_jobs_limit) { 10 }
it 'is enabled' do
expect(limit).to be_enabled
end
end
context 'when limit is not enabled' do
it 'is not enabled' do
expect(limit).not_to be_enabled
end
end
end
describe '#exceeded?' do
let(:active_jobs_limit) { 2 }
context 'when pipelines created recently' do
context 'and pipelines are running' do
let(:pipeline1) { create(:ci_pipeline, project: project, status: 'created', created_at: Time.now) }
let(:pipeline2) { create(:ci_pipeline, project: project, status: 'created', created_at: Time.now) }
before do
create(:ci_build, pipeline: pipeline1)
create(:ci_build, pipeline: pipeline2)
end
context 'when count of jobs in alive pipelines is below the limit' do
it 'is not exceeded' do
expect(limit).not_to be_exceeded
end
end
context 'when count of jobs in alive pipelines is above the limit' do
before do
create(:ci_build, pipeline: pipeline2)
end
it 'is exceeded' do
expect(limit).to be_exceeded
end
end
end
context 'and pipelines are completed' do
before do
create(:ci_pipeline, project: project, status: 'success', created_at: Time.now).tap do |pipeline|
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
end
end
it 'is not exceeded' do
expect(limit).not_to be_exceeded
end
end
end
context 'when pipelines are older than 24 hours' do
before do
create(:ci_pipeline, project: project, status: 'created', created_at: 25.hours.ago).tap do |pipeline|
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
end
end
it 'is not exceeded' do
expect(limit).not_to be_exceeded
end
end
end
describe '#message' do
context 'when limit is exceeded' do
let(:active_jobs_limit) { 1 }
before do
create(:ci_pipeline, project: project, status: 'created', created_at: Time.now).tap do |pipeline|
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
end
end
it 'returns info about pipeline activity limit exceeded' do
expect(limit.message)
.to eq "Active jobs limit exceeded by 2 jobs in the past 24 hours!"
end
end
end
end
......@@ -43,7 +43,7 @@ describe ::Gitlab::Ci::Pipeline::Chain::Limit::Activity do
end
end
context 'when pipeline size limit is not exceeded' do
context 'when pipeline activity limit is not exceeded' do
before do
step.perform!
end
......
require 'spec_helper'
describe ::Gitlab::Ci::Pipeline::Chain::Limit::JobActivity do
set(:namespace) { create(:namespace) }
set(:project) { create(:project, namespace: namespace) }
set(:user) { create(:user) }
let(:command) do
double('command', project: project, current_user: user)
end
let(:pipeline) do
create(:ci_pipeline, project: project)
end
let(:step) { described_class.new(pipeline, command) }
context 'when active jobs limit is exceeded' do
before do
gold_plan = create(:gold_plan, active_jobs_limit: 2)
create(:gitlab_subscription, namespace: namespace, hosted_plan: gold_plan)
pipeline = create(:ci_pipeline, project: project, status: 'running', created_at: Time.now)
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
create(:ci_build, pipeline: pipeline)
step.perform!
end
it 'drops the pipeline' do
expect(pipeline.reload).to be_failed
end
it 'persists the pipeline' do
expect(pipeline).to be_persisted
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'sets a valid failure reason' do
expect(pipeline.job_activity_limit_exceeded?).to be true
end
end
context 'when job activity limit is not exceeded' do
before do
step.perform!
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'does not invalidate the pipeline' do
expect(pipeline.errors).to be_empty
end
end
end
......@@ -3,6 +3,22 @@
require 'spec_helper'
describe EE::Issuable do
describe "Validation" do
context 'general validations' do
subject { build(:epic) }
before do
allow(InternalId).to receive(:generate_next).and_return(nil)
end
it { is_expected.to validate_presence_of(:iid) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1_000_000) }
end
end
describe '.labels_hash' do
let(:feature_label) { create(:label, title: 'Feature') }
let(:second_label) { create(:label, title: 'Second Label') }
......@@ -50,4 +66,12 @@ describe EE::Issuable do
end
end
end
describe '#matches_cross_reference_regex?' do
context "epic description with long path string" do
let(:mentionable) { build(:epic, description: "/a" * 50000) }
it_behaves_like 'matches_cross_reference_regex? fails fast'
end
end
end
......@@ -130,6 +130,10 @@ describe DesignManagement::Design do
end
end
describe '#to_ability_name' do
it { expect(described_class.new.to_ability_name).to eq('design') }
end
describe '#status' do
context 'the design is new' do
subject { build(:design) }
......
......@@ -346,6 +346,45 @@ describe Namespace do
end
end
describe '#max_active_jobs' do
context 'when there is no limit defined' do
it 'returns zero' do
expect(namespace.max_active_jobs).to be_zero
end
end
context 'when free plan has limit defined' do
before do
free_plan.update_column(:active_jobs_limit, 100)
end
it 'returns a free plan limits' do
expect(namespace.max_active_jobs).to be 100
end
end
context 'when associated plan has no limit defined' do
before do
create(:gitlab_subscription, namespace: namespace, hosted_plan: gold_plan)
end
it 'returns zero' do
expect(namespace.max_active_jobs).to be_zero
end
end
context 'when limit is defined' do
before do
gold_plan.update_column(:active_jobs_limit, 10)
create(:gitlab_subscription, namespace: namespace, hosted_plan: gold_plan)
end
it 'returns a number of maximum active jobs' do
expect(namespace.max_active_jobs).to eq 10
end
end
end
describe '#shared_runners_enabled?' do
subject { namespace.shared_runners_enabled? }
......
......@@ -10,4 +10,66 @@ describe Note do
let(:backref_text) { issue.gfm_reference }
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
end
describe '#visible_for?' do
let(:owner) { create(:group_member, :owner, group: group, user: create(:user)).user }
let(:guest) { create(:group_member, :guest, group: group, user: create(:user)).user }
let(:reporter) { create(:group_member, :reporter, group: group, user: create(:user)).user }
let(:maintainer) { create(:group_member, :maintainer, group: group, user: create(:user)).user }
let(:group) { create(:group) }
let(:epic) { create(:epic, group: group, author: owner, created_at: 1.day.ago) }
before do
stub_licensed_features(epics: true)
end
context 'note created after epic' do
let(:note) { create(:system_note, noteable: epic, created_at: 1.minute.ago) }
it 'returns true for an owner' do
expect(note.visible_for?(owner)).to be_truthy
end
it 'returns true for a reporter' do
expect(note.visible_for?(reporter)).to be_truthy
end
it 'returns true for a maintainer' do
expect(note.visible_for?(maintainer)).to be_truthy
end
it 'returns true for a guest user' do
expect(note.visible_for?(guest)).to be_truthy
end
it 'returns true for a nil user' do
expect(note.visible_for?(nil)).to be_truthy
end
end
context 'when note is older than epic' do
let(:older_note) { create(:system_note, noteable: epic, created_at: 2.days.ago) }
it 'returns true for the owner' do
expect(older_note.visible_for?(owner)).to be_truthy
end
it 'returns true for a reporter' do
expect(older_note.visible_for?(reporter)).to be_truthy
end
it 'returns true for a maintainer' do
expect(older_note.visible_for?(maintainer)).to be_truthy
end
it 'returns false for a guest user' do
expect(older_note.visible_for?(guest)).to be_falsy
end
it 'returns false for a nil user' do
expect(older_note.visible_for?(nil)).to be_falsy
end
end
end
end
......@@ -26,5 +26,37 @@ describe API::Notes do
let(:noteable) { epic }
let(:note) { epic_note }
end
context 'when issue was promoted to epic' do
let!(:promoted_issue_epic) { create(:epic, group: group, author: owner, created_at: 1.day.ago) }
let!(:owner) { create(:group_member, :owner, user: create(:user), group: group).user }
let!(:reporter) { create(:group_member, :reporter, user: create(:user), group: group).user }
let!(:guest) { create(:group_member, :guest, user: create(:user), group: group).user }
let!(:previous_note) { create(:note, :system, noteable: promoted_issue_epic, created_at: 2.days.ago) }
let!(:previous_note2) { create(:note, :system, noteable: promoted_issue_epic, created_at: 2.minutes.ago) }
let!(:epic_note) { create(:note, noteable: promoted_issue_epic, author: owner) }
context 'when user is reporter' do
it 'returns previous issue system notes' do
get api("/groups/#{group.id}/epics/#{promoted_issue_epic.id}/notes", reporter)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
end
end
context 'when user is guest' do
it 'does not return previous issue system notes' do
get api("/groups/#{group.id}/epics/#{promoted_issue_epic.id}/notes", guest)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
end
end
end
end
end
require 'spec_helper'
describe TodoService do
include DesignManagementTestHelpers
let(:author) { create(:user, username: 'author') }
let(:non_member) { create(:user, username: 'non_member') }
let(:member) { create(:user, username: 'member') }
......@@ -309,6 +311,8 @@ describe TodoService do
let(:design) { create(:design, issue: issue) }
before do
enable_design_management
project.add_guest(author)
project.add_developer(john_doe)
end
......
......@@ -239,7 +239,7 @@ module API
# because notes are redacted if they point to projects that
# cannot be accessed by the user.
notes = prepare_notes_for_rendering(notes)
notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes.select { |n| n.visible_for?(current_user) }
end
# rubocop: enable CodeReuse/ActiveRecord
end
......
......@@ -1174,6 +1174,9 @@ module API
attributes.delete(:performance_bar_enabled)
attributes.delete(:allow_local_requests_from_hooks_and_services)
# let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key)
attributes
end
......
......@@ -12,7 +12,7 @@ module API
end
def update_note(noteable, note_id)
note = noteable.notes.find(params[:note_id])
note = noteable.notes.find(note_id)
authorize! :admin_note, note
......@@ -61,8 +61,8 @@ module API
end
def get_note(noteable, note_id)
note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = !note.cross_reference_not_visible_for?(current_user)
note = noteable.notes.with_metadata.find(note_id)
can_read_note = note.visible_for?(current_user)
if can_read_note
present note, with: Entities::Note
......
......@@ -42,7 +42,7 @@ module API
# array returned, but this is really a edge-case.
notes = paginate(raw_notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = notes.select { |note| note.visible_for?(current_user) }
present notes, with: Entities::Note
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -36,6 +36,10 @@ module API
given akismet_enabled: ->(val) { val } do
requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
end
optional :asset_proxy_enabled, type: Boolean, desc: 'Enable proxying of assets'
optional :asset_proxy_url, type: String, desc: 'URL of the asset proxy server'
optional :asset_proxy_secret_key, type: String, desc: 'Shared secret with the asset proxy server'
optional :asset_proxy_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted.'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
......@@ -104,6 +108,11 @@ module API
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
end
optional :login_recaptcha_protection_enabled, type: Boolean, desc: 'Helps prevent brute-force attacks'
given login_recaptcha_protection_enabled: ->(val) { val } do
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
end
optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
optional :repository_storages, type: Array[String], desc: 'Storage paths for new projects'
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication'
......@@ -123,7 +132,7 @@ module API
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated"
optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
given snowplow_enabled: ->(val) { val } do
......
# frozen_string_literal: true
module API
module Validations
module Types
class CommaSeparatedToArray
def self.coerce
lambda do |value|
case value
when String
value.split(',').map(&:strip)
when Array
value.map { |v| v.to_s.split(',').map(&:strip) }.flatten
else
[]
end
end
end
end
end
end
end
......@@ -7,6 +7,14 @@ module Banzai
class AbstractReferenceFilter < ReferenceFilter
include CrossProjectReference
# REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
# reference (which we replace with placeholder during re-scaping). The
# random number helps ensure it's pretty close to unique. Since it's a
# transitory value (it never gets saved) we can initialize once, and it
# doesn't matter if it changes on a restart.
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
def self.object_class
# Implement in child class
# Example: MergeRequest
......@@ -389,6 +397,14 @@ module Banzai
def escape_html_entities(text)
CGI.escapeHTML(text.to_s)
end
def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text)
escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
placeholder_data[$1.to_i]
end
end
end
end
end
......
# frozen_string_literal: true
module Banzai
module Filter
# Proxy's images/assets to another server. Reduces mixed content warnings
# as well as hiding the customer's IP address when requesting images.
# Copies the original img `src` to `data-canonical-src` then replaces the
# `src` with a new url to the proxy server.
class AssetProxyFilter < HTML::Pipeline::CamoFilter
def initialize(text, context = nil, result = nil)
super
end
def validate
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
end
def asset_host_whitelisted?(host)
context[:asset_proxy_domain_regexp] ? context[:asset_proxy_domain_regexp].match?(host) : false
end
def self.transform_context(context)
context[:disable_asset_proxy] = !Gitlab.config.asset_proxy.enabled
unless context[:disable_asset_proxy]
context[:asset_proxy_enabled] = !context[:disable_asset_proxy]
context[:asset_proxy] = Gitlab.config.asset_proxy.url
context[:asset_proxy_secret_key] = Gitlab.config.asset_proxy.secret_key
context[:asset_proxy_domain_regexp] = Gitlab.config.asset_proxy.domain_regexp
end
context
end
# called during an initializer. Caching the values in Gitlab.config significantly increased
# performance, rather than querying Gitlab::CurrentSettings.current_application_settings
# over and over. However, this does mean that the Rails servers need to get restarted
# whenever the application settings are changed
def self.initialize_settings
application_settings = Gitlab::CurrentSettings.current_application_settings
Gitlab.config['asset_proxy'] ||= Settingslogic.new({})
if application_settings.respond_to?(:asset_proxy_enabled)
Gitlab.config.asset_proxy['enabled'] = application_settings.asset_proxy_enabled
Gitlab.config.asset_proxy['url'] = application_settings.asset_proxy_url
Gitlab.config.asset_proxy['secret_key'] = application_settings.asset_proxy_secret_key
Gitlab.config.asset_proxy['whitelist'] = application_settings.asset_proxy_whitelist || [Gitlab.config.gitlab.host]
Gitlab.config.asset_proxy['domain_regexp'] = compile_whitelist(Gitlab.config.asset_proxy.whitelist)
else
Gitlab.config.asset_proxy['enabled'] = ::ApplicationSetting.defaults[:asset_proxy_enabled]
end
end
def self.compile_whitelist(domain_list)
return if domain_list.empty?
escaped = domain_list.map { |domain| Regexp.escape(domain).gsub('\*', '.*?') }
Regexp.new("^(#{escaped.join('|')})$", Regexp::IGNORECASE)
end
end
end
end
......@@ -14,10 +14,10 @@ module Banzai
# such as on `mailto:` links. Since we've been using it, do an
# initial parse for validity and then use Addressable
# for IDN support, etc
uri = uri_strict(node['href'].to_s)
uri = uri_strict(node_src(node))
if uri
node.set_attribute('href', uri.to_s)
addressable_uri = addressable_uri(node['href'])
node.set_attribute(node_src_attribute(node), uri.to_s)
addressable_uri = addressable_uri(node_src(node))
else
addressable_uri = nil
end
......@@ -35,6 +35,16 @@ module Banzai
private
# if this is a link to a proxied image, then `src` is already the correct
# proxied url, so work with the `data-canonical-src`
def node_src_attribute(node)
node['data-canonical-src'] ? 'data-canonical-src' : 'href'
end
def node_src(node)
node[node_src_attribute(node)]
end
def uri_strict(href)
URI.parse(href)
rescue URI::Error
......@@ -72,7 +82,7 @@ module Banzai
return unless uri
return unless context[:emailable_links]
unencoded_uri_str = Addressable::URI.unencode(node['href'])
unencoded_uri_str = Addressable::URI.unencode(node_src(node))
if unencoded_uri_str == node.content && idn?(uri)
node.content = uri.normalize
......
......@@ -18,6 +18,9 @@ module Banzai
rel: 'noopener noreferrer'
)
# make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
link.children = img.clone
img.replace(link)
......
......@@ -14,24 +14,24 @@ module Banzai
find_labels(parent_object).find(id)
end
def self.references_in(text, pattern = Label.reference_pattern)
unescape_html_entities(text).gsub(pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~
end
end
def references_in(text, pattern = Label.reference_pattern)
unescape_html_entities(text).gsub(pattern) do |match|
labels = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
namespace, project = $~[:namespace], $~[:project]
project_path = full_project_path(namespace, project)
label = find_label(project_path, $~[:label_id], $~[:label_name])
if label
yield match, label.id, project, namespace, $~
labels[label.id] = yield match, label.id, project, namespace, $~
"#{REFERENCE_PLACEHOLDER}#{label.id}"
else
escape_html_entities(match)
match
end
end
return text if labels.empty?
escape_with_placeholders(unescaped_html, labels)
end
def find_label(parent_ref, label_id, label_name)
......
......@@ -51,15 +51,21 @@ module Banzai
# default implementation.
return super(text, pattern) if pattern != Milestone.reference_pattern
unescape_html_entities(text).gsub(pattern) do |match|
milestones = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
if milestone
yield match, milestone.id, $~[:project], $~[:namespace], $~
milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
"#{REFERENCE_PLACEHOLDER}#{milestone.id}"
else
escape_html_entities(match)
match
end
end
return text if milestones.empty?
escape_with_placeholders(unescaped_html, milestones)
end
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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