diff options
70 files changed, 866 insertions, 251 deletions
@@ -652,4 +652,4 @@ gem 'net-http', '= 0.1.1' # rubocop:todo Gemfile/MissingFeatureCategory gem 'duo_api', '~> 1.3' # rubocop:todo Gemfile/MissingFeatureCategory -gem 'gitlab-sdk', feature_category: :application_instrumentation +gem 'gitlab-sdk', '~> 0.3.0', feature_category: :application_instrumentation diff --git a/Gemfile.checksum b/Gemfile.checksum index e5cf7f7719a..709edff1329 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -221,7 +221,7 @@ {"name":"gitlab-mail_room","version":"0.0.24","platform":"ruby","checksum":"c7bf3df73dbcc024bc98dbf72514520ac2ff2b6d0124de496279fe56c13c3cb3"}, {"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"}, {"name":"gitlab-net-dns","version":"0.9.2","platform":"ruby","checksum":"f726d978479d43810819f12a45c0906d775a07e34df111bbe693fffbbef3059d"}, -{"name":"gitlab-sdk","version":"0.2.3","platform":"ruby","checksum":"e891278a20860ab1f861312813dce5f2e73081bcc10def2ae4ee138b10a2d0d6"}, +{"name":"gitlab-sdk","version":"0.3.0","platform":"ruby","checksum":"22260f148451155c2e7bdfa1ea9f3e50061a7c31700cb80f8859713560b88903"}, {"name":"gitlab-styles","version":"11.0.0","platform":"ruby","checksum":"0dd8ec066ce9955ac51d3616c6bfded30f75bb526f39ff392ece6f43d5b9406b"}, {"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"}, {"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"}, diff --git a/Gemfile.lock b/Gemfile.lock index 9d9617019d7..f5886bf39d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -712,7 +712,7 @@ GEM redis-namespace (>= 1.8.2) gitlab-markup (1.9.0) gitlab-net-dns (0.9.2) - gitlab-sdk (0.2.3) + gitlab-sdk (0.3.0) activesupport (>= 5.2.0) rake (~> 13.0) snowplow-tracker (~> 0.8.0) @@ -1903,7 +1903,7 @@ DEPENDENCIES gitlab-rspec_flaky! gitlab-safe_request_store! gitlab-schema-validation! - gitlab-sdk + gitlab-sdk (~> 0.3.0) gitlab-secret_detection! gitlab-sidekiq-fetcher! gitlab-styles (~> 11.0.0) diff --git a/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue index 841a80b6ce4..59a3e715827 100644 --- a/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue +++ b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue @@ -16,7 +16,7 @@ export default { project: { title: s__('GroupsEmptyState|Create new project'), description: s__( - 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.', + 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of GitLab.', ), }, }, diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js index 3e05e4d0a4c..efddc16dec5 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/index.js +++ b/app/assets/javascripts/organizations/groups_and_projects/index.js @@ -28,6 +28,7 @@ export const initOrganizationsGroupsAndProjects = () => { dataset: { appData }, } = el; const { + organizationGid, projectsEmptyStateSvgPath, groupsEmptyStateSvgPath, newGroupPath, @@ -46,6 +47,7 @@ export const initOrganizationsGroupsAndProjects = () => { apolloProvider, router, provide: { + organizationGid, projectsEmptyStateSvgPath, groupsEmptyStateSvgPath, newGroupPath, diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js index 92381087917..56aa0ea28a9 100644 --- a/app/assets/javascripts/organizations/mock_data.js +++ b/app/assets/javascripts/organizations/mock_data.js @@ -33,105 +33,123 @@ export const organizations = [ }, ]; -export const organizationProjects = { - nodes: [ - { - id: 'gid://gitlab/Project/8', - nameWithNamespace: 'Twitter / Typeahead.Js', - webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js', - topics: ['JavaScript', 'Vue.js', 'GraphQL', 'Jest', 'CSS', 'HTML'], - forksCount: 4, - avatarUrl: null, - starCount: 0, - visibility: 'public', - openIssuesCount: 48, - descriptionHtml: - '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi. Sed sit amet iaculis neque. Morbi vel convallis elit. Aliquam vitae arcu orci. Aenean sem velit, dapibus eget enim id, tempor lobortis orci. Pellentesque dignissim nec velit eget sagittis. Maecenas lectus sapien, tincidunt ac cursus a, aliquam eu ipsum. Aliquam posuere maximus augue, ut vehicula elit vulputate condimentum. In libero leo, vehicula nec risus in, ullamcorper convallis risus. Phasellus sit amet lectus sit amet sem volutpat cursus. Nullam facilisis nulla nec lacus pretium, in pretium ex aliquam.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - isForked: true, - accessLevel: { - integerValue: 30, - }, +export const organizationProjects = [ + { + id: 'gid://gitlab/Project/8', + nameWithNamespace: 'Twitter / Typeahead.Js', + webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js', + topics: ['JavaScript', 'Vue.js', 'GraphQL', 'Jest', 'CSS', 'HTML'], + forksCount: 4, + avatarUrl: null, + starCount: 0, + visibility: 'public', + openMergeRequestsCount: 5, + openIssuesCount: 48, + descriptionHtml: + '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi. Sed sit amet iaculis neque. Morbi vel convallis elit. Aliquam vitae arcu orci. Aenean sem velit, dapibus eget enim id, tempor lobortis orci. Pellentesque dignissim nec velit eget sagittis. Maecenas lectus sapien, tincidunt ac cursus a, aliquam eu ipsum. Aliquam posuere maximus augue, ut vehicula elit vulputate condimentum. In libero leo, vehicula nec risus in, ullamcorper convallis risus. Phasellus sit amet lectus sit amet sem volutpat cursus. Nullam facilisis nulla nec lacus pretium, in pretium ex aliquam.</p>', + mergeRequestsAccessLevel: { + stringValue: 'ENABLED', }, - { - id: 'gid://gitlab/Project/7', - nameWithNamespace: 'Flightjs / Flight', - webUrl: 'http://127.0.0.1:3000/flightjs/Flight', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'private', - openIssuesCount: 37, - descriptionHtml: - '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - isForked: false, - accessLevel: { - integerValue: 20, - }, + issuesAccessLevel: { + stringValue: 'ENABLED', }, - { - id: 'gid://gitlab/Project/6', - nameWithNamespace: 'Jashkenas / Underscore', - webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'private', - openIssuesCount: 34, - descriptionHtml: - '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - isForked: false, - accessLevel: { - integerValue: 40, - }, + forkingAccessLevel: { + stringValue: 'ENABLED', }, - { - id: 'gid://gitlab/Project/5', - nameWithNamespace: 'Commit451 / Lab Coat', - webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'internal', - openIssuesCount: 49, - descriptionHtml: - '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - isForked: false, - accessLevel: { - integerValue: 10, - }, + }, + { + id: 'gid://gitlab/Project/7', + nameWithNamespace: 'Flightjs / Flight', + webUrl: 'http://127.0.0.1:3000/flightjs/Flight', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'private', + openMergeRequestsCount: 10, + openIssuesCount: 37, + descriptionHtml: + '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>', + mergeRequestsAccessLevel: { + stringValue: 'ENABLED', }, - { - id: 'gid://gitlab/Project/1', - nameWithNamespace: 'Toolbox / Gitlab Smoke Tests', - webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'internal', - openIssuesCount: 34, - descriptionHtml: - '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - isForked: false, - accessLevel: { - integerValue: 30, - }, + issuesAccessLevel: { + stringValue: 'ENABLED', }, - ], -}; + forkingAccessLevel: { + stringValue: 'ENABLED', + }, + }, + { + id: 'gid://gitlab/Project/6', + nameWithNamespace: 'Jashkenas / Underscore', + webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'private', + openMergeRequestsCount: 0, + openIssuesCount: 34, + descriptionHtml: + '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>', + mergeRequestsAccessLevel: { + stringValue: 'ENABLED', + }, + issuesAccessLevel: { + stringValue: 'ENABLED', + }, + forkingAccessLevel: { + stringValue: 'ENABLED', + }, + }, + { + id: 'gid://gitlab/Project/5', + nameWithNamespace: 'Commit451 / Lab Coat', + webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'internal', + openMergeRequestsCount: 3, + openIssuesCount: 49, + descriptionHtml: + '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>', + mergeRequestsAccessLevel: { + stringValue: 'ENABLED', + }, + issuesAccessLevel: { + stringValue: 'ENABLED', + }, + forkingAccessLevel: { + stringValue: 'ENABLED', + }, + }, + { + id: 'gid://gitlab/Project/1', + nameWithNamespace: 'Toolbox / Gitlab Smoke Tests', + webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'internal', + openMergeRequestsCount: 20, + openIssuesCount: 34, + descriptionHtml: + '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>', + mergeRequestsAccessLevel: { + stringValue: 'ENABLED', + }, + issuesAccessLevel: { + stringValue: 'ENABLED', + }, + forkingAccessLevel: { + stringValue: 'ENABLED', + }, + }, +]; export const organizationGroups = { nodes: [ diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue index 323a8895821..5feb871740a 100644 --- a/app/assets/javascripts/organizations/shared/components/projects_view.vue +++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue @@ -14,7 +14,7 @@ export default { emptyState: { title: s__("Organization|You don't have any projects yet."), description: s__( - 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.', + 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of GitLab.', ), primaryButtonText: __('New project'), }, @@ -25,6 +25,7 @@ export default { GlEmptyState, }, inject: { + organizationGid: {}, projectsEmptyStateSvgPath: {}, newProjectPath: { default: null, @@ -44,14 +45,26 @@ export default { }, data() { return { - projects: [], + projects: {}, }; }, apollo: { projects: { query: projectsQuery, - update(data) { - return formatProjects(data.organization.projects.nodes); + variables() { + return { + id: this.organizationGid, + }; + }, + update({ + organization: { + projects: { nodes, pageInfo }, + }, + }) { + return { + nodes: formatProjects(nodes), + pageInfo, + }; }, error(error) { createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); @@ -59,6 +72,9 @@ export default { }, }, computed: { + nodes() { + return this.projects.nodes || []; + }, isLoading() { return this.$apollo.queries.projects.loading; }, @@ -87,8 +103,8 @@ export default { <template> <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> <projects-list - v-else-if="projects.length" - :projects="projects" + v-else-if="nodes.length" + :projects="nodes" show-project-icon :list-item-class="listItemClass" /> diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql index 2a7971e1106..caf82ef90ca 100644 --- a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql +++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql @@ -1,5 +1,5 @@ -query getOrganizationProjects { - organization @client { +query getOrganizationProjects($id: OrganizationsOrganizationID!) { + organization(id: $id) { id projects { nodes { @@ -11,13 +11,17 @@ query getOrganizationProjects { avatarUrl starCount visibility + openMergeRequestsCount openIssuesCount descriptionHtml - issuesAccessLevel - forkingAccessLevel - isForked - accessLevel { - integerValue + mergeRequestsAccessLevel { + stringValue + } + issuesAccessLevel { + stringValue + } + forkingAccessLevel { + stringValue } } } diff --git a/app/assets/javascripts/organizations/shared/utils.js b/app/assets/javascripts/organizations/shared/utils.js index c1aafefc553..fd172f09ec9 100644 --- a/app/assets/javascripts/organizations/shared/utils.js +++ b/app/assets/javascripts/organizations/shared/utils.js @@ -2,19 +2,28 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants'; export const formatProjects = (projects) => - projects.map(({ id, nameWithNamespace, accessLevel, webUrl, ...project }) => ({ - ...project, - id: getIdFromGraphQLId(id), - name: nameWithNamespace, - permissions: { - projectAccess: { - accessLevel: accessLevel.integerValue, - }, - }, - webUrl, - editPath: `${webUrl}/edit`, - availableActions: [ACTION_EDIT, ACTION_DELETE], - })); + projects.map( + ({ + id, + nameWithNamespace, + mergeRequestsAccessLevel, + issuesAccessLevel, + forkingAccessLevel, + webUrl, + ...project + }) => ({ + ...project, + id: getIdFromGraphQLId(id), + name: nameWithNamespace, + mergeRequestsAccessLevel: mergeRequestsAccessLevel.stringValue, + issuesAccessLevel: issuesAccessLevel.stringValue, + forkingAccessLevel: forkingAccessLevel.stringValue, + webUrl, + isForked: false, + editPath: `${webUrl}/edit`, + availableActions: [ACTION_EDIT, ACTION_DELETE], + }), + ); export const formatGroups = (groups) => groups.map(({ id, webUrl, ...group }) => ({ diff --git a/app/assets/javascripts/organizations/show/index.js b/app/assets/javascripts/organizations/show/index.js index 83a9c37e325..8265b6ef8bf 100644 --- a/app/assets/javascripts/organizations/show/index.js +++ b/app/assets/javascripts/organizations/show/index.js @@ -28,6 +28,7 @@ export const initOrganizationsShow = () => { dataset: { appData }, } = el; const { + organizationGid, organization, groupsAndProjectsOrganizationPath, projectsEmptyStateSvgPath, @@ -49,6 +50,7 @@ export const initOrganizationsShow = () => { apolloProvider, router, provide: { + organizationGid, projectsEmptyStateSvgPath, groupsEmptyStateSvgPath, newGroupPath, diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue index 3a077d09e40..119799f55e4 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue @@ -31,6 +31,7 @@ export default { stars: __('Stars'), forks: __('Forks'), issues: __('Issues'), + mergeRequests: __('Merge requests'), archived: __('Archived'), topics: __('Topics'), topicsPopoverTargetText: __('+ %{count} more'), @@ -121,20 +122,32 @@ export default { starsHref() { return `${this.project.webUrl}/-/starrers`; }, + mergeRequestsHref() { + return `${this.project.webUrl}/-/merge_requests`; + }, forksHref() { return `${this.project.webUrl}/-/forks`; }, issuesHref() { return `${this.project.webUrl}/-/issues`; }, + isMergeRequestsEnabled() { + return ( + this.project.mergeRequestsAccessLevel?.toLowerCase() === FEATURABLE_ENABLED && + this.project.openMergeRequestsCount !== undefined + ); + }, isForkingEnabled() { return ( - this.project.forkingAccessLevel === FEATURABLE_ENABLED && + this.project.forkingAccessLevel?.toLowerCase() === FEATURABLE_ENABLED && this.project.forksCount !== undefined ); }, isIssuesEnabled() { - return this.project.issuesAccessLevel === FEATURABLE_ENABLED; + return ( + this.project.issuesAccessLevel?.toLowerCase() === FEATURABLE_ENABLED && + this.project.openIssuesCount !== undefined + ); }, hasTopics() { return this.project.topics.length; @@ -148,6 +161,13 @@ export default { starCount() { return numberToMetricPrefix(this.project.starCount); }, + openMergeRequestsCount() { + if (!this.isMergeRequestsEnabled) { + return null; + } + + return numberToMetricPrefix(this.project.openMergeRequestsCount); + }, forksCount() { if (!this.isForkingEnabled) { return null; @@ -328,6 +348,16 @@ export default { <span>{{ forksCount }}</span> </gl-link> <gl-link + v-if="isMergeRequestsEnabled" + v-gl-tooltip="$options.i18n.mergeRequests" + :href="mergeRequestsHref" + :aria-label="$options.i18n.mergeRequests" + class="gl-text-secondary" + > + <gl-icon name="git-merge" /> + <span>{{ openMergeRequestsCount }}</span> + </gl-link> + <gl-link v-if="isIssuesEnabled" v-gl-tooltip="$options.i18n.issues" :href="issuesHref" @@ -360,6 +390,7 @@ export default { v-model="isDeleteModalVisible" :confirm-phrase="project.name" :is-fork="project.isForked" + :merge-requests-count="openMergeRequestsCount" :issues-count="openIssuesCount" :forks-count="forksCount" :stars-count="starCount" diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index de2f7887237..771178873e4 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -345,7 +345,12 @@ export default { </span> <slot name="timeframe"></slot> </span> - <p v-if="labels.length" role="group" :aria-label="__('Labels')" class="gl-mt-1 gl-mb-0"> + <p + v-if="labels.length" + role="group" + :aria-label="__('Labels')" + class="gl-mt-1 gl-mb-0 gl-display-flex gl-flex-wrap gl-gap-2" + > <gl-label v-for="(label, index) in labels" :key="index" @@ -354,7 +359,6 @@ export default { :description="label.description" :scoped="scopedLabel(label)" :target="labelTarget(label)" - class="gl-mr-2" size="sm" /> </p> diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 05ba317057d..c24b41f0643 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -125,10 +125,11 @@ class Import::FogbugzController < Import::BaseController end def verify_blocked_uri - Gitlab::UrlBlocker.validate!( + Gitlab::HTTP_V2::UrlBlocker.validate!( params[:uri], allow_localhost: allow_local_requests?, allow_local_network: allow_local_requests?, + deny_all_requests_except_allowed: deny_all_requests_except_allowed?, schemes: %w[http https] ) rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e @@ -138,4 +139,8 @@ class Import::FogbugzController < Import::BaseController def allow_local_requests? Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? end + + def deny_all_requests_except_allowed? + Gitlab::CurrentSettings.deny_all_requests_except_allowed? + end end diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index 445dd3a1f6f..85f31e4cb99 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -14,7 +14,7 @@ module Organizations projects: 5, users: 1050 } - }.merge(shared_groups_and_projects_app_data).to_json + }.merge(shared_groups_and_projects_app_data(organization)).to_json end def organization_new_app_data @@ -28,8 +28,8 @@ module Organizations }.merge(shared_new_settings_general_app_data).to_json end - def organization_groups_and_projects_app_data - shared_groups_and_projects_app_data.to_json + def organization_groups_and_projects_app_data(organization) + shared_groups_and_projects_app_data(organization).to_json end def organization_index_app_data @@ -54,8 +54,9 @@ module Organizations private - def shared_groups_and_projects_app_data + def shared_groups_and_projects_app_data(organization) { + organization_gid: organization.to_global_id, projects_empty_state_svg_path: image_path('illustrations/empty-state/empty-projects-md.svg'), groups_empty_state_svg_path: image_path('illustrations/empty-state/empty-groups-md.svg'), new_group_path: new_group_path, diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb index dcbda8d1ed0..eaf492e7768 100644 --- a/app/models/integrations/mattermost_slash_commands.rb +++ b/app/models/integrations/mattermost_slash_commands.rb @@ -46,7 +46,11 @@ module Integrations end def redirect_url(team, channel, url) - return if Gitlab::UrlBlocker.blocked_url?(url, schemes: %w[http https], enforce_sanitization: true) + return if Gitlab::HTTP_V2::UrlBlocker.blocked_url?( + url, + schemes: %w[http https], + enforce_sanitization: true, + deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?) origin = Addressable::URI.parse(url).origin format(MATTERMOST_URL, ORIGIN: origin, TEAM: team, CHANNEL: channel) diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index b453098e27a..ec9a4f9f4a6 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -8,18 +8,19 @@ module Members DEFAULT_INVITE_LIMIT = 100 - attr_reader :membership_locked + attr_reader :membership_locked, :http_status def initialize(*args) super @errors = [] + @http_status = nil @invites = invites_from_params @source = params[:source] end def execute - raise Gitlab::Access::AccessDeniedError unless can?(current_user, create_member_permission(source), source) + validate_source_type! if adding_at_least_one_owner && cannot_assign_owner_responsibilities_to_member_in_project? raise Gitlab::Access::AccessDeniedError @@ -65,6 +66,10 @@ module Members params[:user_id].to_s.split(',').uniq end + def validate_source_type! + raise "Unknown source type: #{source.class}!" unless source.is_a?(Group) || source.is_a?(Project) + end + def validate_invite_source! raise ArgumentError, s_('AddMember|No invite source provided.') unless invite_source.present? end @@ -102,6 +107,7 @@ module Members end def process_result(member) + @http_status = :unauthorized if member.errors.added? :base, :unauthorized existing_errors = member.errors.full_messages # calling invalid? clears any errors that were added outside of the @@ -174,7 +180,7 @@ module Members def result if errors.any? - error(formatted_errors) + error(formatted_errors, http_status) else success end @@ -194,17 +200,6 @@ module Members }) ) end - - def create_member_permission(source) - case source - when Group - :admin_group_member - when Project - :admin_project_member - else - raise "Unknown source type: #{source.class}!" - end - end end end diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb index d7bf073d8e9..57159c14b3b 100644 --- a/app/services/members/creator_service.rb +++ b/app/services/members/creator_service.rb @@ -176,7 +176,7 @@ module Members end end - # overridden in Members::Groups::CreatorService + # overridden in EE:Members::Groups::CreatorService def member_role_too_high? false end @@ -243,7 +243,7 @@ module Members _('not authorized to update member') end - member.errors.add(:base, msg) + member.errors.add(:base, :unauthorized, message: msg) end def add_member_role_error diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index d4cc60c6de0..14cdb83b4e5 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -185,7 +185,7 @@ module Members def destroy_member_permission(member) case member when GroupMember - :destroy_group_member + destroy_group_member_permission(member) when ProjectMember :destroy_project_member else @@ -193,6 +193,11 @@ module Members end end + # overridden in EE::Members::DestroyService + def destroy_group_member_permission(_member) + :destroy_group_member + end + def destroy_bot_member_permission(member) raise "Unsupported bot member type: #{member}" unless member.is_a?(ProjectMember) diff --git a/app/views/organizations/organizations/groups_and_projects.html.haml b/app/views/organizations/organizations/groups_and_projects.html.haml index a993e1c9404..006048d4e5f 100644 --- a/app/views/organizations/organizations/groups_and_projects.html.haml +++ b/app/views/organizations/organizations/groups_and_projects.html.haml @@ -1,3 +1,3 @@ - page_title _('Groups and projects') -#js-organizations-groups-and-projects{ data: { app_data: organization_groups_and_projects_app_data } } +#js-organizations-groups-and-projects{ data: { app_data: organization_groups_and_projects_app_data(@organization) } } diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 21a74d30ba5..0327f923ddb 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -38,7 +38,7 @@ = sprite_icon('branch', size: 12, css_class: 'fork-sprite') = merge_request.target_branch - if merge_request.labels.any? - .gl-mt-1{ role: 'group', 'aria-label': _('Labels') } + .gl-mt-1.gl-display-flex.gl-flex-wrap.gl-gap-2{ role: 'group', 'aria-label': _('Labels') } - presented_labels_sorted_by_title(merge_request.labels, merge_request.project).each do |label| = link_to_label(label, type: :merge_request, small: true) diff --git a/config/initializers/rest-client-hostname_override.rb b/config/initializers/rest-client-hostname_override.rb index 41988fad282..c12c0d431fd 100644 --- a/config/initializers/rest-client-hostname_override.rb +++ b/config/initializers/rest-client-hostname_override.rb @@ -7,10 +7,13 @@ module RestClient module UrlBlocker def transmit(uri, req, payload, &block) begin - ip, hostname_override = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_settings_local_requests?, - allow_localhost: allow_settings_local_requests?, - dns_rebind_protection: dns_rebind_protection?, - schemes: %w[http https]) + ip, hostname_override = Gitlab::HTTP_V2::UrlBlocker.validate!( + uri, + allow_local_network: allow_settings_local_requests?, + allow_localhost: allow_settings_local_requests?, + dns_rebind_protection: dns_rebind_protection?, + deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?, + schemes: %w[http https]) self.hostname_override = hostname_override rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index c67e69ab692..cca7c4d8150 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -209,6 +209,8 @@ - 1 - - compliance_management_standards_gitlab_prevent_approval_by_committer_group - 1 +- - compliance_management_standards_refresh + - 1 - - compliance_management_update_default_framework - 1 - - compliance_management_violation_export_mailer diff --git a/db/click_house/migrate/main/20240115122100_drop_audit_events.rb b/db/click_house/migrate/main/20240115122100_drop_audit_events.rb new file mode 100644 index 00000000000..198e83ff7ee --- /dev/null +++ b/db/click_house/migrate/main/20240115122100_drop_audit_events.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class DropAuditEvents < ClickHouse::Migration + def up + execute <<~SQL + DROP TABLE IF EXISTS audit_events; + SQL + end + + def down + execute <<~SQL + CREATE TABLE IF NOT EXISTS audit_events + ( + id UInt64 DEFAULT 0, + author_id UInt64 DEFAULT 0, + author_name String DEFAULT '', + created_at DateTime64(6, 'UTC') DEFAULT now(), + details String DEFAULT '', + entity_id UInt64 DEFAULT 0, + entity_path String DEFAULT '', + entity_type LowCardinality(String) DEFAULT '', + ip_address String DEFAULT '', + target_details String DEFAULT '', + target_id UInt64 DEFAULT 0, + target_type LowCardinality(String) DEFAULT '', + is_deleted UInt8 DEFAULT 0, + ) ENGINE = ReplacingMergeTree(created_at, is_deleted) + PARTITION BY toYear(created_at) + ORDER BY (entity_type, entity_id, author_id, created_at, id); + SQL + + execute <<~SQL + ALTER TABLE audit_events + ADD PROJECTION IF NOT EXISTS by_id (SELECT * ORDER BY id); + SQL + end +end diff --git a/db/click_house/migrate/main/20240115162101_recreate_audit_events.rb b/db/click_house/migrate/main/20240115162101_recreate_audit_events.rb new file mode 100644 index 00000000000..0fcef508c4f --- /dev/null +++ b/db/click_house/migrate/main/20240115162101_recreate_audit_events.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class RecreateAuditEvents < ClickHouse::Migration + def up + execute <<~SQL + CREATE TABLE IF NOT EXISTS audit_events + ( + id UInt64 DEFAULT 0, + author_id Int64 DEFAULT 0, + author_name String DEFAULT '', + created_at DateTime64(6, 'UTC') DEFAULT now(), + details String DEFAULT '', + entity_id Int64 DEFAULT 0, + entity_path String DEFAULT '', + entity_type LowCardinality(String) DEFAULT '', + ip_address String DEFAULT '', + target_details String DEFAULT '', + target_id Int64 DEFAULT 0, + target_type LowCardinality(String) DEFAULT '', + is_deleted UInt8 DEFAULT 0, + ) ENGINE = ReplacingMergeTree(created_at, is_deleted) + PARTITION BY toYear(created_at) + ORDER BY (entity_type, entity_id, author_id, created_at, id); + SQL + + execute <<~SQL + ALTER TABLE audit_events + ADD PROJECTION IF NOT EXISTS by_id (SELECT * ORDER BY id); + SQL + end + + def down + execute <<~SQL + DROP TABLE audit_events + SQL + end +end diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml index 6dccaeeeb50..a09e0ee8d05 100644 --- a/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml +++ b/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml @@ -5,3 +5,4 @@ feature_category: source_code_management introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134337 milestone: '16.6' queued_migration_version: 20231016173129 +finalized_by: 20240111195101 diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml index 72e59e6e00f..c884d7cfbf8 100644 --- a/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml +++ b/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml @@ -5,3 +5,4 @@ feature_category: source_code_management introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134337 milestone: '16.6' queued_migration_version: 20231016194927 +finalized_by: 20240111194808 diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml index 35c76b78894..41bf2ab9e03 100644 --- a/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml +++ b/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml @@ -5,3 +5,4 @@ feature_category: source_code_management introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134337 milestone: '16.6' queued_migration_version: 20231016194943 +finalized_by: 20240111194603 diff --git a/db/docs/deleted_tables/ci_editor_ai_conversation_messages.yml b/db/docs/deleted_tables/ci_editor_ai_conversation_messages.yml index e37b9bfc75c..dd7a4a6b45c 100644 --- a/db/docs/deleted_tables/ci_editor_ai_conversation_messages.yml +++ b/db/docs/deleted_tables/ci_editor_ai_conversation_messages.yml @@ -4,8 +4,7 @@ classes: - Ci::Editor::AiConversation::Message feature_categories: - pipeline_composition -description: Represents an ai message for a user and project for the pipeline editor - bot. +description: Represents an ai message for a user and project for the pipeline editor bot. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119840 milestone: '16.0' gitlab_schema: gitlab_ci diff --git a/db/post_migrate/20231215130625_schedule_index_to_events_author_group_action_target_type_created_at.rb b/db/post_migrate/20231215130625_schedule_index_to_events_author_group_action_target_type_created_at.rb index 063aff58176..1e3658aa697 100644 --- a/db/post_migrate/20231215130625_schedule_index_to_events_author_group_action_target_type_created_at.rb +++ b/db/post_migrate/20231215130625_schedule_index_to_events_author_group_action_target_type_created_at.rb @@ -6,7 +6,6 @@ class ScheduleIndexToEventsAuthorGroupActionTargetTypeCreatedAt < Gitlab::Databa INDEX_NAME = 'index_events_author_id_group_id_action_target_type_created_at' COLUMNS = [:author_id, :group_id, :action, :target_type, :created_at] - # TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/435524 def up prepare_async_index :events, COLUMNS, name: INDEX_NAME end diff --git a/db/post_migrate/20240111131500_add_async_index_merge_request_metrics_on_merged_by_id_target_project_id_m_r_id.rb b/db/post_migrate/20240111131500_add_async_index_merge_request_metrics_on_merged_by_id_target_project_id_m_r_id.rb new file mode 100644 index 00000000000..cec9020f20b --- /dev/null +++ b/db/post_migrate/20240111131500_add_async_index_merge_request_metrics_on_merged_by_id_target_project_id_m_r_id.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddAsyncIndexMergeRequestMetricsOnMergedByIdTargetProjectIdMRId < Gitlab::Database::Migration[2.2] + milestone '16.9' + + TABLE_NAME = :merge_request_metrics + INDEX_NAME = :idx_merge_request_metrics_on_merged_by_project_and_mr + INDEX_COLUMNS = %i[merged_by_id target_project_id merge_request_id] + + def up + prepare_async_index TABLE_NAME, INDEX_COLUMNS, name: INDEX_NAME + end + + def down + unprepare_async_index TABLE_NAME, INDEX_COLUMNS, name: INDEX_NAME + end +end diff --git a/db/post_migrate/20240111194603_finalize_delete_invalid_protected_tag_create_access_levels.rb b/db/post_migrate/20240111194603_finalize_delete_invalid_protected_tag_create_access_levels.rb new file mode 100644 index 00000000000..9aeb09f8dde --- /dev/null +++ b/db/post_migrate/20240111194603_finalize_delete_invalid_protected_tag_create_access_levels.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class FinalizeDeleteInvalidProtectedTagCreateAccessLevels < Gitlab::Database::Migration[2.2] + milestone '16.8' + MIGRATION = 'DeleteInvalidProtectedTagCreateAccessLevels' + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + ensure_batched_background_migration_is_finished( + job_class_name: MIGRATION, + table_name: :protected_tag_create_access_levels, + column_name: :id, + job_arguments: [], + finalize: true + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20240111194658_drop_temp_index_on_protected_tag_create_access_levels.rb b/db/post_migrate/20240111194658_drop_temp_index_on_protected_tag_create_access_levels.rb new file mode 100644 index 00000000000..6e20d4e4a11 --- /dev/null +++ b/db/post_migrate/20240111194658_drop_temp_index_on_protected_tag_create_access_levels.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class DropTempIndexOnProtectedTagCreateAccessLevels < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '16.8' + + INDEX_NAME = 'tmp_idx_protected_tag_create_access_levels_on_id_with_group' + + def up + remove_concurrent_index_by_name( + :protected_tag_create_access_levels, + INDEX_NAME + ) + end + + def down + add_concurrent_index( + :protected_tag_create_access_levels, + %i[id], + where: 'group_id IS NOT NULL', + name: INDEX_NAME + ) + end +end diff --git a/db/post_migrate/20240111194808_finalize_delete_invalid_protected_branch_push_access_levels.rb b/db/post_migrate/20240111194808_finalize_delete_invalid_protected_branch_push_access_levels.rb new file mode 100644 index 00000000000..1af26ecf62a --- /dev/null +++ b/db/post_migrate/20240111194808_finalize_delete_invalid_protected_branch_push_access_levels.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class FinalizeDeleteInvalidProtectedBranchPushAccessLevels < Gitlab::Database::Migration[2.2] + milestone '16.8' + MIGRATION = 'DeleteInvalidProtectedBranchPushAccessLevels' + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + ensure_batched_background_migration_is_finished( + job_class_name: MIGRATION, + table_name: :protected_branch_push_access_levels, + column_name: :id, + job_arguments: [], + finalize: true + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20240111194925_drop_temp_index_on_protected_branch_push_access_levels.rb b/db/post_migrate/20240111194925_drop_temp_index_on_protected_branch_push_access_levels.rb new file mode 100644 index 00000000000..4c6f96a97c1 --- /dev/null +++ b/db/post_migrate/20240111194925_drop_temp_index_on_protected_branch_push_access_levels.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class DropTempIndexOnProtectedBranchPushAccessLevels < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '16.8' + + INDEX_NAME = 'tmp_idx_protected_branch_push_access_levels_on_id_with_group' + + def up + remove_concurrent_index_by_name( + :protected_branch_push_access_levels, + INDEX_NAME + ) + end + + def down + add_concurrent_index( + :protected_branch_push_access_levels, + %i[id], + where: 'group_id IS NOT NULL', + name: INDEX_NAME + ) + end +end diff --git a/db/post_migrate/20240111195101_finalize_delete_invalid_protected_branch_merge_access_levels.rb b/db/post_migrate/20240111195101_finalize_delete_invalid_protected_branch_merge_access_levels.rb new file mode 100644 index 00000000000..ca5f520c50f --- /dev/null +++ b/db/post_migrate/20240111195101_finalize_delete_invalid_protected_branch_merge_access_levels.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class FinalizeDeleteInvalidProtectedBranchMergeAccessLevels < Gitlab::Database::Migration[2.2] + milestone '16.8' + MIGRATION = 'DeleteInvalidProtectedBranchMergeAccessLevels' + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + ensure_batched_background_migration_is_finished( + job_class_name: MIGRATION, + table_name: :protected_branch_merge_access_levels, + column_name: :id, + job_arguments: [], + finalize: true + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20240111195145_drop_temp_index_on_protected_branch_merge_access_levels.rb b/db/post_migrate/20240111195145_drop_temp_index_on_protected_branch_merge_access_levels.rb new file mode 100644 index 00000000000..f2eb2aa0ea1 --- /dev/null +++ b/db/post_migrate/20240111195145_drop_temp_index_on_protected_branch_merge_access_levels.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class DropTempIndexOnProtectedBranchMergeAccessLevels < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '16.8' + + INDEX_NAME = 'tmp_idx_protected_branch_merge_access_levels_on_id_with_group' + + def up + remove_concurrent_index_by_name( + :protected_branch_merge_access_levels, + INDEX_NAME + ) + end + + def down + add_concurrent_index( + :protected_branch_merge_access_levels, + %i[id], + where: 'group_id IS NOT NULL', + name: INDEX_NAME + ) + end +end diff --git a/db/post_migrate/20240112143548_add_index_to_events_author_group_action_target_type.rb b/db/post_migrate/20240112143548_add_index_to_events_author_group_action_target_type.rb new file mode 100644 index 00000000000..88e5f95ef68 --- /dev/null +++ b/db/post_migrate/20240112143548_add_index_to_events_author_group_action_target_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexToEventsAuthorGroupActionTargetType < Gitlab::Database::Migration[2.2] + milestone '16.9' + + disable_ddl_transaction! + + INDEX_NAME = 'index_events_author_id_group_id_action_target_type_created_at' + COLUMNS = [:author_id, :group_id, :action, :target_type, :created_at] + + def up + add_concurrent_index :events, COLUMNS, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :events, INDEX_NAME + end +end diff --git a/db/schema_migrations/20240111131500 b/db/schema_migrations/20240111131500 new file mode 100644 index 00000000000..77d7e821e64 --- /dev/null +++ b/db/schema_migrations/20240111131500 @@ -0,0 +1 @@ +dc8b922002930d7227e75b50617c3f4d4949fae4c1444c92767316d97fab49be
\ No newline at end of file diff --git a/db/schema_migrations/20240111194603 b/db/schema_migrations/20240111194603 new file mode 100644 index 00000000000..8c9da9a87cb --- /dev/null +++ b/db/schema_migrations/20240111194603 @@ -0,0 +1 @@ +cd476c18128010183f94f09c4543716f2e3bdd78c11209dcec500d5506e3f7b2
\ No newline at end of file diff --git a/db/schema_migrations/20240111194658 b/db/schema_migrations/20240111194658 new file mode 100644 index 00000000000..90a6a94f4d8 --- /dev/null +++ b/db/schema_migrations/20240111194658 @@ -0,0 +1 @@ +5f10603c43a631785057b2ad8c5f4e0de84c2499b3f862b5b851e28a3c9769ea
\ No newline at end of file diff --git a/db/schema_migrations/20240111194808 b/db/schema_migrations/20240111194808 new file mode 100644 index 00000000000..69f70190f2c --- /dev/null +++ b/db/schema_migrations/20240111194808 @@ -0,0 +1 @@ +80a807f90560abd387b7579255e2301ba3b8bde4f25ca4ff1ecb83e4b3b42163
\ No newline at end of file diff --git a/db/schema_migrations/20240111194925 b/db/schema_migrations/20240111194925 new file mode 100644 index 00000000000..c6272136c31 --- /dev/null +++ b/db/schema_migrations/20240111194925 @@ -0,0 +1 @@ +7cf8a9d321e212f49de7419a3827e3c18504f7a98900738c1fb7e82661743c92
\ No newline at end of file diff --git a/db/schema_migrations/20240111195101 b/db/schema_migrations/20240111195101 new file mode 100644 index 00000000000..b1c6f09b587 --- /dev/null +++ b/db/schema_migrations/20240111195101 @@ -0,0 +1 @@ +560e3a35eee5ace51635ebc504d785b6008914316e8078a68f7cb735e14fc6c1
\ No newline at end of file diff --git a/db/schema_migrations/20240111195145 b/db/schema_migrations/20240111195145 new file mode 100644 index 00000000000..965dd52f86e --- /dev/null +++ b/db/schema_migrations/20240111195145 @@ -0,0 +1 @@ +6e6b2669c731d4922cbe42adf003b5b934f4690a5466b166b9112f05681db62b
\ No newline at end of file diff --git a/db/schema_migrations/20240112143548 b/db/schema_migrations/20240112143548 new file mode 100644 index 00000000000..0c3c4f8cc20 --- /dev/null +++ b/db/schema_migrations/20240112143548 @@ -0,0 +1 @@ +efb53be25e54b05e213256b985a9b28b3af5754763436772b6552688c066a591
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 10e32e7cdca..f17904b1c64 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -33624,6 +33624,8 @@ CREATE INDEX index_et_errors_on_project_id_and_status_first_seen_at_id_desc ON e CREATE INDEX index_et_errors_on_project_id_and_status_last_seen_at_id_desc ON error_tracking_errors USING btree (project_id, status, last_seen_at DESC, id DESC); +CREATE INDEX index_events_author_id_group_id_action_target_type_created_at ON events USING btree (author_id, group_id, action, target_type, created_at); + CREATE INDEX index_events_author_id_project_id_action_target_type_created_at ON events USING btree (author_id, project_id, action, target_type, created_at); CREATE INDEX index_events_for_followed_users ON events USING btree (author_id, target_type, action, id); @@ -36130,12 +36132,6 @@ CREATE INDEX tmp_idx_orphaned_approval_merge_request_rules ON approval_merge_req CREATE INDEX tmp_idx_orphaned_approval_project_rules ON approval_project_rules USING btree (id) WHERE ((report_type = ANY (ARRAY[2, 4])) AND (security_orchestration_policy_configuration_id IS NULL)); -CREATE INDEX tmp_idx_protected_branch_merge_access_levels_on_id_with_group ON protected_branch_merge_access_levels USING btree (id) WHERE (group_id IS NOT NULL); - -CREATE INDEX tmp_idx_protected_branch_push_access_levels_on_id_with_group ON protected_branch_push_access_levels USING btree (id) WHERE (group_id IS NOT NULL); - -CREATE INDEX tmp_idx_protected_tag_create_access_levels_on_id_with_group ON protected_tag_create_access_levels USING btree (id) WHERE (group_id IS NOT NULL); - CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL)); CREATE INDEX tmp_index_cis_vulnerability_reads_on_id ON vulnerability_reads USING btree (id) WHERE (report_type = 7); diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md index eb1ee203469..39546345355 100644 --- a/doc/administration/auth/ldap/ldap-troubleshooting.md +++ b/doc/administration/auth/ldap/ldap-troubleshooting.md @@ -393,6 +393,12 @@ the rails console. UIDs here should match the 'Identifier' from the LDAP identity checked earlier. If it doesn't, the user does not appear to be in the LDAP group. +#### Cannot add service account user to group when LDAP sync is enabled + +When LDAP sync is enabled for a group, you cannot use the "invite" dialog to invite new group members. + +To resolve this issue in GitLab 16.8 and later, you can invite service accounts to and remove them from a group using the [group members API endpoints](../../../api/members.md#add-a-member-to-a-group-or-project). + #### Administrator privileges not granted When [Administrator sync](ldap_synchronization.md#administrator-sync) has been configured diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index c150eda720c..2f33040acb0 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -13,6 +13,89 @@ Configuration for [approvals on all merge requests](../user/project/merge_requests/approvals/index.md) in the project. Must be authenticated for all endpoints. +## Group-level MR approvals **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/428051) in GitLab 16.7 [with a flag](../administration/feature_flags.md) named `approval_group_rules`. Disabled by default. This feature is an [Experiment](../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `approval_group_rules`. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +Group approval rules apply to all protected branches of projects belonging to the group. This feature is an [Experiment](../policy/experiment-beta-support.md). + +### Create group-level approval rules + +Users with at least the Maintainer role can create group level approval rules using the following endpoint: + +```plaintext +POST /groups/:id/approval_rules +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|-------------------------------------|-------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | Yes | The ID or [URL-encoded path of a group](rest/index.md#namespaced-path-encoding). | +| `approvals_required` | integer | Yes | The number of required approvals for this rule. | +| `name` | string | Yes | The name of the approval rule. | +| `group_ids` | array | No | The IDs of groups as approvers. | +| `report_type` | string | No | The report type required when the rule type is `report_approver`. The supported report types are `license_scanning` [(Deprecated in GitLab 15.9)](../update/deprecations.md#license-check-and-the-policies-tab-on-the-license-compliance-page) and `code_coverage`. | +| `rule_type` | string | No | The type of rule. `any_approver` is a pre-configured default rule with `approvals_required` at `0`. Other rules are `regular` and `report_approver`. | +| `user_ids` | array | No | The IDs of users as approvers. | + +Example request: + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ + --url "https://gitlab.example.com/api/v4/groups/29/approval_rules?name=security&approvals_required=2" +``` + +Example response: + +```json +{ + "id": 5, + "name": "security", + "rule_type": "any_approver", + "eligible_approvers": [], + "approvals_required": 2, + "users": [], + "groups": [], + "contains_hidden_groups": false, + "protected_branches": [ + { + "id": 5, + "name": "master", + "push_access_levels": [ + { + "id": 5, + "access_level": 40, + "access_level_description": "Maintainers", + "deploy_key_id": null, + "user_id": null, + "group_id": null + } + ], + "merge_access_levels": [ + { + "id": 5, + "access_level": 40, + "access_level_description": "Maintainers", + "user_id": null, + "group_id": null + } + ], + "allow_force_push": false, + "unprotect_access_levels": [], + "code_owner_approval_required": false, + "inherited": false + } + ], + "applies_to_all_protected_branches": true +} +``` + ## Project-level MR approvals ### Get Configuration diff --git a/doc/ci/triggers/index.md b/doc/ci/triggers/index.md index 4eee34af402..49ff0ee2356 100644 --- a/doc/ci/triggers/index.md +++ b/doc/ci/triggers/index.md @@ -39,10 +39,12 @@ To create a trigger token: - You can only see the first 4 characters for tokens created by other project members. WARNING: -It is a security risk to save tokens in plain text in public projects. Potential -attackers could use a trigger token exposed in the `.gitlab-ci.yml` file to impersonate -the user that created the token. Use [masked CI/CD variables](../variables/index.md#mask-a-cicd-variable) -to improve the security of trigger tokens. +It is a security risk to save tokens in plain text in public projects, or store them +in a way that malicious users could access them. A leaked trigger token could be +used to force an unscheduled deployment, attempt to access CI/CD variables, +or other malicious uses. [Masked CI/CD variables](../variables/index.md#mask-a-cicd-variable) +help improve the security of trigger tokens. For more information about keeping tokens secure, +see the [security considerations](../../security/token_overview.md#security-considerations). ## Trigger a pipeline diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 05633cac3b0..84e9533f725 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -416,6 +416,7 @@ The following table lists group permissions available for each role: | [Migrate groups](group/import/index.md) | | | | | ✓ | | Manage [subscriptions, and purchase storage and compute minutes](../subscriptions/gitlab_com/index.md) | | | | | ✓ | | Manage group-level custom roles | | | | | ✓ | +| Manage [group approval rules](project/merge_requests/approvals/settings.md) (group settings) | | | | ✓ | ✓ | <!-- markdownlint-disable MD029 --> diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index dec42e74a58..2e6bd44ff3b 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -85,6 +85,14 @@ At any time, you can revoke a personal access token. 1. In the **Active personal access tokens** area, select **Revoke** for the relevant token. 1. On the confirmation dialog, select **Revoke**. +## Disable personal access tokens **(PREMIUM SELF)** + +Prerequisites: + +- You must be an administrator. + +In GitLab 15.7 and later, you can [use the application settings API to disable personal access tokens](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls). + ## View the last time a token was used > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33162) in GitLab 13.2. Token usage information is updated every 24 hours. diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index bf8a7468199..38cf2403841 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -39,6 +39,7 @@ In addition: - An _internal_ group to a _public_ project. - A _public_ group to a _public_ project. +- If a group in the project's hierarchy [does not allow sub-projects to be shared with groups](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups), the option to **Invite a group** is not available. - If the project's root ancestor group [does not allow the project to be shared outside the hierarchy](../../group/access_and_permissions.md#prevent-group-sharing-outside-the-group-hierarchy), the invited group or subgroup must be in the project's [namespace](../../namespace/index.md). For example, a project in the namespace `group/subgroup01/project`: - Can be shared with `group/subgroup02` or `group/subgroup01/subgroup03`. @@ -129,3 +130,4 @@ A list of shared projects is displayed. ## Related topics - [Prevent a project from being shared with groups](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups). +- [Prevent group sharing outside the group hierarchy](../../group/access_and_permissions.md#prevent-group-sharing-outside-the-group-hierarchy). diff --git a/doc/user/project/merge_requests/changes.md b/doc/user/project/merge_requests/changes.md index 094d2cf5730..f1fc1bfe233 100644 --- a/doc/user/project/merge_requests/changes.md +++ b/doc/user/project/merge_requests/changes.md @@ -34,6 +34,66 @@ To view the diff of changes included in a merge request: Files with many changes are collapsed to improve performance. GitLab displays the message: **Some changes are not shown**. To view the changes for that file, select **Expand file**. +### Collapse generated files **(FREE SELF)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140180) in GitLab 16.8 [with a flag](../../../administration/feature_flags.md) named `collapse_generated_diff_files`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flag](../../../administration/feature_flags.md) +named `collapse_generated_diff_files`. +On GitLab.com, this feature is not available. + +To help reviewers focus on the files needed to perform a code review, GitLab collapses +several common types of generated files. These files are collapsed by default, because +they are unlikely to require code reviews: + +1. Files with `.nib`, `.xcworkspacedata`, or `.xcurserstate` extensions. +1. Package lock files such as `package-lock.json` or `Gopkg.lock`. +1. Files in the `node_modules` folder. +1. Minified `js` or `css` files. +1. Source map reference files. +1. Generated Go files, including the generated files by protocol buffer compiler. + +If you want to automatically collapse additional files or file types, you can use the `gitlab-generated` attribute. To mark or unmark certain files/paths as generated if the default doesn't suit +your preference. See [overriding syntax highlighting](../highlighting.md#override-syntax-highlighting-for-a-file-type) for more +detail on how to use override attributes. + +#### View a collapsed file + +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Code > Merge requests** and find your merge request. +1. Below the merge request title, select **Changes**. +1. Find the file you want to view, and select **Expand file**. + +#### Configure collapse behavior for a file type + +To change the default collapse behavior for a file type: + +1. If a `.gitattributes` file does not exist in the root directory of your project, + create a blank file with this name. +1. For each file type you want to modify, add a line to the `.gitattributes` file + declaring the file extension and your desired behavior: + + ```conf + # Collapse all files with a .txt extension + *.txt gitlab-generated + + # Collapse all files within the docs directory + docs/** gitlab-generated + + # Do not collapse package-lock.json + package-json -gitlab-generated + ``` + +1. Commit, push, and merge your changes into your default branch. + +After the changes merge into your [default branch](../repository/branches/default.md), +all files of this type in your project use this behavior in merge requests. + +For technical details about how generated files are detected, see the +[`go-enry`](https://github.com/go-enry/go-enry/blob/master/data/generated.go) repository. + ## Show one file at a time For larger merge requests, you can review one file at a time. You can change this setting diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index 1a23dcd0d3c..6d1cd9d8cd9 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -85,20 +85,30 @@ module API user_id = create_service_params[:user_id] user = User.find_by(id: user_id) # rubocop: disable CodeReuse/ActiveRecord - if user - conflict!('Member already exists') if member_already_exists?(source, user_id) + not_found!('User') unless user - instance = ::Members::CreateService.new(current_user, create_service_params) - instance.execute + conflict!('Member already exists') if member_already_exists?(source, user_id) - not_allowed! if instance.membership_locked # This currently can only be reached in EE if group membership is locked + instance = ::Members::CreateService.new(current_user, create_service_params) + result = instance.execute - member = instance.single_member - render_validation_error!(member) if member.invalid? + # This currently can only be reached in EE if group membership is locked + not_allowed! if instance.membership_locked - present_members(member) + if result[:status] == :error && result[:http_status] == :unauthorized + raise Gitlab::Access::AccessDeniedError + end + + # prefer responding with model validations, if present + member = instance.single_member + render_validation_error!(member) if member.invalid? + + # if errors occurred besides model validations or authorization failures, + # render those appropriately + if result[:status] == :error + render_structured_api_error!(result, :bad_request) else - not_found!('User') + present_members(member) end end diff --git a/lib/api/members.rb b/lib/api/members.rb index 908733d4aa1..e4bd29640cd 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -119,8 +119,6 @@ module API post ":id/members", feature_category: feature_category do source = find_source(source_type, params[:id]) - authorize_admin_source_member!(source_type, source) - create_service_params = params.merge(source: source) if add_multiple_members?(params[:user_id].to_s) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 024e188bcba..e191e928bb0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23864,7 +23864,7 @@ msgstr "" msgid "GroupsEmptyState|No subgroups or projects." msgstr "" -msgid "GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab." +msgid "GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of GitLab." msgstr "" msgid "GroupsEmptyState|You do not have necessary permissions to create a subgroup or project in this group. Please contact an owner of this group to create a new subgroup or project." diff --git a/package.json b/package.json index 3466f9594d8..a1dec1ec305 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", "@gitlab/svgs": "3.75.0", - "@gitlab/ui": "^72.5.1", + "@gitlab/ui": "^72.8.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "^0.0.1-dev-20231211152737", "@mattiasbuelens/web-streams-adapter": "^0.1.0", diff --git a/qa/qa/support/formatters/test_metrics_formatter.rb b/qa/qa/support/formatters/test_metrics_formatter.rb index f6f8940b360..aeb9da01e12 100644 --- a/qa/qa/support/formatters/test_metrics_formatter.rb +++ b/qa/qa/support/formatters/test_metrics_formatter.rb @@ -107,6 +107,7 @@ module QA status: status(example), smoke: example.metadata.key?(:smoke).to_s, reliable: example.metadata.key?(:reliable).to_s, + blocking: example.metadata.key?(:blocking).to_s, quarantined: quarantined(example.metadata), retried: (retry_attempts(example.metadata) > 0).to_s, job_name: job_name, diff --git a/qa/spec/support/formatters/test_metrics_formatter_spec.rb b/qa/spec/support/formatters/test_metrics_formatter_spec.rb index 0e8673f1c0f..79e19d1a11d 100644 --- a/qa/spec/support/formatters/test_metrics_formatter_spec.rb +++ b/qa/spec/support/formatters/test_metrics_formatter_spec.rb @@ -19,6 +19,7 @@ describe QA::Support::Formatters::TestMetricsFormatter do let(:run_type) { 'staging-full' } let(:smoke) { 'false' } let(:reliable) { 'false' } + let(:blocking) { 'false' } let(:quarantined) { 'false' } let(:influx_client) { instance_double('InfluxDB2::Client', create_write_api: influx_write_api) } let(:influx_write_api) { instance_double('InfluxDB2::WriteApi', write: nil) } @@ -48,6 +49,7 @@ describe QA::Support::Formatters::TestMetricsFormatter do status: :passed, smoke: smoke, reliable: reliable, + blocking: blocking, quarantined: quarantined, retried: 'false', job_name: 'test-job', @@ -153,6 +155,19 @@ describe QA::Support::Formatters::TestMetricsFormatter do end end + context 'with blocking spec' do + let(:blocking) { 'true' } + + it 'exports data to influxdb with correct blocking tag' do + run_spec do + it('spec', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} + end + + expect(influx_write_api).to have_received(:write).once + expect(influx_write_api).to have_received(:write).with(data: [data]) + end + end + context 'with product group tag' do let(:product_group) { :import } @@ -310,7 +325,7 @@ describe QA::Support::Formatters::TestMetricsFormatter do end end - context 'with fabrication resources' do + context 'with fabrication resources' do # rubocop:disable RSpec/MultipleMemoizedHelpers let(:fabrication_resources) do { 'QA::Resource::Project' => [{ diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js index 3cc71927bfa..c406ba2cd47 100644 --- a/spec/frontend/organizations/shared/components/projects_view_spec.js +++ b/spec/frontend/organizations/shared/components/projects_view_spec.js @@ -2,19 +2,18 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import ProjectsView from '~/organizations/shared/components/projects_view.vue'; +import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql'; import { formatProjects } from '~/organizations/shared/utils'; -import resolvers from '~/organizations/shared/graphql/resolvers'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; import { createAlert } from '~/alert'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { organizationProjects } from '~/organizations/mock_data'; +import { organizationProjects as nodes, pageInfo, pageInfoEmpty } from '~/organizations/mock_data'; jest.mock('~/alert'); Vue.use(VueApollo); -jest.useFakeTimers(); describe('ProjectsView', () => { let wrapper; @@ -23,14 +22,29 @@ describe('ProjectsView', () => { const defaultProvide = { projectsEmptyStateSvgPath: 'illustrations/empty-state/empty-projects-md.svg', newProjectPath: '/projects/new', + organizationGid: 'gid://gitlab/Organizations::Organization/1', }; const defaultPropsData = { listItemClass: 'gl-px-5', }; - const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => { - mockApollo = createMockApollo([], mockResolvers); + const projects = { + nodes, + pageInfo, + }; + + const successHandler = jest.fn().mockResolvedValue({ + data: { + organization: { + id: defaultProvide.organizationGid, + projects, + }, + }, + }); + + const createComponent = ({ handler = successHandler, propsData = {} } = {}) => { + mockApollo = createMockApollo([[projectsQuery, handler]]); wrapper = shallowMountExtended(ProjectsView, { apolloProvider: mockApollo, @@ -42,45 +56,45 @@ describe('ProjectsView', () => { }); }; + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findProjectsList = () => wrapper.findComponent(ProjectsList); + afterEach(() => { mockApollo = null; }); describe('when API call is loading', () => { - beforeEach(() => { - const mockResolvers = { - Query: { - organization: jest.fn().mockReturnValueOnce(new Promise(() => {})), - }, - }; - - createComponent({ mockResolvers }); - }); - it('renders loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + createComponent(); + + expect(findLoadingIcon().exists()).toBe(true); }); }); describe('when API call is successful', () => { describe('when there are no projects', () => { - it('renders empty state without buttons by default', async () => { - const mockResolvers = { - Query: { - organization: jest.fn().mockResolvedValueOnce({ - projects: { nodes: [] }, - }), + const emptyHandler = jest.fn().mockResolvedValue({ + data: { + organization: { + id: defaultProvide.organizationGid, + projects: { + nodes: [], + pageInfo: pageInfoEmpty, + }, }, - }; - createComponent({ mockResolvers }); + }, + }); + + it('renders empty state without buttons by default', async () => { + createComponent({ handler: emptyHandler }); - jest.runAllTimers(); await waitForPromises(); - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + expect(findEmptyState().props()).toMatchObject({ title: "You don't have any projects yet.", description: - 'Projects are where you can store your code, access issues, wiki, and other features of Gitlab.', + 'Projects are where you can store your code, access issues, wiki, and other features of GitLab.', svgHeight: 144, svgPath: defaultProvide.projectsEmptyStateSvgPath, primaryButtonLink: null, @@ -90,19 +104,14 @@ describe('ProjectsView', () => { describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => { it('renders empty state with buttons', async () => { - const mockResolvers = { - Query: { - organization: jest.fn().mockResolvedValueOnce({ - projects: { nodes: [] }, - }), - }, - }; - createComponent({ mockResolvers, propsData: { shouldShowEmptyStateButtons: true } }); + createComponent({ + handler: emptyHandler, + propsData: { shouldShowEmptyStateButtons: true }, + }); - jest.runAllTimers(); await waitForPromises(); - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + expect(findEmptyState().props()).toMatchObject({ primaryButtonLink: defaultProvide.newProjectPath, primaryButtonText: 'New project', }); @@ -116,11 +125,10 @@ describe('ProjectsView', () => { }); it('renders `ProjectsList` component and passes correct props', async () => { - jest.runAllTimers(); await waitForPromises(); - expect(wrapper.findComponent(ProjectsList).props()).toEqual({ - projects: formatProjects(organizationProjects.nodes), + expect(findProjectsList().props()).toMatchObject({ + projects: formatProjects(nodes), showProjectIcon: true, listItemClass: defaultPropsData.listItemClass, }); @@ -132,13 +140,7 @@ describe('ProjectsView', () => { const error = new Error(); beforeEach(() => { - const mockResolvers = { - Query: { - organization: jest.fn().mockRejectedValueOnce(error), - }, - }; - - createComponent({ mockResolvers }); + createComponent({ handler: jest.fn().mockRejectedValue(error) }); }); it('displays error alert', async () => { diff --git a/spec/frontend/organizations/shared/utils_spec.js b/spec/frontend/organizations/shared/utils_spec.js index 778a18ab2bc..d8d5279b670 100644 --- a/spec/frontend/organizations/shared/utils_spec.js +++ b/spec/frontend/organizations/shared/utils_spec.js @@ -5,21 +5,19 @@ import { organizationProjects, organizationGroups } from '~/organizations/mock_d describe('formatProjects', () => { it('correctly formats the projects', () => { - const [firstMockProject] = organizationProjects.nodes; - const formattedProjects = formatProjects(organizationProjects.nodes); + const [firstMockProject] = organizationProjects; + const formattedProjects = formatProjects(organizationProjects); const [firstFormattedProject] = formattedProjects; expect(firstFormattedProject).toMatchObject({ id: getIdFromGraphQLId(firstMockProject.id), name: firstMockProject.nameWithNamespace, - permissions: { - projectAccess: { - accessLevel: firstMockProject.accessLevel.integerValue, - }, - }, + mergeRequestsAccessLevel: firstMockProject.mergeRequestsAccessLevel.stringValue, + issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue, + forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue, availableActions: [ACTION_EDIT, ACTION_DELETE], }); - expect(formattedProjects.length).toBe(organizationProjects.nodes.length); + expect(formattedProjects.length).toBe(organizationProjects.length); }); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap index 6af9e38192e..009d9c959cb 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -18,7 +18,6 @@ exports[`packages_list_app renders 1`] = ` alt="" class="gl-dark-invert-keep-hue gl-max-w-full" height="144" - role="img" src="helpSvg" /> </div> diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap index d6c3d98efa3..89b55b69893 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap @@ -56,7 +56,7 @@ exports[`PypiInstallation renders all the messages 1`] = ` class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay" id="reference-2" role="listbox" - tabindex="-1" + tabindex="0" > <li aria-hidden="true" diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js index a5a5a43effe..9900baa6bdb 100644 --- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js @@ -36,6 +36,8 @@ describe('ProjectsListItem', () => { }; const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); + const findMergeRequestsLink = () => + wrapper.findByRole('link', { name: ProjectsListItem.i18n.mergeRequests }); const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues }); const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks }); const findProjectTopics = () => wrapper.findByTestId('project-topics'); @@ -148,6 +150,42 @@ describe('ProjectsListItem', () => { }); }); + describe('when merge requests are enabled', () => { + it('renders merge requests count', () => { + createComponent({ + propsData: { + project: { + ...project, + openMergeRequestsCount: 5, + }, + }, + }); + + const mergeRequestsLink = findMergeRequestsLink(); + const tooltip = getBinding(mergeRequestsLink.element, 'gl-tooltip'); + + expect(tooltip.value).toBe(ProjectsListItem.i18n.mergeRequests); + expect(mergeRequestsLink.attributes('href')).toBe(`${project.webUrl}/-/merge_requests`); + expect(mergeRequestsLink.text()).toBe('5'); + expect(mergeRequestsLink.findComponent(GlIcon).props('name')).toBe('git-merge'); + }); + }); + + describe('when merge requests are not enabled', () => { + it('does not render merge requests count', () => { + createComponent({ + propsData: { + project: { + ...project, + mergeRequestsAccessLevel: FEATURABLE_DISABLED, + }, + }, + }); + + expect(findMergeRequestsLink().exists()).toBe(false); + }); + }); + describe('when issues are enabled', () => { it('renders issues count', () => { createComponent(); diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 0f2f4ed1b54..0535a860b94 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do let_it_be(:organization_detail) { build_stubbed(:organization_detail, description_html: '<em>description</em>') } let_it_be(:organization) { organization_detail.organization } + let_it_be(:organization_gid) { 'gid://gitlab/Organizations::Organization/1' } let_it_be(:new_group_path) { '/groups/new' } let_it_be(:new_project_path) { '/projects/new' } let_it_be(:organizations_empty_state_svg_path) { 'illustrations/empty-state/empty-organizations-md.svg' } @@ -15,6 +16,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do let_it_be(:preview_markdown_organizations_path) { '/-/organizations/preview_markdown' } before do + allow(organization).to receive(:to_global_id).and_return(organization_gid) allow(helper).to receive(:new_group_path).and_return(new_group_path) allow(helper).to receive(:new_project_path).and_return(new_project_path) allow(helper).to receive(:image_path).with(organizations_empty_state_svg_path) @@ -41,6 +43,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do ) ).to eq( { + 'organization_gid' => organization_gid, 'organization' => { 'id' => organization.id, 'name' => organization.name, @@ -66,10 +69,11 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do it 'returns expected json' do expect( Gitlab::Json.parse( - helper.organization_groups_and_projects_app_data + helper.organization_groups_and_projects_app_data(organization) ) ).to eq( { + 'organization_gid' => organization_gid, 'new_group_path' => new_group_path, 'new_project_path' => new_project_path, 'groups_empty_state_svg_path' => groups_empty_state_svg_path, @@ -139,7 +143,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do it 'returns expected json' do expect(Gitlab::Json.parse(helper.organization_user_app_data(organization))).to eq( { - 'organization_gid' => organization.to_global_id.to_s, + 'organization_gid' => organization_gid, 'paths' => { 'admin_user' => admin_user_path(:id) } diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 7fc58140fb6..01a8ca055a5 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -346,6 +346,24 @@ RSpec.describe API::Members, feature_category: :groups_and_projects do expect(json_response['access_level']).to eq(Member::DEVELOPER) end + it 'returns the error message if there was an error adding the member to the group' do + error_message = 'Test CreateService Error Message' + allow_next_instance_of(::Members::CreateService) do |service| + expect(service).to receive(:execute).and_return(status: :error, message: error_message) + allow(service).to receive(:single_member).and_return( + instance_double(Member, invalid?: false) + ) + end + + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: stranger.id, access_level: Member::DEVELOPER } + end.not_to change { source.members.count } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq(error_message) + end + context 'with invite_source considerations', :snowplow do let(:params) { { user_id: stranger.id, access_level: Member::DEVELOPER } } diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index c08b40e9528..f019e7f4046 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -31,8 +31,10 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ context 'when the current user does not have permission to create members' do let(:current_user) { create(:user) } - it 'raises a Gitlab::Access::AccessDeniedError' do - expect { execute_service }.to raise_error(Gitlab::Access::AccessDeniedError) + it 'returns an unauthorized http_status' do + expect(execute_service[:status]).to eq(:error) + # this is expected by API::Helpers::MembersHelpers#add_single_member_by_user_id + expect(execute_service[:http_status]).to eq(:unauthorized) end context 'when a project maintainer attempts to add owners' do @@ -56,6 +58,15 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ end end + context 'when trying to create a Membership with invalid params' do + let(:additional_params) { Hash[invite_source: '_invite_source_', expires_at: 3.days.ago] } + + it 'returns an error response' do + expect(execute_service[:status]).to eq(:error) + expect(execute_service[:http_status]).to be_nil + end + end + context 'when passing valid parameters' do it 'adds a user to members' do expect(execute_service[:status]).to eq(:success) @@ -251,6 +262,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'does not update the member' do expect(execute_service[:status]).to eq(:error) + expect(execute_service[:http_status]).to eq(:unauthorized) expect(execute_service[:message]).to eq("#{project_bot.username}: not authorized to update member") expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false) end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 01d6642e814..43b60fad67d 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -139,6 +139,7 @@ RSpec.shared_examples_for "member creation" do expect(source.reload).to have_user(project_bot) expect(member).to be_persisted expect(member.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(member.errors.added?(:base, :unauthorized)).to eq(true) expect(member.errors.full_messages).to include(/not authorized to update member/) end end @@ -169,6 +170,7 @@ RSpec.shared_examples_for "member creation" do expect(member).not_to be_persisted expect(source).not_to have_user(user) + expect(member.errors.added?(:base, :unauthorized)).to eq(true) expect(member.errors.full_messages).to include(/not authorized to create member/) end end diff --git a/spec/support/shared_examples/requests/api/members_shared_examples.rb b/spec/support/shared_examples/requests/api/members_shared_examples.rb index 9136f60eb93..135a984f8f9 100644 --- a/spec/support/shared_examples/requests/api/members_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/members_shared_examples.rb @@ -14,7 +14,7 @@ end RSpec.shared_examples 'a 403 response when user does not have rights to manage members of a specific access level' do it 'returns 403' do - route + expect { route }.not_to change { Member.count } expect(response).to have_gitlab_http_status(:forbidden) end diff --git a/yarn.lock b/yarn.lock index 1a687e46e59..0dffdd18009 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1321,10 +1321,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.75.0.tgz#31e4a3ab66cab3e405c4cff1f065ee3924fe7a98" integrity sha512-TAmlxByyZcZvr/hxipfI53XLNlzpVZCf5izRdoIhR4QFHjZ56HIwQfoVCGYRNLDV8eTnO4ljNBFLZCfyipdPoQ== -"@gitlab/ui@^72.5.1": - version "72.5.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-72.5.1.tgz#8f0ea0cabaa4be41dbf5f78b18f635117ebd41de" - integrity sha512-KhagcFu6RXDCweybecwBbx3Q3DvO26hkRj32jFsGA8H4TJ/5VLZC8Gwct7+IrtuYWijprzRKoAaoeXkrCcBdbw== +"@gitlab/ui@^72.8.0": + version "72.8.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-72.8.0.tgz#66e50de09077eeeec7b8e893155ec32f1a19232d" + integrity sha512-ZszjCxOfdJffrfu21/BwIqk5t450deBx+IxF2jwLqFdKTP3g89+VtHXew3xO9BAyxGlQQ8h3AxDXRhWprodayQ== dependencies: "@floating-ui/dom" "1.4.3" bootstrap-vue "2.23.1" |