diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-27 09:12:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-27 09:12:29 +0300 |
commit | 2af65f9153baf358bfc6acdd8de9652b39264497 (patch) | |
tree | d6af38edbdded9835907da8da5dc11bca09c7749 | |
parent | 39c9abe4fe410ac402d57a364bf89938a2067315 (diff) |
Add latest changes from gitlab-org/gitlab@master
45 files changed, 710 insertions, 173 deletions
diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js index 4e63a85df89..633bc4d8b15 100644 --- a/app/assets/javascripts/admin/users/components/actions/index.js +++ b/app/assets/javascripts/admin/users/components/actions/index.js @@ -9,6 +9,8 @@ import Reject from './reject.vue'; import Unban from './unban.vue'; import Unblock from './unblock.vue'; import Unlock from './unlock.vue'; +import Trust from './trust_user.vue'; +import Untrust from './untrust_user.vue'; export default { Activate, @@ -22,4 +24,6 @@ export default { Unblock, Unlock, Reject, + Trust, + Untrust, }; diff --git a/app/assets/javascripts/admin/users/components/actions/trust_user.vue b/app/assets/javascripts/admin/users/components/actions/trust_user.vue new file mode 100644 index 00000000000..41ff8d4120d --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/trust_user.vue @@ -0,0 +1,62 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; +import { sprintf, s__, __ } from '~/locale'; +import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = ` + <p>${s__('AdminUsers|When not being monitored for spam:')}</p> + <ul> + <li>${s__( + 'AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked.', + )}</li> + </ul> + <p>${s__('AdminUsers|You can untrust this user in the future.')}</p> +`; + +export default { + components: { + GlDisclosureDropdownItem, + }, + props: { + username: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + methods: { + onClick() { + eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, { + path: this.path, + method: 'put', + modalAttributes: { + title: sprintf(s__('AdminUsers|Stop monitoring %{username} for possible spam?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.trust, + attributes: { variant: 'confirm' }, + }, + messageHtml, + }, + }); + }, + }, +}; +</script> + +<template> + <gl-disclosure-dropdown-item @action="onClick"> + <template #list-item> + <slot></slot> + </template> + </gl-disclosure-dropdown-item> +</template> diff --git a/app/assets/javascripts/admin/users/components/actions/untrust_user.vue b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue new file mode 100644 index 00000000000..da59833af07 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue @@ -0,0 +1,56 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; +import { sprintf, s__, __ } from '~/locale'; +import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = `<p>${s__( + 'AdminUsers|You can trust this user in the future if necessary.', +)}</p>`; + +export default { + components: { + GlDisclosureDropdownItem, + }, + props: { + username: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + methods: { + onClick() { + eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, { + path: this.path, + method: 'put', + modalAttributes: { + title: sprintf(s__('AdminUsers|Re-enable spam monitoring for %{username}?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.untrust, + attributes: { variant: 'confirm' }, + }, + messageHtml, + }, + }); + }, + }, +}; +</script> + +<template> + <gl-disclosure-dropdown-item @action="onClick"> + <template #list-item> + <slot></slot> + </template> + </gl-disclosure-dropdown-item> +</template> diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js index 9cd61d6b1db..43c9a8749cd 100644 --- a/app/assets/javascripts/admin/users/constants.js +++ b/app/assets/javascripts/admin/users/constants.js @@ -19,4 +19,6 @@ export const I18N_USER_ACTIONS = { deleteWithContributions: s__('AdminUsers|Delete user and contributions'), ban: s__('AdminUsers|Ban user'), unban: s__('AdminUsers|Unban user'), + trust: s__('AdminUsers|Trust user'), + untrust: s__('AdminUsers|Untrust user'), }; diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue index 0ec2ef30c20..477d28c6c28 100644 --- a/app/assets/javascripts/ci/runner/components/runner_details.vue +++ b/app/assets/javascripts/ci/runner/components/runner_details.vue @@ -120,12 +120,9 @@ export default { }} </p> <p class="gl-mb-0"> - <gl-link - :href="tokenExpirationHelpUrl" - target="_blank" - class="gl-reset-font-size" - >{{ __('Learn more') }}</gl-link - > + <gl-link :href="tokenExpirationHelpUrl" target="_blank">{{ + __('Learn more') + }}</gl-link> </p> </help-popover> </template> @@ -156,12 +153,9 @@ export default { " > <template #link="{ content }" - ><gl-link - :href="$options.RUNNER_MANAGERS_HELP_URL" - target="_blank" - class="gl-reset-font-size" - >{{ content }}</gl-link - ></template + ><gl-link :href="$options.RUNNER_MANAGERS_HELP_URL" target="_blank">{{ + content + }}</gl-link></template > </gl-sprintf> </help-popover> diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue index e3d3360cd0c..3b9f14a218f 100644 --- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue +++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue @@ -47,16 +47,20 @@ export default { <template> <li class="gl-mt-5 gl-pb-5 gl-border-b gl-relative"> - <time-ago-tooltip :time="event.created_at" class="gl-float-right gl-text-secondary" /> + <time-ago-tooltip + :time="event.created_at" + class="gl-float-right gl-font-sm gl-text-secondary gl-mt-2" + /> <gl-avatar-link :href="author.web_url"> <gl-avatar-labeled :label="author.name" :sub-label="authorUsername" + inline-labels :src="author.avatar_url" - :size="32" + :size="24" /> </gl-avatar-link> - <div class="gl-pl-8 gl-mt-2" data-testid="event-body"> + <div class="gl-pl-7" data-testid="event-body"> <div class="gl-text-secondary"> <gl-icon :class="iconClass" :name="iconName" /> <gl-sprintf v-if="message" :message="message"> diff --git a/app/assets/javascripts/issues/show/components/issue_header.vue b/app/assets/javascripts/issues/show/components/issue_header.vue index 211f3217ddc..c205a6361c7 100644 --- a/app/assets/javascripts/issues/show/components/issue_header.vue +++ b/app/assets/javascripts/issues/show/components/issue_header.vue @@ -115,11 +115,9 @@ export default { <template #status-badge> <gl-sprintf v-if="closedStatusLink" :message="statusText"> <template #link> - <gl-link - class="gl-reset-color! gl-reset-font-size gl-text-decoration-underline" - :href="closedStatusLink" - >{{ closedStatusText }}</gl-link - > + <gl-link class="gl-reset-color! gl-text-decoration-underline" :href="closedStatusLink">{{ + closedStatusText + }}</gl-link> </template> </gl-sprintf> <template v-else>{{ statusText }}</template> diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue new file mode 100644 index 00000000000..ae22bedd69a --- /dev/null +++ b/app/assets/javascripts/organizations/users/components/app.vue @@ -0,0 +1,51 @@ +<script> +import { __, s__ } from '~/locale'; +import { createAlert } from '~/alert'; +import organizationUsersQuery from '../graphql/organization_users.query.graphql'; + +export default { + name: 'OrganizationsUsersApp', + i18n: { + users: __('Users'), + loadingPlaceholder: __('Loading'), + errorMessage: s__( + 'Organization|An error occurred loading the organization users. Please refresh the page to try again.', + ), + }, + inject: ['organizationGid'], + data() { + return { + users: [], + }; + }, + apollo: { + users: { + query: organizationUsersQuery, + variables() { + return { id: this.organizationGid }; + }, + update(data) { + return data.organization.organizationUsers.nodes; + }, + error(error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + }, + }, + }, + computed: { + loading() { + return this.$apollo.queries.users.loading; + }, + }, +}; +</script> + +<template> + <section> + <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1> + <template v-if="loading"> + {{ $options.i18n.loadingPlaceholder }} + </template> + <div data-testid="organization-users">{{ users }}</div> + </section> +</template> diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql new file mode 100644 index 00000000000..7b37186ba1a --- /dev/null +++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql @@ -0,0 +1,14 @@ +query getOrganizationUsers($id: OrganizationsOrganizationID!) { + organization(id: $id) { + id + organizationUsers { + nodes { + badges + id + user { + id + } + } + } + } +} diff --git a/app/assets/javascripts/organizations/users/index.js b/app/assets/javascripts/organizations/users/index.js new file mode 100644 index 00000000000..76656243075 --- /dev/null +++ b/app/assets/javascripts/organizations/users/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import OrganizationsUsersApp from './components/app.vue'; + +export const initOrganizationsUsers = () => { + const el = document.getElementById('js-organizations-users'); + + if (!el) return false; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + const { organizationGid } = convertObjectPropsToCamelCase(el.dataset); + + return new Vue({ + el, + name: 'OrganizationsUsersRoot', + apolloProvider, + provide: { + organizationGid, + }, + render(createElement) { + return createElement(OrganizationsUsersApp); + }, + }); +}; diff --git a/app/assets/javascripts/pages/organizations/organizations/users/index.js b/app/assets/javascripts/pages/organizations/organizations/users/index.js new file mode 100644 index 00000000000..12d53207b22 --- /dev/null +++ b/app/assets/javascripts/pages/organizations/organizations/users/index.js @@ -0,0 +1,3 @@ +import { initOrganizationsUsers } from '~/organizations/users'; + +initOrganizationsUsers(); diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 8fe45d4bb9d..d6500529170 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -65,8 +65,6 @@ min-width: 0; } -// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466 -.gl-font-size-inherit, .font-size-inherit { font-size: inherit; } .gl-w-16 { width: px-to-rem($grid-size * 2); } .gl-w-64 { width: px-to-rem($grid-size * 8); } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9634257209d..ee78d5a8c35 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -310,7 +310,7 @@ class Admin::UsersController < Admin::ApplicationController end def users_with_included_associations(users) - users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord + users.includes(:authorized_projects, :trusted_with_spam_attribute) # rubocop: disable CodeReuse/ActiveRecord end def admin_making_changes_for_another_user? diff --git a/app/helpers/admin/user_actions_helper.rb b/app/helpers/admin/user_actions_helper.rb index 969c5d5a0b5..ba40b3c8a8d 100644 --- a/app/helpers/admin/user_actions_helper.rb +++ b/app/helpers/admin/user_actions_helper.rb @@ -16,6 +16,7 @@ module Admin unlock_actions delete_actions ban_actions + trust_actions @actions end @@ -66,5 +67,19 @@ module Admin @actions << 'ban' end end + + def trust_actions + return if @user.internal? || + @user.blocked_pending_approval? || + @user.banned? || + @user.blocked? || + @user.deactivated? + + @actions << if @user.trusted? + 'untrust' + else + 'trust' + end + end end end diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index 48ba9d4ab84..312f55c11d2 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -42,6 +42,12 @@ module Organizations } end + def organization_user_app_data(organization) + { + organization_gid: organization.to_global_id + } + end + private def shared_groups_and_projects_app_data diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 1bdc0cbb453..75ff2da2322 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -262,7 +262,9 @@ module UsersHelper delete_with_contributions: admin_user_path(:id, hard_delete: true), admin_user: admin_user_path(:id), ban: ban_admin_user_path(:id), - unban: unban_admin_user_path(:id) + unban: unban_admin_user_path(:id), + trust: trust_admin_user_path(:id), + untrust: untrust_admin_user_path(:id) } end diff --git a/app/models/user.rb b/app/models/user.rb index fdc0b531521..bc256ef0f31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -601,6 +601,12 @@ class User < MainClusterwide::ApplicationRecord scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) } scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) } scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) } + scope :trusted, -> do + where('EXISTS (?)', ::UserCustomAttribute + .select(1) + .where('user_id = users.id') + .trusted_with_spam) + end strip_attributes! :name @@ -769,6 +775,8 @@ class User < MainClusterwide::ApplicationRecord external when 'deactivated' deactivated + when "trusted" + trusted else active_without_ghosts end diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index d4a9009a0cf..bbb068c3680 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -44,6 +44,9 @@ = gl_tab_link_to admin_users_path(filter: "wop"), { item_active: active_when(params[:filter] == 'wop'), class: 'gl-border-0!' } do = s_('AdminUsers|Without projects') = gl_tab_counter_badge(limited_counter_with_delimiter(User.without_projects)) + = gl_tab_link_to admin_users_path(filter: "trusted"), { item_active: active_when(params[:filter] == 'trusted'), class: 'gl-border-0!' } do + = s_('AdminUsers|Trusted') + = gl_tab_counter_badge(limited_counter_with_delimiter(User.trusted)) .nav-controls = render_if_exists 'admin/users/admin_email_users' = render_if_exists 'admin/users/admin_export_user_permissions' diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 83f7d743755..a3be188854d 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -2,7 +2,7 @@ - if event.visible_to_user?(current_user) .event-item - .event-item-timestamp + .event-item-timestamp.gl-font-sm #{time_ago_with_tooltip(event.created_at)} - if event.wiki_page? diff --git a/app/views/organizations/organizations/users.html.haml b/app/views/organizations/organizations/users.html.haml index a01c5f5045e..5fb9d786e0b 100644 --- a/app/views/organizations/organizations/users.html.haml +++ b/app/views/organizations/organizations/users.html.haml @@ -1 +1,4 @@ - page_title _('Users') + +#js-organizations-users{ data: organization_user_app_data(@organization) } + diff --git a/config/feature_flags/development/create_project_subscription_graphql_endpoint.yml b/config/feature_flags/development/create_project_subscription_graphql_endpoint.yml new file mode 100644 index 00000000000..a39664a875d --- /dev/null +++ b/config/feature_flags/development/create_project_subscription_graphql_endpoint.yml @@ -0,0 +1,8 @@ +--- +name: create_project_subscription_graphql_endpoint +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133308 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429339 +milestone: '16.6' +type: development +group: group::pipeline execution +default_enabled: false diff --git a/doc/administration/moderate_users.md b/doc/administration/moderate_users.md index b30294c5fe0..c12eb2b9a95 100644 --- a/doc/administration/moderate_users.md +++ b/doc/administration/moderate_users.md @@ -287,6 +287,45 @@ You can also delete a user and their contributions, such as merge requests, issu NOTE: Before 15.1, additionally groups of which deleted user were the only owner among direct members were deleted. +## Trust and untrust users + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132402) in GitLab 16.5. + +You can trust and untrust users from the Admin Area. + +By default, a user is not trusted and is blocked from creating issues, notes, and snippets considered to be spam. When you trust a user, they can create issues, notes, and snippets without being blocked. + +Prerequisite: + +- You must be an administrator. + +::Tabs + +:::TabTitle Trust a user + +1. On the left sidebar, select **Search or go to**. +1. Select **Admin Area**. +1. Select **Overview > Users**. +1. Select a user. +1. From the **User administration** dropdown list, select **Trust user**. +1. On the confirmation dialog, select **Trust user**. + +The user is trusted. + +:::TabTitle Untrust a user + +1. On the left sidebar, select **Search or go to**. +1. Select **Admin Area**. +1. Select **Overview > Users**. +1. Select the **Trusted** tab. +1. Select a user. +1. From the **User administration** dropdown list, select **Untrust user**. +1. On the confirmation dialog, select **Untrust user**. + +The user is untrusted. + +::EndTabs + ## Troubleshooting When moderating users, you may need to perform bulk actions on them based on certain conditions. The following rails console scripts show some examples of this. You may [start a rails console session](../administration/operations/rails_console.md#starting-a-rails-console-session) and use scripts similar to the following: diff --git a/doc/administration/review_spam_logs.md b/doc/administration/review_spam_logs.md index 35cc78a9bf3..e3b96cdae95 100644 --- a/doc/administration/review_spam_logs.md +++ b/doc/administration/review_spam_logs.md @@ -38,15 +38,3 @@ You can resolve a spam log with one of the following effects: NOTE: Users can be [blocked](../api/users.md#block-user) and [unblocked](../api/users.md#unblock-user) using the GitLab API. - -<!-- ## Troubleshooting - -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. - -Each scenario can be a third-level heading, for example `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1a84a872942..23dd74d2678 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5941,6 +5941,26 @@ Input type: `ProjectSetLockedInput` | <a id="mutationprojectsetlockederrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationprojectsetlockedproject"></a>`project` | [`Project`](#project) | Project after mutation. | +### `Mutation.projectSubscriptionCreate` + +Input type: `ProjectSubscriptionCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationprojectsubscriptioncreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationprojectsubscriptioncreateprojectpath"></a>`projectPath` | [`String!`](#string) | Full path of the downstream project of the Project Subscription. | +| <a id="mutationprojectsubscriptioncreateupstreampath"></a>`upstreamPath` | [`String!`](#string) | Full path of the upstream project of the Project Subscription. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationprojectsubscriptioncreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationprojectsubscriptioncreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationprojectsubscriptioncreatesubscription"></a>`subscription` | [`CiSubscriptionsProject`](#cisubscriptionsproject) | Project Subscription created by the mutation. | + ### `Mutation.projectSyncFork` WARNING: @@ -15115,6 +15135,17 @@ Represents the Geo replication and verification state of a ci_secure_file. | <a id="cistagename"></a>`name` | [`String`](#string) | Name of the stage. | | <a id="cistagestatus"></a>`status` | [`String`](#string) | Status of the pipeline stage. | +### `CiSubscriptionsProject` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cisubscriptionsprojectauthor"></a>`author` | [`UserCore`](#usercore) | Author of the subscription. | +| <a id="cisubscriptionsprojectdownstreamproject"></a>`downstreamProject` | [`Project`](#project) | Downstream project of the subscription. | +| <a id="cisubscriptionsprojectid"></a>`id` | [`CiSubscriptionsProjectID`](#cisubscriptionsprojectid) | Global ID of the subscription. | +| <a id="cisubscriptionsprojectupstreamproject"></a>`upstreamProject` | [`Project`](#project) | Upstream project of the subscription. | + ### `CiTemplate` GitLab CI/CD configuration template. @@ -30333,6 +30364,12 @@ A `CiStageID` is a global ID. It is encoded as a string. An example `CiStageID` is: `"gid://gitlab/Ci::Stage/1"`. +### `CiSubscriptionsProjectID` + +A `CiSubscriptionsProjectID` is a global ID. It is encoded as a string. + +An example `CiSubscriptionsProjectID` is: `"gid://gitlab/Ci::Subscriptions::Project/1"`. + ### `CiTriggerID` A `CiTriggerID` is a global ID. It is encoded as a string. diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md index 092b47ba115..b09b85f33a0 100644 --- a/doc/ci/troubleshooting.md +++ b/doc/ci/troubleshooting.md @@ -321,7 +321,7 @@ If the merge train pipeline was canceled before the merge request was merged, wi ### Merge request rules widget shows a scan result policy is invalid or duplicated **(ULTIMATE SELF)** -On GitLab self-managed 15.0 and later, the most likely cause is that the project was exported from a +On GitLab self-managed from 15.0 to 16.4, the most likely cause is that the project was exported from a group and imported into another, and had scan result policy rules. These rules are stored in a separate project to the one that was exported. As a result, the project contains policy rules that reference entities that don't exist in the imported project's group. The result is policy rules that diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md index cfd4ce2dc4d..7582dd8bca3 100644 --- a/doc/user/application_security/policies/scan-result-policies.md +++ b/doc/user/application_security/policies/scan-result-policies.md @@ -279,7 +279,25 @@ actions: - adalberto.dare ``` -## Example situations where scan result policies require additional approval +## Understanding scan result policy approvals + +### Scope of scan result policy comparison + +- To determine when approval is required on a merge request, we compare the latest completed pipelines for each supported pipeline source for the source and target branch (for example, `feature`/`main`). This ensures the most comprehensive evaluation of scan results. +- We compare findings from the latest completed pipelines that ran on `HEAD` of the source and target branch. +- Scan result policies considers all supported pipeline sources (based on the [`CI_PIPELINE_SOURCE` variable](../../../ci/variables/predefined_variables.md)) when comparing results from both the source and target branches when determining if a merge request requires approval. Pipeline sources `webide` and `parent_pipeline` are not supported. + +### Accepting risk and ignoring vulnerabilities in future merge requests + +For scan result policies that are scoped to `newly_detected` findings, it's important to understand the implications of this vulnerability state. A finding is considered `newly_detected` if it exists on the merge request's branch but not on the default branch. When a merge request whose branch contains `newly_detected` findings is approved and merged, approvers are "accepting the risk" of those vulnerabilities. If one or more of the same vulnerabilities were detected after this time, their status would be `previously_detected` and so not be out of scope of a policy aimed at `newly_detected` findings. For example: + +- A scan result policy is created to block critical SAST findings. If a SAST finding for CVE-1234 is approved, future merge requests with the same violation will not require approval in the project. + +When using license approval policies, the combination of project, component (dependency), and license are considered in the evaluation. If a license is approved as an exception, future merge requests don't require approval for the same combination of project, component (dependency), and license. The component's version is not be considered in this case. If a previously approved package is updated to a new version, approvers will not need to re-approve. For example: + +- A license approval policy is created to block merge requests with newly detected licenses matching `AGPL-1.0`. A change is made in project `demo` for component `osframework` that violates the policy. If approved and merged, future merge requests to `osframework` in project `demo` with the license `AGPL-1.0` don't require approval. + +### Multiple approvals There are several situations where the scan result policy requires an additional approval step. For example: @@ -296,3 +314,11 @@ There are several situations where the scan result policy requires an additional - Someone stops a pipeline security job, and users can't skip the security scan. - A job in a merge request fails and is configured with `allow_failure: false`. As a result, the pipeline is in a blocked state. - A pipeline has a manual job that must run successfully for the entire pipeline to pass. + +### Known issues + +We have identified in [epic 11020](https://gitlab.com/groups/gitlab-org/-/epics/11020) common areas of confusion in scan result findings that need to be addressed. Below are a few of the known issues: + +- When using `newly_detected`, some findings may require approval when they are not introduced by the merge request (such as a new CVE on a related dependency). We currently use `main tip` of the target branch for comparison. In the future, we plan to use `merge base` for `newly_detected` policies (see [issue 428518](https://gitlab.com/gitlab-org/gitlab/-/issues/428518)). +- Findings or errors that cause approval to be required on a scan result policy may not be evident in the Security MR Widget. By using `merge base` in [issue 428518](https://gitlab.com/gitlab-org/gitlab/-/issues/428518) some cases will be addressed. We will additionally be [displaying more granular details](https://gitlab.com/groups/gitlab-org/-/epics/11185) about what caused security policy violations. +- Security policy violations are distinct compared to findings displayed in the MR widgets. Some violations may not be present in the MR widget. We are working to harmonize our features in [epic 11020](https://gitlab.com/groups/gitlab-org/-/epics/11020) and to display policy violations explicitly in merge requests in [epic 11185](https://gitlab.com/groups/gitlab-org/-/epics/11185). diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md index fa03cb54ba3..3230d26a5aa 100644 --- a/doc/user/shortcuts.md +++ b/doc/user/shortcuts.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Manage +group: Foundations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments type: reference --- diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 3d2bde6f0a7..e134fb31879 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -4,7 +4,6 @@ module Gitlab module Git class Blame include Gitlab::EncodingHelper - include Gitlab::Git::WrapsGitalyErrors attr_reader :lines, :blames, :range @@ -35,11 +34,9 @@ module Gitlab end def fetch_raw_blame - wrapped_gitaly_errors do - @repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec) - end - # Return empty result when blame range is out-of-range + @repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec) rescue ArgumentError + # Return an empty result when the blame range is out-of-range or path is not found "" end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 1ef5b0f96c2..3949e8e6416 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -418,6 +418,15 @@ module Gitlab response = gitaly_client_call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout) response.reduce([]) { |memo, msg| memo << msg.data }.join + rescue GRPC::BadStatus => e + detailed_error = GitalyClient.decode_detailed_error(e) + + case detailed_error.try(:error) + when :out_of_range, :path_not_found + raise ArgumentError, e.details + else + raise e + end end def find_commit(revision) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 45a1cd28629..47904507122 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4056,6 +4056,9 @@ msgstr "" msgid "AdminUsers|Projects, issues, merge requests, and comments of this user are hidden from other users." msgstr "" +msgid "AdminUsers|Re-enable spam monitoring for %{username}?" +msgstr "" + msgid "AdminUsers|Reactivating a user will:" msgstr "" @@ -4095,9 +4098,15 @@ msgstr "" msgid "AdminUsers|Sort by" msgstr "" +msgid "AdminUsers|Stop monitoring %{username} for possible spam?" +msgstr "" + msgid "AdminUsers|The maximum compute minutes that jobs in this namespace can use on shared runners each month. Set 0 for unlimited. Set empty to inherit the global setting of %{minutes}" msgstr "" +msgid "AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked." +msgstr "" + msgid "AdminUsers|The user can't access git repositories." msgstr "" @@ -4128,6 +4137,12 @@ msgstr "" msgid "AdminUsers|To confirm, type %{username}." msgstr "" +msgid "AdminUsers|Trust user" +msgstr "" + +msgid "AdminUsers|Trusted" +msgstr "" + msgid "AdminUsers|Unban user" msgstr "" @@ -4143,6 +4158,9 @@ msgstr "" msgid "AdminUsers|Unlock user %{username}?" msgstr "" +msgid "AdminUsers|Untrust user" +msgstr "" + msgid "AdminUsers|User administration" msgstr "" @@ -4176,6 +4194,9 @@ msgstr "" msgid "AdminUsers|When banned:" msgstr "" +msgid "AdminUsers|When not being monitored for spam:" +msgstr "" + msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account" msgstr "" @@ -4206,9 +4227,15 @@ msgstr "" msgid "AdminUsers|You can ban their account in the future if necessary." msgstr "" +msgid "AdminUsers|You can trust this user in the future if necessary." +msgstr "" + msgid "AdminUsers|You can unban their account in the future. Their data remains intact." msgstr "" +msgid "AdminUsers|You can untrust this user in the future." +msgstr "" + msgid "AdminUsers|You cannot remove your own administrator access." msgstr "" @@ -33268,6 +33295,9 @@ msgstr "" msgid "Organization|An error occurred loading the groups. Please refresh the page to try again." msgstr "" +msgid "Organization|An error occurred loading the organization users. Please refresh the page to try again." +msgstr "" + msgid "Organization|An error occurred loading the projects. Please refresh the page to try again." msgstr "" diff --git a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb index bd98cb17332..2a763d08276 100644 --- a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb @@ -29,7 +29,7 @@ module QA Page::File::Show.perform do |file| aggregate_failures 'file details' do - expect(file).to have_notice('Your changes have been successfully committed.') + expect(file).to have_notice('Your changes have been committed successfully.') expect(file).to have_file_content(edited_readme_content) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb index 4d1cad4c4f9..fafa47a4a4f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', product_group: :ide, - quarantine: { - only: { job: 'slow-network' }, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387609', - type: :flaky - } do + RSpec.describe 'Create', product_group: :ide do describe 'Add a directory in Web IDE' do let(:project) { create(:project, :with_readme, name: 'webide-add-directory-project') } diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb index 2049ef78962..c038a6d8a87 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', product_group: :ide, - quarantine: { - only: { job: 'slow-network' }, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387609', - type: :flaky - } do + RSpec.describe 'Create', product_group: :ide do describe 'Upload a file in Web IDE' do let(:file_path) { File.absolute_path(File.join('qa', 'fixtures', 'web_ide', file_name)) } let(:project) { create(:project, :with_readme, name: 'webide-upload-file-project') } diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb deleted file mode 100644 index ab035d3b52c..00000000000 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -# TODO: remove this test when coverage is replaced or deemed irrelevant -module QA - RSpec.describe 'Create', :skip_live_env, product_group: :ide do - before do - skip("Skipped but kept as reference. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115741#note_1330720944") - end - - describe 'Upload a file in Web IDE' do - let(:file_path) { Runtime::Path.fixture('web_ide', file_name) } - let(:project) { create(:project, :with_readme, name: 'upload-file-project') } - - before do - Flow::Login.sign_in - - project.visit! - Page::Project::Show.perform(&:open_web_ide!) - end - - context 'when a file with the same name already exists' do - let(:file_name) { 'README.md' } - - it 'throws an error', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347850' do - Page::Project::WebIDE::Edit.perform do |ide| - ide.wait_until_ide_loads - ide.upload_file(file_path) - end - - expect(page).to have_content('The name "README.md" is already taken in this directory.') - end - end - - context 'when the file is a text file' do - let(:file_name) { 'text_file.txt' } - - it 'shows the Edit tab with the text', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347852' do - Page::Project::WebIDE::Edit.perform do |ide| - ide.wait_until_ide_loads - ide.upload_file(file_path) - - expect(ide).to have_file(file_name) - expect(ide).to have_file_addition_icon(file_name) - expect(ide).to have_text('Simple text') - - ide.commit_changes - - expect(ide).to have_file(file_name) - end - end - end - - context 'when the file is binary' do - let(:file_name) { 'logo_sample.svg' } - - it 'shows a Download button', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347851' do - Page::Project::WebIDE::Edit.perform do |ide| - ide.upload_file(file_path) - - expect(ide).to have_file(file_name) - expect(ide).to have_file_addition_icon(file_name) - expect(ide).to have_download_button(file_name) - - ide.commit_changes - - expect(ide).to have_file(file_name) - end - end - end - - context 'when the file is an image' do - let(:file_name) { 'dk.png' } - - it 'shows an image viewer', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347853' do - Page::Project::WebIDE::Edit.perform do |ide| - ide.upload_file(file_path) - - expect(ide).to have_file(file_name) - expect(ide).to have_file_addition_icon(file_name) - expect(ide).to have_image_viewer(file_name) - - ide.commit_changes - - expect(ide).to have_file(file_name) - end - end - end - end - end -end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index ca08bc9e577..9ab5b1fd3bb 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -82,4 +82,12 @@ RSpec.describe "Admin::Users", feature_category: :user_management do end end end + + it 'does not perform N+1 queries' do + control_queries = ActiveRecord::QueryRecorder.new { visit admin_users_path } + + expect { create(:user) }.to change { User.count }.by(1) + + expect { visit admin_users_path }.not_to exceed_query_limit(control_queries) + end end diff --git a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json index 44d8e48a972..61472b273e1 100644 --- a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json +++ b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json @@ -1,19 +1,51 @@ { "type": "object", "properties": { - "edit": { "type": "string" }, - "approve": { "type": "string" }, - "reject": { "type": "string" }, - "unblock": { "type": "string" }, - "block": { "type": "string" }, - "deactivate": { "type": "string" }, - "activate": { "type": "string" }, - "unlock": { "type": "string" }, - "delete": { "type": "string" }, - "delete_with_contributions": { "type": "string" }, - "admin_user": { "type": "string" }, - "ban": { "type": "string" }, - "unban": { "type": "string" } + "edit": { + "type": "string" + }, + "approve": { + "type": "string" + }, + "reject": { + "type": "string" + }, + "unblock": { + "type": "string" + }, + "block": { + "type": "string" + }, + "deactivate": { + "type": "string" + }, + "activate": { + "type": "string" + }, + "unlock": { + "type": "string" + }, + "delete": { + "type": "string" + }, + "delete_with_contributions": { + "type": "string" + }, + "admin_user": { + "type": "string" + }, + "ban": { + "type": "string" + }, + "unban": { + "type": "string" + }, + "trust": { + "type": "string" + }, + "untrust": { + "type": "string" + } }, "required": [ "edit", @@ -28,7 +60,9 @@ "delete_with_contributions", "admin_user", "ban", - "unban" + "unban", + "trust", + "untrust" ], "additionalProperties": false } diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js index d341eb03b1b..39e8e51f43c 100644 --- a/spec/frontend/admin/users/constants.js +++ b/spec/frontend/admin/users/constants.js @@ -9,6 +9,8 @@ const REJECT = 'reject'; const APPROVE = 'approve'; const BAN = 'ban'; const UNBAN = 'unban'; +const TRUST = 'trust'; +const UNTRUST = 'untrust'; export const EDIT = 'edit'; @@ -24,6 +26,8 @@ export const CONFIRMATION_ACTIONS = [ UNBAN, APPROVE, REJECT, + TRUST, + UNTRUST, ]; export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS]; diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js index 310966243d1..f5850401b8e 100644 --- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js +++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js @@ -38,7 +38,7 @@ describe('ContributionEventBase', () => { expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url); expect(avatarLabeled.attributes()).toMatchObject({ src: defaultPropsData.event.author.avatar_url, - size: '32', + size: '24', }); expect(avatarLabeled.props()).toMatchObject({ label: defaultPropsData.event.author.name, diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js new file mode 100644 index 00000000000..b30fd984099 --- /dev/null +++ b/spec/frontend/organizations/users/components/app_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/alert'; +import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql'; +import OrganizationsUsersApp from '~/organizations/users/components/app.vue'; +import { MOCK_ORGANIZATION_GID, MOCK_USERS } from '../mock_data'; + +jest.mock('~/alert'); + +Vue.use(VueApollo); + +const mockError = new Error(); + +const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {})); +const successfulResolver = (nodes) => + jest.fn().mockResolvedValue({ + data: { organization: { id: 1, organizationUsers: { nodes } } }, + }); +const errorResolver = jest.fn().mockRejectedValueOnce(mockError); + +describe('OrganizationsUsersApp', () => { + let wrapper; + let mockApollo; + + const createComponent = (mockResolvers = successfulResolver(MOCK_USERS)) => { + mockApollo = createMockApollo([[organizationUsersQuery, mockResolvers]]); + + wrapper = shallowMountExtended(OrganizationsUsersApp, { + apolloProvider: mockApollo, + provide: { + organizationGid: MOCK_ORGANIZATION_GID, + }, + }); + }; + + afterEach(() => { + mockApollo = null; + }); + + const findOrganizationUsersLoading = () => wrapper.findByText('Loading'); + const findOrganizationUsers = () => wrapper.findByTestId('organization-users'); + + describe.each` + description | mockResolver | loading | userData | error + ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false} + ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS} | ${false} + ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false} + ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true} + `('$description', ({ mockResolver, loading, userData, error }) => { + beforeEach(async () => { + createComponent(mockResolver); + await waitForPromises(); + }); + + it(`does ${ + loading ? '' : 'not ' + }render the organization users view with loading placeholder`, () => { + expect(findOrganizationUsersLoading().exists()).toBe(loading); + }); + + it(`renders the organization users view with ${ + userData.length ? 'correct' : 'empty' + } users array raw data`, () => { + expect(JSON.parse(findOrganizationUsers().text())).toStrictEqual(userData); + }); + + it(`does ${error ? '' : 'not '}render an error message`, () => { + return error + ? expect(createAlert).toHaveBeenCalledWith({ + message: + 'An error occurred loading the organization users. Please refresh the page to try again.', + error: mockError, + captureError: true, + }) + : expect(createAlert).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js new file mode 100644 index 00000000000..5503b063ad2 --- /dev/null +++ b/spec/frontend/organizations/users/mock_data.js @@ -0,0 +1,19 @@ +export const MOCK_ORGANIZATION_GID = 'gid://gitlab/Organizations::Organization/1'; + +export const MOCK_USERS = [ + { + badges: [], + id: 'gid://gitlab/Organizations::OrganizationUser/3', + user: { id: 'gid://gitlab/User/3' }, + }, + { + badges: [], + id: 'gid://gitlab/Organizations::OrganizationUser/2', + user: { id: 'gid://gitlab/User/2' }, + }, + { + badges: ['Admin', "It's you!"], + id: 'gid://gitlab/Organizations::OrganizationUser/1', + user: { id: 'gid://gitlab/User/1' }, + }, +]; diff --git a/spec/helpers/admin/user_actions_helper_spec.rb b/spec/helpers/admin/user_actions_helper_spec.rb index 87d2308690c..abfdabf3413 100644 --- a/spec/helpers/admin/user_actions_helper_spec.rb +++ b/spec/helpers/admin/user_actions_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Admin::UserActionsHelper do +RSpec.describe Admin::UserActionsHelper, feature_category: :user_management do describe '#admin_actions' do let_it_be(:current_user) { build(:user) } @@ -29,13 +29,33 @@ RSpec.describe Admin::UserActionsHelper do context 'the user is a standard user' do let_it_be(:user) { create(:user) } - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") } + it do + is_expected.to contain_exactly( + "edit", + "block", + "ban", + "deactivate", + "delete", + "delete_with_contributions", + "trust" + ) + end end context 'the user is an admin user' do let_it_be(:user) { create(:user, :admin) } - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") } + it do + is_expected.to contain_exactly( + "edit", + "block", + "ban", + "deactivate", + "delete", + "delete_with_contributions", + "trust" + ) + end end context 'the user is blocked by LDAP' do @@ -59,7 +79,16 @@ RSpec.describe Admin::UserActionsHelper do context 'the user is deactivated' do let_it_be(:user) { create(:user, :deactivated) } - it { is_expected.to contain_exactly("edit", "block", "ban", "activate", "delete", "delete_with_contributions") } + it do + is_expected.to contain_exactly( + "edit", + "block", + "ban", + "activate", + "delete", + "delete_with_contributions" + ) + end end context 'the user is locked' do @@ -77,7 +106,8 @@ RSpec.describe Admin::UserActionsHelper do "deactivate", "unlock", "delete", - "delete_with_contributions" + "delete_with_contributions", + "trust" ) } end @@ -88,6 +118,21 @@ RSpec.describe Admin::UserActionsHelper do it { is_expected.to contain_exactly("edit", "unban", "delete", "delete_with_contributions") } end + context 'the user is trusted' do + let_it_be(:user) { create(:user, :trusted) } + + it do + is_expected.to contain_exactly("edit", + "block", + "deactivate", + "ban", + "delete", + "delete_with_contributions", + "untrust" + ) + end + end + context 'the current_user does not have permission to delete the user' do let_it_be(:user) { build(:user) } @@ -95,7 +140,7 @@ RSpec.describe Admin::UserActionsHelper do allow(helper).to receive(:can?).with(current_user, :destroy_user, user).and_return(false) end - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate") } + it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "trust") } end context 'the user is a sole owner of a group' do @@ -106,7 +151,7 @@ RSpec.describe Admin::UserActionsHelper do group.add_owner(user) end - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions") } + it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions", "trust") } end context 'the user is a bot' do diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 5e54fea065f..77ff508b1c8 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -107,4 +107,14 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do ) end end + + describe '#organization_user_app_data' do + it 'returns expected data object' do + expect(helper.organization_user_app_data(organization)).to eq( + { + organization_gid: organization.to_global_id + } + ) + end + end end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 77361b09857..751611be5d2 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -48,6 +48,14 @@ RSpec.describe Gitlab::Git::Blame, feature_category: :source_code_management do end end + context 'when path is missing' do + let(:path) { 'unknown_file' } + + it 'returns an empty array' do + expect(result).to eq([]) + end + end + context "ISO-8859 encoding" do let(:path) { 'encoding/iso8859.txt' } diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 2ee9d85c723..02c7abadd99 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -1041,9 +1041,11 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do end describe '#raw_blame' do - let(:project) { create(:project, :test_repo) } + let_it_be(:project) { create(:project, :test_repo) } + let(:revision) { 'blame-on-renamed' } let(:path) { 'files/plain_text/renamed' } + let(:range) { nil } let(:blame_headers) do [ @@ -1073,6 +1075,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do is_expected.not_to include(blame_headers[0], blame_headers[1], blame_headers[4]) end end + + context 'when out of range' do + let(:range) { '9999,99999' } + + it { expect { blame }.to raise_error(ArgumentError, 'range is outside of the file length') } + end + + context 'when a file path is not found' do + let(:path) { 'unknown/path' } + + it { expect { blame }.to raise_error(ArgumentError, 'path not found in revision') } + end + + context 'when an unknown exception is raised' do + let(:gitaly_exception) { GRPC::BadStatus.new(GRPC::Core::StatusCodes::NOT_FOUND) } + + before do + expect_any_instance_of(Gitaly::CommitService::Stub) + .to receive(:raw_blame) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_raise(gitaly_exception) + end + + it { expect { blame }.to raise_error(gitaly_exception) } + end end describe '#get_commit_signatures' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c9da1a31c86..09eb92e01e6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1419,6 +1419,16 @@ RSpec.describe User, feature_category: :user_profile do 'ORDER BY "users"."current_sign_in_at" ASC NULLS LAST') end end + + describe '.trusted' do + let_it_be(:trusted_user1) { create(:user, :trusted) } + let_it_be(:trusted_user2) { create(:user, :trusted) } + let_it_be(:user3) { create(:user) } + + it 'returns only the trusted users' do + expect(described_class.trusted).to match_array([trusted_user1, trusted_user2]) + end + end end context 'strip attributes' do @@ -2858,6 +2868,12 @@ RSpec.describe User, feature_category: :user_profile do expect(described_class.filter_items('wop')).to include user end + + it 'filters by trusted' do + expect(described_class).to receive(:trusted).and_return([user]) + + expect(described_class.filter_items('trusted')).to include user + end end describe '.without_projects' do |