diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-08 18:07:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-08 18:07:19 +0300 |
commit | a34d7fd9a723d6cc9c7348be2afe522bdc2be67f (patch) | |
tree | 5971e13ca0832ae06c599b3d5eec2e2fe71d884f /app | |
parent | 5f89187f0433fc84d8387de25220185235d61ed1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
33 files changed, 433 insertions, 144 deletions
diff --git a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js deleted file mode 100644 index 91cb48e181b..00000000000 --- a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js +++ /dev/null @@ -1,28 +0,0 @@ -import Vue from 'vue'; -import ActivityChart from './components/activity_chart.vue'; - -export default () => { - const containers = document.querySelectorAll('.js-project-analytics-chart'); - - if (!containers) { - return false; - } - - return containers.forEach((container) => { - const { chartData } = container.dataset; - const formattedData = JSON.parse(chartData); - - return new Vue({ - el: container, - components: { - ActivityChart, - }, - provide: { - formattedData, - }, - render(createElement) { - return createElement('activity-chart'); - }, - }); - }); -}; diff --git a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue deleted file mode 100644 index 2be9ebda87a..00000000000 --- a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue +++ /dev/null @@ -1,45 +0,0 @@ -<script> -import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { s__ } from '~/locale'; - -export default { - i18n: { - noDataMsg: s__( - 'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.', - ), - }, - components: { - GlColumnChart, - }, - inject: { - formattedData: { - default: {}, - }, - }, - computed: { - barSeriesData() { - return [ - { - name: 'full', - data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]), - }, - ]; - }, - }, -}; -</script> - -<template> - <div class="gl-xs-w-full"> - <gl-column-chart - v-if="formattedData.keys" - :bars="barSeriesData" - :x-axis-title="__('Value')" - :y-axis-title="__('Number of events')" - :x-axis-type="'category'" - /> - <p v-else data-testid="noActivityChartData"> - {{ $options.i18n.noDataMsg }} - </p> - </div> -</template> diff --git a/app/assets/javascripts/api/bulk_imports_api.js b/app/assets/javascripts/api/bulk_imports_api.js index d636cfdff0b..248f5601705 100644 --- a/app/assets/javascripts/api/bulk_imports_api.js +++ b/app/assets/javascripts/api/bulk_imports_api.js @@ -2,6 +2,21 @@ import { buildApiUrl } from '~/api/api_utils'; import axios from '~/lib/utils/axios_utils'; const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/entities'; +const BULK_IMPORT_ENTITIES_FAILURES_PATH = + '/api/:version/bulk_imports/:id/entities/:entity_id/failures'; export const getBulkImportsHistory = (params) => axios.get(buildApiUrl(BULK_IMPORT_ENTITIES_PATH), { params }); + +export const getBulkImportFailures = (id, entityId, { page, perPage }) => { + const failuresPath = buildApiUrl(BULK_IMPORT_ENTITIES_FAILURES_PATH) + .replace(':id', encodeURIComponent(id)) + .replace(':entity_id', encodeURIComponent(entityId)); + + return axios.get(failuresPath, { + params: { + page, + per_page: perPage, + }, + }); +}; diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js index ddf69a8fcdf..b02eb3c4307 100644 --- a/app/assets/javascripts/import/constants.js +++ b/app/assets/javascripts/import/constants.js @@ -1,6 +1,18 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { __, s__ } from '~/locale'; +export const BULK_IMPORT_STATIC_ITEMS = { + badges: __('Badge'), + boards: s__('IssueBoards|Board'), + epics: __('Epic'), + issues: __('Issue'), + labels: __('Label'), + members: __('Member'), + merge_requests: __('Merge request'), + milestones: __('Milestone'), + project: __('Project'), +}; + const STATISTIC_ITEMS = { diff_note: __('Diff notes'), issue: __('Issues'), diff --git a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue index d6c16075482..5da16454032 100644 --- a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue +++ b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue @@ -1,15 +1,44 @@ <script> +import { __ } from '~/locale'; import ImportDetailsTable from '~/import/details/components/import_details_table.vue'; export default { name: 'BulkImportDetailsApp', components: { ImportDetailsTable }, + + fields: [ + { + key: 'relation', + label: __('Type'), + tdClass: 'gl-white-space-nowrap', + }, + { + key: 'source_title', + label: __('Title'), + tdClass: 'gl-md-w-30 gl-word-break-word', + }, + { + key: 'error', + label: __('Error'), + }, + { + key: 'correlation_id_value', + label: __('Correlation ID'), + }, + ], + + LOCAL_STORAGE_KEY: 'gl-bulk-import-details-page-size', }; </script> <template> <div> <h1>{{ s__('Import|GitLab Migration details') }}</h1> - <import-details-table /> + + <import-details-table + bulk-import + :fields="$options.fields" + :local-storage-key="$options.LOCAL_STORAGE_KEY" + /> </div> </template> diff --git a/app/assets/javascripts/import/details/components/import_details_app.vue b/app/assets/javascripts/import/details/components/import_details_app.vue index 3aa60c00ff8..f654dc61e07 100644 --- a/app/assets/javascripts/import/details/components/import_details_app.vue +++ b/app/assets/javascripts/import/details/components/import_details_app.vue @@ -1,14 +1,44 @@ <script> +import { __ } from '~/locale'; import ImportDetailsTable from './import_details_table.vue'; export default { + name: 'ImportDetailsApp', components: { ImportDetailsTable }, + + fields: [ + { + key: 'type', + label: __('Type'), + tdClass: 'gl-white-space-nowrap', + }, + { + key: 'title', + label: __('Title'), + tdClass: 'gl-md-w-30 gl-word-break-word', + }, + { + key: 'provider_url', + label: __('URL'), + tdClass: 'gl-white-space-nowrap', + }, + { + key: 'details', + label: __('Details'), + }, + ], + + LOCAL_STORAGE_KEY: 'gl-import-details-page-size', }; </script> <template> <div> <h1>{{ s__('Import|GitHub import details') }}</h1> - <import-details-table /> + + <import-details-table + :fields="$options.fields" + :local-storage-key="$options.LOCAL_STORAGE_KEY" + /> </div> </template> diff --git a/app/assets/javascripts/import/details/components/import_details_table.vue b/app/assets/javascripts/import/details/components/import_details_table.vue index 813dc1f2645..535ccb525ac 100644 --- a/app/assets/javascripts/import/details/components/import_details_table.vue +++ b/app/assets/javascripts/import/details/components/import_details_table.vue @@ -1,12 +1,13 @@ <script> import { GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; +import { s__ } from '~/locale'; import { createAlert } from '~/alert'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { getParameterValues } from '~/lib/utils/url_utility'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; -import { STATISTIC_ITEMS } from '../../constants'; +import { getBulkImportFailures } from '~/rest_api'; +import { BULK_IMPORT_STATIC_ITEMS, STATISTIC_ITEMS } from '../../constants'; import { fetchImportFailures } from '../api'; const DEFAULT_PAGE_SIZE = 20; @@ -21,28 +22,6 @@ export default { PaginationBar, }, STATISTIC_ITEMS, - LOCAL_STORAGE_KEY: 'gl-import-details-page-size', - fields: [ - { - key: 'type', - label: __('Type'), - tdClass: 'gl-white-space-nowrap', - }, - { - key: 'title', - label: __('Title'), - tdClass: 'gl-md-w-30 gl-word-break-word', - }, - { - key: 'provider_url', - label: __('URL'), - tdClass: 'gl-white-space-nowrap', - }, - { - key: 'details', - label: __('Details'), - }, - ], i18n: { fetchErrorMessage: s__('Import|An error occurred while fetching import details.'), @@ -55,6 +34,25 @@ export default { }, }, + props: { + bulkImport: { + type: Boolean, + required: false, + default: false, + }, + + fields: { + type: Array, + required: true, + }, + + localStorageKey: { + type: String, + required: false, + default: '', + }, + }, + data() { return { items: [], @@ -97,18 +95,28 @@ export default { this.loadImportFailures(); }, + fetchFn(params) { + return this.bulkImport + ? getBulkImportFailures( + getParameterValues('id')[0], + getParameterValues('entity_id')[0], + params, + ) + : fetchImportFailures(this.failuresPath, { + projectId: getParameterValues('project_id')[0], + ...params, + }); + }, + async loadImportFailures() { - if (!this.failuresPath) { + if (!this.bulkImport && !this.failuresPath) { return; } this.loading = true; + try { - const response = await fetchImportFailures(this.failuresPath, { - projectId: getParameterValues('project_id')[0], - page: this.page, - perPage: this.perPage, - }); + const response = await this.fetchFn({ page: this.page, perPage: this.perPage }); const { page, perPage, totalPages, total } = parseIntPagination( normalizeHeaders(response.headers), @@ -123,13 +131,17 @@ export default { } this.loading = false; }, + + itemTypeText(type) { + return (this.bulkImport ? BULK_IMPORT_STATIC_ITEMS[type] : STATISTIC_ITEMS[type]) || type; + }, }, }; </script> <template> <div> - <gl-table :fields="$options.fields" :items="items" class="gl-mt-5" :busy="loading" show-empty> + <gl-table :fields="fields" :items="items" class="gl-mt-5" :busy="loading" show-empty> <template #table-busy> <gl-loading-icon size="lg" class="gl-my-5" /> </template> @@ -139,7 +151,7 @@ export default { </template> <template #cell(type)="{ item: { type } }"> - {{ $options.STATISTIC_ITEMS[type] }} + {{ itemTypeText(type) }} </template> <template #cell(provider_url)="{ item: { provider_url } }"> <gl-link v-if="provider_url" :href="provider_url" target="_blank"> @@ -147,12 +159,30 @@ export default { <gl-icon name="external-link" /> </gl-link> </template> + + <template #cell(relation)="{ item: { relation } }"> + {{ itemTypeText(relation) }} + </template> + <template #cell(source_title)="{ item: { source_title, source_url } }"> + <gl-link v-if="source_url" :href="source_url" target="_blank"> + {{ source_title }} + <gl-icon name="external-link" /> + </gl-link> + <span v-else> + {{ source_title }} + </span> + </template> + <template #cell(error)="{ item: { exception_class, exception_message } }"> + <strong>{{ exception_class }}</strong> + <p>{{ exception_message }}</p> + </template> </gl-table> + <pagination-bar v-if="hasItems" :page-info="pageInfo" class="gl-mt-5" - :storage-key="$options.LOCAL_STORAGE_KEY" + :storage-key="localStorageKey" @set-page="setPage" @set-page-size="setPageSize" /> diff --git a/app/assets/javascripts/pages/import/bulk_imports/details/index.js b/app/assets/javascripts/pages/import/bulk_imports/details/index.js index ca5de576536..5c2571af60f 100644 --- a/app/assets/javascripts/pages/import/bulk_imports/details/index.js +++ b/app/assets/javascripts/pages/import/bulk_imports/details/index.js @@ -8,14 +8,9 @@ export const initBulkImportDetails = () => { return null; } - const { failuresPath } = el.dataset; - return new Vue({ el, name: 'BulkImportDetailsRoot', - provide: { - failuresPath, - }, render(createElement) { return createElement(BulkImportDetailsApp); }, diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js deleted file mode 100644 index ba03fccdb03..00000000000 --- a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle'; - -initActivityCharts(); diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue index 8edf2cfb4aa..6f22af4bd26 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue @@ -6,6 +6,7 @@ import { GlFormInputGroup, GlFormInput, GlLink, + GlFormSelect, GlSprintf, } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -28,6 +29,11 @@ import { I18N_FORM_SMTP_USERNAME_LABEL, I18N_FORM_SMTP_PASSWORD_LABEL, I18N_FORM_SMTP_PASSWORD_DESCRIPTION, + I18N_FORM_SMTP_AUTHENTICATION_LABEL, + I18N_FORM_SMTP_AUTHENTICATION_NONE, + I18N_FORM_SMTP_AUTHENTICATION_PLAIN, + I18N_FORM_SMTP_AUTHENTICATION_LOGIN, + I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5, I18N_FORM_SUBMIT_LABEL, I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL, I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS, @@ -47,6 +53,7 @@ export default { GlFormGroup, GlFormInputGroup, GlFormInput, + GlFormSelect, GlLink, GlSprintf, }, @@ -61,6 +68,11 @@ export default { I18N_FORM_SMTP_USERNAME_LABEL, I18N_FORM_SMTP_PASSWORD_LABEL, I18N_FORM_SMTP_PASSWORD_DESCRIPTION, + I18N_FORM_SMTP_AUTHENTICATION_LABEL, + I18N_FORM_SMTP_AUTHENTICATION_NONE, + I18N_FORM_SMTP_AUTHENTICATION_PLAIN, + I18N_FORM_SMTP_AUTHENTICATION_LOGIN, + I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5, I18N_FORM_SUBMIT_LABEL, I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL, I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS, @@ -87,6 +99,7 @@ export default { smtpPort: '587', smtpUsername: '', smtpPassword: '', + smtpAuthentication: null, validationState: { customEmail: null, smtpAddress: null, @@ -118,6 +131,7 @@ export default { smtp_port: this.smtpPort, smtp_username: this.smtpUsername, smtp_password: this.smtpPassword, + smtp_authentication: this.smtpAuthentication, }; }, onCustomEmailChange() { @@ -150,6 +164,26 @@ export default { this.validateSmtpUsername(); this.validateSmtpPassword(); }, + getSmtpAuthenticationOptions() { + return [ + { + text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_NONE, + value: null, + }, + { + text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_PLAIN, + value: 'plain', + }, + { + text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_LOGIN, + value: 'login', + }, + { + text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5, + value: 'cram_md5', + }, + ]; + }, }, }; </script> @@ -303,6 +337,20 @@ export default { /> </gl-form-group> + <gl-form-group + :label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL" + label-for="custom-email-form-smtp-password" + class="gl-mt-3" + > + <gl-form-select + id="custom-email-form-smtp-authentication" + v-model.trim="smtpAuthentication" + :options="getSmtpAuthenticationOptions()" + :aria-label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL" + :disabled="isSubmitting" + /> + </gl-form-group> + <gl-button type="submit" variant="confirm" diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js index aafd77bd25e..8ac186e292c 100644 --- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js +++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js @@ -37,6 +37,13 @@ export const I18N_FORM_SMTP_PORT_DESCRIPTION = s__( export const I18N_FORM_SMTP_USERNAME_LABEL = s__('ServiceDesk|SMTP username'); export const I18N_FORM_SMTP_PASSWORD_LABEL = s__('ServiceDesk|SMTP password'); export const I18N_FORM_SMTP_PASSWORD_DESCRIPTION = s__('ServiceDesk|Minimum 8 characters long.'); +export const I18N_FORM_SMTP_AUTHENTICATION_LABEL = s__('ServiceDesk|SMTP authentication method'); +export const I18N_FORM_SMTP_AUTHENTICATION_NONE = s__( + 'ServiceDesk|Let GitLab select a server-supported method (recommended)', +); +export const I18N_FORM_SMTP_AUTHENTICATION_PLAIN = s__('ServiceDesk|Plain'); +export const I18N_FORM_SMTP_AUTHENTICATION_LOGIN = s__('ServiceDesk|Login'); +export const I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5 = s__('ServiceDesk|CRAM-MD5'); export const I18N_FORM_SUBMIT_LABEL = s__('ServiceDesk|Save and test connection'); export const I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL = s__( diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index a3a5864240c..a29393d9f93 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -236,7 +236,7 @@ export default { }; </script> <template> - <div class="js-mr-approvals mr-section-container mr-widget-workflow"> + <div v-if="approvals" class="js-mr-approvals mr-section-container mr-widget-workflow"> <state-container :is-loading="$apollo.queries.approvals.loading" :mr="mr" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 24ec740c910..bc3d8ad7824 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -63,6 +63,10 @@ export default { }, manual: true, result({ data }) { + if (!data.project) { + return; + } + if (Object.keys(this.state).length === 0) { this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch || diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js index 564e9321d54..8bb2f2898eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js @@ -18,8 +18,16 @@ export default { iid: `${this.mr.iid}`, }; }, - update: (data) => data.project.mergeRequest, + update: (data) => data.project?.mergeRequest, result({ data }) { + // This case can occur when backend returns an empty project due to expired session. + // See https://gitlab.com/gitlab-org/gitlab/-/issues/413627 for more information. + if (!data.project) { + // Needed to suppress several errors. + this.mr.setApprovals({}); + return; + } + const { mergeRequest } = data.project; this.disableCommittersApproval = data.project.mergeRequestsDisableCommittersApproval; diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 83409c7e096..4163ff8727c 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -34,6 +34,7 @@ class JwtController < ApplicationController authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, request: request) + @raw_token = password if @authentication_result.failed? log_authentication_failed(login, @authentication_result) @@ -80,6 +81,7 @@ class JwtController < ApplicationController def additional_params { scopes: scopes_param, + raw_token: @raw_token, deploy_token: @authentication_result.deploy_token, auth_type: @authentication_result.type }.compact diff --git a/app/finders/organizations/user_organizations_finder.rb b/app/finders/organizations/user_organizations_finder.rb new file mode 100644 index 00000000000..739940c44ca --- /dev/null +++ b/app/finders/organizations/user_organizations_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Organizations + class UserOrganizationsFinder + def initialize(current_user, target_user, params = {}) + @current_user = current_user + @target_user = target_user + @params = params + end + + def execute + return Organizations::Organization.none unless can_read_user_organizations? + return Organizations::Organization.none if target_user.blank? + + target_user.organizations + end + + private + + attr_reader :current_user, :target_user, :params + + def can_read_user_organizations? + current_user&.can?(:read_user_organizations, target_user) + end + end +end diff --git a/app/graphql/resolvers/users/frecent_groups_resolver.rb b/app/graphql/resolvers/users/frecent_groups_resolver.rb new file mode 100644 index 00000000000..2fc757e31ab --- /dev/null +++ b/app/graphql/resolvers/users/frecent_groups_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + module Users + class FrecentGroupsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type [Types::GroupType], null: true + + def resolve + return unless current_user.present? + + if Feature.disabled?(:frecent_namespaces_suggestions, current_user) + raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled") + end + + return unless Feature.enabled?(:frecent_namespaces_suggestions, current_user) + + ::Users::GroupVisit.frecent_groups(user_id: current_user.id) + end + end + end +end diff --git a/app/graphql/resolvers/users/frecent_projects_resolver.rb b/app/graphql/resolvers/users/frecent_projects_resolver.rb new file mode 100644 index 00000000000..397d4ca0cfd --- /dev/null +++ b/app/graphql/resolvers/users/frecent_projects_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + module Users + class FrecentProjectsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type [Types::ProjectType], null: true + + def resolve + return unless current_user.present? + + if Feature.disabled?(:frecent_namespaces_suggestions, current_user) + raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled") + end + + ::Users::ProjectVisit.frecent_projects(user_id: current_user.id) + end + end + end +end diff --git a/app/graphql/resolvers/users/organizations_resolver.rb b/app/graphql/resolvers/users/organizations_resolver.rb new file mode 100644 index 00000000000..ffc1a141eb6 --- /dev/null +++ b/app/graphql/resolvers/users/organizations_resolver.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Resolvers + module Users + class OrganizationsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::Organizations::OrganizationType.connection_type, null: true + + authorize :read_user_organizations + authorizes_object! + + def resolve(**args) + ::Organizations::UserOrganizationsFinder.new(current_user, object, args).execute + end + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index ce458af4e60..173e877d86c 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -55,6 +55,14 @@ module Types null: false, description: 'Fields related to design management.' field :echo, resolver: Resolvers::EchoResolver + field :frecent_groups, [Types::GroupType], + resolver: Resolvers::Users::FrecentGroupsResolver, + description: "A user's frecently visited groups. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.", + alpha: { milestone: '16.6' } + field :frecent_projects, [Types::ProjectType], + resolver: Resolvers::Users::FrecentProjectsResolver, + description: "A user's frecently visited projects. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.", + alpha: { milestone: '16.6' } field :gitpod_enabled, GraphQL::Types::Boolean, null: true, description: "Whether Gitpod is enabled in application settings." diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index ca1b6eaa900..7a43d5891d7 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -71,6 +71,11 @@ module Types type: GraphQL::Types::String, null: false, description: 'Web path of the user.' + field :organizations, + resolver: Resolvers::Users::OrganizationsResolver, + null: true, + alpha: { milestone: '16.6' }, + description: 'Organizations where the user has access.' field :group_memberships, type: Types::GroupMemberType.connection_type, null: true, diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index a1afb0493d5..8b5c0707d08 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -263,21 +263,6 @@ module SortingHelper sort_direction_button(url, reverse_sort, sort_value) end - def packages_reverse_sort_order_hash - { - sort_value_recently_created => sort_value_oldest_created, - sort_value_oldest_created => sort_value_recently_created, - sort_value_name => sort_value_name_desc, - sort_value_name_desc => sort_value_name, - sort_value_version_desc => sort_value_version_asc, - sort_value_version_asc => sort_value_version_desc, - sort_value_type_desc => sort_value_type_asc, - sort_value_type_asc => sort_value_type_desc, - sort_value_project_name_desc => sort_value_project_name_asc, - sort_value_project_name_asc => sort_value_project_name_desc - } - end - def forks_sort_direction_button(sort_value, without = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]) reverse_sort = forks_reverse_sort_options_hash[sort_value] url = page_filter_path(sort: reverse_sort, without: without) diff --git a/app/models/concerns/enums/package_metadata.rb b/app/models/concerns/enums/package_metadata.rb index 3f107987ef6..cba6cd2db2e 100644 --- a/app/models/concerns/enums/package_metadata.rb +++ b/app/models/concerns/enums/package_metadata.rb @@ -14,7 +14,8 @@ module Enums apk: 9, rpm: 10, deb: 11, - cbl_mariner: 12 + cbl_mariner: 12, + wolfi: 13 }.with_indifferent_access.freeze ADVISORY_SOURCES = { diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb index 59aafc32d94..64e0a7653d6 100644 --- a/app/models/concerns/enums/sbom.rb +++ b/app/models/concerns/enums/sbom.rb @@ -18,7 +18,8 @@ module Enums apk: 9, rpm: 10, deb: 11, - cbl_mariner: 12 + cbl_mariner: 12, + wolfi: 13 }.with_indifferent_access.freeze def self.component_types diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb index cb8e5fdc682..029d60d61ee 100644 --- a/app/models/concerns/users/visitable.rb +++ b/app/models/concerns/users/visitable.rb @@ -13,6 +13,45 @@ module Users time = time.to_datetime where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes)) end + + scope :for_user, ->(user_id) { where(user_id: user_id) } + + scope :recently_visited, -> do + where('visited_at > ?', 3.months.ago) + .where('visited_at <= ?', Time.current) + end + + def self.grouped_by_week_start_and_entity_for_user(user_id:) + recently_visited + .for_user(user_id) + .group(:week_start, :entity_id) + .select( + :entity_id, + "COUNT(entity_id) AS week_count", + "DATE_TRUNC('week', visited_at)::date AS week_start", + "DENSE_RANK() OVER (ORDER BY DATE_TRUNC('week', visited_at)::date)" + ) + end + + def self.frecent_visits_scores(user_id:, limit:) + ranked_entity_visits_query = grouped_by_week_start_and_entity_for_user(user_id: user_id).to_sql + sql = <<~SQL + SELECT + entity_id, + SUM(week_count * dense_rank) AS score + FROM + (#{ranked_entity_visits_query}) as ranked_entity_visits + GROUP BY + entity_id + ORDER BY + score DESC + LIMIT #{limit} + SQL + + ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do + connection.execute(sql).to_a + end + end end end end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 920321a1699..2405ff3d252 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -11,7 +11,6 @@ class DeployToken < ApplicationRecord AVAILABLE_SCOPES = %i[read_repository read_registry write_registry read_package_registry write_package_registry].freeze GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token' - REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze attribute :expires_at, default: -> { Forever.date } @@ -57,7 +56,7 @@ class DeployToken < ApplicationRecord def valid_for_dependency_proxy? group_type? && active? && - REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) } + (Gitlab::Auth::REGISTRY_SCOPES & scopes).size == Gitlab::Auth::REGISTRY_SCOPES.size end def revoke! diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb index 0bcfda049fc..d7c76e2ee2c 100644 --- a/app/models/users/group_visit.rb +++ b/app/models/users/group_visit.rb @@ -13,5 +13,12 @@ module Users validates :entity_id, presence: true validates :user_id, presence: true validates :visited_at, presence: true + + MAX_FRECENT_ITEMS = 3 + + def self.frecent_groups(user_id:) + ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id") + Group.find(ids) + end end end diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb index 1d076e0be56..9ff3d8d2c91 100644 --- a/app/models/users/project_visit.rb +++ b/app/models/users/project_visit.rb @@ -13,5 +13,12 @@ module Users validates :entity_id, presence: true validates :user_id, presence: true validates :visited_at, presence: true + + MAX_FRECENT_ITEMS = 5 + + def self.frecent_projects(user_id:) + ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id") + Project.find(ids) + end end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index ca170133105..f927d976f0d 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -69,7 +69,9 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy end condition(:dependency_proxy_access_allowed) do - access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token + valid_dependency_proxy_human_token || + valid_dependency_proxy_group_access_token || + valid_dependency_proxy_deploy_token end desc "Deploy token with read_package_registry scope" @@ -386,6 +388,18 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy user.is_a?(User) end + def user_is_human? + user_is_user? && user.human? + end + + def user_is_project_bot? + user_is_user? && user.project_bot? + end + + def user_is_deploy_token? + user.is_a?(DeployToken) + end + def group @subject end @@ -406,8 +420,16 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy resource_access_token_create_feature_available? && group.root_ancestor.namespace_settings.resource_access_token_creation_allowed? end + def valid_dependency_proxy_human_token + user_is_human? && access_level(for_any_session: true) >= GroupMember::GUEST + end + + def valid_dependency_proxy_group_access_token + user_is_project_bot? && access_level(for_any_session: true) >= GroupMember::GUEST + end + def valid_dependency_proxy_deploy_token - @user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject) + user_is_deploy_token? && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject) end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 2fd198b8cf4..04fbc8467c9 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -29,6 +29,7 @@ class UserPolicy < BasePolicy enable :read_user_personal_access_tokens enable :read_group_count enable :read_user_groups + enable :read_user_organizations enable :read_saved_replies enable :read_user_email_address enable :admin_user_email_address diff --git a/app/services/auth/dependency_proxy_authentication_service.rb b/app/services/auth/dependency_proxy_authentication_service.rb index 164594d6f6c..9033baf8c15 100644 --- a/app/services/auth/dependency_proxy_authentication_service.rb +++ b/app/services/auth/dependency_proxy_authentication_service.rb @@ -5,10 +5,11 @@ module Auth AUDIENCE = 'dependency_proxy' HMAC_KEY = 'gitlab-dependency-proxy' DEFAULT_EXPIRE_TIME = 1.minute + REQUIRED_ABILITIES = %i[read_container_image create_container_image].freeze def execute(authentication_abilities:) return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled - return error('access forbidden', 403) unless valid_user_actor? + return error('access forbidden', 403) unless valid_user_actor?(authentication_abilities) { token: authorized_token.encoded } end @@ -33,8 +34,17 @@ module Auth private - def valid_user_actor? - current_user || valid_deploy_token? + def valid_user_actor?(authentication_abilities) + valid_human_user? || valid_group_access_token?(authentication_abilities) || valid_deploy_token? + end + + def valid_human_user? + current_user.is_a?(User) && current_user.human? + end + + def valid_group_access_token?(authentication_abilities) + current_user&.project_bot? && group_access_token&.active? && + (REQUIRED_ABILITIES & authentication_abilities).size == REQUIRED_ABILITIES.size end def valid_deploy_token? @@ -49,8 +59,18 @@ module Auth end end + def group_access_token + return unless current_user&.project_bot? + + PersonalAccessTokensFinder.new(state: 'active').find_by_token(raw_token) + end + def deploy_token params[:deploy_token] end + + def raw_token + params[:raw_token] + end end end diff --git a/app/services/service_desk/custom_emails/create_service.rb b/app/services/service_desk/custom_emails/create_service.rb index 305f5b3fa11..c06c836f0fa 100644 --- a/app/services/service_desk/custom_emails/create_service.rb +++ b/app/services/service_desk/custom_emails/create_service.rb @@ -42,6 +42,8 @@ module ServiceDesk def create_credential credential = ::ServiceDesk::CustomEmailCredential.new(create_credential_params.merge(project: project)) credential.save + rescue ArgumentError + false end def create_verification @@ -53,7 +55,7 @@ module ServiceDesk end def create_credential_params - ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password) + ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password, :smtp_authentication) end def ensure_params diff --git a/app/views/import/bulk_imports/details.html.haml b/app/views/import/bulk_imports/details.html.haml index 0efe71362a6..511bf2c38a1 100644 --- a/app/views/import/bulk_imports/details.html.haml +++ b/app/views/import/bulk_imports/details.html.haml @@ -2,4 +2,4 @@ - add_to_breadcrumbs _('Import group'), new_group_path(anchor: 'import-group-pane') - page_title s_('Import|GitLab Migration details') -.js-bulk-import-details{ data: { failures_path: '' } } +.js-bulk-import-details |