...@@ -24,7 +24,8 @@ ...@@ -24,7 +24,8 @@
- apk add --update openssl - apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/trigger-build-docs - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/trigger-build-docs
- chmod 755 trigger-build-docs - chmod 755 trigger-build-docs
- gem install gitlab --no-document - gem install httparty --no-document --version 0.17.3
- gem install gitlab --no-document --version 4.13.0
# Always trigger a docs build in gitlab-docs only on docs-only branches. # Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live. # Useful to preview the docs changes live.
... ...
......
...@@ -244,7 +244,9 @@ webpack-dev-server: ...@@ -244,7 +244,9 @@ webpack-dev-server:
dependencies: ["setup-test-env", "compile-assets pull-cache"] dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables: variables:
WEBPACK_MEMORY_TEST: "true" WEBPACK_MEMORY_TEST: "true"
WEBPACK_VENDOR_DLL: "true"
script: script:
- yarn webpack-vendor
- node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js - node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js
artifacts: artifacts:
name: webpack-dev-server name: webpack-dev-server
... ...
......
...@@ -102,7 +102,7 @@ gem 'hashie-forbidden_attributes' ...@@ -102,7 +102,7 @@ gem 'hashie-forbidden_attributes'
gem 'kaminari', '~> 1.0' gem 'kaminari', '~> 1.0'
# HAML # HAML
gem 'hamlit', '~> 2.10.0' gem 'hamlit', '~> 2.11.0'
# Files attachments # Files attachments
gem 'carrierwave', '~> 1.3' gem 'carrierwave', '~> 1.3'
... ...
......
...@@ -471,7 +471,7 @@ GEM ...@@ -471,7 +471,7 @@ GEM
rainbow rainbow
rubocop (>= 0.50.0) rubocop (>= 0.50.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hamlit (2.10.0) hamlit (2.11.0)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
tilt tilt
...@@ -1029,10 +1029,10 @@ GEM ...@@ -1029,10 +1029,10 @@ GEM
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3) rack (>= 1, < 3)
thor (0.19.4) thor (0.20.3)
thread_safe (0.3.6) thread_safe (0.3.6)
thrift (0.11.0.0) thrift (0.11.0.0)
tilt (2.0.9) tilt (2.0.10)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
toml (0.2.0) toml (0.2.0)
...@@ -1224,7 +1224,7 @@ DEPENDENCIES ...@@ -1224,7 +1224,7 @@ DEPENDENCIES
gssapi gssapi
guard-rspec guard-rspec
haml_lint (~> 0.34.0) haml_lint (~> 0.34.0)
hamlit (~> 2.10.0) hamlit (~> 2.11.0)
hangouts-chat (~> 0.0.5) hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
... ...
......
...@@ -12,6 +12,7 @@ import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from ' ...@@ -12,6 +12,7 @@ import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from '
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue'; import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import setupToggleButtons from '../toggle_buttons'; import setupToggleButtons from '../toggle_buttons';
import initProjectSelectDropdown from '~/project_select'; import initProjectSelectDropdown from '~/project_select';
...@@ -144,6 +145,8 @@ export default class Clusters { ...@@ -144,6 +145,8 @@ export default class Clusters {
() => this.handlePollError(), () => this.handlePollError(),
); );
} }
this.initRemoveClusterActions();
} }
initApplications(type) { initApplications(type) {
...@@ -205,6 +208,25 @@ export default class Clusters { ...@@ -205,6 +208,25 @@ export default class Clusters {
}); });
} }
initRemoveClusterActions() {
const el = document.querySelector('#js-cluster-remove-actions');
if (el && el.dataset) {
const { clusterName, clusterPath } = el.dataset;
this.removeClusterAction = new Vue({
el,
render(createElement) {
return createElement(RemoveClusterConfirmation, {
props: {
clusterName,
clusterPath,
},
});
},
});
}
}
handleClusterEnvironmentsSuccess(data) { handleClusterEnvironmentsSuccess(data) {
this.store.toggleFetchEnvironments(false); this.store.toggleFetchEnvironments(false);
this.store.updateEnvironments(data.data); this.store.updateEnvironments(data.data);
... ...
......
<script>
import _ from 'underscore';
import SplitButton from '~/vue_shared/components/split_button.vue';
import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
const splitButtonActionItems = [
{
title: s__('ClusterIntegration|Remove integration and resources'),
description: s__(
'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal',
),
eventName: 'remove-cluster-and-cleanup',
},
{
title: s__('ClusterIntegration|Remove integration'),
description: s__(
'ClusterIntegration|Removes cluster from project but keeps associated resources',
),
eventName: 'remove-cluster',
},
];
export default {
splitButtonActionItems,
components: {
SplitButton,
GlModal,
GlButton,
GlFormInput,
},
props: {
clusterPath: {
type: String,
required: true,
},
clusterName: {
type: String,
required: true,
},
},
data() {
return {
enteredClusterName: '',
confirmCleanup: false,
};
},
computed: {
csrfToken() {
return csrf.token;
},
modalTitle() {
return this.confirmCleanup
? s__('ClusterIntegration|Remove integration and resources?')
: s__('ClusterIntegration|Remove integration?');
},
warningMessage() {
return this.confirmCleanup
? s__(
'ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster.',
)
: s__('ClusterIntegration|You are about to remove your cluster integration.');
},
warningToBeRemoved() {
return s__(`ClusterIntegration|
This will permanently delete the following resources:
<ul>
<li>All installed applications and related resources</li>
<li>The <code>gitlab-managed-apps</code> namespace</li>
<li>Any project namespaces</li>
<li><code>clusterroles</code></li>
<li><code>clusterrolebindings</code></li>
</ul>
`);
},
confirmationTextLabel() {
return sprintf(
this.confirmCleanup
? s__(
'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
)
: s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'),
{
clusterName: `<code>${_.escape(this.clusterName)}</code>`,
},
false,
);
},
canSubmit() {
return this.enteredClusterName === this.clusterName;
},
},
methods: {
handleClickRemoveCluster(cleanup = false) {
this.confirmCleanup = cleanup;
this.$refs.modal.show();
},
handleCancel() {
this.$refs.modal.hide();
this.enteredClusterName = '';
},
handleSubmit(cleanup = false) {
this.$refs.cleanup.name = cleanup === true ? 'cleanup' : 'no_cleanup';
this.$refs.form.submit();
this.enteredClusterName = '';
},
},
};
</script>
<template>
<div>
<split-button
:action-items="$options.splitButtonActionItems"
menu-class="dropdown-menu-large"
variant="danger"
@remove-cluster="handleClickRemoveCluster(false)"
@remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
/>
<gl-modal
ref="modal"
size="lg"
modal-id="delete-cluster-modal"
:title="modalTitle"
kind="danger"
>
<template>
<p>{{ warningMessage }}</p>
<div v-if="confirmCleanup" v-html="warningToBeRemoved"></div>
<strong v-html="confirmationTextLabel"></strong>
<form ref="form" :action="clusterPath" method="post" class="append-bottom-20">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input ref="cleanup" type="hidden" name="cleanup" value="true" />
<gl-form-input
v-model="enteredClusterName"
autofocus
type="text"
name="confirm_cluster_name_input"
autocomplete="off"
/>
</form>
<span v-if="confirmCleanup">{{
s__(
'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
)
}}</span>
</template>
<template slot="modal-footer">
<gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
<gl-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{
s__('ClusterIntegration|Remove integration')
}}</gl-button>
<gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit(true)">{{
s__('ClusterIntegration|Remove integration and resources')
}}</gl-button>
</template>
<template v-else>
<gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{
s__('ClusterIntegration|Remove integration')
}}</gl-button>
</template>
</template>
</gl-modal>
</div>
</template>
...@@ -4,6 +4,7 @@ import $ from 'jquery'; ...@@ -4,6 +4,7 @@ import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import sanitize from 'sanitize-html'; import sanitize from 'sanitize-html';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -116,7 +117,7 @@ export default class ProjectFindFile { ...@@ -116,7 +117,7 @@ export default class ProjectFindFile {
if (searchText) { if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText); matches = fuzzaldrinPlus.match(filePath, searchText);
} }
const blobItemUrl = `${this.options.blobUrlTemplate}/${encodeURIComponent(filePath)}`; const blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html)); results.push(this.element.find('.tree-table > tbody').append(html));
} }
... ...
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
variant: {
type: String,
required: false,
default: 'secondary',
},
}, },
data() { data() {
...@@ -53,6 +58,7 @@ export default { ...@@ -53,6 +58,7 @@ export default {
:menu-class="`dropdown-menu-selectable ${menuClass}`" :menu-class="`dropdown-menu-selectable ${menuClass}`"
split split
:text="dropdownToggleText" :text="dropdownToggleText"
:variant="variant"
v-bind="$attrs" v-bind="$attrs"
@click="triggerEvent" @click="triggerEvent"
> >
... ...
......
...@@ -45,7 +45,7 @@ class GroupsFinder < UnionFinder ...@@ -45,7 +45,7 @@ class GroupsFinder < UnionFinder
def all_groups def all_groups
return [owned_groups] if params[:owned] return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level? return [groups_with_min_access_level] if min_access_level?
return [Group.all] if current_user&.full_private_access? && all_available? return [Group.all] if current_user&.can_read_all_resources? && all_available?
groups = [] groups = []
groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects if current_user groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects if current_user
... ...
......
...@@ -127,7 +127,7 @@ class IssuesFinder < IssuableFinder ...@@ -127,7 +127,7 @@ class IssuesFinder < IssuableFinder
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues) return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
return @user_can_see_all_confidential_issues = false if current_user.blank? return @user_can_see_all_confidential_issues = false if current_user.blank?
return @user_can_see_all_confidential_issues = true if current_user.full_private_access? return @user_can_see_all_confidential_issues = true if current_user.can_read_all_resources?
@user_can_see_all_confidential_issues = @user_can_see_all_confidential_issues =
if project? && project if project? && project
... ...
......
...@@ -15,15 +15,43 @@ class KeysFinder ...@@ -15,15 +15,43 @@ class KeysFinder
def execute def execute
raise GitLabAccessDeniedError unless current_user.admin? raise GitLabAccessDeniedError unless current_user.admin?
raise InvalidFingerprint unless valid_fingerprint_param?
Key.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord keys = by_key_type
keys = by_user(keys)
keys = sort(keys)
by_fingerprint(keys)
end end
private private
attr_reader :current_user, :params attr_reader :current_user, :params
def by_key_type
if params[:key_type] == 'ssh'
Key.regular_keys
else
Key.all
end
end
def sort(keys)
keys.order_last_used_at_desc
end
def by_user(keys)
return keys unless params[:user]
keys.for_user(params[:user])
end
def by_fingerprint(keys)
return keys unless params[:fingerprint].present?
raise InvalidFingerprint unless valid_fingerprint_param?
keys.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord
end
def valid_fingerprint_param? def valid_fingerprint_param?
if fingerprint_type == "sha256" if fingerprint_type == "sha256"
Base64.decode64(fingerprint).length == 32 Base64.decode64(fingerprint).length == 32
... ...
......
...@@ -11,15 +11,23 @@ class MergeRequestTargetProjectFinder ...@@ -11,15 +11,23 @@ class MergeRequestTargetProjectFinder
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def execute def execute(include_routes: false)
if @source_project.fork_network if source_project.fork_network
@source_project.fork_network.projects include_routes ? projects.inc_routes : projects
.public_or_visible_to_user(current_user)
.non_archived
.with_feature_available_for_user(:merge_requests, current_user)
else else
Project.where(id: source_project) Project.where(id: source_project)
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private
def projects
source_project
.fork_network
.projects
.public_or_visible_to_user(current_user)
.non_archived
.with_feature_available_for_user(:merge_requests, current_user)
end
end end
...@@ -13,18 +13,26 @@ class PersonalAccessTokensFinder ...@@ -13,18 +13,26 @@ class PersonalAccessTokensFinder
tokens = PersonalAccessToken.all tokens = PersonalAccessToken.all
tokens = by_user(tokens) tokens = by_user(tokens)
tokens = by_impersonation(tokens) tokens = by_impersonation(tokens)
by_state(tokens) tokens = by_state(tokens)
sort(tokens)
end end
private private
# rubocop: disable CodeReuse/ActiveRecord
def by_user(tokens) def by_user(tokens)
return tokens unless @params[:user] return tokens unless @params[:user]
tokens.where(user: @params[:user]) tokens.for_user(@params[:user])
end
def sort(tokens)
available_sort_orders = PersonalAccessToken.simple_sorts.keys
return tokens unless available_sort_orders.include?(params[:sort])
tokens.order_by(params[:sort])
end end
# rubocop: enable CodeReuse/ActiveRecord
def by_impersonation(tokens) def by_impersonation(tokens)
case @params[:impersonation] case @params[:impersonation]
... ...
......
...@@ -88,7 +88,7 @@ module MergeRequestsHelper ...@@ -88,7 +88,7 @@ module MergeRequestsHelper
def target_projects(project) def target_projects(project)
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project) MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project)
.execute .execute(include_routes: true)
end end
def merge_request_button_visibility(merge_request, closed) def merge_request_button_visibility(merge_request, closed)
... ...
......
...@@ -44,6 +44,14 @@ module UsersHelper ...@@ -44,6 +44,14 @@ module UsersHelper
current_user_menu_items.include?(item) current_user_menu_items.include?(item)
end end
# Used to preload when you are rendering many projects and checking access
#
# rubocop: disable CodeReuse/ActiveRecord: `projects` can be array which also responds to pluck
def load_max_project_member_accesses(projects)
current_user&.max_member_access_for_project_ids(projects.pluck(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
def max_project_member_access(project) def max_project_member_access(project)
current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
end end
... ...
......
...@@ -887,7 +887,7 @@ module Ci ...@@ -887,7 +887,7 @@ module Ci
def each_report(report_types) def each_report(report_types)
job_artifacts_for_types(report_types).each do |report_artifact| job_artifacts_for_types(report_types).each do |report_artifact|
report_artifact.each_blob do |blob| report_artifact.each_blob do |blob|
yield report_artifact.file_type, blob yield report_artifact.file_type, blob, report_artifact
end end
end end
end end
... ...
......
...@@ -242,7 +242,7 @@ class Issue < ApplicationRecord ...@@ -242,7 +242,7 @@ class Issue < ApplicationRecord
return false unless readable_by?(user) return false unless readable_by?(user)
user.full_private_access? || user.can_read_all_resources? ||
::Gitlab::ExternalAuthorization.access_allowed?( ::Gitlab::ExternalAuthorization.access_allowed?(
user, project.external_authorization_classification_label) user, project.external_authorization_classification_label)
end end
... ...
......
...@@ -39,6 +39,10 @@ class Key < ApplicationRecord ...@@ -39,6 +39,10 @@ class Key < ApplicationRecord
alias_attribute :fingerprint_md5, :fingerprint alias_attribute :fingerprint_md5, :fingerprint
scope :preload_users, -> { preload(:user) }
scope :for_user, -> (user) { where(user: user) }
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
def self.regular_keys def self.regular_keys
where(type: ['Key', nil]) where(type: ['Key', nil])
end end
... ...
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class PersonalAccessToken < ApplicationRecord class PersonalAccessToken < ApplicationRecord
include Expirable include Expirable
include TokenAuthenticatable include TokenAuthenticatable
include Sortable
add_authentication_token_field :token, digest: true add_authentication_token_field :token, digest: true
...@@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord ...@@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") } scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) } scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) } scope :without_impersonation, -> { where(impersonation: false) }
scope :for_user, -> (user) { where(user: user) }
scope :preload_users, -> { preload(:user) }
validates :scopes, presence: true validates :scopes, presence: true
validate :validate_scopes validate :validate_scopes
... ...
......
...@@ -408,6 +408,7 @@ class Project < ApplicationRecord ...@@ -408,6 +408,7 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :inc_routes, -> { includes(:route, namespace: :route) }
scope :with_statistics, -> { includes(:statistics) } scope :with_statistics, -> { includes(:statistics) }
scope :with_service, ->(service) { joins(service).eager_load(service) } scope :with_service, ->(service) { joins(service).eager_load(service) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
... ...
......