diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-16 00:08:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-16 00:08:29 +0300 |
commit | 2cfe59f72a03a796bb1cd262f337d2b6764ef1d5 (patch) | |
tree | ce657730bc164f06c560b884d192a5c5b3b822d9 | |
parent | 854a0164ea775b84f5ef2508926780144bbc981a (diff) |
Add latest changes from gitlab-org/gitlab@master
14 files changed, 425 insertions, 12 deletions
diff --git a/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue new file mode 100644 index 00000000000..ddf240fcafa --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue @@ -0,0 +1,67 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import StorageUsageStatistics from 'ee_else_ce/usage_quotas/storage/components/storage_usage_statistics.vue'; + +export default { + name: 'NamespaceStorageApp', + components: { + GlAlert, + StorageUsageStatistics, + }, + props: { + namespaceLoadingError: { + type: Boolean, + required: false, + default: false, + }, + projectsLoadingError: { + type: Boolean, + required: false, + default: false, + }, + isNamespaceStorageStatisticsLoading: { + type: Boolean, + required: false, + default: false, + }, + namespace: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + usedStorage() { + return ( + // This is the coefficient adjusted forked repo size, only used in EE + this.namespace.rootStorageStatistics?.costFactoredStorageSize ?? + // This is the actual storage size value, used in CE or when the above is disabled + this.namespace.rootStorageStatistics?.storageSize + ); + }, + }, +}; +</script> +<template> + <div> + <gl-alert + v-if="namespaceLoadingError || projectsLoadingError" + variant="danger" + :dismissible="false" + class="gl-mt-4" + > + {{ + s__( + 'UsageQuota|An error occured while loading the storage usage details. Please refresh the page to try again.', + ) + }} + </gl-alert> + <storage-usage-statistics + :additional-purchased-storage-size="namespace.additionalPurchasedStorageSize" + :used-storage="usedStorage" + :loading="isNamespaceStorageStatisticsLoading" + /> + + <slot name="ee-storage-app"></slot> + </div> +</template> diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue new file mode 100644 index 00000000000..6e73fc0ba69 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue @@ -0,0 +1,51 @@ +<script> +import { GlCard, GlSkeletonLoader } from '@gitlab/ui'; +import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue'; + +export default { + name: 'StorageUsageOverviewCard', + components: { + GlCard, + GlSkeletonLoader, + NumberToHumanSize, + }, + props: { + usedStorage: { + type: Number, + required: false, + default: null, + }, + loading: { + type: Boolean, + required: true, + }, + }, +}; +</script> + +<template> + <gl-card> + <gl-skeleton-loader v-if="loading" :height="64"> + <rect width="140" height="30" x="5" y="0" rx="4" /> + <rect width="240" height="10" x="5" y="40" rx="4" /> + <rect width="340" height="10" x="5" y="54" rx="4" /> + </gl-skeleton-loader> + + <div v-else> + <div class="gl-font-weight-bold" data-testid="namespace-storage-card-title"> + {{ s__('UsageQuota|Namespace storage used') }} + </div> + <div class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-my-3"> + <number-to-human-size label-class="gl-font-lg" :value="Number(usedStorage)" plain-zero /> + </div> + <hr class="gl-my-4" /> + <p> + {{ + s__( + 'UsageQuota|Namespace total storage represents the sum of storage consumed by all projects, Container Registry, and Dependency Proxy.', + ) + }} + </p> + </div> + </gl-card> +</template> diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue new file mode 100644 index 00000000000..d7e550dd715 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue @@ -0,0 +1,33 @@ +<script> +import StorageUsageOverviewCard from './storage_usage_overview_card.vue'; + +export default { + name: 'StorageUsageStatistics', + components: { + StorageUsageOverviewCard, + }, + props: { + usedStorage: { + type: Number, + required: false, + default: 0, + }, + loading: { + type: Boolean, + required: true, + }, + }, +}; +</script> +<template> + <div> + <h3 data-testid="overview-subtitle">{{ s__('UsageQuota|Namespace overview') }}</h3> + <div class="gl-display-grid gl-md-grid-template-columns-2 gl-gap-5 gl-py-4"> + <storage-usage-overview-card + :used-storage="usedStorage" + :loading="loading" + data-testid="namespace-usage-total" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue index 503328f7b03..78afb9a04ef 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue @@ -131,7 +131,7 @@ export default { class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base gl-gap-3" data-testid="links-child" > - <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0"> + <div class="gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0"> <div class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0" > diff --git a/doc/development/permissions/custom_roles.md b/doc/development/permissions/custom_roles.md index d2986aa3a59..53589658f56 100644 --- a/doc/development/permissions/custom_roles.md +++ b/doc/development/permissions/custom_roles.md @@ -205,6 +205,95 @@ If you have any concerns, put the new ability behind a feature flag. - When you enable the ability by default, add the `feature_flag_enabled_milestone` and `feature_flag_enabled_mr` attributes to the appropriate ability YAML file and regenerate the documentation. - You do not have to include these attributes in the YAML file if the feature flag is enabled by default in the same release as the ability is introduced. +#### Testing + +Unit tests are preferred to test out changes to any policies affected by the +addition of new custom permissions. Custom Roles is an Ultimate tier feature so +these tests can be found in the `ee/spec/policies` directory. The [spec file](https://gitlab.com/gitlab-org/gitlab/-/blob/13baa4e8c92a56260591a5bf0a58d3339890ee10/ee/spec/policies/project_policy_spec.rb#L2726-2740) for +the `ProjectPolicy` contains shared examples that can be used to test out the +following conditions: + +- when the `custom_roles` licensed feature is not enabled +- when the `custom_roles` licensed feature is enabled + - when a user is a member of a custom role via an inherited group member + - when a user is a member of a custom role via a direct group member + - when a user is a member of a custom role via a direct project membership + +Below is an example for testing out `ProjectPolicy` related changes. + +```ruby + context 'for a role with `custom_permission` enabled' do + let(:member_role_abilities) { { custom_permission: true } } + let(:allowed_abilities) { [:custom_permission] } + + it_behaves_like 'custom roles abilities' + end +``` + +Request specs are preferred to test out any endpoint that allow access via a custom role permission. +This includes controllers, REST API, and GraphQL. Examples of request specs can be found in `ee/spec/requests/custom_roles/`. In this directory you will find a sub-directory named after each permission that can be enabled via a custom role. +The `custom_roles` licensed feature must be enabled to test this functionality. + +Below is an example of the typical setup that is required to test out a +Rails Controller endpoint. + +```ruby + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, :in_group) } + let_it_be(:role) { create(:member_role, :guest, namespace: project.group, custom_permission: true) } + let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, project: project) } + + before do + stub_licensed_features(custom_roles: true) + sign_in(user) + end + + describe MyController do + describe '#show' do + it 'allows access' do + get my_controller_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end +``` + +Below is an example of the typical setup that is required to test out a GraphQL +mutation. + +```ruby + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, :in_group) } + let_it_be(:role) { create(:member_role, :guest, namespace: project.group, custom_permission: true) } + let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, project: project) } + + before do + stub_licensed_features(custom_roles: true) + end + + describe MyMutation do + include GraphqlHelpers + + describe '#show' do + it 'allows access' do + post_graphql_mutation(graphql_mutation(:my_mutation, { + example: "Example" + }), current_user: user) + + expect(response).to have_gitlab_http_status(:success) + mutation_response = graphql_mutation_response(:my_mutation) + expect(mutation_response).to be_present + expect(mutation_response["errors"]).to be_empty + end + end + end +``` + +[`GITLAB_DEBUG_POLICIES=true`](#finding-existing-abilities-checks) can be used +to troubleshoot runtime policy decisions. + ## Custom abilities definition All new custom abilities must have a type definition stored in `ee/config/custom_abilities` that contains a single source of truth for every ability that is part of custom roles feature. @@ -217,6 +306,7 @@ To add a new custom ability: - Use the `ee/bin/custom-ability` CLI to create the YAML definition automatically. - Perform manual steps to create a new file in `ee/config/custom_abilities/` with the filename matching the name of the ability name. 1. Add contents to the file that conform to the [schema](#schema) defined in `ee/config/custom_abilities/types/type_schema.json`. +1. Add [tests](#testing) for the new ability in `ee/spec/requests/custom_roles/` with a new directory named after the ability name. ### Schema diff --git a/doc/development/permissions/predefined_roles.md b/doc/development/permissions/predefined_roles.md index 7cb00977e1f..577cf6192c4 100644 --- a/doc/development/permissions/predefined_roles.md +++ b/doc/development/permissions/predefined_roles.md @@ -83,10 +83,10 @@ module): - Maintainer (`40`) - Owner (`50`) -If a user is the member of both a project and the project parent groups, the +If a user is a member of both a project and the project parent groups, the highest permission is the applied access level for the project. -If a user is the member of a project, but not the parent groups, they +If a user is a member of a project, but not the parent groups, they can still view the groups and their entities (like epics). Project membership (where the group membership is already taken into account) diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md index 7ddf4a52f01..e2ec6b8ae56 100644 --- a/doc/user/application_security/policies/scan-result-policies.md +++ b/doc/user/application_security/policies/scan-result-policies.md @@ -92,9 +92,6 @@ the following sections and tables provide an alternative. > The `approval_settings` fields were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4 [with flags](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`, `scan_result_any_merge_request`, or `scan_result_policies_block_force_push`. See the `approval_settings` section below for more information. -FLAG: -On self-managed GitLab, by default the `approval_settings` field is available. To hide the feature, an administrator can [disable the feature flags](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`, `scan_result_any_merge_request` and `scan_result_policies_block_force_push`. See the `approval_settings` section below for more information. On GitLab.com, the `approval_settings` field is available. - | Field | Type | Required | Possible values | Description | |---------------------|--------------------|----------|-----------------|----------------------------------------------------------| | `name` | `string` | true | | Name of the policy. Maximum of 255 characters. | @@ -195,7 +192,6 @@ the defined policy. FLAG: On self-managed GitLab, by default the `block_branch_modification` field is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`. On GitLab.com, this feature is available. -On self-managed GitLab, by default the `prevent_approval_by_author`, `prevent_approval_by_commit_author`, `remove_approvals_with_new_commit`, and `require_password_to_approve` fields are available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `scan_result_any_merge_request`. On GitLab.com, this feature is available. On self-managed GitLab, by default the `prevent_pushing_and_force_pushing` field is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `scan_result_policies_block_force_push`. On GitLab.com, this feature is available. The settings set in the policy overwrite settings in the project. diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 46d7159d71f..eccc5c0c748 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -143,7 +143,8 @@ then ruby -r './tooling/lib/tooling/find_changes' -e "Tooling::FindChanges.new( from: :api, changed_files_pathname: '${DOC_CHANGES_FILE}', - file_filter: ->(file) { !file['deleted_file'] && file['new_path'] =~ %r{doc/.*\.md|lint-doc\.sh|docs\.gitlab-ci\.yml} } + file_filter: ->(file) { !file['deleted_file'] && file['new_path'] =~ %r{doc/.*\.md|\.vale|\.markdownlint|lint-doc\.sh|docs\.gitlab-ci\.yml} }, + only_new_paths: true ).execute" if grep -E "\.vale|\.markdownlint|lint-doc\.sh|docs\.gitlab-ci\.yml" < $DOC_CHANGES_FILE then diff --git a/spec/frontend/usage_quotas/storage/components/namespace_storage_app_spec.js b/spec/frontend/usage_quotas/storage/components/namespace_storage_app_spec.js new file mode 100644 index 00000000000..e4f99d401a2 --- /dev/null +++ b/spec/frontend/usage_quotas/storage/components/namespace_storage_app_spec.js @@ -0,0 +1,51 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NamespaceStorageApp from '~/usage_quotas/storage/components/namespace_storage_app.vue'; +import StorageUsageStatistics from '~/usage_quotas/storage/components/storage_usage_statistics.vue'; +import { defaultNamespaceProvideValues } from '../mock_data'; + +const defaultProps = { + namespaceLoadingError: false, + projectsLoadingError: false, + isNamespaceStorageStatisticsLoading: false, + // hardcoding object until we move test_fixtures from ee/ to here + namespace: { + rootStorageStatistics: { + storageSize: 1234, + }, + }, +}; + +describe('NamespaceStorageApp', () => { + /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ + let wrapper; + + const findStorageUsageStatistics = () => wrapper.findComponent(StorageUsageStatistics); + + const createComponent = ({ provide = {}, props = {} } = {}) => { + wrapper = shallowMountExtended(NamespaceStorageApp, { + provide: { + ...defaultNamespaceProvideValues, + ...provide, + }, + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + describe('Namespace usage overview', () => { + describe('StorageUsageStatistics', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes the correct props to StorageUsageStatistics', () => { + expect(findStorageUsageStatistics().props()).toMatchObject({ + usedStorage: defaultProps.namespace.rootStorageStatistics.storageSize, + loading: false, + }); + }); + }); + }); +}); diff --git a/spec/frontend/usage_quotas/storage/components/storage_usage_overview_card_spec.js b/spec/frontend/usage_quotas/storage/components/storage_usage_overview_card_spec.js new file mode 100644 index 00000000000..c79b6b94ac1 --- /dev/null +++ b/spec/frontend/usage_quotas/storage/components/storage_usage_overview_card_spec.js @@ -0,0 +1,44 @@ +import { GlSkeletonLoader } from '@gitlab/ui'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import StorageUsageOverviewCard from '~/usage_quotas/storage/components/storage_usage_overview_card.vue'; +import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('StorageUsageOverviewCard', () => { + /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ + let wrapper; + const defaultProps = { + purchasedStorage: 0, + // hardcoding value until we move test_fixtures from ee/ to here + usedStorage: 1234, + loading: false, + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(StorageUsageOverviewCard, { + propsData: { ...defaultProps, ...props }, + stubs: { + NumberToHumanSize, + }, + }); + }; + + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + + it('displays the used storage value', () => { + createComponent(); + expect(wrapper.text()).toContain(numberToHumanSize(defaultProps.usedStorage, 1)); + }); + + describe('skeleton loader', () => { + it('renders skeleton loader when loading prop is true', () => { + createComponent({ props: { loading: true } }); + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('does not render skeleton loader when loading prop is false', () => { + createComponent({ props: { loading: false } }); + expect(findSkeletonLoader().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js b/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js new file mode 100644 index 00000000000..73d02dc273f --- /dev/null +++ b/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js @@ -0,0 +1,43 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import StorageUsageStatistics from '~/usage_quotas/storage/components/storage_usage_statistics.vue'; +import StorageUsageOverviewCard from '~/usage_quotas/storage/components/storage_usage_overview_card.vue'; + +const defaultProps = { + // hardcoding value until we move test_fixtures from ee/ to here + usedStorage: 1234, + loading: false, +}; + +describe('StorageUsageStatistics', () => { + /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */ + let wrapper; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(StorageUsageStatistics, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const findOverviewSubtitle = () => wrapper.findByTestId('overview-subtitle'); + const findStorageUsageOverviewCard = () => wrapper.findComponent(StorageUsageOverviewCard); + + beforeEach(() => { + createComponent(); + }); + + it('shows the namespace storage overview subtitle', () => { + expect(findOverviewSubtitle().text()).toBe('Namespace overview'); + }); + + describe('StorageStatisticsCard', () => { + it('passes the correct props to StorageUsageOverviewCard', () => { + expect(findStorageUsageOverviewCard().props()).toEqual({ + usedStorage: defaultProps.usedStorage, + loading: false, + }); + }); + }); +}); diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js index 16c03a13028..266c1150815 100644 --- a/spec/frontend/usage_quotas/storage/mock_data.js +++ b/spec/frontend/usage_quotas/storage/mock_data.js @@ -6,3 +6,5 @@ export const mockEmptyResponse = { data: { project: null } }; export const defaultProjectProvideValues = { projectPath: '/project-path', }; + +export const defaultNamespaceProvideValues = {}; diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb index 85e3eadac6f..be28b228edd 100644 --- a/spec/tooling/lib/tooling/find_changes_spec.rb +++ b/spec/tooling/lib/tooling/find_changes_spec.rb @@ -16,7 +16,8 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do predictive_tests_pathname: predictive_tests_pathname, frontend_fixtures_mapping_pathname: frontend_fixtures_mapping_pathname, from: from, - file_filter: file_filter) + file_filter: file_filter, + only_new_paths: only_new_paths) end let(:changed_files_pathname) { changed_files_file.path } @@ -25,6 +26,7 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do let(:from) { :api } let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles let(:file_filter) { ->(_) { true } } + let(:only_new_paths) { false } around do |example| self.changed_files_file = Tempfile.new('changed_files_file') @@ -122,6 +124,37 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do expect(File.read(changed_files_file)).to eq('doc/index.md') end end + + context 'when used with only_new_paths' do + let(:only_new_paths) { true } + + let(:mr_changes_array) do + [ + { + "new_path" => "scripts/test.js", + "old_path" => "scripts/test.js" + }, + { + "new_path" => "doc/renamed_index.md", + "old_path" => "doc/index.md" + } + ] + end + + before do + # rubocop:disable RSpec/VerifiedDoubles -- The class from the GitLab gem isn't public, so we cannot use verified doubles for it. + allow(gitlab_client).to receive(:merge_request_changes) + .with('dummy-project', '1234') + .and_return(double(changes: mr_changes_array)) + # rubocop:enable RSpec/VerifiedDoubles + end + + it 'only writes new file paths to output' do + subject + + expect(File.read(changed_files_file)).to eq('doc/renamed_index.md scripts/test.js') + end + end end context 'when fetching changes from changed files' do diff --git a/tooling/lib/tooling/find_changes.rb b/tooling/lib/tooling/find_changes.rb index f6fdf042c15..8a92242e167 100755 --- a/tooling/lib/tooling/find_changes.rb +++ b/tooling/lib/tooling/find_changes.rb @@ -15,7 +15,8 @@ module Tooling changed_files_pathname: nil, predictive_tests_pathname: nil, frontend_fixtures_mapping_pathname: nil, - file_filter: ->(_) { true } + file_filter: ->(_) { true }, + only_new_paths: false ) raise ArgumentError, ':from can only be :api or :changed_files' unless @@ -30,6 +31,7 @@ module Tooling @frontend_fixtures_mapping_pathname = frontend_fixtures_mapping_pathname @from = from @file_filter = file_filter + @api_path_attributes = only_new_paths ? %w[new_path] : %w[new_path old_path] end def execute @@ -53,7 +55,7 @@ module Tooling attr_reader :gitlab_token, :gitlab_endpoint, :mr_project_path, :mr_iid, :changed_files_pathname, :predictive_tests_pathname, - :frontend_fixtures_mapping_pathname, :file_filter + :frontend_fixtures_mapping_pathname, :file_filter, :api_path_attributes def gitlab @gitlab ||= begin @@ -86,7 +88,7 @@ module Tooling case @from when :api mr_changes.changes.select(&file_filter).flat_map do |change| - change.to_h.values_at('old_path', 'new_path') + change.to_h.values_at(*api_path_attributes) end.uniq else read_array_from_file(changed_files_pathname) |