diff options
Diffstat (limited to 'app')
22 files changed, 283 insertions, 23 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index f4a7e64ceee..d990d2677a8 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -12,6 +12,7 @@ import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from ' import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import Applications from './components/applications.vue'; +import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue'; import setupToggleButtons from '../toggle_buttons'; import initProjectSelectDropdown from '~/project_select'; @@ -144,6 +145,8 @@ export default class Clusters { () => this.handlePollError(), ); } + + this.initRemoveClusterActions(); } initApplications(type) { @@ -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) { this.store.toggleFetchEnvironments(false); this.store.updateEnvironments(data.data); diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue new file mode 100644 index 00000000000..c31ba7ef14a --- /dev/null +++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue @@ -0,0 +1,168 @@ +<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> diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 81c4f6711e7..d6cdd37a2c3 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -4,6 +4,7 @@ import $ from 'jquery'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import sanitize from 'sanitize-html'; import axios from '~/lib/utils/axios_utils'; +import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; import { __ } from '~/locale'; @@ -116,7 +117,7 @@ export default class ProjectFindFile { if (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); results.push(this.element.find('.tree-table > tbody').append(html)); } diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue index f7dc00a345c..9aacde49264 100644 --- a/app/assets/javascripts/vue_shared/components/split_button.vue +++ b/app/assets/javascripts/vue_shared/components/split_button.vue @@ -26,6 +26,11 @@ export default { required: false, default: '', }, + variant: { + type: String, + required: false, + default: 'secondary', + }, }, data() { @@ -53,6 +58,7 @@ export default { :menu-class="`dropdown-menu-selectable ${menuClass}`" split :text="dropdownToggleText" + :variant="variant" v-bind="$attrs" @click="triggerEvent" > diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 7d419103b1c..54715557399 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -45,7 +45,7 @@ class GroupsFinder < UnionFinder def all_groups return [owned_groups] if params[:owned] 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 << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects if current_user diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 74e89a1e66c..641b4422db9 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -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 = 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 = if project? && project diff --git a/app/finders/keys_finder.rb b/app/finders/keys_finder.rb index d6ba7cb290d..6fd914c88cd 100644 --- a/app/finders/keys_finder.rb +++ b/app/finders/keys_finder.rb @@ -15,15 +15,43 @@ class KeysFinder def execute 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 private 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? if fingerprint_type == "sha256" Base64.decode64(fingerprint).length == 32 diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index 5f0589f6c8b..85a73e0c6ff 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -11,15 +11,23 @@ class MergeRequestTargetProjectFinder end # rubocop: disable CodeReuse/ActiveRecord - def execute - if @source_project.fork_network - @source_project.fork_network.projects - .public_or_visible_to_user(current_user) - .non_archived - .with_feature_available_for_user(:merge_requests, current_user) + def execute(include_routes: false) + if source_project.fork_network + include_routes ? projects.inc_routes : projects else Project.where(id: source_project) end end # 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 diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index bd95dcd323f..7b15a3b0c10 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -13,18 +13,26 @@ class PersonalAccessTokensFinder tokens = PersonalAccessToken.all tokens = by_user(tokens) tokens = by_impersonation(tokens) - by_state(tokens) + tokens = by_state(tokens) + + sort(tokens) end private - # rubocop: disable CodeReuse/ActiveRecord def by_user(tokens) 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 - # rubocop: enable CodeReuse/ActiveRecord def by_impersonation(tokens) case @params[:impersonation] diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 46db3b78fd0..7940ec1162b 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -88,7 +88,7 @@ module MergeRequestsHelper def target_projects(project) MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project) - .execute + .execute(include_routes: true) end def merge_request_button_visibility(merge_request, closed) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 195e4154c67..e87bb27cf62 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -44,6 +44,14 @@ module UsersHelper current_user_menu_items.include?(item) 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) current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b6c71f81a49..ba87369f30b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -887,7 +887,7 @@ module Ci def each_report(report_types) job_artifacts_for_types(report_types).each do |report_artifact| report_artifact.each_blob do |blob| - yield report_artifact.file_type, blob + yield report_artifact.file_type, blob, report_artifact end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index f16fd8e63ec..88df3baa809 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -242,7 +242,7 @@ class Issue < ApplicationRecord return false unless readable_by?(user) - user.full_private_access? || + user.can_read_all_resources? || ::Gitlab::ExternalAuthorization.access_allowed?( user, project.external_authorization_classification_label) end diff --git a/app/models/key.rb b/app/models/key.rb index f66aa4fb329..e549c59b58f 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -39,6 +39,10 @@ class Key < ApplicationRecord 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 where(type: ['Key', nil]) end diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 9ccc90fb74d..af079f7ebc4 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -3,6 +3,7 @@ class PersonalAccessToken < ApplicationRecord include Expirable include TokenAuthenticatable + include Sortable add_authentication_token_field :token, digest: true @@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord scope :inactive, -> { where("revoked = true OR expires_at < NOW()") } scope :with_impersonation, -> { where(impersonation: true) } scope :without_impersonation, -> { where(impersonation: false) } + scope :for_user, -> (user) { where(user: user) } + scope :preload_users, -> { preload(:user) } validates :scopes, presence: true validate :validate_scopes diff --git a/app/models/project.rb b/app/models/project.rb index 5ed47032dab..bc8757ea888 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -408,6 +408,7 @@ class Project < ApplicationRecord scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } 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 :inc_routes, -> { includes(:route, namespace: :route) } scope :with_statistics, -> { includes(:statistics) } scope :with_service, ->(service) { joins(service).eager_load(service) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) } diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index caa65d32c86..4973c7761c1 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -186,7 +186,7 @@ class ProjectFeature < ApplicationRecord def team_access?(user, feature) return unless user - return true if user.full_private_access? + return true if user.can_read_all_resources? project.team.member?(user, ProjectFeature.required_minimum_access_level(feature)) end diff --git a/app/models/user.rb b/app/models/user.rb index 441ad1e70be..18bf5ceaa0e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1473,9 +1473,7 @@ class User < ApplicationRecord self.admin = (new_level == 'admin') end - # Does the user have access to all private groups & projects? - # Overridden in EE to also check auditor? - def full_private_access? + def can_read_all_resources? can?(:read_all_resources) end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 8f5c6957a20..3a16f7dc239 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -40,6 +40,7 @@ class BasePolicy < DeclarativePolicy::Base prevent :read_cross_project end + # Policy extended in EE to also enable auditors rule { admin }.enable :read_all_resources rule { default }.enable :read_cross_project diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 5e34b457231..77f7c478ffa 100644 --- a/app/views/clusters/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -41,4 +41,5 @@ = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") - = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) + + #js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster), cluster_name: @cluster.name } } diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 5bb92c8d1cf..71fef5df5bc 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -182,6 +182,8 @@ %strong.fly-out-top-item-name = _('Deploy Keys') + = render_if_exists 'layouts/nav/sidebar/credentials_link' + = nav_link(controller: :services) do = link_to admin_application_settings_services_path do .nav-icon-container diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 59b4facdbe5..fab7ee9d763 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -35,6 +35,7 @@ .js-projects-list-holder{ data: { qa_selector: 'projects_list' } } - if any_projects?(projects) - load_pipeline_status(projects) if pipeline_status + - load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below %ul.projects-list{ class: css_classes } - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil |