...@@ -64,7 +64,7 @@ gem 'u2f', '~> 0.2.1' ...@@ -64,7 +64,7 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages # GitLab Pages
gem 'validates_hostname', '~> 1.0.6' gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.3.0', require: 'zip' gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support # GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.2' gem 'acme-client', '~> 2.0.2'
...@@ -141,7 +141,7 @@ gem 'gitlab-markup', '~> 1.7.0' ...@@ -141,7 +141,7 @@ gem 'gitlab-markup', '~> 1.7.0'
gem 'github-markup', '~> 1.7.0', require: 'github/markup' gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.17' gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 6.0' gem 'rdoc', '~> 6.1.2'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
... ...
......
...@@ -262,7 +262,7 @@ GEM ...@@ -262,7 +262,7 @@ GEM
et-orbi (1.2.1) et-orbi (1.2.1)
tzinfo tzinfo
eventmachine (1.2.7) eventmachine (1.2.7)
excon (0.62.0) excon (0.71.1)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extended-markdown-filter (0.6.0) extended-markdown-filter (0.6.0)
...@@ -760,7 +760,8 @@ GEM ...@@ -760,7 +760,8 @@ GEM
rack (>= 0.4) rack (>= 0.4)
rack-attack (6.2.0) rack-attack (6.2.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.0.2) rack-cors (1.0.6)
rack (>= 1.6.0)
rack-oauth2 (1.9.3) rack-oauth2 (1.9.3)
activesupport activesupport
attr_required attr_required
...@@ -817,7 +818,7 @@ GEM ...@@ -817,7 +818,7 @@ GEM
ffi (>= 1.0.6) ffi (>= 1.0.6)
msgpack (>= 0.4.3) msgpack (>= 0.4.3)
optimist (>= 3.0.0) optimist (>= 3.0.0)
rdoc (6.0.4) rdoc (6.1.2)
re2 (1.1.1) re2 (1.1.1)
recaptcha (4.13.1) recaptcha (4.13.1)
json json
...@@ -926,7 +927,7 @@ GEM ...@@ -926,7 +927,7 @@ GEM
sexp_processor (~> 4.9) sexp_processor (~> 4.9)
rubyntlm (0.6.2) rubyntlm (0.6.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.3.0) rubyzip (2.0.0)
rugged (0.28.3.1) rugged (0.28.3.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
...@@ -1295,7 +1296,7 @@ DEPENDENCIES ...@@ -1295,7 +1296,7 @@ DEPENDENCIES
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbtrace (~> 0.4) rbtrace (~> 0.4)
rdoc (~> 6.0) rdoc (~> 6.1.2)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 4.11) recaptcha (~> 4.11)
redis (~> 4.0) redis (~> 4.0)
...@@ -1319,7 +1320,7 @@ DEPENDENCIES ...@@ -1319,7 +1320,7 @@ DEPENDENCIES
ruby-prof (~> 1.0.0) ruby-prof (~> 1.0.0)
ruby-progressbar ruby-progressbar
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rubyzip (~> 1.3.0) rubyzip (~> 2.0.0)
rugged (~> 0.28) rugged (~> 0.28)
sanitize (~> 4.6) sanitize (~> 4.6)
sassc-rails (~> 2.1.0) sassc-rails (~> 2.1.0)
... ...
......
...@@ -5,7 +5,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -5,7 +5,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import store from '../store/'; import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants'; 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 FrequentItemsSearchInput from './frequent_items_search_input.vue';
import FrequentItemsList from './frequent_items_list.vue'; import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin'; import frequentItemsMixin from './frequent_items_mixin';
...@@ -64,7 +64,9 @@ export default { ...@@ -64,7 +64,9 @@ export default {
this.fetchFrequentItems(); this.fetchFrequentItems();
} }
}, },
logItemAccess(storageKey, item) { logItemAccess(storageKey, unsanitizedItem) {
const item = sanitizeItem(unsanitizedItem);
if (!AccessorUtilities.isLocalStorageAccessSafe()) { if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return false; return false;
} }
... ...
......
<script> <script>
import FrequentItemsListItem from './frequent_items_list_item.vue'; import FrequentItemsListItem from './frequent_items_list_item.vue';
import frequentItemsMixin from './frequent_items_mixin'; import frequentItemsMixin from './frequent_items_mixin';
import { sanitizeItem } from '../utils';
export default { export default {
components: { components: {
...@@ -48,6 +49,9 @@ export default { ...@@ -48,6 +49,9 @@ export default {
? this.translations.itemListErrorMessage ? this.translations.itemListErrorMessage
: this.translations.itemListEmptyMessage; : this.translations.itemListEmptyMessage;
}, },
sanitizedItems() {
return this.items.map(sanitizeItem);
},
}, },
}; };
</script> </script>
...@@ -59,7 +63,7 @@ export default { ...@@ -59,7 +63,7 @@ export default {
{{ listEmptyMessage }} {{ listEmptyMessage }}
</li> </li>
<frequent-items-list-item <frequent-items-list-item
v-for="item in items" v-for="item in sanitizedItems"
v-else v-else
:key="item.id" :key="item.id"
:item-id="item.id" :item-id="item.id"
... ...
......
import _ from 'underscore'; import _ from 'underscore';
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import sanitize from 'sanitize-html';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => { export const isMobile = () => {
...@@ -47,3 +48,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => { ...@@ -47,3 +48,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => {
lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn, 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 $ from 'jquery';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import Api from './api'; import Api from './api';
import { escape } from 'lodash';
import { normalizeHeaders } from './lib/utils/common_utils'; import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -75,10 +76,12 @@ const groupsSelect = () => { ...@@ -75,10 +76,12 @@ const groupsSelect = () => {
} }
}, },
formatResult(object) { 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) { formatSelection(object) {
return object.full_name; return escape(object.full_name);
}, },
dropdownCssClass: 'ajax-groups-dropdown select2-infinite', dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results // we do not want to escape markup since we are displaying html in results
... ...
......
...@@ -5,6 +5,7 @@ require 'fogbugz' ...@@ -5,6 +5,7 @@ require 'fogbugz'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Gitlab::GonHelper include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
include SafeParamsHelper include SafeParamsHelper
...@@ -54,7 +55,6 @@ class ApplicationController < ActionController::Base ...@@ -54,7 +55,6 @@ class ApplicationController < ActionController::Base
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
# concerns due to caching private data. # concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store" 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| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -238,9 +238,9 @@ class ApplicationController < ActionController::Base ...@@ -238,9 +238,9 @@ class ApplicationController < ActionController::Base
end end
def no_cache_headers def no_cache_headers
headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility headers[k] = v
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' end
end end
def default_headers def default_headers
... ...
......
...@@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end end
end end
end end
...@@ -37,6 +37,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -37,6 +37,7 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events) Events::RenderService.new(current_user).execute(@events)
end end
... ...
......
...@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController ...@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json("events/_events", @events.count { |event| event.visible_to_user?(current_user) })
end end
end end
end end
...@@ -210,6 +210,7 @@ class GroupsController < Groups::ApplicationController ...@@ -210,6 +210,7 @@ class GroupsController < Groups::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups) .new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
.to_a .to_a
.map(&:present)
Events::RenderService Events::RenderService
.new(current_user) .new(current_user)
... ...
......
...@@ -116,7 +116,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -116,7 +116,7 @@ class ProjectsController < Projects::ApplicationController
format.html format.html
format.json do format.json do
load_events load_events
pager_json('events/_events', @events.count) pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end end
end end
end end
...@@ -337,6 +337,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -337,6 +337,7 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
... ...
......
...@@ -365,8 +365,8 @@ module ProjectsHelper ...@@ -365,8 +365,8 @@ module ProjectsHelper
@project.grafana_integration&.grafana_url @project.grafana_integration&.grafana_url
end end
def grafana_integration_token def grafana_integration_masked_token
@project.grafana_integration&.token @project.grafana_integration&.masked_token
end end
def grafana_integration_enabled? def grafana_integration_enabled?
... ...
......
# frozen_string_literal: true # frozen_string_literal: true
class GenericCommitStatus < CommitStatus class GenericCommitStatus < CommitStatus
EXTERNAL_STAGE_IDX = 1_000_000
before_validation :set_default_values before_validation :set_default_values
validates :target_url, addressable_url: true, validates :target_url, addressable_url: true,
length: { maximum: 255 }, length: { maximum: 255 },
allow_nil: true allow_nil: true
validate :name_uniqueness_across_types, unless: :importing?
# GitHub compatible API # GitHub compatible API
alias_attribute :context, :name alias_attribute :context, :name
...@@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus ...@@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus
def set_default_values def set_default_values
self.context ||= 'default' self.context ||= 'default'
self.stage ||= 'external' self.stage ||= 'external'
self.stage_idx ||= 1000000 self.stage_idx ||= EXTERNAL_STAGE_IDX
end end
def tags def tags
...@@ -25,4 +28,14 @@ class GenericCommitStatus < CommitStatus ...@@ -25,4 +28,14 @@ class GenericCommitStatus < CommitStatus
.new(self, current_user) .new(self, current_user)
.fabricate! .fabricate!
end end
private
def name_uniqueness_across_types
return if !pipeline || name.blank?
if pipeline.statuses.where(name: name).where.not(type: type).exists?
errors.add(:name, :taken)
end
end
end end
...@@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord ...@@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord
algorithm: 'aes-256-gcm', algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32 key: Settings.attr_encrypted_db_key_base_32
before_validation :check_token_changes
validates :grafana_url, validates :grafana_url,
length: { maximum: 1024 }, length: { maximum: 1024 },
addressable_url: { enforce_sanitization: true, ascii_only: true } 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] } validates :enabled, inclusion: { in: [true, false] }
...@@ -23,4 +25,28 @@ class GrafanaIntegration < ApplicationRecord ...@@ -23,4 +25,28 @@ class GrafanaIntegration < ApplicationRecord
@client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token) @client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token)
end 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 end
...@@ -537,7 +537,8 @@ class Note < ApplicationRecord ...@@ -537,7 +537,8 @@ class Note < ApplicationRecord
# if they are not equal, then there are private/confidential references as well # 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 user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
else else
referenced_mentionables(user).any? refs = all_references(user)
refs.all.any? && refs.stateful_not_visible_counter == 0
end end
end end
... ...
......
...@@ -2274,6 +2274,10 @@ class Project < ApplicationRecord ...@@ -2274,6 +2274,10 @@ class Project < ApplicationRecord
end end
end end
def template_source?
false
end
private private
def closest_namespace_setting(name) def closest_namespace_setting(name)
... ...
......
...@@ -21,6 +21,14 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -21,6 +21,14 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? } 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 with_options scope: :user, score: 0
condition(:external_user) { @user.nil? || @user.external? } condition(:external_user) { @user.nil? || @user.external? }
... ...
......
...@@ -36,6 +36,13 @@ class GlobalPolicy < BasePolicy ...@@ -36,6 +36,13 @@ class GlobalPolicy < BasePolicy
enable :use_slash_commands enable :use_slash_commands
end end
rule { inactive }.policy do
prevent :log_in
prevent :access_api
prevent :access_git
prevent :use_slash_commands
end
rule { blocked | internal }.policy do rule { blocked | internal }.policy do
prevent :log_in prevent :log_in
prevent :access_api prevent :access_api
... ...
......
...@@ -3,6 +3,18 @@ ...@@ -3,6 +3,18 @@
class EventPresenter < Gitlab::View::Presenter::Delegated class EventPresenter < Gitlab::View::Presenter::Delegated
presents :event 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 def resource_parent_name
resource_parent&.full_name || '' resource_parent&.full_name || ''
end end
... ...
......
...@@ -18,7 +18,7 @@ class CompareService ...@@ -18,7 +18,7 @@ class CompareService
return unless raw_compare && raw_compare.base && raw_compare.head return unless raw_compare && raw_compare.base && raw_compare.head
Compare.new(raw_compare, Compare.new(raw_compare,
target_project, start_project,
base_sha: base_sha, base_sha: base_sha,
straight: straight) straight: straight)
end end
... ...
......
...@@ -6,6 +6,12 @@ module Projects ...@@ -6,6 +6,12 @@ module Projects
def execute(group_link) def execute(group_link)
return false unless 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 group_link.destroy
end end
end end
... ...
......