From ec7d105651556433dda5bda07f76b3e405097419 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 22 Aug 2024 09:14:39 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/layout/line_length.yml | 1 - .../rspec/before_all_role_assignment.yml | 1 - .rubocop_todo/rspec/context_wording.yml | 1 - .../components/clusters_empty_state.vue | 2 +- .../components/feature_flags.vue | 2 +- .../components/related_merge_requests.vue | 99 ++++------- .../components/table/member_source.vue | 87 +++++---- .../components/table/members_table.vue | 9 +- .../components/table/members_table_cell.vue | 5 - app/assets/javascripts/members/constants.js | 2 +- app/assets/javascripts/members/utils.js | 10 +- .../settings/general/components/app.vue | 4 +- .../general/components/visibility_level.vue | 77 ++++++++ .../organizations/settings/general/index.js | 2 +- .../organizations/shared/constants.js | 1 + .../show/components/organization_avatar.vue | 10 +- .../javascripts/visibility_level/constants.js | 14 ++ .../vue_shared/components/crud_component.vue | 19 +- .../incubation/incubation_alert.vue | 2 +- .../promo_page_link.stories.js | 4 +- .../promo_page_link/promo_page_link.vue | 8 +- app/assets/stylesheets/framework.scss | 1 - app/assets/stylesheets/framework/callout.scss | 63 ------- app/assets/stylesheets/framework/crud.scss | 10 ++ .../page_bundles/_ide_theme_overrides.scss | 10 -- .../stylesheets/page_bundles/settings.scss | 1 - app/components/layouts/crud_component.haml | 6 +- .../concerns/dependency_proxy/group_access.rb | 10 +- .../application_controller.rb | 52 ++---- ...endency_proxy_for_containers_controller.rb | 6 + .../concerns/packages/finder_helper.rb | 18 +- .../organizations/organization_helper.rb | 4 +- app/policies/group_policy.rb | 8 +- .../group_link/group_link_entity.rb | 4 + app/serializers/member_entity.rb | 24 ++- app/services/packages/nuget/search_service.rb | 10 +- ..._dependency_proxy_pass_token_to_policy.yml | 9 - doc/administration/get_started.md | 2 +- doc/development/permissions/custom_roles.md | 2 +- doc/topics/git/index.md | 3 +- doc/tutorials/update_git_remote_url/index.md | 166 ++++++++++++++++++ doc/user/custom_roles.md | 2 +- doc/user/group/access_and_permissions.md | 4 +- doc/user/group/index.md | 8 +- doc/user/group/saml_sso/group_sync.md | 2 +- doc/user/group/subgroups/index.md | 4 +- doc/user/organization/index.md | 9 + .../members/img/project_members_v17_4.png | Bin 0 -> 59577 bytes doc/user/project/members/index.md | 71 +++----- .../packages/nuget/private_endpoints.rb | 10 +- lib/api/helpers/packages/nuget.rb | 8 +- lib/api/nuget_group_packages.rb | 11 +- locale/gitlab.pot | 45 +++-- .../dependency_proxy_auth_controller_spec.rb | 48 +---- ...cy_proxy_for_containers_controller_spec.rb | 58 +----- .../groups/members/sort_members_spec.rb | 8 +- .../features/projects/members/sorting_spec.rb | 8 +- .../concerns/packages/finder_helper_spec.rb | 137 +++++++++------ .../components/related_merge_requests_spec.js | 10 +- .../filter_sort/sort_dropdown_spec.js | 4 +- .../components/table/member_source_spec.js | 109 ++++-------- .../table/members_table_cell_spec.js | 27 --- .../components/table/members_table_spec.js | 9 +- spec/frontend/members/mock_data.js | 12 +- spec/frontend/members/utils_spec.js | 11 -- .../settings/general/components/app_spec.js | 5 + .../components/visibility_level_spec.js | 58 ++++++ .../components/organization_avatar_spec.js | 7 +- .../promo_page_link/promo_page_link_spec.js | 22 ++- .../organizations/organization_helper_spec.rb | 6 +- spec/policies/group_policy_spec.rb | 40 ----- .../requests/api/nuget_group_packages_spec.rb | 18 +- .../group_link/group_link_entity_spec.rb | 24 ++- spec/serializers/member_entity_spec.rb | 126 +++++++++---- ...dency_proxy_authentication_service_spec.rb | 24 --- .../packages/nuget/search_service_spec.rb | 55 ++++-- spec/support/rspec_order_todo.yml | 1 - .../api/nuget_packages_shared_examples.rb | 32 +++- 78 files changed, 976 insertions(+), 826 deletions(-) create mode 100644 app/assets/javascripts/organizations/settings/general/components/visibility_level.vue delete mode 100644 app/assets/stylesheets/framework/callout.scss delete mode 100644 config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml create mode 100644 doc/tutorials/update_git_remote_url/index.md create mode 100644 doc/user/project/members/img/project_members_v17_4.png create mode 100644 spec/frontend/organizations/settings/general/components/visibility_level_spec.js diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 805467b2c33..5b863ab2ef1 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -4280,7 +4280,6 @@ Layout/LineLength: - 'spec/services/packages/maven/metadata/sync_service_spec.rb' - 'spec/services/packages/npm/create_tag_service_spec.rb' - 'spec/services/packages/nuget/create_dependency_service_spec.rb' - - 'spec/services/packages/nuget/search_service_spec.rb' - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb' - 'spec/services/packages/rubygems/process_gem_service_spec.rb' - 'spec/services/personal_access_tokens/create_service_spec.rb' diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml index 8d2642ee033..44f36ce1dbb 100644 --- a/.rubocop_todo/rspec/before_all_role_assignment.yml +++ b/.rubocop_todo/rspec/before_all_role_assignment.yml @@ -1284,7 +1284,6 @@ RSpec/BeforeAllRoleAssignment: - 'spec/services/notification_service_spec.rb' - 'spec/services/packages/mark_packages_for_destruction_service_spec.rb' - 'spec/services/packages/maven/metadata/sync_service_spec.rb' - - 'spec/services/packages/nuget/search_service_spec.rb' - 'spec/services/packages/rubygems/dependency_resolver_service_spec.rb' - 'spec/services/pages_domains/create_service_spec.rb' - 'spec/services/post_receive_service_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 89167523d77..f80fd968bb9 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -2540,7 +2540,6 @@ RSpec/ContextWording: - 'spec/services/packages/helm/process_file_service_spec.rb' - 'spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb' - 'spec/services/packages/maven/metadata/sync_service_spec.rb' - - 'spec/services/packages/nuget/search_service_spec.rb' - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb' - 'spec/services/packages/rubygems/dependency_resolver_service_spec.rb' - 'spec/services/packages/rubygems/process_gem_service_spec.rb' diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue index 05371700085..d7631a49a27 100644 --- a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue +++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue @@ -46,7 +46,7 @@ export default { diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue index e47995de8d6..48786c96b0c 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue @@ -161,7 +161,7 @@ export default { {{ featureFlagsLimit }} diff --git a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue index 2d50269acdb..4c00440274f 100644 --- a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue @@ -1,9 +1,9 @@ diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue index 007506134fd..fdcdf027b9e 100644 --- a/app/assets/javascripts/members/components/table/member_source.vue +++ b/app/assets/javascripts/members/components/table/member_source.vue @@ -1,5 +1,5 @@ diff --git a/app/assets/javascripts/organizations/settings/general/components/visibility_level.vue b/app/assets/javascripts/organizations/settings/general/components/visibility_level.vue new file mode 100644 index 00000000000..eb3d572ce58 --- /dev/null +++ b/app/assets/javascripts/organizations/settings/general/components/visibility_level.vue @@ -0,0 +1,77 @@ + + + diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js index 8fc086b5309..705ff64917f 100644 --- a/app/assets/javascripts/organizations/settings/general/index.js +++ b/app/assets/javascripts/organizations/settings/general/index.js @@ -14,7 +14,7 @@ export const initOrganizationsSettingsGeneral = () => { dataset: { appData }, } = el; const { organization, organizationsPath, rootUrl, previewMarkdownPath } = - convertObjectPropsToCamelCase(JSON.parse(appData)); + convertObjectPropsToCamelCase(JSON.parse(appData), { deep: true }); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js index 1e4c67f193e..7444ca70d90 100644 --- a/app/assets/javascripts/organizations/shared/constants.js +++ b/app/assets/javascripts/organizations/shared/constants.js @@ -11,6 +11,7 @@ export const FORM_FIELD_ID = 'id'; export const FORM_FIELD_PATH = 'path'; export const FORM_FIELD_DESCRIPTION = 'description'; export const FORM_FIELD_AVATAR = 'avatar'; +export const FORM_FIELD_VISIBILITY_LEVEL = 'visibilityLevel'; export const MAX_DESCRIPTION_COUNT = 1024; diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue index 0a7a3cc35a4..49391dd4a03 100644 --- a/app/assets/javascripts/organizations/show/components/organization_avatar.vue +++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue @@ -3,11 +3,7 @@ import { GlAvatar, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import { - VISIBILITY_TYPE_ICON, - ORGANIZATION_VISIBILITY_TYPE, - VISIBILITY_LEVEL_PUBLIC_STRING, -} from '~/visibility_level/constants'; +import { VISIBILITY_TYPE_ICON, ORGANIZATION_VISIBILITY_TYPE } from '~/visibility_level/constants'; export default { name: 'OrganizationAvatar', @@ -28,10 +24,10 @@ export default { }, computed: { visibilityIcon() { - return VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING]; + return VISIBILITY_TYPE_ICON[this.organization.visibility]; }, visibilityTooltip() { - return ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]; + return ORGANIZATION_VISIBILITY_TYPE[this.organization.visibility]; }, }, }; diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js index 3060b55ce25..be6ea12119d 100644 --- a/app/assets/javascripts/visibility_level/constants.js +++ b/app/assets/javascripts/visibility_level/constants.js @@ -49,6 +49,12 @@ export const ORGANIZATION_VISIBILITY_TYPE = { [VISIBILITY_LEVEL_PUBLIC_STRING]: s__( 'Organization|Public - The organization can be accessed without any authentication.', ), + [VISIBILITY_LEVEL_INTERNAL_STRING]: s__( + 'Organization|Internal - The organization can be accessed by any signed in user except external users.', + ), + [VISIBILITY_LEVEL_PRIVATE_STRING]: s__( + 'Organization|Private - The organization can only be viewed by members.', + ), }; export const GROUP_VISIBILITY_LEVEL_DESCRIPTIONS = { @@ -63,6 +69,14 @@ export const GROUP_VISIBILITY_LEVEL_DESCRIPTIONS = { ), }; +export const ORGANIZATION_VISIBILITY_LEVEL_DESCRIPTIONS = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: s__('Organization|Accessible without any authentication.'), + [VISIBILITY_LEVEL_INTERNAL_STRING]: s__( + 'Organization|Accessible by any signed in user except external users.', + ), + [VISIBILITY_LEVEL_PRIVATE_STRING]: s__('Organization|Only accessible by organization members.'), +}; + export const VISIBILITY_LEVEL_LABELS = { [VISIBILITY_LEVEL_PUBLIC_STRING]: s__('VisibilityLevel|Public'), [VISIBILITY_LEVEL_INTERNAL_STRING]: s__('VisibilityLevel|Internal'), diff --git a/app/assets/javascripts/vue_shared/components/crud_component.vue b/app/assets/javascripts/vue_shared/components/crud_component.vue index 7be8b2a2a28..c52b3ba75a2 100644 --- a/app/assets/javascripts/vue_shared/components/crud_component.vue +++ b/app/assets/javascripts/vue_shared/components/crud_component.vue @@ -131,12 +131,18 @@ export default { :class="{ 'gl-mt-5': isCollapsible }" >

