Read about our upcoming Code of Conduct on this issue

Commit 3dee439d authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent ee9ec51f9502
Please view this file on the master branch, on stable branches it's out of date.
## 12.7.3
- No changes.
## 12.7.1
### Fixed (1 change)
......@@ -95,6 +99,14 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove "creations" in gitlab_subscription_histories on gitlab.com. !22278
## 12.6.6
- No changes.
## 12.6.5
- No changes.
## 12.6.4
- No changes.
......@@ -207,6 +219,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update the alerts used in the Dependency List to follow GitLab design guidelines. !21760
## 12.5.8
- No changes.
## 12.5.5
- No changes.
......
......@@ -65,7 +65,7 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.3.0', require: 'zip'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.5'
......
......@@ -274,7 +274,7 @@ GEM
et-orbi (1.2.1)
tzinfo
eventmachine (1.2.7)
excon (0.62.0)
excon (0.71.1)
execjs (2.6.0)
expression_parser (0.9.0)
extended-markdown-filter (0.6.0)
......@@ -961,7 +961,7 @@ GEM
sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.3.0)
rubyzip (2.0.0)
rugged (0.28.4.1)
safe_yaml (1.0.4)
sanitize (4.6.6)
......@@ -1361,7 +1361,7 @@ DEPENDENCIES
ruby-prof (~> 1.0.0)
ruby-progressbar
ruby_parser (~> 3.8)
rubyzip (~> 1.3.0)
rubyzip (~> 2.0.0)
rugged (~> 0.28)
sanitize (~> 4.6)
sassc-rails (~> 2.1.0)
......
......@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem } from '../utils';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin';
......@@ -64,7 +64,9 @@ export default {
this.fetchFrequentItems();
}
},
logItemAccess(storageKey, item) {
logItemAccess(storageKey, unsanitizedItem) {
const item = sanitizeItem(unsanitizedItem);
if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return false;
}
......
<script>
import FrequentItemsListItem from './frequent_items_list_item.vue';
import frequentItemsMixin from './frequent_items_mixin';
import { sanitizeItem } from '../utils';
export default {
components: {
......@@ -48,6 +49,9 @@ export default {
? this.translations.itemListErrorMessage
: this.translations.itemListEmptyMessage;
},
sanitizedItems() {
return this.items.map(sanitizeItem);
},
},
};
</script>
......@@ -59,7 +63,7 @@ export default {
{{ listEmptyMessage }}
</li>
<frequent-items-list-item
v-for="item in items"
v-for="item in sanitizedItems"
v-else
:key="item.id"
:item-id="item.id"
......
import _ from 'underscore';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import sanitize from 'sanitize-html';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
......@@ -43,3 +44,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => {
lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
};
};
export const sanitizeItem = item => ({
...item,
name: sanitize(item.name.toString(), { allowedTags: [] }),
namespace: sanitize(item.namespace.toString(), { allowedTags: [] }),
});
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import Api from './api';
import { escape } from 'lodash';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
......@@ -75,10 +76,12 @@ const groupsSelect = () => {
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
return `<div class='group-result'> <div class='group-name'>${escape(
object.full_name,
)}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
return escape(object.full_name);
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
......
......@@ -14,7 +14,7 @@ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { __ } from '~/locale';
import initUserPopovers from '../../user_popovers';
import initUserPopovers from '~/user_popovers';
export default {
name: 'NotesApp',
......
......@@ -3,108 +3,92 @@ import Vue from 'vue';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
let renderedPopover;
let renderFn;
const handleUserPopoverMouseOut = event => {
const { target } = event;
target.removeEventListener('mouseleave', handleUserPopoverMouseOut);
if (renderFn) {
clearTimeout(renderFn);
}
if (renderedPopover) {
renderedPopover.$destroy();
renderedPopover = null;
}
target.removeAttribute('aria-describedby');
const removeTitle = el => {
// Removing titles so its not showing tooltips also
el.dataset.originalTitle = '';
el.setAttribute('title', '');
};
const getPreloadedUserInfo = dataset => {
const userId = dataset.user || dataset.userId;
const { username, name, avatarUrl } = dataset;
return {
userId,
username,
name,
avatarUrl,
};
};
/**
* Adds a UserPopover component to the body, hands over as much data as the target element has in data attributes.
* loads based on data-user-id more data about a user from the API and sets it on the popover
*/
const handleUserPopoverMouseOver = event => {
const { target } = event;
// Add listener to actually remove it again
target.addEventListener('mouseleave', handleUserPopoverMouseOut);
renderFn = setTimeout(() => {
// Helps us to use current markdown setup without maybe breaking or duplicating for now
if (target.dataset.user) {
target.dataset.userId = target.dataset.user;
// Removing titles so its not showing tooltips also
target.dataset.originalTitle = '';
target.setAttribute('title', '');
}
const { userId, username, name, avatarUrl } = target.dataset;
const populateUserInfo = user => {
const { userId } = user;
return Promise.all([UsersCache.retrieveById(userId), UsersCache.retrieveStatusById(userId)]).then(
([userData, status]) => {
if (userData) {
Object.assign(user, {
avatarUrl: userData.avatar_url,
username: userData.username,
name: userData.name,
location: userData.location,
bio: userData.bio,
organization: userData.organization,
loaded: true,
});
}
if (status) {
Object.assign(user, {
status,
});
}
return user;
},
);
};
export default (elements = document.querySelectorAll('.js-user-link')) => {
const userLinks = Array.from(elements);
return userLinks.map(el => {
const UserPopoverComponent = Vue.extend(UserPopover);
const user = {
userId,
username,
name,
avatarUrl,
location: null,
bio: null,
organization: null,
status: null,
loaded: false,
};
if (userId || username) {
const UserPopoverComponent = Vue.extend(UserPopover);
renderedPopover = new UserPopoverComponent({
propsData: {
target,
user,
},
});
renderedPopover.$mount();
UsersCache.retrieveById(userId)
.then(userData => {
if (!userData) {
return undefined;
}
Object.assign(user, {
avatarUrl: userData.avatar_url,
username: userData.username,
name: userData.name,
location: userData.location,
bio: userData.bio,
organization: userData.organization,
status: userData.status,
loaded: true,
});
if (userData.status) {
return Promise.resolve();
}
return UsersCache.retrieveStatusById(userId);
})
.then(status => {
if (!status) {
return;
}
Object.assign(user, {
status,
});
})
.catch(() => {
renderedPopover.$destroy();
renderedPopover = null;
});
}
}, 200); // 200ms delay so not every mouseover triggers Popover + API Call
};
const renderedPopover = new UserPopoverComponent({
propsData: {
target: el,
user,
},
});
renderedPopover.$mount();
el.addEventListener('mouseenter', ({ target }) => {
removeTitle(target);
const preloadedUserInfo = getPreloadedUserInfo(target.dataset);
Object.assign(user, preloadedUserInfo);
export default elements => {
const userLinks = elements || [...document.querySelectorAll('.js-user-link')];
if (preloadedUserInfo.userId) {
populateUserInfo(user);
}
});
el.addEventListener('mouseleave', ({ target }) => {
target.removeAttribute('aria-describedby');
});
userLinks.forEach(el => {
el.addEventListener('mouseenter', handleUserPopoverMouseOver);
return renderedPopover;
});
};
......@@ -56,19 +56,16 @@ export default {
</script>
<template>
<gl-popover :target="target" boundary="viewport" placement="top" offset="0, 1" show>
<!-- 200ms delay so not every mouseover triggers Popover -->
<gl-popover :target="target" :delay="200" boundary="viewport" triggers="hover" placement="top">
<div class="user-popover d-flex">
<div class="p-1 flex-shrink-1">
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
</div>
<div class="p-1 w-100">
<h5 class="m-0">
{{ user.name }}
<gl-skeleton-loading
v-if="nameIsLoading"
:lines="1"
class="animation-container-small mb-1"
/>
<span v-if="user.name">{{ user.name }}</span>
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</h5>
<div class="text-secondary mb-2">
<span v-if="user.username">@{{ user.username }}</span>
......
......@@ -5,6 +5,7 @@
class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper
include PageLayoutHelper
include SafeParamsHelper
......@@ -55,7 +56,6 @@ class ApplicationController < ActionController::Base
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
# concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store"
DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache"
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
......@@ -247,9 +247,9 @@ def render_503
end
def no_cache_headers
headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE
headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
headers[k] = v
end
end
def default_headers
......
......@@ -19,7 +19,7 @@ def activity
format.json do
load_events
pager_json("events/_events", @events.count)
pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end
end
end
......@@ -37,6 +37,7 @@ def load_events
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events)
end
......
......@@ -91,7 +91,7 @@ def activity
format.json do
load_events
pager_json("events/_events", @events.count)
pager_json("events/_events", @events.count { |event| event.visible_to_user?(current_user) })
end
end
end
......@@ -209,8 +209,9 @@ def load_events
.includes(:namespace)
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
.to_a
.new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
.to_a
.map(&:present)
Events::RenderService
.new(current_user)
......
......@@ -118,7 +118,7 @@ def activity
format.html
format.json do
load_events
pager_json('events/_events', @events.count)
pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end
end
end
......@@ -343,6 +343,7 @@ def load_events
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
......
......@@ -10,6 +10,8 @@ def initialize(*args, **kwargs, &block)
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
@constant_complexity = !!kwargs[:complexity]
kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class])
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
super(*args, **kwargs, &block)
end
......@@ -28,8 +30,27 @@ def constant_complexity?
@constant_complexity
end
def visible?(context)
return false if feature_flag.present? && !Feature.enabled?(feature_flag)
super
end
private
attr_reader :feature_flag
def feature_documentation_message(key, description)
"#{description}. Available only when feature flag #{key} is enabled."
end
def check_feature_flag(args)
args[:description] = feature_documentation_message(args[:feature_flag], args[:description]) if args[:feature_flag].present?
args.delete(:feature_flag)
args
end
def field_complexity(resolver_class)
if resolver_class
field_resolver_complexity
......
......@@ -10,14 +10,19 @@ class GrafanaIntegrationType < ::Types::BaseObject
description: 'Internal ID of the Grafana integration'
field :grafana_url, GraphQL::STRING_TYPE, null: false,
description: 'Url for the Grafana host for the Grafana integration'
field :token, GraphQL::STRING_TYPE, null: false,
description: 'API token for the Grafana integration'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates whether Grafana integration is enabled'
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s creation'
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s last activity'
field :token, GraphQL::STRING_TYPE, null: false,
deprecation_reason: 'Plain text token has been masked for security reasons',
description: 'API token for the Grafana integration. Field is permanently masked.'
def token
object.masked_token
end
end
end
......@@ -368,8 +368,8 @@ def grafana_integration_url
@project.grafana_integration&.grafana_url
end
def grafana_integration_token
@project.grafana_integration&.token
def grafana_integration_masked_token
@project.grafana_integration&.masked_token
end
def grafana_integration_enabled?
......
# frozen_string_literal: true
class GenericCommitStatus < CommitStatus
EXTERNAL_STAGE_IDX = 1_000_000
before_validation :set_default_values
validates :target_url, addressable_url: true,
length: { maximum: 255 },
allow_nil: true
validate :name_uniqueness_across_types, unless: :importing?
# GitHub compatible API
alias_attribute :context, :name
......@@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus
def set_default_values
self.context ||= 'default'
self.stage ||= 'external'
self.stage_idx ||= 1000000
self.stage_idx ||= EXTERNAL_STAGE_IDX
end
def tags
......@@ -25,4 +28,14 @@ def detailed_status(current_user)
.new(self, current_user)
.fabricate!
end
private
def name_uniqueness_across_types
return if !pipeline || name.blank?
if pipeline.statuses.by_name(name).where.not(type: type).exists?
errors.add(:name, :taken)
end
end
end
......@@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32
before_validation :check_token_changes
validates :grafana_url,
length: { maximum: 1024 },
addressable_url: { enforce_sanitization: true, ascii_only: true }
validates :token, :project, presence: true
validates :encrypted_token, :project, presence: true
validates :enabled, inclusion: { in: [true, false] }
......@@ -23,4 +25,28 @@ def client
@client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token)
end
def masked_token
mask(encrypted_token)
end
def masked_token_was
mask(encrypted_token_was)
end
private
def token
decrypt(:token, encrypted_token)
end
def check_token_changes
return unless [encrypted_token_was