summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-30 21:08:47 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-30 21:08:47 +0000
commitc8f773a8593926f4f2dec6f446a3b3e59e9c9909 (patch)
tree4e5ea1d3b861ff99015f6112da567de7873868aa /app
parent929b887e5391dea7cb53b88b77b9a35351c87d99 (diff)
downloadgitlab-ce-c8f773a8593926f4f2dec6f446a3b3e59e9c9909.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue6
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue6
-rw-r--r--app/assets/javascripts/frequent_items/utils.js7
-rw-r--r--app/assets/javascripts/groups_select.js7
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/user_popovers.js160
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue11
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/dashboard_controller.rb3
-rw-r--r--app/controllers/groups_controller.rb7
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/graphql/types/base_field.rb21
-rw-r--r--app/graphql/types/grafana_integration_type.rb11
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/models/generic_commit_status.rb15
-rw-r--r--app/models/grafana_integration.rb28
-rw-r--r--app/models/note.rb3
-rw-r--r--app/models/project.rb4
-rw-r--r--app/policies/base_policy.rb8
-rw-r--r--app/policies/global_policy.rb7
-rw-r--r--app/presenters/event_presenter.rb12
-rw-r--r--app/services/compare_service.rb2
-rw-r--r--app/services/projects/group_links/destroy_service.rb6
-rw-r--r--app/services/projects/import_export/export_service.rb6
-rw-r--r--app/views/projects/settings/operations/_grafana_integration.html.haml2
25 files changed, 229 insertions, 120 deletions
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 8cf939254c1..2ffecce0a56 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -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;
}
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index 67ffa97a046..0ece64692ae 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -1,6 +1,7 @@
<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"
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index cc1668b1a0d..5188d6118ac 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -1,5 +1,6 @@
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: [] }),
+});
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index a5e38022b8d..4daa8c60e58 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,6 +1,7 @@
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
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index be2adb07526..762228dd138 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -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',
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 157d89a3a40..5b9e3817f3a 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -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;
});
};
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 37e3643bf6c..ca25d9ee738 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -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>
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 789bccf268a..fa88ca91170 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,6 +5,7 @@ require 'fogbugz'
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 @@ class ApplicationController < ActionController::Base
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
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 1a97b39d3ae..1668cf004f8 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController
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 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+ .map(&:present)
Events::RenderService.new(current_user).execute(@events)
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 0953ca96317..958dc27984f 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
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 @@ class GroupsController < Groups::ApplicationController
.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)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index f4f2a16b82b..d39a4c373ff 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -118,7 +118,7 @@ class ProjectsController < Projects::ApplicationController
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 @@ class ProjectsController < Projects::ApplicationController
@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
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index efeee4a7a4d..3ade1300c2d 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -10,6 +10,8 @@ module Types
@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 @@ module Types
@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
diff --git a/app/graphql/types/grafana_integration_type.rb b/app/graphql/types/grafana_integration_type.rb
index e6c865fea53..f234008ee0d 100644
--- a/app/graphql/types/grafana_integration_type.rb
+++ b/app/graphql/types/grafana_integration_type.rb
@@ -10,14 +10,19 @@ module Types
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
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index b8d7685c2cf..011871f373f 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -368,8 +368,8 @@ module ProjectsHelper
@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?
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 8a768b3a2c0..6c8bfc35334 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -1,11 +1,14 @@
# 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 @@ class GenericCommitStatus < CommitStatus
.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
diff --git a/app/models/grafana_integration.rb b/app/models/grafana_integration.rb
index ed4c279965a..00213732fee 100644
--- a/app/models/grafana_integration.rb
+++ b/app/models/grafana_integration.rb
@@ -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 @@ class GrafanaIntegration < ApplicationRecord
@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, masked_token_was].include?(token)
+
+ clear_attribute_changes [:token, :encrypted_token, :encrypted_token_iv]
+ end
+
+ def mask(token)
+ token&.squish&.gsub(/./, '*')
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 0434f0963d3..8af650e27aa 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -545,7 +545,8 @@ class Note < ApplicationRecord
# if they are not equal, then there are private/confidential references as well
user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
else
- referenced_mentionables(user).any?
+ refs = all_references(user)
+ refs.all.any? && refs.stateful_not_visible_counter == 0
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 8cb35904d92..064c647ac59 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2366,6 +2366,10 @@ class Project < ApplicationRecord
end
end
+ def template_source?
+ false
+ end
+
private
def closest_namespace_setting(name)
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 3a16f7dc239..c93a19bdc3d 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -21,6 +21,14 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? }
+ desc "User email is unconfirmed or user account is locked"
+ with_options scope: :user, score: 0
+ condition(:inactive) do
+ Feature.enabled?(:inactive_policy_condition, default_enabled: true) &&
+ @user &&
+ !@user&.active_for_authentication?
+ end
+
with_options scope: :user, score: 0
condition(:external_user) { @user.nil? || @user.external? }
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 764d61a9e22..2bde7bcca08 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -36,6 +36,13 @@ class GlobalPolicy < BasePolicy
enable :use_slash_commands
end
+ rule { inactive }.policy do
+ prevent :log_in
+ prevent :access_api
+ prevent :access_git
+ prevent :use_slash_commands
+ end
+
rule { blocked | internal }.policy do
prevent :log_in
prevent :access_api
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index f31d362d5fa..5657e0b96bc 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -3,6 +3,18 @@
class EventPresenter < Gitlab::View::Presenter::Delegated
presents :event
+ def initialize(subject, **attributes)
+ super
+
+ @visible_to_user_cache = ActiveSupport::Cache::MemoryStore.new
+ end
+
+ # Caching `visible_to_user?` method in the presenter beause it might be called multiple times.
+ def visible_to_user?(user = nil)
+ @visible_to_user_cache.fetch(user&.id) { super(user) }
+ end
+
+ # implement cache here
def resource_parent_name
resource_parent&.full_name || ''
end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 3f0aedfbfb2..569b91de73e 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -18,7 +18,7 @@ class CompareService
return unless raw_compare && raw_compare.base && raw_compare.head
Compare.new(raw_compare,
- target_project,
+ start_project,
base_sha: base_sha,
straight: straight)
end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
index c96dcaae8d5..ea7d05551fd 100644
--- a/app/services/projects/group_links/destroy_service.rb
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -6,6 +6,12 @@ module Projects
def execute(group_link)
return false unless group_link
+ if group_link.project.private?
+ TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
+ else
+ TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
+ end
+
group_link.destroy
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 8344397f67d..38859c1efa4 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -4,6 +4,12 @@ module Projects
module ImportExport
class ExportService < BaseService
def execute(after_export_strategy = nil, options = {})
+ unless project.template_source? || can?(current_user, :admin_project, project)
+ raise ::Gitlab::ImportExport::Error.new(
+ "User with ID: %s does not have permission to Project %s with ID: %s." %
+ [current_user.id, project.name, project.id])
+ end
+
@shared = project.import_export_shared
save_all!
diff --git a/app/views/projects/settings/operations/_grafana_integration.html.haml b/app/views/projects/settings/operations/_grafana_integration.html.haml
index cd5b5abd9ce..69e42a6c4fb 100644
--- a/app/views/projects/settings/operations/_grafana_integration.html.haml
+++ b/app/views/projects/settings/operations/_grafana_integration.html.haml
@@ -1,2 +1,2 @@
.js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
- grafana_integration: { url: grafana_integration_url, token: grafana_integration_token, enabled: grafana_integration_enabled?.to_s } } }
+ grafana_integration: { url: grafana_integration_url, token: grafana_integration_masked_token, enabled: grafana_integration_enabled?.to_s } } }