@@ -179,10 +185,7 @@ export default { @click="showForm" >{{ toggleText }} -

+
{{ $options.i18n.contentLabel }} - + {{ $options.i18n.learnMoreLabel }} diff --git a/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.stories.js b/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.stories.js index e7d06880ff1..0df5cd8bc19 100644 --- a/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.stories.js +++ b/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.stories.js @@ -13,13 +13,13 @@ const Template = (args, { argTypes }) => ({ export const Default = Template.bind({}); Default.args = { - href: 'pricing', + path: 'pricing', }; export const LinkWithLeadingSlash = Template.bind({}); LinkWithLeadingSlash.args = { ...Default.args, - href: '/sales', + path: '/sales', }; export const CustomAttributes = Template.bind({}); diff --git a/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.vue b/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.vue index db51715d093..33f4c0ca255 100644 --- a/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.vue +++ b/app/assets/javascripts/vue_shared/components/promo_page_link/promo_page_link.vue @@ -7,7 +7,7 @@ import { joinPaths } from '~/lib/utils/url_utility'; * Component to link to GitLab website. * * @example - * + * * Usage Quotas help. * */ @@ -17,17 +17,17 @@ export default { GlLink, }, props: { - href: { + path: { type: String, required: true, }, }, computed: { compiledHref() { - return joinPaths(PROMO_URL, this.href); + return joinPaths(PROMO_URL, this.path); }, attributes() { - const { href, ...attrs } = this.$attrs; + const { path, ...attrs } = this.$attrs; return attrs; }, }, diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 432f36180ad..39dcd870d1a 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -13,7 +13,6 @@ @import 'framework/breadcrumbs'; @import 'framework/buttons'; @import 'framework/calendar'; -@import 'framework/callout'; @import 'framework/common'; @import 'framework/dropdowns'; @import 'framework/files'; diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss deleted file mode 100644 index 588aa7927c8..00000000000 --- a/app/assets/stylesheets/framework/callout.scss +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Callouts from Bootstrap3 docs - * - * Not quite alerts, but custom and helpful notes for folks reading the docs. - * Requires a base and modifier class. - */ - -/* Common styles for all types */ -.bs-callout { - margin: $gl-padding 0; - padding: $gl-padding; - border-color: $border-color; - border-style: solid; - border-width: 0 0 0 3px; - color: $text-color; - background: $gray-10; - - h4 { - margin-top: 0; - margin-bottom: 5px; - } - - p:last-child { - margin-bottom: 0; - } -} - -/* Variations */ -.bs-callout-danger { - background-color: $red-100; - border-color: $red-200; - color: $red-700; - - a { - color: $red-700; - } -} - -.bs-callout-warning { - background-color: $orange-50; - border-color: $orange-200; - color: $gray-900; - - a { - color: $blue-600; - } -} - -.bs-callout-info { - background-color: $blue-100; - border-color: $blue-200; - color: $blue-700; - - h4 { - color: $blue-700; - } -} - -.bs-callout-success { - background-color: $green-100; - border-color: $green-200; - color: $green-700; -} diff --git a/app/assets/stylesheets/framework/crud.scss b/app/assets/stylesheets/framework/crud.scss index adf3db8a311..9838ae89e36 100644 --- a/app/assets/stylesheets/framework/crud.scss +++ b/app/assets/stylesheets/framework/crud.scss @@ -1,3 +1,13 @@ +// Defines min height of the header +// to avoid a 1px flickering between +// expanded/collapsed mode +// and with/without actions +$crud-header-min-height: px-to-rem(49px); + +.crud-header { + min-height: $crud-header-min-height; +} + .crud-body:has(.gl-table) { margin-block: -1px; margin-inline: 0; diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index 34f79f75b9d..7bf86242cad 100644 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -29,7 +29,6 @@ .dropdown-menu li button, .dropdown-menu-selectable li a.is-active, .dropdown-menu-inner-title, - .bs-callout, .ide-pipeline .top-bar, .ide-pipeline .top-bar .controllers .controllers-buttons, .controllers-buttons svg, @@ -100,20 +99,11 @@ background-color: var(--ide-background, $gray-100); } - .bs-callout, .ide-pipeline .top-bar, .ide-terminal .top-bar { background-color: var(--ide-background, $gray-10); } - .bs-callout { - border-color: var(--ide-dropdown-background, $border-color); - - code { - background-color: var(--ide-dropdown-background, $gray-100); - } - } - .common-note-form .md-area { border-color: var(--ide-input-border, $border-color); } diff --git a/app/assets/stylesheets/page_bundles/settings.scss b/app/assets/stylesheets/page_bundles/settings.scss index 8963a2706ac..020db34e30c 100644 --- a/app/assets/stylesheets/page_bundles/settings.scss +++ b/app/assets/stylesheets/page_bundles/settings.scss @@ -130,7 +130,6 @@ background-color: var(--gl-background-color-subtle); } - .bs-callout, .form-check:first-child, .form-check .form-text.text-muted, .form-check + .form-text.text-muted { diff --git a/app/components/layouts/crud_component.haml b/app/components/layouts/crud_component.haml index 4e925c7a219..cd79476bb64 100644 --- a/app/components/layouts/crud_component.haml +++ b/app/components/layouts/crud_component.haml @@ -1,8 +1,8 @@ %section .crud.gl-bg-subtle.gl-border.gl-border-default.gl-rounded-base{ @options, class: ('js-toggle-container' if @toggle_text) } - %header.gl-flex.gl-flex-wrap.gl-justify-between.gl-gap-x-5.gl-gap-y-2.gl-px-5.gl-py-4.gl-bg-default.gl-border-b.gl-border-default.gl-rounded-t-base + %header.crud-header.gl-flex.gl-flex-wrap.gl-justify-between.gl-gap-x-5.gl-gap-y-2.gl-px-5.gl-py-4.gl-bg-default.gl-border-b.gl-border-default.gl-rounded-t-base .gl-flex.gl-flex-col.gl-self-center - %h2.gl-text-base.gl-font-bold.gl-leading-24.gl-inline-flex.gl-gap-3.gl-m-0{ data: { testid: 'crud-title' } } + %h2.gl-text-base.gl-font-bold.gl-leading-normal.gl-inline-flex.gl-gap-3.gl-m-0{ data: { testid: 'crud-title' } } = @title - if @count %span.gl-inline-flex.gl-items-center.gl-gap-2.gl-text-sm.gl-text-subtle{ data: { testid: 'crud-count' } } @@ -11,7 +11,7 @@ %span{ @count_options } = @count - if description? || @description - .gl-text-sm.gl-text-subtle.gl-mt-1.gl-mb-0{ data: { testid: 'crud-description' } } + .gl-text-sm.gl-text-subtle.gl-leading-normal.gl-mt-2.gl-mb-0{ data: { testid: 'crud-description' } } = description || @description .gl-flex.gl-gap-3.gl-items-baseline{ data: { testid: 'crud-actions' } } - if @toggle_text diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb index 76d6535a21f..593543766a7 100644 --- a/app/controllers/concerns/dependency_proxy/group_access.rb +++ b/app/controllers/concerns/dependency_proxy/group_access.rb @@ -31,14 +31,10 @@ module DependencyProxy # TODO: Split the authorization logic into dedicated methods # https://gitlab.com/gitlab-org/gitlab/-/issues/452145 def authorize_read_dependency_proxy! - if Feature.enabled?(:packages_dependency_proxy_pass_token_to_policy, group) - if auth_user_or_token.is_a?(User) - authorize_read_dependency_proxy_for_users! - else - authorize_read_dependency_proxy_for_tokens! - end - else + if auth_user_or_token.is_a?(User) authorize_read_dependency_proxy_for_users! + else + authorize_read_dependency_proxy_for_tokens! end end diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb index 0c83cccbe4b..de5bdb6d832 100644 --- a/app/controllers/groups/dependency_proxy/application_controller.rb +++ b/app/controllers/groups/dependency_proxy/application_controller.rb @@ -3,8 +3,6 @@ module Groups module DependencyProxy class ApplicationController < ::ApplicationController - include Gitlab::Utils::StrongMemoize - EMPTY_AUTH_RESULT = Gitlab::Auth::Result.new(nil, nil, nil, nil).freeze delegate :actor, to: :@authentication_result, allow_nil: true @@ -21,18 +19,17 @@ module Groups authenticate_with_http_token do |token, _| @authentication_result = EMPTY_AUTH_RESULT - if Feature.enabled?(:packages_dependency_proxy_pass_token_to_policy, group) - user_or_token = ::DependencyProxy::AuthTokenService.user_or_token_from_jwt(token) - sign_in_and_setup_authentication_result(user_or_token) - else - user_or_token = ::DependencyProxy::AuthTokenService.user_or_deploy_token_from_jwt(token) - case user_or_token - when User - @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :user, []) - sign_in(user_or_token) unless user_or_token.project_bot? || user_or_token.service_account? - when DeployToken - @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :deploy_token, []) - end + user_or_token = ::DependencyProxy::AuthTokenService.user_or_token_from_jwt(token) + + case user_or_token + when User + set_auth_result(user_or_token, :user) + sign_in(user_or_token) if can_sign_in?(user_or_token) + when PersonalAccessToken + set_auth_result(user_or_token.user, :personal_access_token) + @personal_access_token = user_or_token + when DeployToken + set_auth_result(user_or_token, :deploy_token) end end @@ -43,40 +40,21 @@ module Groups attr_reader :personal_access_token - # TODO: We only need this here to get the group for the Feature flag evaluation. - # Move this back to app/controllers/groups/dependency_proxy_for_containers_controller.rb - # when we rollout the FF packages_dependency_proxy_pass_token_to_policy - def group - Group.find_by_full_path(params[:group_id], follow_redirects: true) - end - strong_memoize_attr :group - def request_bearer_token! # unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header render plain: '', status: :unauthorized end - # When we rollout packages_dependency_proxy_pass_token_to_policy, - # we can move the body of this method inline, inside authenticate_user_from_jwt_token! - def sign_in_and_setup_authentication_result(user_or_token) - case user_or_token - when User - @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :user, []) - sign_in(user_or_token) if can_sign_in?(user_or_token) - when PersonalAccessToken - @authentication_result = Gitlab::Auth::Result.new(user_or_token.user, nil, :personal_access_token, []) - @personal_access_token = user_or_token - when DeployToken - @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :deploy_token, []) - end - end - def can_sign_in?(user_or_token) return false if user_or_token.project_bot? || user_or_token.service_account? true end + + def set_auth_result(actor, type) + @authentication_result = Gitlab::Auth::Result.new(actor, nil, type, []) + end end end end diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb index 48a1b9d3c54..38fd16d0ba7 100644 --- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb +++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb @@ -5,6 +5,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy include SendFileUpload include ::PackagesHelper # for event tracking include WorkhorseRequest + include Gitlab::Utils::StrongMemoize before_action :ensure_group before_action :ensure_token_granted!, only: [:blob, :manifest] @@ -97,6 +98,11 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy private + def group + Group.find_by_full_path(params[:group_id], follow_redirects: true) + end + strong_memoize_attr :group + def send_manifest(manifest, from_cache:) response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest response.headers['Content-Length'] = manifest.size diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb index 52a9e057148..f1b18c8b2ca 100644 --- a/app/finders/concerns/packages/finder_helper.rb +++ b/app/finders/concerns/packages/finder_helper.rb @@ -39,13 +39,7 @@ module Packages end def packages_visible_to_user_including_public_registries(user, within_group:) - return ::Packages::Package.none unless within_group - - return ::Packages::Package.none unless Ability.allowed?(user, :read_package_within_public_registries, - within_group.packages_policy_subject) - - projects = projects_visible_to_reporters(user, within_group: within_group, - within_public_package_registry: !Ability.allowed?(user, :read_group, within_group)) + projects = projects_visible_to_user_including_public_registries(user, within_group: within_group) ::Packages::Package.for_projects(projects.select(:id)).installable end @@ -57,6 +51,16 @@ module Packages projects_visible_to_reporters(user, within_group: within_group) end + def projects_visible_to_user_including_public_registries(user, within_group:) + return ::Project.none unless within_group + + return ::Project.none unless Ability.allowed?(user, :read_package_within_public_registries, + within_group.packages_policy_subject) + + projects_visible_to_reporters(user, within_group: within_group, + within_public_package_registry: !Ability.allowed?(user, :read_group, within_group)) + end + def projects_visible_to_reporters(user, within_group:, within_public_package_registry: false) return user.accessible_projects if user.is_a?(DeployToken) diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index bf1d3e422ed..477b5d6b5af 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -10,7 +10,7 @@ module Organizations def organization_show_app_data(organization) { - organization: organization.slice(:id, :name, :description_html) + organization: organization.slice(:id, :name, :description_html, :visibility) .merge({ avatar_url: organization.avatar_url(size: 128) }), groups_and_projects_organization_path: groups_and_projects_organization_path(organization), users_organization_path: users_organization_path(organization), @@ -24,7 +24,7 @@ module Organizations def organization_settings_general_app_data(organization) { - organization: organization.slice(:id, :name, :path, :description) + organization: organization.slice(:id, :name, :path, :description, :visibility_level) .merge({ avatar: organization.avatar_url(size: 192) }) }.merge(shared_new_settings_general_app_data).to_json end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 62afa4ea78b..23f183e1214 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -80,7 +80,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy end condition(:dependency_proxy_access_allowed) do - access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token + access_level(for_any_session: true) >= GroupMember::GUEST end desc "Deploy token with read_package_registry scope" @@ -440,12 +440,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy resource_access_token_create_feature_available? && group.root_ancestor.namespace_settings.resource_access_token_creation_allowed? end - # TODO: Remove this when we rollout the feature flag packages_dependency_proxy_pass_token_to_policy - # https://gitlab.com/gitlab-org/gitlab/-/issues/441588 - def valid_dependency_proxy_deploy_token - @user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject) - end - # rubocop:disable Cop/UserAdmin -- specifically check the admin attribute def owns_group_organization? return false unless @user diff --git a/app/serializers/group_link/group_link_entity.rb b/app/serializers/group_link/group_link_entity.rb index 1b8313c2536..05c3fe7c140 100644 --- a/app/serializers/group_link/group_link_entity.rb +++ b/app/serializers/group_link/group_link_entity.rb @@ -43,6 +43,10 @@ module GroupLink direct_member?(group_link, options) end + expose :is_inherited_member do |group_link, options| + !direct_member?(group_link, options) + end + private def can_read_shared_group?(group_link) diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb index ccc161c75f9..2e3f5d5a5d4 100644 --- a/app/serializers/member_entity.rb +++ b/app/serializers/member_entity.rb @@ -28,7 +28,15 @@ class MemberEntity < Grape::Entity expose :last_owner?, as: :is_last_owner expose :is_direct_member do |member, options| - member.source == options[:source] + direct_member?(member, options) + end + + expose :is_inherited_member do |member, options| + inherited_member?(member, options) + end + + expose :is_shared_member do |member, options| + !direct_member?(member, options) && !inherited_member?(member, options) end expose :access_level do @@ -81,6 +89,20 @@ class MemberEntity < Grape::Entity def current_user options[:current_user] end + + def direct_member?(member, options) + member.source == options[:source] + end + + def inherited_member?(member, options) + if options[:source].is_a?(Project) + return false unless options[:group] + + options[:group].self_and_ancestor_ids.include?(member.source.id) + else + options[:source].ancestor_ids.include?(member.source.id) + end + end end MemberEntity.prepend_mod_with('MemberEntity') diff --git a/app/services/packages/nuget/search_service.rb b/app/services/packages/nuget/search_service.rb index 7d1585f8903..1b1568feeef 100644 --- a/app/services/packages/nuget/search_service.rb +++ b/app/services/packages/nuget/search_service.rb @@ -102,15 +102,19 @@ module Packages def nuget_packages Packages::Package.nuget - .displayable + .installable .has_version - .without_nuget_temporary_name end def project_ids_cte return unless use_project_ids_cte? - query = projects_visible_to_user(@current_user, within_group: @project_or_group) + query = if Feature.enabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, @project_or_group) + projects_visible_to_user_including_public_registries(@current_user, within_group: @project_or_group) + else + projects_visible_to_user(@current_user, within_group: @project_or_group) + end + Gitlab::SQL::CTE.new(:project_ids, query.select(:id)) end strong_memoize_attr :project_ids_cte diff --git a/config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml b/config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml deleted file mode 100644 index 9131f3f01d7..00000000000 --- a/config/feature_flags/gitlab_com_derisk/packages_dependency_proxy_pass_token_to_policy.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: packages_dependency_proxy_pass_token_to_policy -feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434291 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141358 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/441588 -milestone: '17.0' -group: group::container registry -type: gitlab_com_derisk -default_enabled: false diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md index cfc96292150..34f033ece80 100644 --- a/doc/administration/get_started.md +++ b/doc/administration/get_started.md @@ -53,7 +53,7 @@ Get started: - [Run multiple Agile teams](https://www.youtube.com/watch?v=VR2r1TJCDew). - [Sync group memberships by using LDAP](../administration/auth/ldap/ldap_synchronization.md#group-sync). - Manage user access with inherited permissions. Use up to 20 levels of subgroups to organize both teams and projects. - - [Inherited membership](../user/project/members/index.md#inherited-membership). + - [Inherited membership](../user/project/members/index.md#membership-types). - [Example](../user/group/subgroups/index.md). ## Import projects diff --git a/doc/development/permissions/custom_roles.md b/doc/development/permissions/custom_roles.md index 354687f615a..91c7af1e522 100644 --- a/doc/development/permissions/custom_roles.md +++ b/doc/development/permissions/custom_roles.md @@ -24,7 +24,7 @@ With custom roles, the customers can decide which abilities they want to assign - In the default role system, reading of vulnerabilities is limited to a Developer role. - In the custom role system, a customer can assign this ability to a new custom role based on any default role. -Like default roles, custom roles are [inherited](../../user/project/members/index.md#inherited-membership) within a group hierarchy. If a user has custom role for a group, that user will also have a custom role for any projects or subgroups within the group. +Like default roles, custom roles are [inherited](../../user/project/members/index.md#membership-types) within a group hierarchy. If a user has custom role for a group, that user will also have a custom role for any projects or subgroups within the group. ## Technical overview diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md index eb22eb652fc..8cf398e12b1 100644 --- a/doc/topics/git/index.md +++ b/doc/topics/git/index.md @@ -20,4 +20,5 @@ platform for software development. GitLab adds many powerful | [**Get started**](get_started.md) **{chevron-right}**

Overview of how features fit together. | [**Install Git**](how_to_install_git/index.md) **{chevron-right}**

Download, configuration, system requirements. | [**Tutorial: Create your first commit**](../../tutorials/make_first_git_commit/index.md) **{chevron-right}**

Initial commit, Git basics, repository setup. | | [**Clone a repository to your local machine**](clone.md) **{chevron-right}**

Local repository, clone, remote repository, SSH. | [**Create a branch for your changes**](branch.md) **{chevron-right}**

Branching, branch switch, checkout. | [**Add files to your branch**](../../gitlab-basics/add-file.md) **{chevron-right}**

Git add, staging changes, file management, commits. | | [**Stash changes for later**](stash.md) **{chevron-right}**

Temporary storage, work in progress, context switching. | [**Undo changes**](undo.md) **{chevron-right}**

Reverting commits, removing changes, Git reset, unstage. | [**Tutorial: Update Git commit messages**](../../tutorials/update_commit_messages/index.md) **{chevron-right}**

Commit message editing, version history, best practices. | -| [**Rebase to address merge conflicts**](git_rebase.md) **{chevron-right}**

Conflict resolution, rebase, branch management. | [**Common Git commands**](../../gitlab-basics/start-using-git.md) **{chevron-right}**

Git cheatsheet, basic operations, command line. | [**Troubleshooting**](troubleshooting_git.md) **{chevron-right}**

Error resolution, common issues, debugging, Git problems. | +| [**Rebase to address merge conflicts**](git_rebase.md) **{chevron-right}**

Conflict resolution, rebase, branch management. | [**Common Git commands**](../../gitlab-basics/start-using-git.md) **{chevron-right}**

Git cheatsheet, basic operations, command line. | [**Tutorial: Update Git remote URLs**](../../tutorials/update_git_remote_url/index.md) **{chevron-right}**

Change the push/pull URL on a working copy.| +| [**Troubleshooting**](troubleshooting_git.md) **{chevron-right}**

Error resolution, common issues, debugging, Git problems. | | | diff --git a/doc/tutorials/update_git_remote_url/index.md b/doc/tutorials/update_git_remote_url/index.md new file mode 100644 index 00000000000..b3422032d68 --- /dev/null +++ b/doc/tutorials/update_git_remote_url/index.md @@ -0,0 +1,166 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Tutorial: Update Git remote URLs + +DETAILS: +**Tier:** Free, Premium, Ultimate +**Offering:** GitLab.com, Self-managed, GitLab Dedicated + +Update your Git remote URLs if: + +- You imported an existing project from another Git repository host. +- Your organization has moved your projects to a new GitLab instance with a new domain name. +- The project was renamed to a new path in the same GitLab instance. + +NOTE: +If you don't have an existing local working copy from the old remote, then you don't need this tutorial. +You can instead clone the project from the new GitLab URL. + +This tutorial explains how to update the remote URL for your local repository without: + +- Losing any of your local changes that are incomplete. +- Losing changes that are not yet published to GitLab. +- Creating a new cloned working copy of the repository from the new URL. + +This tutorial uses the `git-remote` command to +[manage remote and tracked repositories](https://git-scm.com/docs/git-remote). + +To update Git remote URLs: + +- [Determine existing and new URLs](#determine-existing-and-new-urls) +- [Update Git remote URLs](#update-git-remote-urls) +- [(Optional) Keep original remote URLs](#optional-keep-original-remote-urls) + +## Before you begin + +You must have: + +- A GitLab project with a Git repository and a new GitLab URL. +- A cloned local working copy of the project that you are migrating to the new GitLab URL. +- Git [installed on your local machine](../../topics/git/how_to_install_git/index.md). +- The ability to get to your local machine's command-line interface (CLI). In macOS, + you can use Terminal. In Windows, you can use PowerShell. Linux users are probably + already familiar with their system's CLI. +- Authentication credentials for GitLab: + - You must authenticate with GitLab to update Git remote URLs. If your GitLab account uses + basic username and password authentication, you must have [two factor authentication (2FA)](../../user/profile/account/two_factor_authentication.md) + disabled to authenticate from the CLI. Alternatively, you can [use an SSH key to authenticate with GitLab](../../user/ssh.md). + +## Determine existing and new URLs + +To update the Git remote URL, determine the existing and new URLs for your repository: + +1. Open a terminal or command prompt. + +1. Go to your local repository working copy. To change directory, use `cd`: + + ```shell + cd + ``` + +1. Each repository has a default remote named `origin`. To view the current remote _fetch_ and _push_ URLs +for your remote repository, run: + + ```shell + git remote -v + ``` + +1. Copy and keep note of the returned URLs. They are usually identical. + +1. Get the new URL: + 1. Go to GitLab. + 1. On the left sidebar, select **Search or go to** and find your project. + 1. On the left sidebar, select **Code** > **Repository**, to go to the project's **Repository** page + 1. In the upper-right corner, select **Code** + 1. Depending on which method you use for authentication and cloning with `git`, + copy either the HTTPS or SSH URL. If you're not sure, use the same method as the `origin` URL from the previous step. + 1. Keep note of the copied URL. + +## Update Git remote URLs + +To update the Git remote URL: + +1. Open a terminal or command prompt. + +1. Go to your local repository working copy. To change directory, use `cd`: + + ```shell + cd + ``` + +1. Update the remote URL, replacing `` with the new repository URL you copied: + + ```shell + git remote set-url origin + ``` + +1. Verify that the remote URL update is successful. +The following command displays the new URL for both fetch and push operations, +lists the local branches, and confirms that they are tracked to GitLab: + + ```shell + git remote show origin + ``` + + - If the update was unsuccessful, go back to the previous step, ensure you + have the correct ``, and try again. + +To update the remote URLs for multiple repositories: + +1. Use the `git remote set-url` command. Replace `origin` with the name of the +remote you want to update. For example: + + ```shell + git remote set-url + ``` + +1. Verify each remote URL update: + + ```shell + git remote show + ``` + +After updating the remote URL, you can continue to use Git commands as usual. +Your next `git fetch`, `git pull`, or `git push` uses the new URL from GitLab. + +Congratulations, you have successfully updated the remote URL for your repository. + +## (Optional) Keep original remote URLs + +Your project might have more than one remote location. +For example, you have a forked repository from a project hosted on GitHub, +but you want to work on your fork in GitLab before you make a pull request to GitHub. + +To keep the original remote URL in addition to updating it, and maintain both new and old +remote URLs, you can add a new remote instead of modifying the existing one. + +With this approach, you can gradually transition to the new URL while still maintaining +access to the original repository. + +To add a new remote URL: + +1. Open a terminal or command prompt. + +1. Go to your local repository working copy. + +1. Add a new remote URL. Replace `` with a name for the new remote, +for example, `new-origin`, and `` with the new repository URL: + + ```shell + git remote add + ``` + +1. Verify that the new remote was added: + + ```shell + git remote -v + ``` + +Now you can use both the original and new remotes. For example: + +- To push to the original remote: `git push origin main` +- To push to the new remote: `git push main` diff --git a/doc/user/custom_roles.md b/doc/user/custom_roles.md index ac69706c834..fe7abc783af 100644 --- a/doc/user/custom_roles.md +++ b/doc/user/custom_roles.md @@ -243,7 +243,7 @@ curl --request PUT --header "Content-Type: application/json" --header "Authoriza ## Inheritance If a user belongs to a group, they are a _direct member_ of the group -and an [inherited member](project/members/index.md#inherited-membership) +and an [inherited member](project/members/index.md#membership-types) of any subgroups or projects. If a user is assigned a custom role by the top-level group, the permissions of the role are also inherited by subgroups and projects. diff --git a/doc/user/group/access_and_permissions.md b/doc/user/group/access_and_permissions.md index b6c29cae93a..cdd01846d55 100644 --- a/doc/user/group/access_and_permissions.md +++ b/doc/user/group/access_and_permissions.md @@ -343,7 +343,7 @@ LDAP user permissions can be manually overridden by an administrator. To overrid - More permissions than the parent group membership, that user is displayed as having [direct membership](../project/members/index.md#display-direct-members) of the group. - The same or fewer permissions than the parent group membership, that user is displayed as having - [inherited membership](../project/members/index.md#display-inherited-members) of the group. + [inherited membership](../project/members/index.md#membership-types) of the group. 1. Optional. If the user you want to edit is displayed as having inherited membership, [filter the subgroup to show direct members](index.md#filter-a-group) before overriding LDAP user permissions. @@ -369,7 +369,7 @@ If a group Owner cannot update permissions for a group member, check which membe are listed. Group Owners can only update direct memberships. If a parent group membership has the same or higher role than a subgroup, the -[inherited membership](../project/members/index.md#inherited-membership) is +[inherited membership](../project/members/index.md#membership-types) is listed on the subgroup members page, even if a [direct membership](../project/members/index.md#membership-types) on the group exists. diff --git a/doc/user/group/index.md b/doc/user/group/index.md index bcf90784ed1..110825ba2ed 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -261,7 +261,7 @@ If you change your mind before your request is approved, select ## View group members -To view the direct and inherited members of a group: +To view members of a group: 1. On the left sidebar, select **Search or go to** and find your group. 1. Select **Manage > Members**. @@ -275,7 +275,7 @@ A table displays the member's: For example, if a member has been added to the group both directly and through inheritance, the member is displayed twice in the **Members** table, with different sources, and is counted as two individual members of the group. -- [**Max role**](../project/members/index.md#which-roles-you-can-assign) in the group. +- [**Role**](../project/members/index.md#which-roles-you-can-assign) in the group. - **Expiration** date of their group membership. - **Activity** related to their account. @@ -302,7 +302,7 @@ In lists of group members, entries can display the following badges: 1. Select **Manage > Members**. 1. Above the list of members, in the **Filter members** text box, enter your search criteria. To view: - Direct members of the group, select **Membership = Direct**. - - Members of the group and its subgroups, select **Membership = Inherited**. + - Inherited, shared, and inherited shared members of the group, select **Membership = Indirect**. - Members with two-factor authentication enabled or disabled, select **2FA = Enabled** or **2FA = Disabled**. - Members of the top-level group who are [enterprise users](../enterprise_user/index.md), select **Enterprise = true**. @@ -317,7 +317,7 @@ You can search for members by name, username, or [public email](../profile/index ### Sort members in a group -You can sort members by **Account**, **Access granted**, **Max role**, or **Last sign-in**. +You can sort members by **Account**, **Access granted**, **Role**, or **Last sign-in**. 1. On the left sidebar, select **Search or go to** and find your group. 1. Select **Manage > Members**. diff --git a/doc/user/group/saml_sso/group_sync.md b/doc/user/group/saml_sso/group_sync.md index a17ed074827..24f96c41e6b 100644 --- a/doc/user/group/saml_sso/group_sync.md +++ b/doc/user/group/saml_sso/group_sync.md @@ -84,7 +84,7 @@ Users granted: - A higher role with Group Sync are displayed as having [direct membership](../../project/members/index.md#display-direct-members) of the group. - A lower or the same role with Group Sync are displayed as having - [inherited membership](../../project/members/index.md#display-inherited-members) of the group. + [inherited membership](../../project/members/index.md#membership-types) of the group. ### Use the API diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 2f450bbbaed..1c2b88ed0d6 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -135,9 +135,9 @@ The member's permissions are inherited from the group into all subgroups. Subgroup members can be: 1. [Direct members](../../project/members/index.md#add-users-to-a-project) of the subgroup. -1. [Inherited members](../../project/members/index.md#inherited-membership) of the subgroup from the subgroup's parent group. +1. [Inherited members](../../project/members/index.md#membership-types) of the subgroup from the subgroup's parent group. 1. Members of a group that was [shared with the subgroup's top-level group](../manage.md#share-a-group-with-another-group). -1. [Indirect members](../../project/members/index.md#indirect-membership) include [inherited members](../../project/members/index.md#inherited-membership) and members of a group that was [invited to the subgroup or its ancestors](../manage.md#share-a-group-with-another-group). +1. [Indirect members](../../project/members/index.md#membership-types) include [inherited members](../../project/members/index.md#membership-types) and members of a group that was [invited to the subgroup or its ancestors](../manage.md#share-a-group-with-another-group). ```mermaid %%{init: { "fontFamily": "GitLab Sans" }}%% diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md index d4106264d70..ab5782d12e7 100644 --- a/doc/user/organization/index.md +++ b/doc/user/organization/index.md @@ -66,6 +66,15 @@ To view the organizations you have access to: 1. In the **Organization URL** text box, edit the URL. 1. Select **Change organization URL**. +## View an organization's visibility level + +NOTE: +In [Cells 1.0](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/iterations/cells-1.0/) organizations can be only private. + +1. On the left sidebar, select **Organizations** and find your organization. +1. Select **Settings > General**. +1. Expand the **Visibility** section. + ## Switch organizations NOTE: diff --git a/doc/user/project/members/img/project_members_v17_4.png b/doc/user/project/members/img/project_members_v17_4.png new file mode 100644 index 0000000000000000000000000000000000000000..4fb2f778099dbe89e0cf6c9a0962793e921b0fd8 GIT binary patch literal 59577 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

M((@l z#f$fPzxOk`!nm8U?ExP`*HN?Dy&Zbcs#GcU1%FUf5Hd8s{&mpfc+8D+(P#SAfHj4~ zvKnhMfHR#t!h6HNRIli$8L5>pt)$mSdqR=)709dQYrWESk=)Gehl*(g@dI)|?WBF2f^=hm9;}&eJN1iPBtuW;V3sRh}bPW1dob zWZBq0j{%c(SDyUax0x>&2wfH5l7|KicFuV-sVJI8vAfZjher`@=ZISW>BK0p-T%Dm zWg6WAa`O>h=MOKE^PSa30&SN?d=dJ8#cO;VJm*4ITfPZeR+aT5cbs}_LWznP?}udKMY`0bsw#_gHW|E+uT#C zBiP+(0LX$QnDuT4hx?@9k*|LZk4f=rDg}2$jA=ZskG%}?HMr#7>+maC!S@H!92U~6 znddRcT6r)=4s-la$(z=|m{5Xf}4t6>TNAfPf&;|p0a_mkod)LyEOW!~54XULguOCi+xp%>hThzvh z6N+Ai*6gamY7t&w2BX4)J7d!i5%^3$@C4sT5R{C#|3P(Fc^_d^XZ$5pyx%TkB9ajj z^=9k71g4z_cCy#q3d1G|AbxNe7*N!B?#!S znL_+?V8gg&hTPvSZw!u!MSjno18=|HfRq76+kG#zsE0vsC$hexFHrApkFBC{1Qgfz zzst;hVIQqZ__(uhC)~10(Z=x6i>JmKQUzeg%iZ*s} z#O&Mz6@lNRJ!}Jz5(-A7Pr|zEC>6`eMmRk!X8<|=u{u2|iNK!{OIDcLzY#zVf>N71 zG|#Uhs@*~1{@&T=7FSJ%f^cV9eyB0*FVy}GxS5`mPebJ-U_9sO5S7fAWA_XrL5P3T zeR`y7x)k3*Nhgtlo|f<7XiEGjvUjljLRP@Qo6#p$5+OATU!ZW~)gg_@^Gk&7@yEmY z*=6RrG21K1_c)V*lo+M=(hotkQJ=I|=VC)tOr3(BL^YMrIm2s7%J9p^4J=F8;aDP@-z|%>yP3sr78Jek}emSRMFtI6| zrG2-lJ#OxXc~DG@WaQq-PGRs}4DB(g(66G|klUD;qM&PRQenQ3)%)zrInlnSlZXM) zUzGy^S0^LA$3=(47`>bo#^F+#0blqm^Qc+$Xo{-+4vzvIKrAU*Mrmnktt>=@npAmH z^83CzeOeUyIV|!ZqqL)3medJgruSw4UdiZzJ`>Mo_uYzzE!+LYiQ9$zcdC2u>1}v? zpvAv-1d%D&PEd zC+P~;*EJekQ|iO~kZ*1^dp+j##EYLWHEBNIH?!egi&;hd_ez0Rx`^qrI@qs{09C^> z4sjL!?PN}wZ}rf{sf{1#M%8i%&M}3tgm=yR9Nk=aBElc|c%N4FcCR&CwJ7-MZv9CV z`1+E~k^ae^GZ_g4*`m3GGrGSS_mPK=WCehgGm>EcI3D&T0`V1lf&!WDo@f4&MXiDC zQS~nCKLIk3V9g+FFUu~~6{prppOVHqNhC>5<>53Se_i;7CIvp;ZTKEvl!AA+qdV8P zd12~D-rH*x2Dg~Qy@^3JU)J!y9^{CIV|`3KmZg*8ojLo~WKWL*LU=e}5dq zt`lstc-XqS^>X!rq;gaMD|3|^9=z(wxbT10wWuqmjQ@sts~J9EN$E3l%QwxJR^gVq zv?FtCL8D@6m+55p;yjFqx|Gw53Vj@|5XwJp3H97dmQ{>06`4x!{gN90^{QYx7fKvm zG6%;PLjx-Tuld``&3`y)cs9#yb9qmMZ@jWeaZf{0Tl%Aqr%xCaEn4&yOh^}ae{)2O zRv3hwq-rI!G(l`i|HesV8gkvfjfvO;{5*FVhNBL?B65L~gop~`Glr?FO!v5y`Jycm z9uNSZH&FmV%>|Ew9G1yg9vU?Z-#u;*tpqDmrEPTHy}tl@C*UA?`+B0m!SJDz8R$}Y zY2{#Vk$irqT&nw?svE zMJCeMzN`UVFcKL;?Z5Ov0jpIMe2q(XIbHenU!M%v_rnLb>uLEZ`K}0AEc=Buz1N=p zXb$AVmZ3Ji-d`A{;+qBRr9*{%jIu}sE*q29;^+8KfS{v(#eFhVw(7=d3XWzx|1DSe z>#-B@x|p1;vBiAz88x!DhqF z#+Q}N!^@j}*$$Z|x*IiXKAh2iw-faA`!e|Breyz;wg2gSXs7?z$W91lyqU?w+YTxD zWj<@6P|(BZs;pJZjBDHnZ+8Uw(z>q5R@%q~xH-}D;NY01iU_=TmV&;P2#je@4VYRd z3jHZ4$18*3wlH4ECN>N|I;1Xz07vze_w#7N>1kUhU3T?_g~R3TtSCFp1GlV#f6}Pu z?tt{M<7n+zy=ao8#vW>?SLU#=kGWhfbx^mK_36ct?10s@FYw`h%hd%EZ6NUBrncAO z^u?Cr>TX(S_Y95{g11N{;Az+K?@v!FJY`wyt~=b_|8A7p5>XRk1|R&xVY`Py4IH9A z%@lx36Bn^+62)slKM}Zx1+(F*8DJZ;nO03V3URgu!#gh{jERk`*?AUVaiA*@!_=eR zl5%}Ij>b~$FYP`LXhxb?6@H<3{O}R~~e#Fgq9|+M_x}TX9gaB zZ!>!&moSt`#_u2z%<4OBg3lGKYbY}QAM|@V=`Y?E%72wjH_^GFo*o;3xKTJ&-{%KP zdRD<9I6k-@1MTUH;HgYi9}k}{Bs{aL)s>AWhzHX@k7m#JW|Nu2fQgnoEv!7KMbi>g z6xDv_8^{RD!7xdccraoi=zN1a}zrJmg zUsfFQF3tCDqM<1EKLw44MGQ6Jx(?>3a6v|k^`k&s8?DbztbtnP4w^aFUV{g=TpkV6 z6+*3lvDbADU8Ng};S@ZlAw#7%n(bZ8cL$1XcJ$CT56iPVtqU&0{lH&=OFvg_Ks7olZ)ONAiD0IllZ|Y z5aX&L8Qz^|e~E~4`{syjvWcNK4B~6^iOnYBMi2^2U3|f9g8Wx>y1S?mqX)~Tk)Kpw z+w9LQ3LoZ|E-D1@r2qgL7R4DfyNON*TGrY5_-H_x2GYe!A_zF{U?Q`Yp6yBLI|9{|HKAchD04uFzq5_QuuC@1QS2MOV(HCFDdxBLh4PkSaA~oPwk8I$6xot7kc`eFm901) z3FTkOuTk&4jkyJW%UQWxEiJD5*ZJFiPFXaLi84M8ssDW$=0AT@VJwIykgH1FC9@@J z*q=C6XIQpiVZ0Ig$oh@D``pQn2-i$r#wnp=s8{5UzMHjaahx|D_}7aiXoLs}Ifcf? za!fPvy`}EpnDre5G;Cs$dA*rikGp)9r?>Q^74$qpsmG6i#PrGOeI1mZ(r7azm!ovzdz)#v#wiD+SDcSh` zqxaY~8a~|#PNL{UMJhh7|HJ7tK8vYtV)4n zlLUEI;wt?s@FawapK{UTAA7h)5Wc9ajZEOWqQT*G67F!GBzoeT3ELKY4+9XULczBa zO9K5lUmbHgl@||mB->HdlLM$&D;8BeBV32}Xa>^l^&+5(^!nV5y`-l!+W7Rf&Pt|DB+D8~c~v4H+A@LYpy5RHHqvvgtwuFi z2+*}mtH9RAjefe`t}Nn!p&RWp3@n;vSt~6Kc{b6U;y_Ap`|3E``FIkr)Go-?Y~^9d&H@0-m;lv4>B*_JV0G^iMQnbP4Yi z4huWX&MMrBKu#$cIc35nX=AxO2|+Zu9-Wi06Uq?V$|e&UD1#f%ZEyNNT*V`diauOo zG27xu(16RX#OcUihw`K-P4tdXfn42*fnIz8LDrdH3`7-3;b2l!I{~!XyZi-0K7fu) z{V>cMKvktja*)OPgrrfK&ownU=@4A1LqY^rjOoMYL5NR?e_} zfKn~T|NHRL8O6Z2`&44Nj6Zb6uC%4|{vlJ^bx{^6o-fvRWj{-02syat>7ZvA#sB)y zPLu8Za9Qg?EM@~jp{vorChB*6(3IaP9`QgwsN|l&=~@0PBDsVZS3LfF=7vLL&JNe< zw=S__RlMHZ9kcF8-UlS<$QB8B-7o-C zIig;N1i6#M^+Jj?oY;1RY#RE!uB05^;8G!5fzGkq|d zfjnRSK&Fo4AY)jD+#ez7Hd>aRU7o`?pw9D7!g&X7l@o1ez|G5CqfFZ~(f-n?(-8W?^xHjXsM=8w*si}~zmd7B@OHIupl!Ma ztqc?KUq|7ADM57ar#6httNH4ZDsp@RV^YlcvL{z0o7V5v0z@8S#m$*^eSYrB@vMczm zMQ}#AZs&9IUYamwJ)HGJeYg+~0Q3stCTNm*K;-ibjzvJw_-cp5DEh#3+T(lzDhy>X zk68tIJxYNVQk+Ac^t1#Hm>xekr%@yK-QFU3XHj?}{`Mb)+YC-Gc>nBnT6o!l{h||f zw)?Fzi&6NIUumI*cZWRG!l26g3+h6Db+{dauuf6bOFIL~<3?G)$_8YyCctPtyD?-4kWV?{Ey22ryy0g1oDX?k8DV#tP z%|$KrXei5!jw$wD;7*K;k308rI^rNUfX?2uSBmM)MUp~VmoDhfhx5A5Rzd@Do18aN z2t3Gk+z&Utn}%A}wnZ{6s$IeAz!U^r7To?L4e`eSYoJW8(Zt$OXT-Z0&?t`qg8qij z48J{RBKXo?06f4W97^d(3Ace=lMx;sP!JA~^J0nfH|Tek@^i$^0Z@a3i-OXD8YoMx z-yjTMJRnLRe}}qIBeIutd}%=5Xz9Ech4c+5Rm)h7=nZ8NaMXy0*EEK2W8TnE?^ibX zce;0TAREONlocMp>PNQAr!?G`^=&!5Zd%|s&bcmMmz*iE-J>%VY12VA_rachO157{ z%0pl2efae1M#cN#w*gZyib25ze?_QOQXCd>CsbvyTy%WQ4rWyp`_W2=28zpsCg9l4U??Z9Qdx+qkT-=FT zN>ZOyYHX)PEeq!f32Y6suRjRxeG$pL9tE+%{gcPix>{YAvSEen5OHuzIeqmXC|!zj z_0T4`VCGt9bOy)O>8IGNKiHd!tGco;GMf}jDC5tlRIpWOD(^=PT6#3eE|SAOA-eFU zz}*bMvt%5*NU%o1-`o^*H<~?@phhf-;>a2dzaMa3(}Fb@WgFPQPnT&W zXT}QN9ws%aJJYq_`_f&E^2UiPqDnHaw}H~2L@MDtM(4B-?>~6E2WLY;<%Hi-s=74a zUzS-$mArL^wo=gFRC5s*m!h-o@7bJM>ASKMLu{b^t5vM;0qA&f^i8?xtIfZXDl|ym zWyMZTzd>E8Pe5y%n=BZdzt;?OkWJ%h@5e~}vt|{4lp?dKX6Wz8vK!AnRwT+YSB!Xj zWw)JnyG|z=s4leguDmByQz8`jp~^99fvP<=Jo-ITV=$g_p|CV5&x}S46#S0yHj&7)10X3WPRdGF%3hRU z10Ln97@B_VD)w8LQE8l=olgT{-c;4z!1dv4(8=9K{C!f%LV@u;ls6*xJ9L+vp3>vL zFb8T0a5$}&-&fE0i8U>=EG=RkKvIg1GbSA|nEWsTf_x`z6 zuiopbsT%8Zy1RO6W=?7GyFe2`=4;1T2!n{`tzwDzk0u#q zNQ)8ga>zF1OpkFleKOf>Z|0WoSYV?CnmuZ9^NxX|gl`s&F%cM@OGa-|^cp93 zH;hWQZJejHo#OD{TKvp$gVS~hM zyQ|}c>JhnQ`v|P!*=C+?EH2iJd-PV7oE=NZD>Uo9&?bFa+r`UGt7|97C5qRYw0-4`Q9*Kr7_kp-e=XTW<-t0DK;%S9;IpYqGrQ~5R}Mn zFb7bQ&D3k1H=lx$d@86f`IgX<)2d#^Rh~LD4O=ACn_cU#`@7{>l~u)iX}b8hj@DCt zrho6*`+0_C&ND|0|GSKmzM2BSY_F9PJz;MHS2TsTVBv2ANY8qDkZVwh`i$7;W^!r z%&^u3(L~GrZ8A42VSVk>r)A|WCNlO5EfF)eu66;zSqHPFIeru{Kmc1EP)18Cm<~tT zoY30+jTDYlMee9ZQB>F7%vOqGh5&wqU?kKqi1v&3Cv~GN zN4y#ajFe3$OdmSP@RNLcQd#s`^I+}`zY1@{(K&D3T081l@{5n$euPg7+r)@fsKT8& zK20UdU)J{Rw}q+`O`@R%Eg6G$wlk}3_zt3TC4s85k5wFHZ@k^@*G1022pTddkEIP2n zb8B2maH>=n_gap4tKgTf#zRVXJI&TlYVcG!!k1&U=0ymHP%Hl|+Lal((tL?06sBlD zBq27Ps99=iE7ri>K}Z(&li5^kzu?EQ7c6@Z^L_pFwnDI#7w<0SKJ|p)U9IOu@F|K+ z0r9y+pnZ76O?%4$+4otGV$T`U)^y~%K!wJj>Ym7o^%`JVps1?->m_qQiv~|l--D&F zBgYQgj`hKZ$B(T{r$Pl9eS+T4?Fo-(jC1`V>^7SBk#ts2b&jflh7Yrk5v_J}L42rp z&J_0}@&$y1M{M5?w$hmMRgkin_4`KM2(BeAyT&#ee?{Ptlohc8p)r5tGcyc}Gh6>s zORnn6pFu7kr(Q2>l!}WJ=9oWoeRicjY$|Z!)iLM0^PKG=@#y{Yq0DtRO;C(?cpsr0 zc}uG}34UJG_VP}98I_D#tBYi69;&_~FnVok74cT81G;w1!E5 zwi~6tQDg>w4{uA9)L?Yd;KX+U08Zd1T0cu#X!_hpPPmytcwijh@8{x|f!5~-0cX?^ zUK>OEF~<|BA(Tce)x%1!@{h&WU94B+81FQZZ^g8U?^{{I%5hjThfq{oKkx45K)A6jv~!GS!+>*VU#swg z`o-Nr?^F!Q@2?y|A%~69HwNUmt7-0?oU3BDnW%}=1Zh|J_Eo*^1XRQoFR~UKfj(8< z`^(YlY~u@E;@VRk-=crdz>S&xsdi0W6;!;xxKgw(IxvG)Utuu;vRQqy5EHeh4bOH_?rj* znTVy!m(%(E{YnJB7M^D9a+mXTaUFe0%183#heK)Nsgd^#V^5c+6dHFO+}-eyexlWE z#_>#bmp}xWzvS?Nl^dW-0q9R{8IzN!PVVG6+<>l z!MV4c6kIZFJT~mjUG$Gn!K`I)<;)y;%awbI89yriO~x%h+rZ*^aB*y(g|()zP}6Fk z6f5eDq_5V}eKd`ho_1-X1{aO}3T=5k{{Zcu++1(IC&OJ7{l&}ERJ1j|@Wduh-Lh+H#VIeH?BC%tY~60^A>x{X}*#_@EVT87?rf zw&jxMwh2W#2}P&F4d;c8swyhL*a!9c-}9alrukwDY{*vZ5gvjSU3-`5GsNNosuwph zU3%~ySydN7D6BA#q%4Sem5CXj_&bZxu@|*}DxMEfd z0kIL0^cVLyluZgO>F0(I?1|jH5mMquI0;&Q@jgdvTR7o82EZT4}E=(S{zy$qpb~F&H5oa3< zT0=gv3TFyZ0-%UOA|=g|W9u6wYZyU>LvYwA_&C~j+x|7^owjQHs2_iEAgit}Th(Ub z;pJCEN6p30h+@^i&NXqpqeC5(04`7+npk0Yu%E?Yw%Q1hit&jVx)ZI9#ezYygBl<) zW9dMF&OS_8quyQNZggcy&XW2;Tc;|ft%}YIlBAgtRDtS1m4i+yjWH6#4apqWUYMPo zJ$OUXKhlPE%UESUXf4aGT&wzF%2*y!`^XO*I&>Hj{bj6%+nahkM^K>TD0X5p$TsYm z{NfO{S5KZ0)K)~2vubYWSSj*QY5CcP+5Pp|TLO&Xfw*jNwAl2feI~-oHJ&zuBYtD^%(dSaC;vt|K?JA5Sqe^9Y@l1C8-#Hsi-c7}+DWpxY3 zo83iQ&LNsf+3dN{PS~)Ni;|&AFBHn8ta>KST@>^r%!a8F z-xP8M=-qtp5Na2uF6dKAY#0s?OZ>X+#3RJXx$US)u9%p|Sg@#*%!tfRGBo&ljU!*j zdui+OFA2sD29EPBEz^<9FF8aN!}3nUf|sZ{jdUp@GN5#LkY%ZObom0Yj8dm(_mxa^ zfS%BIxj;Pd*z2j536ZOO7y45*jkn5J)@HPMBS`mzy{T6$c<=C_`I=V!S6te{yBVBWK(H zx{3R{Lm1e8YvJ!->F(%yafJOfV5$Dq=XGRI?-}G58LXM{hBZpTJ#2xO+oUN^RS3RI zDT~K`KHOvz3;NOeGrW5ICb_v;;Xu-NZY5aP_8^)OL*@mRIlGG2QJ{Pp5v~RZE^lp0&d{ z-3Si0#;Vbse3>bl#j3vL)VMTl|;3l&9GU;-aqmzQ1&-Ka|; zu7PHYK?tJ|-5A60n$NQzAiId{@|_%TsS_MVk|H4|P>!oel@?LGHvcFRZsrXYLh-)z z-F%8icvmdzKv;r%-;A8a$TxcqRx$144P6~`7I%Y!h}WoC1LFFsf>tXlQ8)Zjd_hI# zES_02m}qA1n1J^wf^2H*N|w!*B9Ug81JB->lIweWja9RszLj82bQP@_)b`xMRhI${ zIAr78JxB7wP0$uSt`_1z?kI>~K|Az{%k^bL(lSTVD!67vuM6dMB1L)Yllru}HviQ# zon@Xo#Dwt+^h&gvgxg{@B7-)NLEj>1SdB;mCJy4$mHuYP7_!~r00H2PM z-jm@Fn^#XTd;LCUBSti@1{3t+SF&2zxnR_z#;Jo3jJxt2k36xlmgH~P*M zOyiGv%5WFV>prcYK9IPUif9nS5VxiW-b!f0;K~7)PoyKYK!yAGlAyXMJGLJlhirzx z1RoTwKiI*SbDaq4=^~#_X313YzL5u9uZL3ImA*o1fBGT5QnOx{ua~|GnXij`bdrai z58O|spM0kZ0l9@c@~^T=j-IE)CeyOBHN#kBF@4gb3yKO?!sU(z+#Z>J-cu3fhMXgC zlC|^K*R?&*az$U@br_{ug0P!cX=R}QhT38@|`8{Zf@}nUko%$N|k4_4_=ean{o(tEJmhZ%cy!*Gbm>)T| ztGGW`(%rcL>)H9dF;U#V7@Qr`p}*mJdN_D6(qtSX7Uz(EibgRm5+JHb9A+}V%a62?X0o&1-W5wIlDs2QwRCsNI@kAcxi0?d z2?DYLZ;L{nVxSAfa|VgLKyf*&fA}FflMWIxN4na-P7HpMhT#-Hk#P&iUV>))^K6?Z zl*RjdTx_-7@s&Jz0Kh3kq9uv5_WQ2EO=8YEnjuyERQApe6qA&5NW{p?cWdOX>Erh- zQtA9*f6T!r|DkM?Lm`T;mbo*EMO}Zt$I^}wTq-c%2(Vkw08XrIRf55ZpSTqwuUM{U zCOP5epS`S@6ukwV6qU2HrR=Mbnne0Gss+-RRRD^rsGtL>1JaKpa->K0wNg+aREgJs z(T+U-#;m^POa3l1_dZ!TpftH>5&^6v2WI8LmB7J@3gvHM>?ZlQFCZ(ylsxwKdtkPz zkX7nF0l7lv?)t;&&FOW;!O~ahfZUeUht46=_|*lcBmJfp(p;a@m21a_Zf`wEU}AFchV=M&`l$oCR$im@5h>-{lN>Xm-33Ae*E02liCTD zx<6{tjAxd*yOyE1`o-!aJd#Z)RxsQymGI;|qjpIe&BV3*7JY|K$@^*f$&J8^84O#d zx2(6?g|8}I|NSuSkXz^pWuV0mP&yjxwhxT|i_p6}{+{DU&U*nTgaKdnK^~|!aUImL zXT`!lkkU%6kop=9Ux{zZKM{BqK^_Qi^7}L6>}{DHH$KTiXA4FTT~u^SRgxxO{eZGE zx__#PLc5*jJviOfY`s zb+a)m9LnVPaMF@4lZoH}iR;JjR#+*LKZm}CKxB}K@+Qmq+~JVc($f=j-R213R|EX7 z_uN5^CKbv~!pROb;?#8s{uBCFSf2dPo&DWu!0)%*i9C$>Lg5rdNPjf3`3RUP5?!+T zsZUs2N1_%ngG#eGylSEDK$~NQkm%Z#81vBYnopQc?7ThvGT8S_F=+KxA}*mk1}0e= zB^I_DYHJE_nX$D3bDM>y{S3UtO;dzF{OsSDAfSY8^3N^w4WJFdM- zfXwozM}0H=KU?*T=ba^qvoC#-C@WHo3MTv;JIgOynK+~vhhIHQA+FLACM^4|!1viy zH8>dJ--)NarML&8SaGr>kA=%inb^^lMTrbuOtew{lu6MKRd}L(TlUvzN3K{ao|Us5 zzIO9%f-B5FtFy6j_@-owc*p&59?ZDXiigsY6Ovxw1(+H|UiNB88-JZ#z~!VUS6!BL zCq{y+=DiQ`_fRsu?C^mQ;K!=mGCD0xPELMB+@0J%rXUAp=qk?z=fxA5QWnrq?ldAT z*M*?1s@ZU2+ab$P0fjdf3A2Tkh%HWA1)SwP4t57J%l})u`pnkCj~yxH)IGI>VHnm? zEK3t3HgJ4(M0+|*n*}sPZhriE@Eu}*v{xQ*=1hi;9(#ujlh5;A9BV53r4uDjjHNc7 z&kRN2`Rm`2ACi}S{_X3+#V?BXHa2b6a_dyCXU*5X5rCn@dwb{VI(2e2$+zY9J59a3 zTTS)<*ovNi^0=c2s+cf#YHJG=`3LD=&5VuJw)*@8Cod(n1NMTZAq9buLQMX78cWjy zBn@_=kc4Q`f{wLnhcHq{AU>tC>Gt=2Fl}2u21oI8+A;ni!4UpmNU8rFn$#mA(gMQ7 z0RRHX6*V1uDEuk8(!WX=d@F?i+!QAm0H}*9$M*LJ>~iZ1;{q;G{#W;ZdnWbnY?aE= zpGmsjesQ>)lksp!jwTWP)``~C|A+NQ6vfWgU#)!}5iODpkO(&Wn;#189d6GYIW1fRIwLdN>ArJzbB)Roh80>T zwMxVb-~mE=O&|q`J3lyzhxT`=Tzb=pqie4mb{F2eK|M^EJFZXaaEw~(*MnvPsCJk{ z=_G{aF%7?ZhJSt}rO;gc{g(wA*R}E}#r|%HXX0dz&qb5j)~8UDj^Uf%4W6-3FQGZh z`+xe1&rcTqIvlN6)YdE}hPz@5&pqM)RH3|B;8__}W&F}G?4C2NLCy4Az{$oWhGU*{ zyLJRmKclhtj~)EI2J;H-nI0wC&hhxWxsdoMTZT3!1zP9!OrurDB@*PjgAoj(J`ZH3n2Otc2roF{5Kz~&^xp71QAa5RS&T-PN(D(GK_Kn2* z%$cny2hB&>=23MgaNq%D>-8%Dh-=q>P5}NGjiWZb6SmQXxS7)%VH#)iiGO-pFeJ=a z90}S|&x@9^WgwC<{|~1-PR2)D=7Ccg+7U6F4vxWtw4$j;O zJOfT+(gqXs!Hi`AsT@k!EwDH?_B2aYF_-gG> zlG*5&4$7vO%w3EE$-zvX#-mm_vcpk0we+% z)pO-qeJP|cR@d@~5yhXcxAIlox>|H2Us67fwGp?y{mbJmO)N@k{Ec9kT-)@S-%rS( zP$HMImTZ^SUj);%9u)c_l`1WE{sNIYTn7RDGs&@DKdlfyDXZCHc zvLdcDVvpaMZhJ?{kIKr4s&8DSob_KTdHH5W`Gk%~R+<;M=^t^7Hgj^FN#@z@A1}yk z78~d>U|p_Eu)`*J4Oiw%@Q)~vmZ`uB%p_r0-$LVs>*?WW`fPKxuT`ok!wSaDJPi0@ zDzMkYpBjiiNcWim*!o~WlwX~$5eV+swm;-rEXhP@mbIle+Tyd=*J-@7ym&k7r?8CW z8gX?U8Zl5rChG4 z>Hs6}K%L$yAFkI7NU>pHnXZFyDfxqq?seys{&xI@zU(??$M{!o7ML(ReJdrMD-1?Z z5`tlF74H(U$Ghs>TUh~wO;K~|Z?!4YWQbj>_>*Wy%=Dc@9|w7$neYqG(x+al1?d@O zxf#M8%xAbvx|jBIf!@vTk(34nS6@+NuJbui7bfP!)33Xi0JmaHV<_c@6f{if&A}_M z9u64Y+Pk7XACH6r#AVJY!axqP?T||Fg?Gt_6*Rz!747xW3jaZSKR|=VsjcYkDvV?& z{dLiHH!cQ+!lF*X1&P5N|9ANicaaQ~SPqLq7aB~;q1g4B+^UKnL=8UJB{KAX+~s$9 zb~2ez`oRp&&NgDYbK0#(2Dhf7>nHrpK*boXL9?hgO3@-$LIc$y=#<{hZHOX$h`ksb z<-q@w%s}S~ID7ex z2r&(Pj6R3sgblmmP{d)vlHz?b)|XAYC7s)?8lJsP&be+H{Pcsc@)%liSrh^Yaj`L` zF{nUbH$)y5iShOnXA~k#^9iyHM(U6C!mTg;Bg{)4K9;#S_UesFHI5Gl6e>q_jUP3j zNPG5dkyD1U)hJ3pn0%Lh0AL_&EREwLkGRb~U`H5*O|qm`&I3&dxK@v*HrVEHJG-rx zgP6bTxFowaYEuV0!6gmvD7O~9WY1{qIJ}*@0>Hxz7xr~N2v&=Fq`e+ENcy*rr4kW_mqb*jMc;>1rdX*dou7mJ2TYG!@a@TQjW z4!r9bwkJ)&HugqFYLU~<`J8)$0X5sL*XiOWiT9;lXn}(*Jn_hhcyeQpdZ4yEsVHn^LjD}Iw80H8~C$s;2L}p z?Bio#84)jBt1i{FE?tbl-RgBg!empno7;xwFxdZ^n?yLc93GpO5bIh6llWu`>?LMo zO}K>1`O2P77{BR2TZ!Py9+0ISiwS)+&I)jo{<`tJk|u z*NEi~wgRT?M<&OmTya^85uLLK9haeH_E)Gbdg8E; zx`lRZG&l7)6j+jTkw6%74n*m&So8=f3J>-`Q}>7GQKgjIUIwYo=GK;$M-N9`D@(^Z z&&Q8V0;+?nnu+Wa_nGRMC*F+}HGdeds*Qt7G$_1?Amzuy(ly`0!nBB^@)e2E*)7%wK0Mvx{I7v+7K_E zJWFKT;H34#l)X0!0`(yWWIF+LFlPo3U`cP654H!A<;&{&m|(8t5M<#KY;t#&FLbDp zpCSPw-qEYo74UU1j9|9DX!Fy$O;FjmG+^9cN@cL{;Qmu<+5StzCuJxn>c*vwpX(*E zgq63O)7c=AYNMFGuI-*yj$gntau|Myf4Gd?F$_NhG+v~SgPnJ^$Sp^A0Y9=qNg7dF)G*~qP7Rt*bw^0c!nFhU93dWNCj(s>9slzXnB-ftb7RW& zy}G_J*Tatwc+siVZX#u4Oz80Z;+`XqyRHI_n4$bh8ZYFSmY-(XO~C}=BdD!tws841 zl5O8|CO+Dy)BBjKFdwfYGIs(LH4~7JQ_+fTel(-?q1Rv!EG=kRrlc@C*03AmkA|nx z)6wHyI}%{$#P)+}jxj9N2x3008-adYI!uRcZbzqrMN~;a(80Oe8%9twXoA#QM!fo3!(r3`` z-NZLsxAWmu>N2{|gZkoBmzlAkhbO0MbefLj)e+eh5!zpgf86Nh*4FhIg3$6W0qRf& z^Q%@}9HgTNV$?6g6O=)RW-(`1K|v^0i31v4!Z+ULsWcO7`e&Ed>Mgj>R zY~=n~Dbc_qj-2rhBSV})V8P%kEAe0%%ZOpBubt*7#GQk~(30-#KG89B6PmpD3kmDS z+=`;AUn~JOVMK)7v_cF68Ry$Y%V6AhqNFB7z`RBPQ>XzW_mPK!Vln-1 z{=ySdmI>u#xTA@p8-cXF68N*ezOf;ULpQxt@?zEk&~MLqk@_-mn%1Sc=w|6NY|n|A zl3AoMBmZ}B8MPB}K58!oACvpuo)*R(1k|9$l(LfNg)?{+18buehG$Gfda%dK=2AE@8Y!Gco7h*9(EP@{dvc?1< z!`JELVexD4(?Y6E-iDX!ixp+R9*Qp2!?rx`P8k{%G}tDu`ucQ;R2lVzcM~;zeuwYC z7V@7RrM!uRapH&n(BYN7t~an`x~Op&McEIo`gx;OkX_rzRAC%6*@X~S%c%QvQ%%IY zf~#`sI|)7vtikClh*$wo;drW0(8+;0YryAj6{+0b%S=ysdMO@_EM4=6sG?U(Y1{oX zS7B%|U6B=7j{n1HhO1s^cdAT#x1*SndRE6AtszqFk|BsB^9p4s1v7S=h34{8@SQj= zkZ^?rH7=HFN zLh54m!QRd;rlpcZp7Gv|D%cL6*hamb2umwTI+#a@*jwDJe!94m~M{OVJ8OIeXP$(i(>W4M~Ssm#}RK|56$ z5+q3LO7ea4G=?-Tu5Zxf45FbtFoYG-+1w|3i1s*@mdK*TPM@f*>}KeKTg;;?4)nM4 z4;G~*DmkfZ#vqHQZ*So`Y9BVh1iU&S`Lr?QBxbn-MdIze(C{Hh5HKQ zV~;JNLzSWj%P+&;A2XX0?ZEx{rX->E(rgWQsejCJ1erFa<}&z`C+9m9++&AEoN_g7 zr593qQA-F&f$Pk74Gl;AzqH0lHsBvT&W7s6V^3a&uJ}g$ue6gyOW>=?MRC!>#N#>I z+|FQ3vr|z9CZxrMJvhzW`AQLbjNu6ZJ*`PXBmDD#_cda7x(d)S{q7vGB#k?DK+nR! zVhx<~=`OHbJS6olmS#9^vLGp&%&MXvS@IfSj+2A6J#cBK%YwAhg}-Oyj3WS>k2)h` zaRV7t|07iJ+h!I^a(`SQttD#hH9*>O7XK#Ma!Xu4w29qa^^9U-Pz1{FPZpqHA0N?LOd3nM&&$T8MohmOlgNgZOc0 zm477D-QkR>C9Cup7`DWOr{+gLY*I4v zX8k~hCj=9=WSKrMB~V;@o$7EO6ZyTP9l3h*$vb017xVGW_OqILNk~)bnr(j{sTGsl;Yw$%Z`~g83>zwK!0K9dNy&#nOfWpEi zDpE&r;$Q3OX!&+ow8kltxktwQ^LFJi;+smM2ViTg@2Jar!L2$j^J8K2zFTt|P))g^ z^`|DoyZDQhxHxz8r?1thKxrS8cZGTE&((t>xPnHB+pGk?@o@_qWA~<)%p!&Cnv|sU zBz$~)Bw&}~(|&&Wi1MdgH7?%q#;{04(*VkWXo4esSg-ynxPlk=EF8fw?B;wv8m`Q3 zc~4WCiCHkZOrs0k{-w*epjSqc?q6(+~vCm-k?m zN0h;6jinxq#5my3?NI=;OC=i&>IgW;IN5%Jcj#qhCzG_Yet%!Dv$_&_JSwJhU&=fJ zyDjXi(C6J>7<^nrx-gt+czFTyD+aBUU;!~YpNt{({YhmV;W6B-cklefxDcTh zk-AZ^@7vZIC8yUc< zHeZmIzGToqr*~grq;vvSm&;O~X5gXDmex;oev_lFn}c5B(Muxmj~J!M6oF@vp<+ z+NaNJP6cGYHI0Yd=w+a^*;AJ_+q&{B;Er*<6v`2QFu2)qJ{5wh>;K&ViU_puEM6HE z?q);vDR+}KAs5MCwvxJ*$ygP$5PfO%^wCQZa&2DqO>w<}noVG{3|f!=n|%2~)+M&o z8K~(knJgC|nf(4uYVIH&Iq`u(MLCMKBwLk!GP9obTAih?k{#_ayjNumdXC=nP+e`< z>V80UfJ12X`HEnHm16hyH|aHL{N68=+F`M6xYf}@;$wv7PE@0mCvgPOc&3Da5+39# z5zo*%27vGry=9%0{_dtS9yU>rghwVPy5_4jPM0AI{S0K)-vBz8#C66CRuNA1RiHQ^ z`6N6mkw*_)pMo#`!qi3g9o91YL(&;Nq_-V^Yqyg3@S%N@YPT)|AI zp1HjL8wguUVh9bEoLR!{I>vy{8?b~av}j^V5YLrv64HhBP?k4^c5h0LXfPt_S|@tN z>jl`jKsF%$Pp^L$oHjaMkdXkTPPB9ef<9!8IqR_SLK zNnwPru?|D_c({np^J30*x&E9n;3}1Nd>IGc>GG&mVR>(kql6e5<91xl(1CAx}rCdY4Z^ng<+3mZw*T<_}i_QpZ4V91xm zf|_+{W$1Z&w5+qF1TK0IOt`pw%F|y0lc#-rw6Kc4CeQ+tgFyI|tAf{y5eISKeaqwmbg`5yNAc(1=+eWC zU>~Xb88hVki0$e^qxtSCeyxB9bn~*4*um@1KB2#l1$p>ls!*Qf4*kXHoLuh48zn6( zW!yR&Pev6rWN74kp$)q8@COoKx+~z?BMw00#*t)X;^%^}6p;XFQlvL2cDn$CZ+O6o@p-}U^O}-TQnFo;G5e1SFO#zN*^BTn$%lYzdq`_S=LUK>QZya0H_JA0PFNMHgW;Z zc0U|x@3B_(q6Q!=8o6^^FXppr2b`8&M)y8#A#G= z_?#DoFHrh6??#_X2i+?w`Xc2hs0j-KE^||)>~Zt`rTnQ$EBCQNX$>5&sfaAVjLTr^ ztzsJw2~zW7Fp9D=j9T%OF*jd^qVf2YA7jk*7Z1-Z61Q4@J-eTw2Wa_dfJ+iZFeH%X zxOiqCJ^-|HFf0vgoCifZT=JhhgedD;y?F-?m~v1S8XSg$uuD3c#Gky{tmmo0S)H%{ za8{eg>Q6DNHXdnjjdskmcLUB2X*Y{1u7t=pfE~>NKRW%qFN6a(ut5IB-QY{t^S1{& zI+&D8+{Li-W`C)#kh56xxt}37mDI;l_8MW~>3#7{$wDx{BSa`luujZSnzND3Y^jjc ze`5jdkE}7iczBtpS7y3}Z>vqz0kVXOzQ9A_wc~ijQ!W2?m zwSKl{@C%t5LWN=K(fnGj44K`uv+>+Q16!4&HcR%~dPfj)1q^vjR4zNtd0jncAb@tOD14xjuog)~0?>*Xw(}w$O5QFMeumx4!A%Vc2O$|{GZ0Yh7B~d&3?kSl0{D2bqb5DsK|NNAv zvueu-7)0a(eH}Sf31VcYPS{j$ZKxzPF%N;+4nI)bcbJq>n($S1UC~#LD&uWbz(%b`g8BKf9eN#`t}YpiN!G9 zapi;SQ+ZqL>JYBW)0XxcnSi=XiPc$mlu}z9&6_lXQ|>3tD+X_OHtoQq}2YG7ro%uV`(CGy$vv z;fz3mUPZsFf0tCLKkdA==cPRTKH-BAzQ3e$x&Wd#kC9gtTfVx&boCbaI+L`cZ>9<> z_x3(U+*R4S0y)VU!dRXlNW6G@i^j%ZpzISIx=3)2mTrw@KV2;5WCLWV} zWfsIbXIM7sV*T&i@~pDl2StR-`m-8(Um&Q@4lLU^bp{lMx|6~uP^6&@k3$pzGbTK_ zOeLFsqB_`Hf}bZ`t)+JS^NiH?giOXa_yga_eO+GMyXZFZ%vCF>dp}?x8ofmj^3DH?*Nz#)f|w=?kC==H^-T)X3~(?{VU8VG22I z-jQt3!Wc=evbG;ZCZ0US?k^TpgWX@SSdvO6Pv!{aaVXVRH^_oE4x)G9+M9=A*p$6l zq4!xA3Ad-2BIKOqd(GwT>RETN;w%O)5zFuE&5oVL4*-Vxn93kQy{JqJG#MTCaiQ)~ z=K2%{j|Xs|t^KK!4TA%3!fk=nf(^fiA6J_hZXC|fbpNPZf>=>Qlh5h=dcF;>`F*!X zS(ra?XIsQO5+=vb^=LZOm}<8>6eQmE@C}{l4IM!zR&o&02`rIh=dnlybRQqm-thvF zR1lH8h~s@8_rne2)B;@9{v9NY;Ft)Tfdv{z0cQ}ytdOK@h&Gv%1I20wrDx{nE@L6C?l z+IGo6_c)l__na3e5}@w_NJ53nM@q+-+^`CzMlsLDEJP_{rT`M?#ymePF)H$H`9$nUp#_Y9{Pw zNwUIXeua}?%daKOdBc&NUG2{mW3e=y;td#V|*r+_Uy1u3Yy=hTj_!IW~JLNSyZ z3K|+3)*>#k`j6r2PrVPy1R;O9kl3xcpWH7cI>4lJ7_> zQptbZTm$3W-<=OtJQKX$E(6D2#l&7w`NQx6KJ6m7gBP5SzCNF6+Ps&xFNGC}O7y&W zYwZ`~553ORH0pJyGa?dt1b7hC`OX(_7^3%bj*HS}ceH*N`>}`YV;dD%%Al&B*(MO# z_T7N{A6dgjI9O0|Q(7S3(48oP?sMw8QxnQNByDo?J|1g3;O5L6uaoz$ZKpO5H#c_x zfcg0X9QgX88gN~Z^ZcsXDOgATA1CkS&FUad82}8S0FaHKQqg_-5&Y6*3%^e$_GJ+8 z`~|y|;D5-i|Hod}vHBA~%%QFDjcFzUKxC{Hpd*Lz|ItT#R8R-Bon||#ez7I``{thP zUNtRg>n+#hs6ZLppq3k-LMx0%7#{%(n@~)rU?PDY((?H;5DvI4HEW}#jGHg?onwcl zBTdPwo}*7Wi+_mbX*k_G&wl^QDgEM8R*Jfs)htXy{Xy|Bcj(h<<@NQyoo&H8NZGp_ zU0&XWtt4nLaV&8w;KICR&TJYBs0XTkKcCPB0%XfLH)%H;KkvwagV#DO*FVs=%A)y$UNZ`a61$CX}L6W3EFO zbu$d~_`75oCcfh%~-q7YsjC?|2EB=qN2@* zr^)vwxP{cv&(%^O&vSPe3R#;l{xx}8^`DM0gS393Hl6>{Z+DB$z3jj24V%ZBTCQAt zAsB=Hr|t5Rqi2led{qf$UJX3O$1jmKGM~??@@ji05#clCE>o%R4sDk2wFg=C#=Qgy0kQzB{o^zwio_S0LUdb+94Q>MMW;{_WHTCYRxZ+3HM7eVroZB=tAuYt+heV ze?EbA2zAUZ5a>FGm6c%KDbZC-0)&k0%ghTUvUc=8AcnOyB})P!kZZvQL>I`Fu!H=g z-;5-uc(4wHx2EgtqQ3g<7*xobAm<|HQqUXnF!Al;rKRjJUzY&m(!t7bWw$3`r@4{K z%lWKg_Xh6i9B1Xr^783Z-P{o!(Kf`PPvt!R1fU;ZEGs;CaN~xm_PEf`U>#byvix9K z<%J&f2DxW*GiXy1jIa*DRKt6xYM4u*@xdSzo%HV7m2U?Kv{>SR0o#PMs|ec;p< z^Z@~~Qg+M-gv@MLvR_dJ`i_}EcuVBU1_DZIY#_zGlge6!Fw$N*Z&jK^>!L_(>ngbj zSY!{7$1T_ZA;f(^K7R@Lc|=C)Orr!dEB{d^+d+UXJT&Cf;(L8w06-IuTD8Of!65jK)}_1iCOIep z`t;W>MXmeX&uvO$^N#q&MgtP`6aDKWwhKZ^-iMaED|ZLwp8MS&;cUO?2I1`>w6u>| z3%hf4fHnW~hT%Q+=2(oL#eUo6Y!FPB8z95Zdw_htHP}0Tv^;jS6!#OZFPPB8{<4yM z9SCtGi3pi{`(bGhOCa=d6J(PN!lI%WxRy>?7c{xYAS^!@nRaz1`H)Z<2c@jLbF1?(;zhgwVr1%=Pf|0*`mEof?+Zma|*{jSmLFx7Q3o zDSJVAeSp8u(o(1s+f80$G+mb4(B4Pr?8 zP`{e;;zbZZNZT7E0G(_$9oNz_aejFvS>szbjBUaZhdSeh`=YHKk(Y z^9P{U+O4`x?V5o9;f(X{5%`(~>fvNdg zUXZAh1x+BN_b~J2TtkFsE-%?ZAT$hBGIM={SPYtg^loOJ&jS`Yo0$tkVlicf{cN~~ znXj7wgjjHnP|Sl9>;q|Xk4NtRXL2W040iiG=V~X-AQ-Dg2C|G9(H(XU0dP1byl`%_UG1gzcA&km2V&&<`bN zsgva#8Fb80&WN$`l z4X2@y^G!Mc2KcxV52o6yBR_4b04#QO31yS2nk|Sv$BM_FgV`o0V@rba6+z zds2)o9vW)0+qB62|5@%$?YRKzdJ7yN;ACU^+KxhQ&22e1Iz&qh>clg+ao+%)>U;)g>3jm}aEkGZa0Welq#B=V7+|vLL=K?PFB47NAMqFQV04U}b zzM2bCBDB|x+ zOGBfh#jxG#hYn^T_iD};Tc**N-22!A9Z~a)1f5KzLx^qb>LYl#(gs;Lzt<#IS?pCB zVfH{)qW;#dv66*-IqqaT;sjVs*mhjD^&5GwJ$Pf}9+UertQXb2EGT9D(5|beJ<7QRvm5u5*QMfeZE9EH^ zavEfUAeXCRavzzu10KegQSIHunU>1sWx{i*OrDtB}aRa z##xYQTjfp)o8_ClayO8`K<^mA(TBfF;eF*^aj+0F$;iL&+?Y{G zU*JHJ`GsKV7#-U7m`3-@g(HY^WaZ(7UyIzuR7iq~;mphUd>Mjup2xFX##78*?5er0 zL(65Zax*spxz`I5(5qh1^>~Bs!gK+`dl?sXV31opvH}C%h%}iOc=N z)TSW=-UE_hY;~z>_V{!}?a{wseri}C)E`^+zT1KA{ZMNj7R9;nQ^v* zAlS}{kmAGimXACT_1a@{kJgY`le^N2<=mrX#Aa`ZjaNS(!8*B(mwE3($O^))8D_3c z1CVo+`Dn-nKTnv~(mCeI1?Kxco96Zp2f=sQ4aokPAt$JI3TW;oz|ni&47h{2NaKhJ zXyPhlaZya}p_#JHa+}@cyltsX_uJ zlpsaI?Nf<;aJU^XtoIrSM4yh_38qlzWclRaa7#jULt0z8YK}Rgt@T%+Gn)wRng$7* zUjC-6ORtVJQJK*&APj+|tb8~b31p}U3zvX@8F7+R7?-iUMkvH^P>+=wO zmEK~&SwGfi^QM7-D!)-W-5MX}=@=`D9;*DYoGb~b;4cw96!|SG9vMJ!S4m3+2=10$ zqHCA83M7D|CHb!C5sbX;F)!i8b0zI^WA?(_rPw_8GuKpN@}7;igxm!WuU%7T#Q!{t z1SD5cniU`b4tH^e3^D*2#c37*I%_Jhf7OFdbw0>ZLagWzKBu8#h}E(vGgfL?gU$FLt$Z| zcTd$DhQjm@KW!)lL6{;}8gMe0hW_&IfH(R+v7u%1G~n z0a?eYB>>wjclF~rh(xaCH}6nYLVQ3(`MWPYpMBH2fUVx37?kwyLh?b_vu|MERIn)> z1QZ+tLY=B70^y0Xl>r!l!tRN~p$rfe)|`*dml><90Q$Pq0DGe%>-w6R!>2MK)>goH zH~>iDlc;EJ@*bR+$_EI=&F9aD#g?(XZky$9QD@0nS~(L0Im1dI7_1U(8B#1#%uoXh zvg%;9B8&578Gy~IMiCARqzZb?FCQ^-&(490!DNBJVKI<}#Fs0B07)z@I#_G~Oco13 z6o|Cwbjl6`AgNtqhzy%0E;fQ}wIFZU2@)gJes^|`O{X?(?{c{!QjqR;yHwn6d&=R$ z_mcY--~EOSziLqw==SmTvXb$o0OET$7QG;Jv1LsxF%rkodS~)h6ekF_UG6`}>p;$4 zSlwFg8>T|i%Dq>Ka!)K%Vt7Nb5$(e|J_%@ZE5n8;vfdHhO%mP=>9d^?43|RkG(;wwGEg09 z9gL(nItu{=Mk14A8OpHwU6IM0c)BySq&ZTbJ75IWg({mnoXCr`W=B2;szQ;;E<5P0 z>SFnqS!g5)h7I8kHk9t94O;}2#D2VCM8Y%C>m#;b?%5E0&RH-H{k@}f@3;%5 ziCaUyj&aTd^7-^)KlfC?eg61aUv40qk9Acbhl5>}TncpX@xeuF{5Q&DVH=3xJX{U> z$dNlN4cQquH{1q$aB$FD-8|`rrLquGLomB34`YoTxn09KDE@A;qb1#1z6S^lG_-UL zge_SMOg`v!*N zOoe+lMeaKR0g^)k=uk+j1*|uHoGtC*6jN*3IV`{<$6mcm`r!VINRZEp*n>g$OJfhDv(QXD% z%xyq`O8^vb3KqE#aY!_qZ4hqftdAvk+r^9LPJZ{@IWy2!TUfNO9?+M!I)S=@CfI_C z+=WOXd+P}(E4RTI$p`dtR#3x`Hv`jzt21u{D?qf0+g7+h>HWwG5Ch zj)-R7@LOL6q4IEOB&0IZ-yiDl_3pIXfBnixGACNz1wu8Ca?f^`WwE?erpDaB@@}!Z zLiiQVtcnlWTb=zvUHxcy-`~R$q5qqo%zq3F*g3YopxQn(KXHF)p#^$ArB8FF9ggU9 zqs<^f0cQlb3p!9^w`3pfhb$wH13(VRxK6L zHhiG(!L1|rgYBgq&AbS-EsXNg(q5ji((z*;LN)9Ctc<2p;}!a$QV^j|k-Lu>0^WCk@8?3Dq6<~dF6x6!C){FdfPd66zK;=k=rtn zo|tn_x?B2_2to=otCRV+6O;aiFgB^mzH$NpU3sVys|tBTZLF?ClS$1*q`Y})RtE2b=B-nIyHPJPm#-ZagK9hFZd?XB;jlH?G!R`jg*jcvDxx4<} zXoZsp+v);=ls1xvIw2D}xFTOID?-7uR{fAKF85N-EeZsRi(P8uPS|L7_I%t6sydK? zNZA#fECYnx)z^?y9+SIz2hU)3eX*r5=tkRG1S3&;7%fRj|T3vdTIBi zOYu8YA4XftAT{fL!2saGr5n}?#34x4GmSxCfB61D8Yoc_wB={+sZIaTV6k&-C`k(| zB?*MwLfUHA%_rZ$5O?>@y9YK&?oZ!7`E0F=aQpEc>>yjumXQf#+y&c0GXzB$a7-Lb z2W-8kO-}9vV7}Z8^Jq-&Re+bX6loSqo8=AyzVv?*Qy~e?a937OAE2-FNr1V$luU8C zSB0|xbj9SZ=G@)GT>#9@9WZT>JAB!CKpLrZXH1?0&{taredkF)A1Q+%#esL1yVci( ziXD(U(Ql8StuDurVCyYG(q6(N>;Poe-M9gT3s(q;-k~ZoG_Tc`ApqANM#~R8yk@mx z9U2)rXM@1qJd*-A^3p-j*C}0@bpbsLAXL=lIRVH;ZbzUfe(Pk79Y8QFLu1DvgvVu2 z;xrkGN8*ov_$nrEA{kHrdlNDlh{ynCVZhhZ|Na^j?IL{h-|qn!qPq@!`wa+UizwCy zF?Y861G(bi%Ho?g_!+LL{qdl189 zgP?La+3Nfx6@=^UZ=R1}!0kg{?T^|R3QwnRZW@5AS7BIheH{3(GoGq>6R#~=$@02ixyct<&N7M$%o%IpPvoB_fH zh(78as*_<+ux+^aJN%>34>*U9QAH%Db2vO)EyJ>J4|I_Z5a1mSHcyU(osiEB)a03l zPXdmTp62059$@RKhK!=dzL?;uhaWzX z2O@W~QTnaw1xgx;;Bcm<1BMOi)2BKeAoVDwrrBe|GgIwx(YxAgNJ?Z+&Azm4x4Wg{ z+6oY>_lvEoLXukX{Rjj{mS^p9!r9t$U8bC&@%GwU6kX1PYt#c7xieMBA6;}-)%NO< zb}$@B4J{QSqxoz@l^bznn|X0&PqP2S{iGq3_gY#?s`Ws9wF3leSMJ)LyN90s=Seh% zfA-|b?Prf};`ZGG&z{`5YIyeK?rngcJ_GJN`}*#aXP*II-?<5hA* zZNuGf4&1#Xx(7PI9{sD zEiBT32vSj@$EXf)N6*KiEHhy3tMC(rSb!S>{Q4!>Sd>q0GQJ#qc-1>?^jx~ zF&*SI0}v~JzCJ@yp_E#uC!-w%sVytd1!b6C6`TbM1Ty*qR#U12u;*pDZAJ?~nE@xD zOZE3<65b)_E?I0bERM{>zfbP;dWZdR>|!<%)|ZJ_;xa3_TLRi@nMco2Vx%FTTwv~L z0}-kk#oi`TQ(MeOOC?aOGGTo(%7ozM;~ds)_gN^p9zIl*P4`y6_^q_e^6nJ~R_0z* zB;G$ZvRk^niqhMk_$Uu+B0tAR!LH-8+8YCnw|k zku(slPYm#JKA!@gTK_Shn_}g`u735E{LlX=vH54;KAN8X&p)Scis=2T{ri7-a`(}p zM?c-fXNUd?{OhNGP49p7{b#uO!(ABeKH7ii(PP7%NB=%F{mp0lzxvk?U;Xu)L(_+* zr|;nI^uK?aeumrsdbEFf`YFT%{`sFDo^0QTbx=IITL^9S(cH9QNuOL*$Tv_iob^_} z>(D#QBz&)I1q~kVO`!4o=f6kp1l#KegO6s@=FY^gH2?C#`ECcy6ZgG&4O|9FEQ@=p zuYP>q47e8-ynW5w0;IPO73Rf0ijUCG$GyoYlO3803A}hU_W*=zbGw({a6$E6!UfeM z4=>$2q4xb^1+BkdGXY#50Wc!O-PsBA(KWf}%{QK`vI0`1*5mc=YHoKq_VsU=IBD^A zWj8iKW9J~Wa*xr`!aXo@DS+(JSP5p~-aEMS=ow_(`QiHmSMN?AQsn;lz||-F|9w+_ z`sgnH^zUzh$J0-)e*L|O>TmA9i`&!RUj5S@VeoIJ?|f#s`mgCHU+X@5y#E{dz~g^k z#m(uvBwu|z{q#?_A5VWH9&qUCRo(CZu#WW=DF6}|KqfL^y=#4(V7-^fFVA3{iVkG6Q9!%AF(#}-R}}C9_vb=tXls&-j?7%tB1yI{ACzOdlszmg4>;XEs53q_)0E1U)VsPFHBJ^?P zL4XfUHFB&wh9YVeluwpMLrtpNW6?>R+EhX#0+$ZY6sED{w&Z z_*3BOPme{*@by&yPxgzXju?e+eh>>G_V01?(UYC|m_1!*^qV*d>$(4ry>sn};tJ#V zzt1vrV7RW-kYxjf5UE@iFiHssT1tzxTngc$CJT`&7*?f?qJkJu8@EIWCSt-uP!nTx z6JC%F2_Xa%8c0ldh4lrL@G1NRJ#%J3*sQHspdCEFWIc19jGHhs{5bQ>Gv@@^BQB@l zfBze>X}J>|h1KDp4WT`3ZwUJi1bdIMhU#HJ&8TF*o~=F!L1G+EmD!Q>+y)COdhF|N z11=}=Pzp}4WNb*?ChnNseY%Y|+6*m7KN^r3ryH_x*Vfi}+P zxUNTV=?R-hm-rO#3yYw2Yjxa-pc6Mh$T(r43mM!VsjwiurvRLk?9#_!IFYZ|thOix z)}nQCw-iJ`RdDpzY?Gc4Q$V=iqkzPaXFp3sI!jv3%FKHQ1_vgb4d{D00V0fuw2Tj8 zh^_}!Rmgo3ghA)DkbB?ctND@HTky-hWg4UbzLB|kXN%8}`^>8WsG8>WV&K&*RJn$# zc`k1XK>HZE6Zzd<6aor-%iclfixeR0p&|Egfh<;${QM%T(BLP(Oz|h^Tl29I;zmzG zBol@GscgRyi|#B^#MBZY;bxV)hV6i`s1M!miOJes_#2+-yL9*WclWNJ5|WVlI2KvW z5eTcQw#7V7M0{rkpj>D!Kw^#^h^@@Tk>=w7(o@NhI|-nisEq+A>&#Jeo)B}O=iF5g z99-^^m2GsY*#;8v*$yYCh5LAHegYc7Ga4XwEl}#E!%*nJCdjI~2RB;-!KwVd3r zlD>!?WVyuUe%2^@{Z7QRHkJ$($Jr4$%qsVQ{MnWc-S16(spIat_I_T7T^?;5xPuUS zy&N%B%@?kg-Z^x-Boa#6<0I+0C08?Xo24ZlsIH1fOkwST?3~JC#8ubh9s7D}4y6Bf zvtE`$ZX<+~mGL04Rvwz39%Q#c(9y;_MrLMaJS`I{dA%xt@s59%yLJoem}hkQo!nIn zj*j=K@O1d)KKoWz`q=!WA$K`)H>qzvO74W1jBuzZ5N;1l!n1OqfR(?K`*-4Q(gNGv z@3CRzPCy1b04-p_qKEp_agpq~D47v*2iHs%2yX-Zb!jdnggg2 zA;&{Pjyt|UU4BVf%~?gBk8Om|&N8^a&Rq`$k}7|=8R5^$G&tt~ztX2iX8>PIi(l@8 zhTP{gb$%T1POQq^>49&47F_J({G;#lry+L_(BN(Hp<#3cdiDGmw2@aH42(*-BlZ)M z`<}8>3BlYhYC>ucl@>MDTHncCtnWy$UH^Q8+zEKjG7ylmkLkRt+a#1V`O`AS1VimA zNc@rDzE9r`2YV}7S?CrimtOafUIqE$(z9;8aTz_=u;}#*x$~`sp@2gkT~(dAu1kQ; z3Rnn$eNUV}7{AA^w-%LPZNAgl*?G0G2_S2vYKps3^FqjHH>-+U#bvKlvr_VhJ#Rfw zNrmt2G#dWsn4ECF9QE;pxi0TfA?`^{6{V0~5EuBg2oE|t=H{Hv0gSzLPWnFMmM8Ri zJI1H~bawdAH{UYJzoX-iIq&HBXZnjyQtpYr?}iI}6Kkl36kbRz3g&ieRpX)3(%eFp z4O?p)EJ)yT|C;UNTUJ&PAb0L;3si0W|Ie#sM|H2*YO57kaDat~<@rmac*lN+m6erU zfB3XIW;Yz}t%A_S6Ei~Y55z*LLvX#|jT=v17`;ba>26oIA$O6K2{@ZSJZ{_XNT@4L z#>OQdw4kf+S&k-gjwVOQDSvkW!tI>Q{Mu9d0I0aUI%$>$&)gU^Xy_ZCCJc^yy&m7h zAbb-8K*QXOKCjG(rjC1n=@DomG&nH}vFG>z2m^E88Ewu3(x)Quz8smE8-xe~vlA~T zNBV%lN#94vJv25y9$VDJqEq zeq`SwoLz|qF0=Lxa_7#k-}t1FV6C}WuzCk@5U^HtMgiH(sGC=E(u&*D^3PNs$us-W z-cks|3%%V!?r<9wkwd)cT5F!^^SHaZ4Y#il0wCDaPuF&KmQ*K#5O7NVVP!*2o`@a6 zN)ViEjRXaKSl_3LjSqODf9saNr(%sh9y9pG_%@@#=B25@`b|7`6wAKwZ4&|^@_Pr& zjmBM@@!_ve5|VQr2%?F4G1La--i^c!mX=+9lgk~4Syk4di;e4Z?sjBXKD}`)eSdm9 z?8R9MG!T?rmd`_nlL-lDdiKX2VqXK9KR+zjgE0_9uzjEW%_3-^ErQ|r?*Zlf8}(=y8o zujRorH|Dvyq8&s-0H*$FbWC)JG{8~blyl4uLQtZjAo;ftXmQEL-$NY&;fe7;wTm}K z?}0sKWf&5PM6$;J?cPB09tenvURvn&mqPNvI^3(Jkju-jtuA@|B@)S2Lpq;Kj_$vL zp!F{;FD$BJ0!TNW@TPl6`|78KH*S~w+bWSrB&J0}mwPE?abbCRx%-;CcX@eXIpy`z z@Fhqj5{bmT5ID;4;?l~SH~h1*vh?~Zk{L0HL?SUeM60buB{Er&g+wBeNZt>Te2OiR zNF)-8L?V$${$dXfZ2$;?0DxUyeNccw5d{11$O2ddp)CLa00000000000000000000 z000000012K0dT7n0KB8j48WbnA6qF}pA}Yna7+UL3<3a*?A$d2yYTOk8wrHAz(o09 g0IOV07*qoM6N<$f<+ZowEzGB literal 0 HcmV?d00001 diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 7ca01d56da0..08169dcb7fd 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -28,11 +28,11 @@ Indirect membership can be inherited, shared, or inherited shared. | Membership type | Membership process | | --------------------------------------------- | ------------------ | -| [Direct](#add-users-to-a-project) | The user is added directly to the current group or project. | -| [Indirect](#indirect-membership) | The user is not added directly to the current group or project. Instead, the user becomes a member by inheriting from a parent group, or inviting the current group or project to another group. | -| [Inherited](#inherited-membership) | The user is a member of a parent group that contains the current group or project. | -| [Shared](share_project_with_groups.md) | The user is a member of a group or project invited to the current group or project or one of its ancestors. | -| [Inherited shared](../../group/manage.md#share-a-group-with-another-group) | The user is a member of a parent of a group or project invited to the current group or project. | +| Direct | The user is added directly to the current group or project. | +| Inherited | The user is a member of a parent group that contains the current group or project. | +| [Shared](share_project_with_groups.md) | The user is a member of a group invited to the current group or project. | +| [Inherited shared](../../group/manage.md#share-a-group-with-another-group) | The user is a member of a group invited to an ancestor of the current group or project. | +| Indirect | An umbrella term for inherited, shared, or inherited shared members. | ```mermaid %%{init: { "fontFamily": "GitLab Sans" }}%% @@ -62,6 +62,16 @@ flowchart RL G-->|Group C invited to Project X|E ``` +![Project members page](img/project_members_v17_4.png) + +In the above example: + +- **Administrator** is an inherited member from the **demo** group. +- **User 0** is an inherited member from the **demo** group. +- **User 1** is a shared member from the **Acme** group that is invited to this project. +- **User 2** is an inherited shared member from the **Toolbox** group that is invited to the **demo** group. +- **User 3** is a direct member added to this project. + ## Add users to a project > - Expiring access email notification [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12704) in GitLab 16.2. @@ -115,51 +125,12 @@ role for the group. For example, the maximum role you can set is: The Owner [role](../../permissions.md#project-members-permissions) can be added for the group only. -## Inherited membership - -When your project belongs to a group, project members inherit their role -from the group. - -![Project members page](img/project_members_v14_4.png) - -In this example: - -- Three members have access to the project. -- **User 0** is a Reporter and has inherited their role in the project from the **demo** group, - which contains the project. -- **User 1** has been added directly to the project. In the **Source** column, they are listed - as a **Direct member**. -- **Administrator** is the [Owner](../../permissions.md) and member of all groups. - They have inherited their role in the project from the **demo** group. +## Updating expiration and role If a user is: -- A direct member of a project, the **Expiration** and **Max role** fields can be updated directly on the project. -- An inherited member from a parent group, the **Expiration** and **Max role** fields must be updated on the parent group that the member originates from. - -## Indirect membership - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/444476) in GitLab 16.10 [with a flag](../../feature_flags.md) named `webui_members_inherited_users`. Disabled by default. -> - Feature flag `webui_members_inherited_users` was [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/219230) in GitLab 17.0. - -FLAG: -On self-managed GitLab, by default this feature is available. To hide the feature per user, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `webui_members_inherited_users`. -On GitLab.com and GitLab Dedicated, this feature is available. - -If your project belongs to a group, the users gain membership to the project through either inheritance from a parent group or through sharing the project or the project's parent group with another group. - -![Project members page](img/project_members_v16_10.png) - -In this example: - -- Three members have access to the project. -- **User 0** and **User 1** have the Guest role in the project. They have indirect membership through **Twitter** group, which contains the project. -- **Administrator** is the [Owner](../../permissions.md) of the group. - -If a user is: - -- A direct member of a project, the **Expiration** and **Max role** fields can be updated directly in the project. -- An indirect member from a parent group or shared group, the **Expiration** and **Max role** fields must be updated in the group that the member originates from. +- A direct member of a project, the **Expiration** and **Role** fields can be updated directly on the project. +- An inherited, shared, or inherited shared member, the **Expiration** and **Role** fields must be updated on the group that the member originates from. ## Add groups to a project @@ -291,11 +262,11 @@ You can filter and sort members in a project. 1. In the **Filter members** box, select `Membership` `=` `Direct`. 1. Press Enter. -### Display inherited members +### Display indirect members 1. On the left sidebar, select **Search or go to** and find your project. 1. Select **Manage > Members**. -1. In the **Filter members** box, select `Membership` `=` `Inherited`. +1. In the **Filter members** box, select `Membership` `=` `Indirect`. 1. Press Enter. ### Search for members in a project @@ -313,7 +284,7 @@ You can sort members in ascending or descending order by: - **Account** name - **Access granted** date -- **Max role** the members have in the group +- **Role** the members have in the project - **User created** date - **Last activity** date - **Last sign-in** date diff --git a/lib/api/concerns/packages/nuget/private_endpoints.rb b/lib/api/concerns/packages/nuget/private_endpoints.rb index 984cce3ff5d..9d34a7cb73d 100644 --- a/lib/api/concerns/packages/nuget/private_endpoints.rb +++ b/lib/api/concerns/packages/nuget/private_endpoints.rb @@ -90,21 +90,13 @@ module API tags %w[nuget_packages] end get format: :json, urgency: :low do - search_options = { - include_prerelease_versions: params[:prerelease], - per_page: params[:take], - padding: params[:skip] - } - - results = search_packages(params[:q], search_options) - track_package_event( 'search_package', :nuget, **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages') ) - present ::Packages::Nuget::SearchResultsPresenter.new(results), + present ::Packages::Nuget::SearchResultsPresenter.new(search_packages), with: ::API::Entities::Nuget::SearchResults end end diff --git a/lib/api/helpers/packages/nuget.rb b/lib/api/helpers/packages/nuget.rb index 24940efea69..749b1f6b58d 100644 --- a/lib/api/helpers/packages/nuget.rb +++ b/lib/api/helpers/packages/nuget.rb @@ -30,7 +30,13 @@ module API ) end - def search_packages(_search_term, search_options) + def search_packages + search_options = { + include_prerelease_versions: params[:prerelease], + per_page: params[:take], + padding: params[:skip] + } + ::Packages::Nuget::SearchService .new(current_user, project_or_group, params[:q], search_options) .execute diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb index 87c353220b5..8c73e42f078 100644 --- a/lib/api/nuget_group_packages.rb +++ b/lib/api/nuget_group_packages.rb @@ -42,12 +42,8 @@ module API project_or_group_without_auth.package_settings.nuget_symbol_server_enabled end - def require_authenticated! - unauthorized! unless current_user - end - def snowplow_gitlab_standard_context - { namespace: find_authorized_group! } + { namespace: project_or_group } end def snowplow_gitlab_standard_context_without_auth @@ -63,8 +59,7 @@ module API end def allow_anyone_to_pull_public_packages? - options[:path].first.in?(%w[index *package_version]) && - ::Feature.enabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, project_or_group_without_auth) + ::Feature.enabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, project_or_group_without_auth) end end @@ -86,7 +81,7 @@ module API namespace '/nuget' do after_validation do # This API can't be accessed anonymously - require_authenticated! + authenticate! end include ::API::Concerns::Packages::Nuget::PrivateEndpoints diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3a4e0317d1f..133d01c6cdd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19304,9 +19304,6 @@ msgstr "" msgid "Dimension" msgstr "" -msgid "Direct member" -msgstr "" - msgid "Direct members" msgstr "" @@ -28315,9 +28312,6 @@ msgstr "" msgid "Indexing status" msgstr "" -msgid "Indirect" -msgstr "" - msgid "Inform users without uploaded SSH keys that they can't push over SSH until one is added" msgstr "" @@ -28363,9 +28357,6 @@ msgstr "" msgid "InfrastructureRegistry|You have no Terraform modules in your project" msgstr "" -msgid "Inherited" -msgstr "" - msgid "Inherited:" msgstr "" @@ -32980,9 +32971,6 @@ msgstr[1] "" msgid "Membership" msgstr "" -msgid "Members|%{group} by %{createdBy}" -msgstr "" - msgid "Members|%{time} by %{user}" msgstr "" @@ -33052,6 +33040,9 @@ msgstr "" msgid "Members|Direct" msgstr "" +msgid "Members|Direct member" +msgstr "" + msgid "Members|Direct member by %{createdBy}" msgstr "" @@ -33085,6 +33076,12 @@ msgstr "" msgid "Members|Inherited" msgstr "" +msgid "Members|Inherited from %{group}" +msgstr "" + +msgid "Members|Invited group %{group}" +msgstr "" + msgid "Members|LDAP override enabled." msgstr "" @@ -37411,6 +37408,12 @@ msgstr "" msgid "Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder." msgstr "" +msgid "Organization|Accessible by any signed in user except external users." +msgstr "" + +msgid "Organization|Accessible without any authentication." +msgstr "" + msgid "Organization|An error occurred changing your organization URL. Please try again." msgstr "" @@ -37453,6 +37456,9 @@ msgstr "" msgid "Organization|Changing an organization's URL can have unintended side effects." msgstr "" +msgid "Organization|Choose organization visibility level." +msgstr "" + msgid "Organization|Choose what organization you want to see by default." msgstr "" @@ -37474,6 +37480,12 @@ msgstr "" msgid "Organization|Home organization" msgstr "" +msgid "Organization|Internal - The organization can be accessed by any signed in user except external users." +msgstr "" + +msgid "Organization|Learn more about visibility levels" +msgstr "" + msgid "Organization|Manage" msgstr "" @@ -37489,6 +37501,9 @@ msgstr "" msgid "Organization|No organizations available to switch to." msgstr "" +msgid "Organization|Only accessible by organization members." +msgstr "" + msgid "Organization|Org ID" msgstr "" @@ -37543,6 +37558,9 @@ msgstr "" msgid "Organization|Perform advanced options such as deleting the organization." msgstr "" +msgid "Organization|Private - The organization can only be viewed by members." +msgstr "" + msgid "Organization|Projects are hosted/created in groups. Before creating a project, you must create a group." msgstr "" @@ -37582,6 +37600,9 @@ msgstr "" msgid "Organization|View all" msgstr "" +msgid "Organization|Who can see this organization?" +msgstr "" + msgid "Organization|You can now start using your new organization." msgstr "" diff --git a/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb index b8726bbfd5d..9c0c43815b9 100644 --- a/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb @@ -33,57 +33,23 @@ RSpec.describe Groups::DependencyProxyAuthController, feature_category: :contain end context 'group bot user' do - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - let_it_be(:user) { create(:user, :project_bot) } + let_it_be(:bot_user) { create(:user, :project_bot) } + let_it_be(:user) { create(:personal_access_token, user: bot_user) } - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it { is_expected.to have_gitlab_http_status(:success) } - end - - context 'with packages_dependency_proxy_pass_token_to_policy enabled' do - let_it_be(:bot_user) { create(:user, :project_bot) } - let_it_be(:user) { create(:personal_access_token, user: bot_user) } - - it { is_expected.to have_gitlab_http_status(:success) } - end + it { is_expected.to have_gitlab_http_status(:success) } end context 'service account user' do - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - let_it_be(:user) { create(:user, :service_account) } + let_it_be(:service_account_user) { create(:user, :service_account) } + let_it_be(:user) { create(:personal_access_token, user: service_account_user) } - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it { is_expected.to have_gitlab_http_status(:success) } - end - - context 'with packages_dependency_proxy_pass_token_to_policy enabled' do - let_it_be(:service_account_user) { create(:user, :service_account) } - let_it_be(:user) { create(:personal_access_token, user: service_account_user) } - - it { is_expected.to have_gitlab_http_status(:success) } - end + it { is_expected.to have_gitlab_http_status(:success) } end context 'deploy token' do let_it_be(:user) { create(:deploy_token) } - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it { is_expected.to have_gitlab_http_status(:success) } - end - - context 'with packages_dependency_proxy_pass_token_to_policy enabled' do - it { is_expected.to have_gitlab_http_status(:success) } - end + it { is_expected.to have_gitlab_http_status(:success) } end end diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index c1b6b79faad..9c81e5cb1b5 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -202,24 +202,13 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: context 'with a valid group access token' do let_it_be(:user) { create(:user, :project_bot) } let_it_be_with_reload(:token) { create(:personal_access_token, user: user) } + let_it_be(:jwt) { build_jwt(token) } before do token.update_column(:scopes, Gitlab::Auth::REGISTRY_SCOPES) end - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it_behaves_like 'sends Workhorse instructions' - end - - context 'with packages_dependency_proxy_pass_token_to_policy enabled' do - let_it_be(:jwt) { build_jwt(token) } - - it_behaves_like 'sends Workhorse instructions' - end + it_behaves_like 'sends Workhorse instructions' end context 'with a deploy token' do @@ -321,15 +310,6 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: it_behaves_like 'a successful manifest pull' it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false - context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do - before do - stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) - end - - it_behaves_like 'a successful manifest pull' - it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false - end - context 'with workhorse response' do let(:pull_response) { { status: :success, manifest: nil, from_cache: false } } @@ -361,14 +341,6 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: it_behaves_like 'a successful manifest pull' - context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do - before do - stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) - end - - it_behaves_like 'a successful manifest pull' - end - context 'pulling from a subgroup' do let_it_be_with_reload(:parent_group) { create(:group) } let_it_be_with_reload(:group) { create(:group, parent: parent_group) } @@ -384,26 +356,14 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: context 'a valid group access token' do let_it_be(:user) { create(:user, :project_bot) } let_it_be(:token) { create(:personal_access_token, :dependency_proxy_scopes, user: user) } + let_it_be(:jwt) { build_jwt(token) } before do group.add_guest(user) end - context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it_behaves_like 'a successful manifest pull' - it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false - end - - context 'when packages_dependency_proxy_pass_token_to_policy is enabled' do - let_it_be(:jwt) { build_jwt(token) } - - it_behaves_like 'a successful manifest pull' - it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false - end + it_behaves_like 'a successful manifest pull' + it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false end end @@ -425,14 +385,6 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: it_behaves_like 'without a token' it_behaves_like 'without permission' - context 'when packages_dependency_proxy_pass_token_to_policy is disabled' do - before do - stub_feature_flags(packages_dependency_proxy_containers_scope_check: false) - end - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - context 'a valid user' do before do group.add_guest(user) diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb index d1927d293b2..56a9e80f4c8 100644 --- a/spec/features/groups/members/sort_members_spec.rb +++ b/spec/features/groups/members/sort_members_spec.rb @@ -32,22 +32,22 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :groups expect_sort_by('Account', :asc) end - it 'sorts by max role ascending' do + it 'sorts by role ascending' do visit_members_list(sort: :access_level_asc) expect(first_row.text).to include(developer.name) expect(second_row.text).to include(owner.name) - expect_sort_by('Max role', :asc) + expect_sort_by('Role', :asc) end - it 'sorts by max role descending' do + it 'sorts by role descending' do visit_members_list(sort: :access_level_desc) expect(first_row.text).to include(owner.name) expect(second_row.text).to include(developer.name) - expect_sort_by('Max role', :desc) + expect_sort_by('Role', :desc) end it 'sorts by user created on ascending' do diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index f8ef1c0ab46..7752f1d21d8 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -24,22 +24,22 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :groups_an expect_sort_by('Account', :asc) end - it 'sorts by max role ascending' do + it 'sorts by role ascending' do visit_members_list(sort: :access_level_asc) expect(first_row).to have_content(developer.name) expect(second_row).to have_content(maintainer.name) - expect_sort_by('Max role', :asc) + expect_sort_by('Role', :asc) end - it 'sorts by max role descending' do + it 'sorts by role descending' do visit_members_list(sort: :access_level_desc) expect(first_row).to have_content(maintainer.name) expect(second_row).to have_content(developer.name) - expect_sort_by('Max role', :desc) + expect_sort_by('Role', :desc) end it 'sorts by user created on ascending' do diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb index de7ead20233..07c3b95651c 100644 --- a/spec/finders/concerns/packages/finder_helper_spec.rb +++ b/spec/finders/concerns/packages/finder_helper_spec.rb @@ -267,7 +267,7 @@ RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do end end - describe '#projects_visible_to_user' do + context 'for projecs visibile to user' do using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } @@ -276,8 +276,6 @@ RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do let_it_be_with_reload(:subgroup) { create(:group, parent: group) } let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) } - subject { finder.projects_visible_to_user(user, within_group: group) } - shared_examples 'returning both projects' do it { is_expected.to contain_exactly(project1, project2) } end @@ -286,70 +284,103 @@ RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do it { is_expected.to eq [project1] } end + shared_examples 'returning project2' do + it { is_expected.to eq [project2] } + end + shared_examples 'returning no project' do it { is_expected.to be_empty } end - context 'with a user' do - let_it_be(:user) { create(:user) } + describe '#projects_visible_to_user' do + subject { finder.projects_visible_to_user(user, within_group: group) } - where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :maintainer | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :developer | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :guest | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :anonymous | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :maintainer | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :developer | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :guest | 'returning project1' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :anonymous | 'returning project1' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both projects' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both projects' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning project1' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning project1' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both projects' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both projects' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning no project' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning no project' - end - - with_them do - before do - unless user_role == :anonymous - group.send("add_#{user_role}", user) - subgroup.send("add_#{user_role}", user) - project1.send("add_#{user_role}", user) - project2.send("add_#{user_role}", user) - end - - project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) - subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) - project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) - group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + context 'with a user' do + where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :maintainer | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :developer | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :guest | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :anonymous | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :maintainer | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :developer | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :guest | 'returning project1' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :anonymous | 'returning project1' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both projects' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both projects' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning project1' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning project1' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both projects' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both projects' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning no project' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning no project' end - it_behaves_like params[:shared_example_name] + with_them do + before do + unless user_role == :anonymous + group.send("add_#{user_role}", user) + subgroup.send("add_#{user_role}", user) + project1.send("add_#{user_role}", user) + project2.send("add_#{user_role}", user) + end + + project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) + subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) + project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + end + + it_behaves_like params[:shared_example_name] + end + end + + context 'with a group deploy token' do + let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } + + where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both projects' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both projects' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both projects' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both projects' + end + + with_them do + before do + project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) + subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) + project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + end + + it_behaves_like params[:shared_example_name] + end end end - context 'with a group deploy token' do - let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } + describe '#projects_visible_to_user_including_public_registries' do + subject { finder.projects_visible_to_user_including_public_registries(user, within_group: group) } - where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both projects' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both projects' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both projects' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both projects' + before do + [subgroup, group, project1, project2].each do |entity| + entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false)) + end + project1.project_feature.update!(package_registry_access_level: project1_package_registry_access_level) + project2.project_feature.update!(package_registry_access_level: project2_package_registry_access_level) + end + + where(:project1_package_registry_access_level, :project2_package_registry_access_level, :shared_example_name) do + ::ProjectFeature::PUBLIC | ::ProjectFeature::PUBLIC | 'returning both projects' + ::ProjectFeature::PUBLIC | ::ProjectFeature::PRIVATE | 'returning project1' + ::ProjectFeature::PUBLIC | ::ProjectFeature::DISABLED | 'returning project1' + ::ProjectFeature::PUBLIC | ::ProjectFeature::ENABLED | 'returning project1' + ::ProjectFeature::PRIVATE | ::ProjectFeature::PUBLIC | 'returning project2' + ::ProjectFeature::DISABLED | ::ProjectFeature::PUBLIC | 'returning project2' + ::ProjectFeature::ENABLED | ::ProjectFeature::PUBLIC | 'returning project2' + ::ProjectFeature::PRIVATE | ::ProjectFeature::PRIVATE | 'returning no project' end with_them do - before do - project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) - subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) - project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) - group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) - end - it_behaves_like params[:shared_example_name] end end diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js index f177849433b..ea0522740e4 100644 --- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js +++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js @@ -1,11 +1,12 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import RelatedMergeRequests from '~/issues/related_merge_requests/components/related_merge_requests.vue'; import relatedMergeRequestsQuery from '~/issues/related_merge_requests/queries/related_merge_requests.query.graphql'; import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue'; +import CrudComponent from '~/vue_shared/components/crud_component.vue'; Vue.use(VueApollo); @@ -115,12 +116,15 @@ describe('RelatedMergeRequests', () => { const apolloProvider = createMockApollo([ [relatedMergeRequestsQuery, jest.fn().mockResolvedValue(mockData)], ]); - wrapper = shallowMount(RelatedMergeRequests, { + wrapper = shallowMountExtended(RelatedMergeRequests, { apolloProvider, propsData: { projectPath: 'gitlab-ce', iid: '1', }, + stubs: { + CrudComponent, + }, }); await waitForPromises(); @@ -128,7 +132,7 @@ describe('RelatedMergeRequests', () => { describe('template', () => { it('should render related merge request items', () => { - expect(wrapper.find('[data-testid="count"]').text()).toBe('2'); + expect(wrapper.findByTestId('crud-count').text()).toBe('2'); expect(wrapper.findAllComponents(RelatedIssuableItem)).toHaveLength(2); const props = wrapper.findAllComponents(RelatedIssuableItem).at(1).props(); diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js index e62e8065398..c10c9b53903 100644 --- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js +++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js @@ -69,7 +69,7 @@ describe('SortDropdown', () => { value: 'granted', }, { - text: 'Max role', + text: 'Role', value: 'maxRole', }, { @@ -102,7 +102,7 @@ describe('SortDropdown', () => { createComponent(); - expect(findDropdownToggle().text()).toBe('Max role'); + expect(findDropdownToggle().text()).toBe('Role'); }); describe('select new sort field', () => { diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js index 2ad6786e7e5..2ef3748eb50 100644 --- a/spec/frontend/members/components/table/member_source_spec.js +++ b/spec/frontend/members/components/table/member_source_spec.js @@ -1,41 +1,24 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import MemberSource from '~/members/components/table/member_source.vue'; import PrivateIcon from '~/members/components/icons/private_icon.vue'; +import { directMember, inheritedMember, sharedMember, privateGroup } from '../../mock_data'; describe('MemberSource', () => { let wrapper; - const memberSource = { - id: 102, - fullName: 'Foo bar', - webUrl: 'https://gitlab.com/groups/foo-bar', - }; - - const createdBy = { - name: 'Administrator', - webUrl: 'https://gitlab.com/root', - }; - const createComponent = (propsData) => { wrapper = mountExtended(MemberSource, { propsData: { - memberSource, + member: directMember, ...propsData, }, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, }); }; - const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip'); - describe('when source is private', () => { beforeEach(() => { createComponent({ - isSharedWithGroupPrivate: true, - isDirectMember: false, + member: privateGroup, }); }); @@ -48,22 +31,22 @@ describe('MemberSource', () => { describe('direct member', () => { describe('when created by is available', () => { it('displays "Direct member by "', () => { - createComponent({ - isDirectMember: true, - createdBy, - }); + createComponent(); - expect(wrapper.text()).toBe('Direct member by Administrator'); - expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe( - createdBy.webUrl, - ); + expect(wrapper.text()).toBe(`Direct member by ${directMember.createdBy.name}`); + expect( + wrapper.findByRole('link', { name: directMember.createdBy.name }).attributes('href'), + ).toBe(directMember.createdBy.webUrl); }); }); describe('when created by is not available', () => { it('displays "Direct member"', () => { createComponent({ - isDirectMember: true, + member: { + ...directMember, + createdBy: undefined, + }, }); expect(wrapper.text()).toBe('Direct member'); @@ -72,58 +55,32 @@ describe('MemberSource', () => { }); describe('inherited member', () => { - describe('when created by is available', () => { - beforeEach(() => { - createComponent({ - isDirectMember: false, - createdBy, - }); - }); - - it('displays " by "', () => { - expect(wrapper.text()).toBe('Foo bar by Administrator'); - expect(wrapper.findByRole('link', { name: memberSource.fullName }).attributes('href')).toBe( - memberSource.webUrl, - ); - expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe( - createdBy.webUrl, - ); + beforeEach(() => { + createComponent({ + member: inheritedMember, }); }); - describe('when created by is not available', () => { - beforeEach(() => { - createComponent({ - isDirectMember: false, - }); + it('displays "Inherited from "', () => { + expect(wrapper.text()).toBe(`Inherited from ${inheritedMember.source.fullName}`); + expect( + wrapper.findByRole('link', { name: inheritedMember.source.fullName }).attributes('href'), + ).toBe(inheritedMember.source.webUrl); + }); + }); + + describe('shared member', () => { + beforeEach(() => { + createComponent({ + member: sharedMember, }); + }); - it('displays a link to source group', () => { - expect(wrapper.text()).toBe(memberSource.fullName); - expect(wrapper.attributes('href')).toBe(memberSource.webUrl); - }); - - it('displays tooltip with "Inherited"', () => { - const tooltipDirective = getTooltipDirective(wrapper); - - expect(tooltipDirective).not.toBeUndefined(); - expect(tooltipDirective.value).toBe('Inherited'); - }); - describe('when `webuiMembersInheritedUsers` FF is on', () => { - beforeEach(() => { - gon.features = { webuiMembersInheritedUsers: true }; - createComponent({ - isDirectMember: false, - }); - }); - - it('displays tooltip with "Indirect"', () => { - const tooltipDirective = getTooltipDirective(wrapper); - - expect(tooltipDirective).not.toBeUndefined(); - expect(tooltipDirective.value).toBe('Indirect'); - }); - }); + it('displays "Invited group "', () => { + expect(wrapper.text()).toBe(`Invited group ${sharedMember.source.fullName}`); + expect( + wrapper.findByRole('link', { name: sharedMember.source.fullName }).attributes('href'), + ).toBe(sharedMember.source.webUrl); }); }); }); diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js index 8b5e6f84ba4..250f02ec8b6 100644 --- a/spec/frontend/members/components/table/members_table_cell_spec.js +++ b/spec/frontend/members/components/table/members_table_cell_spec.js @@ -26,10 +26,6 @@ describe('MembersTableCell', () => { type: String, required: true, }, - isDirectMember: { - type: Boolean, - required: true, - }, isCurrentUser: { type: Boolean, required: true, @@ -68,7 +64,6 @@ describe('MembersTableCell', () => { default: ` @@ -113,28 +108,6 @@ describe('MembersTableCell', () => { }, ); - describe('isDirectMember', () => { - it('returns `true` when member source has same ID as `sourceId`', () => { - createComponentWithDirectMember(); - - expect(findWrappedComponent().props('isDirectMember')).toBe(true); - }); - - it('returns `false` when member is inherited', () => { - createComponentWithInheritedMember(); - - expect(findWrappedComponent().props('isDirectMember')).toBe(false); - }); - - it('returns `true` for linked groups', () => { - createComponent({ - member: group, - }); - - expect(findWrappedComponent().props('isDirectMember')).toBe(true); - }); - }); - describe('isCurrentUser', () => { it('returns `true` when `member.user` has the same ID as `currentUserId`', () => { createComponent({ diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index c7f1c8f7e19..b8df0ad0f07 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -104,7 +104,7 @@ describe('MembersTable', () => { ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource} ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt} ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt} - ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${MaxRole} + ${'maxRole'} | ${'Role'} | ${memberCanUpdate} | ${MaxRole} ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker} ${'activity'} | ${'Activity'} | ${memberMock} | ${MemberActivity} `('$label field', ({ field, label, member, expectedComponent }) => { @@ -124,7 +124,7 @@ describe('MembersTable', () => { }); }); - describe('Max role column', () => { + describe('Role column', () => { const createMaxRoleComponent = (member = memberMock) => { createComponent({ members: [member], tableFields: ['maxRole'] }); }; @@ -262,10 +262,7 @@ describe('MembersTable', () => { it('passes correct props to `MemberSource` component', () => { expect(wrapper.findComponent(MemberSource).props()).toMatchObject({ - memberSource: {}, - isDirectMember: true, - isSharedWithGroupPrivate: true, - createdBy: null, + member: privateGroup, }); }); }); diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index 416666e2a14..75a766fcd31 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -11,6 +11,8 @@ export const member = { canOverride: false, isOverridden: false, isDirectMember: false, + isInheritedMember: true, + isSharedMember: false, accessLevel: { integerValue: 50, stringValue: 'Owner' }, source: { id: 178, @@ -52,6 +54,11 @@ export const member = { Owner: 50, }, customRoles: [], + createdBy: { + id: 102, + name: 'John Smith', + webUrl: 'https://gitlab.com/john_smith', + }, }; export const group = { @@ -113,8 +120,9 @@ export const accessRequest = { export const members = [member]; -export const directMember = { ...member, isDirectMember: true }; -export const inheritedMember = { ...member, isDirectMember: false }; +export const directMember = { ...member, isDirectMember: true, isInheritedMember: false }; +export const inheritedMember = member; +export const sharedMember = { ...member, isSharedMember: true, isInheritedMember: false }; export const updateableMember = { ...directMember, canUpdate: true, diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js index 1a6700d6a6b..d94f859df37 100644 --- a/spec/frontend/members/utils_spec.js +++ b/spec/frontend/members/utils_spec.js @@ -10,7 +10,6 @@ import { import { generateBadges, isGroup, - isDirectMember, isCurrentUser, canRemove, canRemoveBlockedByLastOwner, @@ -107,16 +106,6 @@ describe('Members Utils', () => { }); }); - describe('isDirectMember', () => { - it.each` - member | expected - ${directMember} | ${true} - ${inheritedMember} | ${false} - `('returns $expected', ({ member, expected }) => { - expect(isDirectMember(member)).toBe(expected); - }); - }); - describe('isCurrentUser', () => { it.each` currentUserId | expected diff --git a/spec/frontend/organizations/settings/general/components/app_spec.js b/spec/frontend/organizations/settings/general/components/app_spec.js index e954b927715..1dee0b334ce 100644 --- a/spec/frontend/organizations/settings/general/components/app_spec.js +++ b/spec/frontend/organizations/settings/general/components/app_spec.js @@ -1,5 +1,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue'; +import VisibilityLevel from '~/organizations/settings/general/components/visibility_level.vue'; import AdvancedSettings from '~/organizations/settings/general/components/advanced_settings.vue'; import App from '~/organizations/settings/general/components/app.vue'; @@ -18,6 +19,10 @@ describe('OrganizationSettingsGeneralApp', () => { expect(wrapper.findComponent(OrganizationSettings).exists()).toBe(true); }); + it('renders `Visibility` section', () => { + expect(wrapper.findComponent(VisibilityLevel).exists()).toBe(true); + }); + it('renders `Advanced` section', () => { expect(wrapper.findComponent(AdvancedSettings).exists()).toBe(true); }); diff --git a/spec/frontend/organizations/settings/general/components/visibility_level_spec.js b/spec/frontend/organizations/settings/general/components/visibility_level_spec.js new file mode 100644 index 00000000000..9eb3d3ab8f0 --- /dev/null +++ b/spec/frontend/organizations/settings/general/components/visibility_level_spec.js @@ -0,0 +1,58 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import VisibilityLevel from '~/organizations/settings/general/components/visibility_level.vue'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import VisibilityLevelRadioButtons from '~/visibility_level/components/visibility_level_radio_buttons.vue'; +import { + VISIBILITY_LEVEL_PRIVATE_INTEGER, + ORGANIZATION_VISIBILITY_LEVEL_DESCRIPTIONS, +} from '~/visibility_level/constants'; +import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue'; + +describe('VisibilityLevel', () => { + let wrapper; + + const defaultProvide = { + organization: { + id: 1, + name: 'GitLab', + path: 'foo-bar', + description: 'foo bar', + visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER, + }, + }; + + const createComponent = () => { + wrapper = mountExtended(VisibilityLevel, { provide: defaultProvide }); + }; + + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); + const findVisibilityLevelRadioButtons = () => wrapper.findComponent(VisibilityLevelRadioButtons); + const findHelpPageLink = () => wrapper.findComponent(HelpPageLink); + + beforeEach(() => { + createComponent(); + }); + + it('renders settings block with title and description', () => { + expect(findSettingsBlock().text()).toContain( + 'Visibility Choose organization visibility level.', + ); + }); + + it('renders visibility level field with the current visibility as the only option', () => { + expect(findVisibilityLevelRadioButtons().props()).toEqual({ + checked: VISIBILITY_LEVEL_PRIVATE_INTEGER, + visibilityLevels: [VISIBILITY_LEVEL_PRIVATE_INTEGER], + visibilityLevelDescriptions: ORGANIZATION_VISIBILITY_LEVEL_DESCRIPTIONS, + }); + }); + + it('renders label description with link to docs', () => { + expect(wrapper.text()).toContain('Who can see this organization?'); + expect(findHelpPageLink().props()).toEqual({ + href: 'user/organization/index', + anchor: 'view-an-organizations-visibility-level', + }); + expect(findHelpPageLink().text()).toBe('Learn more about visibility levels'); + }); +}); diff --git a/spec/frontend/organizations/show/components/organization_avatar_spec.js b/spec/frontend/organizations/show/components/organization_avatar_spec.js index c98fa14e49b..7abc0477884 100644 --- a/spec/frontend/organizations/show/components/organization_avatar_spec.js +++ b/spec/frontend/organizations/show/components/organization_avatar_spec.js @@ -5,7 +5,7 @@ import OrganizationAvatar from '~/organizations/show/components/organization_ava import { VISIBILITY_TYPE_ICON, ORGANIZATION_VISIBILITY_TYPE, - VISIBILITY_LEVEL_PUBLIC_STRING, + VISIBILITY_LEVEL_PRIVATE_STRING, } from '~/visibility_level/constants'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -16,6 +16,7 @@ describe('OrganizationAvatar', () => { organization: { id: 1, name: 'GitLab', + visibility: VISIBILITY_LEVEL_PRIVATE_STRING, }, }; @@ -49,8 +50,8 @@ describe('OrganizationAvatar', () => { const icon = wrapper.findComponent(GlIcon); const tooltip = getBinding(icon.element, 'gl-tooltip'); - expect(icon.props('name')).toBe(VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING]); - expect(tooltip.value).toBe(ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]); + expect(icon.props('name')).toBe(VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PRIVATE_STRING]); + expect(tooltip.value).toBe(ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]); }); it('renders button to copy organization ID', () => { diff --git a/spec/frontend/vue_shared/components/promo_page_link/promo_page_link_spec.js b/spec/frontend/vue_shared/components/promo_page_link/promo_page_link_spec.js index 397583a3b85..07e5b2da60c 100644 --- a/spec/frontend/vue_shared/components/promo_page_link/promo_page_link_spec.js +++ b/spec/frontend/vue_shared/components/promo_page_link/promo_page_link_spec.js @@ -6,11 +6,9 @@ import { joinPaths } from '~/lib/utils/url_utility'; let wrapper; -const createComponent = (props = {}, slots = {}) => { +const createComponent = (propsData = {}, slots = {}) => { wrapper = shallowMount(PromoPageLink, { - propsData: { - ...props, - }, + propsData, slots, stubs: { GlLink: true, @@ -22,27 +20,27 @@ const findGlLink = () => wrapper.findComponent(GlLink); describe('HelpPageLink', () => { it('renders a link', () => { - const href = 'pricing'; - createComponent({ href }); + const path = 'pricing'; + createComponent({ path }); const link = findGlLink(); - const expectedHref = joinPaths(PROMO_URL, href); + const expectedHref = joinPaths(PROMO_URL, path); expect(link.attributes().href).toBe(expectedHref); }); it('with a leading slash and anchor', () => { - const href = '/pricing#anchor'; - createComponent({ href }); + const path = '/pricing#anchor'; + createComponent({ path }); const link = findGlLink(); - const expectedHref = joinPaths(PROMO_URL, href); + const expectedHref = joinPaths(PROMO_URL, path); expect(link.attributes().href).toBe(expectedHref); }); it('renders slot content', () => { - const href = 'pricing'; + const path = 'pricing'; const slotContent = 'slot content'; - createComponent({ href }, { default: slotContent }); + createComponent({ path }, { default: slotContent }); const link = findGlLink(); expect(link.text()).toBe(slotContent); diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 035eb9c6b50..f00af2d7b5a 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -121,7 +121,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do 'id' => organization.id, 'name' => organization.name, 'description_html' => organization.description_html, - 'avatar_url' => 'avatar.jpg' + 'avatar_url' => 'avatar.jpg', + 'visibility' => organization.visibility }, 'groups_and_projects_organization_path' => '/-/organizations/default/groups_and_projects', 'users_organization_path' => '/-/organizations/default/users', @@ -226,7 +227,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do 'name' => organization.name, 'path' => organization.path, 'description' => organization.description, - 'avatar' => 'avatar.jpg' + 'avatar' => 'avatar.jpg', + 'visibility_level' => organization.visibility_level }, 'organizations_path' => '/-/organizations', 'root_url' => 'http://test.host/', diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 10cfc4e9e14..8eb243dc295 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -1126,7 +1126,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do end end - # This block can be removed when packages_dependency_proxy_pass_token_to_policy is rolled out describe 'dependency proxy' do shared_examples 'disallows all dependency proxy access' do it { is_expected.to be_disallowed(:read_dependency_proxy) } @@ -1180,45 +1179,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do end end - context 'deploy token user' do - let!(:group_deploy_token) do - create(:group_deploy_token, group: group, deploy_token: deploy_token) - end - - subject { described_class.new(deploy_token, group) } - - context 'with insufficient scopes' do - let_it_be(:deploy_token) { create(:deploy_token, :group) } - - it_behaves_like 'disallows all dependency proxy access' - end - - context 'with sufficient scopes' do - let_it_be(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } - - it_behaves_like 'allows dependency proxy read access but not admin' - end - end - - context 'group access token user' do - let_it_be(:bot_user) { create(:user, :project_bot) } - let_it_be(:token) { create(:personal_access_token, user: bot_user, scopes: [Gitlab::Auth::READ_API_SCOPE]) } - - subject { described_class.new(bot_user, group) } - - context 'not a member of the group' do - it_behaves_like 'disallows all dependency proxy access' - end - - context 'a member of the group' do - before do - group.add_guest(bot_user) - end - - it_behaves_like 'allows dependency proxy read access but not admin' - end - end - context 'placeholder user' do let_it_be(:placeholder_user) { create(:user, user_type: :placeholder, developer_of: group) } diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index 97171642930..a206886a842 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -49,7 +49,9 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do guest_requests_status: :not_found } - it_behaves_like 'allows anyone to pull public nuget packages on group level' + it_behaves_like 'allows anyone to pull public nuget packages on group level' do + let(:json_schema) { 'public_api/v4/packages/nuget/packages_metadata' } + end end describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/*package_version' do @@ -64,18 +66,26 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do guest_requests_status: :not_found } - it_behaves_like 'allows anyone to pull public nuget packages on group level' + it_behaves_like 'allows anyone to pull public nuget packages on group level' do + let(:json_schema) { 'public_api/v4/packages/nuget/package_metadata' } + end end describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do + let(:url) { "/groups/#{target.id}/-/packages/nuget/query?#{query_parameters.to_query}" } + it_behaves_like 'handling nuget search requests', example_names_with_status: { anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized, guest_requests_example_name: 'process empty nuget search request', guest_requests_status: :success - } do - let(:url) { "/groups/#{target.id}/-/packages/nuget/query?#{query_parameters.to_query}" } + } + + it_behaves_like 'allows anyone to pull public nuget packages on group level' do + let(:query_parameters) { { q: 'uMmy', take: 26, skip: 0, prerelease: true } } + let(:json_schema) { 'public_api/v4/packages/nuget/search' } + let(:not_found_response) { :ok } end end end diff --git a/spec/serializers/group_link/group_link_entity_spec.rb b/spec/serializers/group_link/group_link_entity_spec.rb index 01c01f492aa..1d8d069f28d 100644 --- a/spec/serializers/group_link/group_link_entity_spec.rb +++ b/spec/serializers/group_link/group_link_entity_spec.rb @@ -5,9 +5,20 @@ require 'spec_helper' RSpec.describe GroupLink::GroupLinkEntity, feature_category: :groups_and_projects do include_context 'group_group_link' - let(:entity) { described_class.new(group_group_link) } + let(:source) { shared_group } + let(:entity) { described_class.new(group_group_link, { source: source }) } let(:entity_hash) { entity.as_json } + shared_examples 'exposes source type properties' do |is_direct_member, is_inherited_member| + it "exposes `is_direct_member` as `#{is_direct_member}`" do + expect(entity_hash[:is_direct_member]).to be(is_direct_member) + end + + it "exposes `is_inherited_member` as `#{is_inherited_member}`" do + expect(entity_hash[:is_inherited_member]).to be(is_inherited_member) + end + end + it 'matches json schema' do expect(entity.to_json).to match_schema('group_link/group_link') end @@ -18,4 +29,15 @@ RSpec.describe GroupLink::GroupLinkEntity, feature_category: :groups_and_project expect(entity_hash[:shared_with_group][:avatar_url]).to match(avatar_url) end + + context 'for direct member' do + it_behaves_like 'exposes source type properties', true, false + end + + context 'for inherited member' do + let_it_be(:subgroup) { build_stubbed(:group, parent: shared_group) } + let(:source) { subgroup } + + it_behaves_like 'exposes source type properties', false, true + end end diff --git a/spec/serializers/member_entity_spec.rb b/spec/serializers/member_entity_spec.rb index 2f4a803d61b..f58c27c078b 100644 --- a/spec/serializers/member_entity_spec.rb +++ b/spec/serializers/member_entity_spec.rb @@ -67,36 +67,36 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do end end - shared_examples 'is_direct_member' do - context 'when `source` is the same as `member.source`' do - let(:source) { direct_member_source } - - it 'exposes `is_direct_member` as `true`' do - expect(entity_hash[:is_direct_member]).to be(true) - end - end - - context 'when `source` is not the same as `member.source`' do - let(:source) { inherited_member_source } - - it 'exposes `is_direct_member` as `false`' do - expect(entity_hash[:is_direct_member]).to be(false) - end - end - end - shared_examples 'user state is blocked_pending_approval' do it 'displays proper user state' do expect(entity_hash[:invite][:user_state]).to eq('blocked_pending_approval') end end + shared_examples 'exposes source type properties' do |is_direct_member, is_inherited_member, is_shared_member| + it "exposes `is_direct_member` as `#{is_direct_member}`" do + expect(entity_hash[:is_direct_member]).to be(is_direct_member) + end + + it "exposes `is_inherited_member` as `#{is_inherited_member}`" do + expect(entity_hash[:is_inherited_member]).to be(is_inherited_member) + end + + it "exposes `is_shared_member` as `#{is_shared_member}`" do + expect(entity_hash[:is_shared_member]).to be(is_shared_member) + end + end + context 'group member' do - let(:group) { create(:group) } - let(:source) { group } + let_it_be(:parent_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: parent_group) } + let_it_be(:shared_group) { create(:group) } + + let(:group) { subgroup } + let(:source) { subgroup } let(:member) do GroupMemberPresenter.new( - create(:group_member, group: group, created_by: current_user), current_user: current_user + create(:group_member, source: subgroup, created_by: current_user), current_user: current_user ) end @@ -105,7 +105,7 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do context 'invite' do let(:member) do GroupMemberPresenter.new( - create(:group_member, :invited, group: group, created_by: current_user), current_user: current_user + create(:group_member, :invited, source: subgroup, created_by: current_user), current_user: current_user ) end @@ -113,11 +113,30 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do it_behaves_like 'invite' end - context 'is_direct_member' do - let(:direct_member_source) { group } - let(:inherited_member_source) { create(:group) } + context 'direct member' do + it_behaves_like 'exposes source type properties', true, false, false + end - it_behaves_like 'is_direct_member' + context 'inherited member' do + let(:member) do + GroupMemberPresenter.new( + create(:group_member, source: parent_group, created_by: current_user), current_user: current_user + ) + end + + it_behaves_like 'exposes source type properties', false, true, false + end + + context 'shared member' do + let(:member) do + GroupMemberPresenter.new( + create(:group_member, source: shared_group, created_by: current_user), current_user: current_user + ) + end + + let(:group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: subgroup) } + + it_behaves_like 'exposes source type properties', false, false, true end context 'is_last_owner' do @@ -144,7 +163,7 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do context 'new member user state is blocked_pending_approval' do let(:user) { create(:user, :blocked_pending_approval) } - let(:group_member) { create(:group_member, :invited, group: group, invite_email: user.email) } + let(:group_member) { create(:group_member, :invited, group: subgroup, invite_email: user.email) } let(:member) { GroupMemberPresenter.new(GroupMember.with_invited_user_state.find(group_member.id), current_user: current_user) } it_behaves_like 'user state is blocked_pending_approval' @@ -152,12 +171,16 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do end context 'project member' do - let(:project) { create(:project) } + let_it_be(:parent_group) { create(:group) } + let_it_be(:project) { create(:project, group: parent_group) } + let_it_be(:shared_group) { create(:group) } + let_it_be(:personal_project) { create(:project) } + let(:group) { project.group } let(:source) { project } let(:member) do ProjectMemberPresenter.new( - create(:project_member, project: project, created_by: current_user), current_user: current_user + create(:project_member, source: source, created_by: current_user), current_user: current_user ) end @@ -166,7 +189,7 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do context 'invite' do let(:member) do ProjectMemberPresenter.new( - create(:project_member, :invited, project: project, created_by: current_user), current_user: current_user + create(:project_member, :invited, source: source, created_by: current_user), current_user: current_user ) end @@ -174,16 +197,49 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do it_behaves_like 'invite' end - context 'is_direct_member' do - let(:direct_member_source) { project } - let(:inherited_member_source) { group } + context 'direct member' do + it_behaves_like 'exposes source type properties', true, false, false - it_behaves_like 'is_direct_member' + context 'personal project' do + let(:source) { personal_project } + let(:group) { nil } + + it_behaves_like 'exposes source type properties', true, false, false + end + end + + context 'inherited member' do + let(:member) do + GroupMemberPresenter.new( + create(:group_member, source: parent_group, created_by: current_user), current_user: current_user + ) + end + + it_behaves_like 'exposes source type properties', false, true, false + end + + context 'shared member' do + let(:member) do + GroupMemberPresenter.new( + create(:group_member, source: shared_group, created_by: current_user), current_user: current_user + ) + end + + let(:project_group_link) { create(:project_group_link, group: shared_group, project: project) } + + it_behaves_like 'exposes source type properties', false, false, true + + context 'personal project' do + let(:source) { personal_project } + let(:group) { nil } + + it_behaves_like 'exposes source type properties', false, false, true + end end context 'new members user state is blocked_pending_approval' do let(:user) { create(:user, :blocked_pending_approval) } - let(:project_member) { create(:project_member, :invited, project: project, invite_email: user.email) } + let(:project_member) { create(:project_member, :invited, source: project, invite_email: user.email) } let(:member) { ProjectMemberPresenter.new(ProjectMember.with_invited_user_state.find(project_member.id), current_user: current_user) } it_behaves_like 'user state is blocked_pending_approval' diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb index c979a59e7cc..d359d544c65 100644 --- a/spec/services/auth/dependency_proxy_authentication_service_spec.rb +++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb @@ -62,14 +62,6 @@ RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :vi it_behaves_like 'returning a token with an encoded field', 'deploy_token' end - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it_behaves_like 'returning a token with an encoded field', 'deploy_token' - end - context 'when the the deploy token is restricted with external_authorization' do before do allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false) @@ -82,14 +74,6 @@ RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :vi context 'with a human user' do it_behaves_like 'returning a token with an encoded field', 'user_id' - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it_behaves_like 'returning a token with an encoded field', 'user_id' - end - context "when the deploy token is restricted with external_authorization" do before do allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false) @@ -136,14 +120,6 @@ RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :vi it_behaves_like 'returning a token with an encoded field', 'group_access_token' - context 'with packages_dependency_proxy_pass_token_to_policy disabled' do - before do - stub_feature_flags(packages_dependency_proxy_pass_token_to_policy: false) - end - - it_behaves_like 'returning a token with an encoded field', 'user_id' - end - context 'revoked' do before do token.revoke! diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb index b5f32c9b727..c4a85bd9777 100644 --- a/spec/services/packages/nuget/search_service_spec.rb +++ b/spec/services/packages/nuget/search_service_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Packages::Nuget::SearchService, feature_category: :package_regist let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') } let_it_be(:package_d) { create(:nuget_package, project: project, name: 'FooBarD') } let_it_be(:other_package_a) { create(:nuget_package, name: 'DummyPackageA') } - let_it_be(:other_package_a) { create(:nuget_package, name: 'DummyPackageB') } + let_it_be(:other_package_b) { create(:nuget_package, name: 'DummyPackageB') } let(:search_term) { 'ummy' } let(:per_page) { 5 } @@ -80,11 +80,11 @@ RSpec.describe Packages::Nuget::SearchService, feature_category: :package_regist it { expect_search_results 4, package_a, packages_b, packages_c, package_d } end - context 'with non-displayable packages' do + context 'with non-installable packages' do let(:search_term) { '' } before do - package_a.update_column(:status, 1) + package_a.update_column(:status, 2) end it { expect_search_results 3, packages_b, packages_c, package_d } @@ -103,19 +103,23 @@ RSpec.describe Packages::Nuget::SearchService, feature_category: :package_regist end context 'with pre release packages' do - let_it_be(:package_e) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1-alpha') } + let_it_be(:package_e) do + create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1-alpha') + end - context 'including them' do + context 'when including them' do it { expect_search_results 4, package_a, packages_b, packages_c, package_e } end - context 'excluding them' do + context 'when excluding them' do let(:include_prerelease_versions) { false } it { expect_search_results 3, package_a, packages_b, packages_c } context 'when mixed with release versions' do - let_it_be(:package_e_release) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1') } + let_it_be(:package_e_release) do + create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1') + end it { expect_search_results 4, package_a, packages_b, packages_c, package_e_release } end @@ -126,7 +130,7 @@ RSpec.describe Packages::Nuget::SearchService, feature_category: :package_regist context 'with project' do let(:target) { project } - before do + before_all do project.add_developer(user) end @@ -136,7 +140,7 @@ RSpec.describe Packages::Nuget::SearchService, feature_category: :package_regist context 'with subgroup' do let(:target) { subgroup } - before do + before_all do subgroup.add_developer(user) end @@ -146,11 +150,38 @@ RSpec.describe Packages::Nuget::SearchService, feature_category: :package_regist context 'with group' do let(:target) { group } - before do - group.add_developer(user) + context 'when user is a group member' do + before_all do + group.add_developer(user) + end + + it_behaves_like 'handling all the conditions' end - it_behaves_like 'handling all the conditions' + context 'when user is not a group member' do + context 'with public registry in private group' do + before_all do + [subgroup, group, project].each do |entity| + entity.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(:PRIVATE, false)) + end + project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC) + end + + before do + stub_application_setting(package_registry_allow_anyone_to_pull_option: true) + end + + it_behaves_like 'handling all the conditions' + + context 'when feaure flag is disabled' do + before do + stub_feature_flags(allow_anyone_to_pull_public_nuget_packages_on_group_level: false) + end + + it { expect_search_results 0, [] } + end + end + end end def expect_search_results(total_count, *results) diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 3badecf5389..f77c6baca5c 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -7978,7 +7978,6 @@ - './spec/services/packages/npm/create_tag_service_spec.rb' - './spec/services/packages/nuget/create_dependency_service_spec.rb' - './spec/services/packages/nuget/metadata_extraction_service_spec.rb' -- './spec/services/packages/nuget/search_service_spec.rb' - './spec/services/packages/nuget/sync_metadatum_service_spec.rb' - './spec/services/packages/nuget/update_package_from_metadata_service_spec.rb' - './spec/services/packages/pypi/create_package_service_spec.rb' diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index f546f82092a..27218cfae0d 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -490,17 +490,31 @@ RSpec.shared_examples 'allows anyone to pull public nuget packages on group leve let_it_be(:package_name) { 'dummy.package' } let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } + let(:not_found_response) { :not_found } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - before do + shared_examples 'successfull reponse' do + it 'returns a successfull response' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to match_schema(json_schema) + end + end + + before_all do [subgroup, group, project].each do |entity| entity.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(:PRIVATE, false)) end project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC) + end + + before do stub_application_setting(package_registry_allow_anyone_to_pull_option: true) end - it_behaves_like 'returning response status', :ok + it_behaves_like 'successfull reponse' context 'when target package is in a private registry and group has another public registry' do let(:other_project) { create(:project, group: target, visibility_level: target.visibility_level) } @@ -510,14 +524,24 @@ RSpec.shared_examples 'allows anyone to pull public nuget packages on group leve other_project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC) end - it_behaves_like 'returning response status', :not_found + it 'returns no packages' do + subject + + expect(response).to have_gitlab_http_status(not_found_response) + + if not_found_response == :ok + expect(json_response).to match_schema(json_schema) + expect(json_response['totalHits']).to eq(0) + expect(json_response['data']).to be_empty + end + end context 'when package is in the project with public registry' do before do package.update!(project: other_project) end - it_behaves_like 'returning response status', :ok + it_behaves_like 'successfull reponse' end end