Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-03-05 00:07:59 +00:00
parent 23a4cb9200
commit 3737826152
35 changed files with 567 additions and 310 deletions

View File

@ -341,7 +341,7 @@ gem 'gitlab_chronic_duration', '~> 0.12' # rubocop:todo Gemfile/MissingFeatureCa
gem 'rack-proxy', '~> 0.7.7' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'cssbundling-rails', '1.3.3', feature_category: :shared
gem 'cssbundling-rails', '1.4.0', feature_category: :shared
gem 'autoprefixer-rails', '10.2.5.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'terser', '1.0.2' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -96,7 +96,7 @@
{"name":"creole","version":"0.5.0","platform":"ruby","checksum":"951701e2d80760f156b1cb2a93471ca97c076289becc067a33b745133ed32c03"},
{"name":"crystalball","version":"0.7.0","platform":"ruby","checksum":"6e729f372a5071daec877adb40c5df4cb25fe21f350635e2a9624373fc151ef2"},
{"name":"css_parser","version":"1.14.0","platform":"ruby","checksum":"f2ce6148cd505297b07bdbe7a5db4cce5cf530071f9b732b9a23538d6cdc0113"},
{"name":"cssbundling-rails","version":"1.3.3","platform":"ruby","checksum":"4aa13311e52a40bc0eb32ca651f44db8df03df273553cf9aeb022570607e1855"},
{"name":"cssbundling-rails","version":"1.4.0","platform":"ruby","checksum":"082034653af0ec53d7662e4cd2f518f36167fe7c014dbcf37a941a4a8324f7db"},
{"name":"cvss-suite","version":"3.0.1","platform":"ruby","checksum":"b5ca9e9e94032a42fd0dc28c1e305378b62c949e35ed7111fc4a1d76f68ad3f9"},
{"name":"danger","version":"9.4.2","platform":"ruby","checksum":"43e552c6731030235a30fdeafe703d2e2ab9c30917154489cb0ecd9ad3259d80"},
{"name":"danger-gitlab","version":"8.0.0","platform":"ruby","checksum":"497dd7d0f6513913de651019223d8058cf494df10acbd17de92b175dfa04a3a8"},

View File

@ -435,7 +435,7 @@ GEM
git
css_parser (1.14.0)
addressable
cssbundling-rails (1.3.3)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
cvss-suite (3.0.1)
danger (9.4.2)
@ -1861,7 +1861,7 @@ DEPENDENCIES
countries (~> 4.0.0)
creole (~> 0.5.0)
crystalball (~> 0.7.0)
cssbundling-rails (= 1.3.3)
cssbundling-rails (= 1.4.0)
csv_builder!
cvss-suite (~> 3.0.1)
database_cleaner-active_record (~> 2.1.0)

View File

@ -16,6 +16,7 @@ import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import runnerForRegistrationQuery from '../../graphql/register/runner_for_registration.query.graphql';
import provisionGoogleCloudRunnerGroup from '../../graphql/register/provision_google_cloud_runner_group.query.graphql';
import provisionGoogleCloudRunnerProject from '../../graphql/register/provision_google_cloud_runner_project.query.graphql';
import {
I18N_FETCH_ERROR,
@ -159,6 +160,8 @@ export default {
provisioningSteps: [],
setupBashScript: '',
showAlert: false,
group: null,
project: null,
};
},
apollo: {
@ -215,6 +218,29 @@ export default {
return !this.projectPath || this.invalidFields.length > 0;
},
},
group: {
query: provisionGoogleCloudRunnerGroup,
variables() {
return {
fullPath: this.groupPath,
cloudProjectId: this.projectId,
region: this.region,
zone: this.zone,
machineType: this.machineType,
runnerToken: this.token,
};
},
result({ data }) {
this.provisioningSteps = data.group.runnerCloudProvisioning?.provisioningSteps;
this.setupBashScript = data.group.runnerCloudProvisioning?.projectSetupShellScript;
},
error(error) {
captureException({ error, component: this.$options.name });
},
skip() {
return !this.groupPath || this.invalidFields.length > 0;
},
},
},
computed: {
isRunnerOnline() {

View File

@ -0,0 +1,27 @@
query provisionGoogleCloudRunnerGroup(
$fullPath: ID!
$cloudProjectId: GoogleCloudProject!
$region: GoogleCloudRegion!
$zone: GoogleCloudZone!
$machineType: GoogleCloudMachineType!
$runnerToken: String
) {
group(fullPath: $fullPath) {
id
runnerCloudProvisioning(provider: GOOGLE_CLOUD, cloudProjectId: $cloudProjectId) {
... on CiRunnerGoogleCloudProvisioning {
projectSetupShellScript
provisioningSteps(
region: $region
zone: $zone
ephemeralMachineType: $machineType
runnerToken: $runnerToken
) {
title
languageIdentifier
instructions
}
}
}
}
}

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Resolvers
module Projects
class DeployKeyResolver < BaseResolver
include LooksAhead
type Types::AccessLevels::DeployKeyType, null: true
def resolve_with_lookahead(**args)
apply_lookahead(Autocomplete::DeployKeysWithWriteAccessFinder.new(current_user,
object).execute(title_search_term: args[:title_query]))
end
def preloads
{
user: [:user]
}
end
end
end
end

View File

@ -704,6 +704,17 @@ module Types
alpha: { milestone: '16.9' },
null: true
field :available_deploy_keys, Types::AccessLevels::DeployKeyType.connection_type,
resolver: Resolvers::Projects::DeployKeyResolver,
description: 'List of available deploy keys',
extras: [:lookahead],
null: true,
authorize: :admin_project do
argument :title_query, GraphQL::Types::String,
required: false,
description: 'Term by which to search deploy key titles'
end
def protectable_branches
ProtectableDropdown.new(project, :branches).protectable_ref_names
end

View File

@ -28,7 +28,6 @@ module Avatarable
mount_uploader :avatar, AvatarUploader
after_initialize :add_avatar_to_batch
after_commit :clear_avatar_caches
end
module ShadowMethods
@ -130,10 +129,4 @@ module Avatarable
def avatar_mounter
strong_memoize(:avatar_mounter) { _mounter(:avatar) }
end
def clear_avatar_caches
return unless respond_to?(:verified_emails) && verified_emails.any? && avatar_changed?
Gitlab::AvatarCache.delete_by_email(*verified_emails)
end
end

View File

@ -1117,23 +1117,7 @@ class MergeRequest < ApplicationRecord
merge_request_diff.persisted? || create_merge_request_diff
end
def eager_fetch_ref!
return unless valid?
# has_internal_id normally attempts to allocate the iid in the
# before_create hook, but we need the iid to be available before
# that to fetch the ref into the target project.
track_target_project_iid!
ensure_target_project_iid!
fetch_ref!
# Prevent the after_create hook from fetching the source branch again.
@skip_fetch_ref = true
end
def create_merge_request_diff
# Callers such as MergeRequests::BuildService may not call eager_fetch_ref!. Just
# in case they haven't, we fetch the ref.
fetch_ref! unless skip_fetch_ref
# n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377

View File

@ -73,6 +73,7 @@ module Groups
end
end
transfer_labels if Feature.enabled?(:group_labels_transfer)
remove_paid_features_for_projects(old_root_ancestor_id)
post_update_hooks(@updated_project_ids, old_root_ancestor_id)
propagate_integrations
@ -81,6 +82,14 @@ module Groups
true
end
def transfer_labels
@group.all_projects.each_batch(of: 10) do |projects|
projects.each do |project|
Labels::TransferService.new(current_user, @group, project).execute
end
end
end
# Overridden in EE
def post_update_hooks(updated_project_ids, old_root_ancestor_id)
refresh_project_authorizations

View File

@ -1,20 +1,20 @@
# frozen_string_literal: true
module ProjectAccessTokens
module ResourceAccessTokens
class RotateService < ::PersonalAccessTokens::RotateService
extend ::Gitlab::Utils::Override
def initialize(current_user, token, resource = nil)
@current_user = current_user
@token = token
@project = resource
@resource = resource
end
def execute(params = {})
super
end
attr_reader :project
attr_reader :resource
private
@ -44,15 +44,34 @@ module ProjectAccessTokens
end
def valid_access_level?
return true if current_user.can_admin_all_resources?
return false unless current_user.can?(:manage_resource_access_tokens, project)
return true if admin_all_resources?
return false unless can_manage_tokens?
token_access_level = project.team.max_member_access(token.user.id).to_i
current_user_access_level = project.team.max_member_access(current_user.id).to_i
token_access_level <= current_user_access_level
end
return true if token_access_level.to_i <= current_user_access_level
def admin_all_resources?
current_user.can_admin_all_resources?
end
false
def can_manage_tokens?
current_user.can?(:manage_resource_access_tokens, resource)
end
def token_access_level
if resource.is_a? Project
resource.team.max_member_access(token.user.id).to_i
else
resource.max_member_access_for_user(token.user).to_i
end
end
def current_user_access_level
if resource.is_a? Project
resource.team.max_member_access(current_user.id).to_i
else
resource.max_member_access_for_user(current_user).to_i
end
end
end
end

View File

@ -8,6 +8,9 @@ class AvatarUploader < GitlabUploader
MIME_ALLOWLIST = %w[image/png image/jpeg image/gif image/bmp image/tiff image/vnd.microsoft.icon].freeze
after :store, :clear_avatar_caches
after :remove, :clear_avatar_caches
def exists?
model.avatar.file && model.avatar.file.present?
end
@ -37,4 +40,10 @@ class AvatarUploader < GitlabUploader
def dynamic_segment
File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
end
def clear_avatar_caches(*)
return unless model.respond_to?(:verified_emails) && model.verified_emails.any?
Gitlab::AvatarCache.delete_by_email(*model.verified_emails)
end
end

View File

@ -0,0 +1,9 @@
---
name: group_labels_transfer
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354890
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146292
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443874
milestone: '16.10'
group: group::project management
type: gitlab_com_derisk
default_enabled: false

View File

@ -110,15 +110,15 @@ You may have connectivity issues due to the following reasons:
curl --verbose "https://customers.gitlab.com/"
```
- Use `nslookup` to identify the target IP addresses that your GitLab instance will need to access.
```shell
nslookup customers.gitlab.com
```
- If the curl command returns an error, either:
- [Configure a proxy](https://docs.gitlab.com/omnibus/settings/environment-variables.html) in `gitlab.rb` to point to your server.
- Contact your network administrator to make changes to an existing proxy or firewall.
- Check your firewall or proxy. The domain `https://customers.gitlab.com` is
fronted by Cloudflare. Ensure your firewall or proxy allows traffic to the Cloudflare
[IPv4](https://www.cloudflare.com/ips-v4/) and
[IPv6](https://www.cloudflare.com/ips-v6/) ranges for activation to work.
- [Configure a proxy](https://docs.gitlab.com/omnibus/settings/environment-variables.html)
in `gitlab.rb` to point to your server.
Contact your network administrator to make changes to an existing proxy or firewall.
- If an SSL inspection appliance is used, you must add the appliance's root CA certificate to `/etc/gitlab/trusted-certs` on your instance, then run `gitlab-ctl reconfigure`.
- **Customers Portal is not operational**:

View File

@ -9052,6 +9052,29 @@ Some of the types in the schema exist solely to model connections. Each connecti
has a distinct, named type, with a distinct named edge type. These are listed separately
below.
#### `AccessLevelDeployKeyConnection`
The connection type for [`AccessLevelDeployKey`](#accessleveldeploykey).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="accessleveldeploykeyconnectionedges"></a>`edges` | [`[AccessLevelDeployKeyEdge]`](#accessleveldeploykeyedge) | A list of edges. |
| <a id="accessleveldeploykeyconnectionnodes"></a>`nodes` | [`[AccessLevelDeployKey]`](#accessleveldeploykey) | A list of nodes. |
| <a id="accessleveldeploykeyconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `AccessLevelDeployKeyEdge`
The edge type for [`AccessLevelDeployKey`](#accessleveldeploykey).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="accessleveldeploykeyedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="accessleveldeploykeyedgenode"></a>`node` | [`AccessLevelDeployKey`](#accessleveldeploykey) | The item at the end of the edge. |
#### `AchievementConnection`
The connection type for [`Achievement`](#achievement).
@ -25377,6 +25400,22 @@ Returns [`[AutocompletedUser!]`](#autocompleteduser).
| ---- | ---- | ----------- |
| <a id="projectautocompleteuserssearch"></a>`search` | [`String`](#string) | Query to search users by name, username, or public email. |
##### `Project.availableDeployKeys`
List of available deploy keys.
Returns [`AccessLevelDeployKeyConnection`](#accessleveldeploykeyconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectavailabledeploykeystitlequery"></a>`titleQuery` | [`String`](#string) | Term by which to search deploy key titles. |
##### `Project.board`
A single board of the project.

View File

@ -12,7 +12,7 @@ DETAILS:
You can enable the Microsoft Azure OAuth 2.0 OmniAuth provider and sign in to
GitLab with your Microsoft Azure credentials. You can configure the provider that uses
[the earlier Azure Active Directory v1.0 endpoint](https://learn.microsoft.com/en-us/azure/active-directory/azuread-dev/v1-protocols-oauth-code),
[the earlier Azure Active Directory v1.0 endpoint](https://learn.microsoft.com/en-us/previous-versions/azure/active-directory/azuread-dev/v1-protocols-oauth-code),
or the provider that uses the v2.0 endpoint.
NOTE:
@ -177,7 +177,7 @@ an Azure application and get a client ID and secret key.
1. Sign in to the [Azure portal](https://portal.azure.com).
1. If you have multiple Azure Active Directory tenants, switch to the desired tenant. Note the tenant ID.
1. [Register an application](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
1. [Register an application](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app)
and provide the following information:
- The redirect URI, which requires the URL of the Azure OAuth callback of your GitLab
installation. For example:
@ -195,7 +195,7 @@ In some Microsoft documentation, the terms are named `Application ID` and
## Add API permissions (scopes)
If you're using the v2.0 endpoint, after you create the application, [configure it to expose a web API](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-expose-web-apis).
If you're using the v2.0 endpoint, after you create the application, [configure it to expose a web API](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis).
Add the following delegated permissions under the Microsoft Graph API:
- `email`
@ -263,7 +263,7 @@ Alternatively, add the `User.Read.All` application permission.
]
```
For [alternative Azure clouds](https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud),
For [alternative Azure clouds](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud),
configure `base_azure_url` under the `args` section. For example, for Azure Government Community Cloud (GCC):
```ruby
@ -303,7 +303,7 @@ Alternatively, add the `User.Read.All` application permission.
tenant_id: "<tenant_id>" } }
```
For [alternative Azure clouds](https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud),
For [alternative Azure clouds](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud),
configure `base_azure_url` under the `args` section. For example, for Azure Government Community Cloud (GCC):
```yaml
@ -315,7 +315,7 @@ Alternatively, add the `User.Read.All` application permission.
base_azure_url: "https://login.microsoftonline.us" } }
```
You can also optionally add the `scope` for [OAuth 2.0 scopes](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) parameter to the `args` section. The default is `openid profile email`.
You can also optionally add the `scope` for [OAuth 2.0 scopes](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow) parameter to the `args` section. The default is `openid profile email`.
1. Save the configuration file.

View File

@ -20,7 +20,7 @@ To enable the GitHub OmniAuth provider, you need an OAuth 2.0 client ID and clie
secret from GitHub:
1. Sign in to GitHub.
1. [Create an OAuth App](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app)
1. [Create an OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
and provide the following information:
- The URL of your GitLab instance, such as `https://gitlab.example.com`.
- The authorization callback URL, such as, `https://gitlab.example.com/users/auth`.

View File

@ -30,7 +30,7 @@ To use the GitLab Gitpod integration, it must be enabled for your GitLab instanc
1. It's [enabled in their user settings](#enable-gitpod-in-your-user-settings).
For more information about Gitpod, see the Gitpod [features](https://www.gitpod.io/) and
[documentation](https://www.gitpod.io/docs/).
[documentation](https://www.gitpod.io/docs).
## Enable Gitpod in your user settings

View File

@ -41,15 +41,15 @@ You can also integrate GitLab with the following security partners:
- [Anchore](https://docs.anchore.com/current/docs/configuration/integration/ci_cd/gitlab/)
- [Bridgecrew](https://docs.bridgecrew.io/docs/integrate-with-gitlab-self-managed)
- [Checkmarx](https://checkmarx.atlassian.net/wiki/spaces/SD/pages/1929937052/GitLab+Integration)
- [CodeSecure](https://codesecure.com/our-integrations/codesonar-sast-gitlab-ci-pipeline/)
- [Deepfactor](https://www.deepfactor.io/docs/integrate-deepfactor-scanner-in-your-ci-cd-pipelines/#gitlab)
- [Fortify](https://www.microfocus.com/en-us/fortify-integrations/gitlab)
- [GrammaTech](https://www.grammatech.com/codesonar-gitlab-integration)
- [Indeni](https://docs.cloudrail.app/#/integrations/gitlab)
- [Jscrambler](https://docs.jscrambler.com/code-integrity/documentation/gitlab-ci-integration)
- [Mend](https://www.mend.io/gitlab/)
- [Semgrep](https://semgrep.dev/for/gitlab)
- [Semgrep](https://semgrep.dev/for/gitlab/)
- [StackHawk](https://docs.stackhawk.com/continuous-integration/gitlab.html)
- [Tenable](https://docs.tenable.com/tenableio/Content/ContainerSecurity/GetStarted.htm)
- [Tenable](https://docs.tenable.com/vulnerability-management/Content/ContainerSecurity/GetStarted.htm)
- [Venafi](https://marketplace.venafi.com/xchange/620d2d6ed419fb06a5c5bd36/solution/6292c2ef7550f2ee553cf223)
- [Veracode](https://community.veracode.com/s/knowledgeitem/gitlab-ci-MCEKSYPRWL35BRTGOVI55SK5RI4A)

View File

@ -19,7 +19,7 @@ You can use the GitLab for Jira Cloud app to link top-level groups or subgroups.
To set up the GitLab for Jira Cloud app on GitLab.com, [install the GitLab for Jira Cloud app](#install-the-gitlab-for-jira-cloud-app).
After you set up the app, you can use the [project toolchain](https://support.atlassian.com/jira-software-cloud/docs/what-is-the-project-toolchain-in-jira)
After you set up the app, you can use the [project toolchain](https://support.atlassian.com/jira-software-cloud/docs/what-is-the-project-toolchain-in-jira/)
developed and maintained by Atlassian to [link GitLab repositories to Jira projects](https://support.atlassian.com/jira-software-cloud/docs/link-repositories-to-a-project/#Link-repositories-using-the-toolchain-feature).
The project toolchain does not affect how development information is synced between GitLab and Jira Cloud.

View File

@ -515,7 +515,7 @@ then override the icon in one of two ways:
- **Embed an image directly in a configuration file**: This example creates a Base64-encoded
version of your image you can serve through a
[Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs):
[Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs):
1. Encode your image file with a GNU `base64` command (such as `base64 -w 0 <logo.png>`)
which returns a single-line `<base64-data>` string.

View File

@ -15,7 +15,7 @@ to sign in to other services.
## Introduction to OpenID Connect
[OpenID Connect](https://openid.net/connect/) \(OIDC) is a simple identity layer on top of the
[OpenID Connect](https://openid.net/developers/how-connect-works/) \(OIDC) is a simple identity layer on top of the
OAuth 2.0 protocol. It allows clients to:
- Verify the identity of the end-user based on the authentication performed by GitLab.
@ -25,7 +25,7 @@ OIDC performs many of the same tasks as OpenID 2.0, but is API-friendly and usab
mobile applications.
On the client side, you can use [OmniAuth::OpenIDConnect](https://github.com/omniauth/omniauth_openid_connect) for Rails
applications, or any of the other available [client implementations](https://openid.net/developers/libraries/#connect).
applications, or any of the other available [client implementations](https://openid.net/developers/certified-openid-connect-implementations/).
The GitLab implementation uses the [doorkeeper-openid_connect](https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website") gem, refer
to its README for more details about which parts of the specifications

View File

@ -716,7 +716,7 @@ Some IdPs have documentation on how to use them as the IdP in SAML configuration
For example:
- [Active Directory Federation Services (ADFS)](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-relying-party-trust)
- [Auth0](https://auth0.com/docs/authenticate/protocols/saml/saml-sso-integrations/configure-auth0-saml-identity-provider)
- [Auth0](https://auth0.com/docs/authenticate/single-sign-on/outbound-single-sign-on/configure-auth0-saml-identity-provider)
If you have any questions on configuring your IdP in a SAML configuration, contact
your provider's support.

View File

@ -19,7 +19,7 @@ Some features are still in development. View details about [support for each sta
| Goal | Feature | Tier/Offering/Status |
|---|---|---|
| Helps you write code more efficiently by showing code suggestions as you type. <br><br><i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=hCAyCTacdAQ) | [Code Suggestions](project/repository/code_suggestions/index.md) | **Tier:** Premium or Ultimate with [GitLab Duo Pro](../subscriptions/subscription-add-ons.md) <br>**Offering:** GitLab.com, Self-managed, GitLab Dedicated |
| Processes and generates text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | [Chat](gitlab_duo_chat.md) | **Beta Access** subject to the [Testing Agreement](https://handbook.gitlab.com/handbook/legal/testing-agreement/):<br>- GitLab.com, Self-managed, GitLab Dedicated <br>- Premium and Ultimate tiers<br><br>**Status:** Beta |
| Processes and generates text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | [Chat](gitlab_duo_chat.md) | **Tier:** Premium, Ultimate <br>**Offering:** GitLab.com, Self-managed, GitLab Dedicated <br>**Status:** Beta (Subject to the [Testing Agreement](https://handbook.gitlab.com/handbook/legal/testing-agreement/)) |
| Helps you discover or recall Git commands when and where you need them. | [Git suggestions](../editor_extensions/gitlab_cli/index.md#gitlab-duo-commands) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Experiment |
| Assists with quickly getting everyone up to speed on lengthy conversations to help ensure you are all on the same page. | [Discussion summary](#summarize-issue-discussions-with-discussion-summary) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Experiment |
| Generates issue descriptions. | [Issue description generation](#summarize-an-issue-with-issue-description-generation) | **Tier:** Ultimate<br>**Offering:** GitLab.com <br>**Status:** Experiment |

View File

@ -153,13 +153,8 @@ module API
token = find_token(resource, params[:token_id]) if resource_accessible
if token
response = if source_type == "project"
::ProjectAccessTokens::RotateService.new(current_user, token, resource)
response = ::ResourceAccessTokens::RotateService.new(current_user, token, resource)
.execute(declared_params)
else
::PersonalAccessTokens::RotateService.new(current_user, token)
.execute(declared_params)
end
if response.success?
status :ok

View File

@ -20841,6 +20841,9 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
msgid "Failed to fetch Namespace: %{fullPath}"
msgstr ""
msgid "Failed to fetch the iteration for this issue. Please try again."
msgstr ""

View File

@ -9,10 +9,12 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import GoogleCloudRegistrationInstructions from '~/ci/runner/components/registration/google_cloud_registration_instructions.vue';
import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
import provisionGoogleCloudRunnerQueryProject from '~/ci/runner/graphql/register/provision_google_cloud_runner_project.query.graphql';
import provisionGoogleCloudRunnerQueryGroup from '~/ci/runner/graphql/register/provision_google_cloud_runner_group.query.graphql';
import {
runnerForRegistration,
mockAuthenticationToken,
googleCloudRunnerProvisionResponse,
projectRunnerCloudProvisioningSteps,
groupRunnerCloudProvisioningSteps,
} from '../../mock_data';
Vue.use(VueApollo);
@ -34,10 +36,18 @@ const mockRunnerWithoutTokenResponse = {
},
};
const mockGoogleCloudRunnerProvisionResponse = {
const mockProjectRunnerCloudSteps = {
data: {
project: {
...googleCloudRunnerProvisionResponse,
...projectRunnerCloudProvisioningSteps,
},
},
};
const mockGroupRunnerCloudSteps = {
data: {
group: {
...groupRunnerCloudProvisioningSteps,
},
},
};
@ -64,20 +74,36 @@ describe('GoogleCloudRegistrationInstructions', () => {
const findInstructionsButton = () => wrapper.findByTestId('show-instructions-button');
const findAlert = () => wrapper.findComponent(GlAlert);
const fillInGoogleForm = () => {
findProjectIdInput().vm.$emit('input', 'dev-gcp-xxx-integrati-xxxxxxxx');
findRegionInput().vm.$emit('input', 'us-central1');
findZoneInput().vm.$emit('input', 'us-central1');
findInstructionsButton().vm.$emit('click');
return waitForPromises();
};
const runnerWithTokenResolver = jest.fn().mockResolvedValue(mockRunnerResponse);
const runnerWithoutTokenResolver = jest.fn().mockResolvedValue(mockRunnerWithoutTokenResponse);
const googleCloudRunnerResolver = jest
.fn()
.mockResolvedValue(mockGoogleCloudRunnerProvisionResponse);
const projectInstructionsResolver = jest.fn().mockResolvedValue(mockProjectRunnerCloudSteps);
const groupInstructionsResolver = jest.fn().mockResolvedValue(mockGroupRunnerCloudSteps);
const defaultHandlers = [[runnerForRegistrationQuery, runnerWithTokenResolver]];
const defaultProps = {
runnerId: mockRunnerId,
projectPath: 'test/project',
};
const createComponent = (mountFn = shallowMountExtended, handlers = defaultHandlers) => {
const createComponent = (
mountFn = shallowMountExtended,
handlers = defaultHandlers,
props = defaultProps,
) => {
wrapper = mountFn(GoogleCloudRegistrationInstructions, {
apolloProvider: createMockApollo(handlers),
propsData: {
runnerId: mockRunnerId,
projectPath: 'test/project',
...props,
},
});
};
@ -149,7 +175,7 @@ describe('GoogleCloudRegistrationInstructions', () => {
it('Hides an alert when the form is valid', async () => {
createComponent(mountExtended, [
[provisionGoogleCloudRunnerQueryProject, googleCloudRunnerResolver],
[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver],
]);
findProjectIdInput().vm.$emit('input', 'dev-gcp-xxx-integrati-xxxxxxxx');
@ -164,20 +190,32 @@ describe('GoogleCloudRegistrationInstructions', () => {
expect(findAlert().exists()).toBe(false);
});
it('Shows a modal with the correspondent scripts', async () => {
it('Shows a modal with the correspondent scripts for a project', async () => {
createComponent(shallowMountExtended, [
[provisionGoogleCloudRunnerQueryProject, googleCloudRunnerResolver],
[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver],
]);
findProjectIdInput().vm.$emit('input', 'dev-gcp-xxx-integrati-xxxxxxxx');
findRegionInput().vm.$emit('input', 'us-central1');
findZoneInput().vm.$emit('input', 'us-central1');
await fillInGoogleForm();
findInstructionsButton().vm.$emit('click');
expect(projectInstructionsResolver).toHaveBeenCalled();
expect(groupInstructionsResolver).not.toHaveBeenCalled();
await waitForPromises();
expect(findModalBashInstructions().text()).not.toBeNull();
expect(findModalTerrarformInstructions().text()).not.toBeNull();
expect(findModalTerrarformApplyInstructions().text).not.toBeNull();
});
expect(googleCloudRunnerResolver).toHaveBeenCalled();
it('Shows a modal with the correspondent scripts for a group', async () => {
createComponent(
shallowMountExtended,
[[provisionGoogleCloudRunnerQueryGroup, groupInstructionsResolver]],
{ runnerId: mockRunnerId, groupPath: 'groups/test' },
);
await fillInGoogleForm();
expect(groupInstructionsResolver).toHaveBeenCalled();
expect(projectInstructionsResolver).not.toHaveBeenCalled();
expect(findModalBashInstructions().text()).not.toBeNull();
expect(findModalTerrarformInstructions().text()).not.toBeNull();

View File

@ -387,7 +387,7 @@ export const mockAuthenticationToken = 'MOCK_AUTHENTICATION_TOKEN';
export const newRunnerPath = '/runners/new';
export const runnerInstallHelpPage = 'https://docs.example.com/runner/install/';
export const googleCloudRunnerProvisionResponse = {
export const projectRunnerCloudProvisioningSteps = {
__typename: 'Project',
id: 'gid://gitlab/Project/1',
runnerCloudProvisioning: {
@ -410,6 +410,29 @@ export const googleCloudRunnerProvisionResponse = {
},
};
export const groupRunnerCloudProvisioningSteps = {
__typename: 'Group',
id: 'gid://gitlab/Group/24',
runnerCloudProvisioning: {
__typename: 'CiRunnerGoogleCloudProvisioning',
projectSetupShellScript: '#!/bin/bash echo "hello world!"',
provisioningSteps: [
{
__typename: 'CiRunnerCloudProvisioningStep',
title: 'Save the Terraform script to a file',
languageIdentifier: 'terraform',
instructions: 'mock instructions...',
},
{
__typename: 'CiRunnerCloudProvisioningStep',
title: 'Apply the Terraform script',
languageIdentifier: 'shell',
instructions: 'mock instructions...',
},
],
},
};
export {
allRunnersData,
allRunnersWithCreatorData,

View File

@ -43,7 +43,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users
ci_cd_settings detailed_import_status value_streams ml_models
allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers is_forked
protectable_branches
protectable_branches available_deploy_keys
]
expect(described_class).to include_graphql_fields(*expected_fields)
@ -1219,4 +1219,59 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
end
end
end
describe 'available_deploy_keys' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
availableDeployKeys{
nodes{
id
title
user {
username
}
}
}
}
}
)
end
subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
let_it_be(:project) { create :project }
let_it_be(:deploy_key) { create(:deploy_keys_project, :write_access, project: project).deploy_key }
let_it_be(:maintainer) { create(:user) }
let(:available_deploy_keys) { subject.dig('data', 'project', 'availableDeployKeys', 'nodes') }
context 'when there are deploy keys' do
before_all do
project.add_maintainer(maintainer)
end
context 'and the current user has access' do
let(:current_user) { maintainer }
it 'returns the deploy keys' do
expect(available_deploy_keys[0]).to include({
'id' => deploy_key.to_global_id.to_s,
'title' => deploy_key.title,
'user' => {
'username' => deploy_key.user.username
}
})
end
end
context 'and the current user does not have access' do
let(:current_user) { create(:user) }
it 'does not return any deploy keys' do
expect(available_deploy_keys).to be_nil
end
end
end
end
end

View File

@ -4752,30 +4752,6 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
describe '#eager_fetch_ref!' do
let(:project) { create(:project, :repository) }
# We use build instead of create to test that an IID is allocated
subject { build(:merge_request, source_project: project) }
it 'fetches the ref and expires the ancestor cache' do
expect(subject).to receive(:expire_ancestor_cache).and_call_original
expect(subject.iid).to be_nil
expect { subject.eager_fetch_ref! }.to change { subject.iid.to_i }.by(1)
expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
end
it 'only fetches the ref once after saved' do
expect(subject.target_project.repository).to receive(:fetch_source_branch!).once.and_call_original
subject.save!
expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
end
end
describe 'removing a merge request' do
it 'refreshes the number of open merge requests of the target project' do
project = subject.target_project

View File

@ -575,21 +575,14 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
context 'when service raises an error' do
let(:error_message) { 'boom!' }
let(:personal_token_service) { PersonalAccessTokens::RotateService }
let(:project_token_service) { ProjectAccessTokens::RotateService }
let(:resource_token_service) { ResourceAccessTokens::RotateService }
before do
resource.add_maintainer(project_bot)
resource.add_owner(user)
if source_type == 'project'
allow_next_instance_of(project_token_service) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
end
else
allow_next_instance_of(personal_token_service) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
end
allow_next_instance_of(resource_token_service) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
end
end

View File

@ -141,6 +141,39 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :grou
end
end
context 'transferring labels' do
let(:new_parent_group) { create(:group, :private) }
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
let(:project) { create(:project, group: group) }
before do
group.add_owner(user)
parent_group.add_owner(user)
new_parent_group.add_owner(user)
end
context 'when the feature flag "group_labels_transfer" is disabled' do
before do
stub_feature_flags(group_labels_transfer: false)
end
it 'does not use Labels::TransferService' do
expect(Labels::TransferService).not_to receive(:new)
transfer_service.execute(new_parent_group)
end
end
it 'delegates transfer to Labels::TransferService' do
expect_next_instance_of(Labels::TransferService, user, project.group, project) do |labels_transfer_service|
expect(labels_transfer_service).to receive(:execute).once.and_call_original
end
transfer_service.execute(new_parent_group)
end
end
describe '#execute' do
context 'when transforming a group into a root group' do
let_it_be_with_reload(:group) { create(:group, :public, :nested) }

View File

@ -1,189 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ProjectAccessTokens::RotateService, feature_category: :system_access do
describe '#execute' do
let_it_be(:token, reload: true) { create(:personal_access_token) }
let(:current_user) { create(:user) }
let(:project) { create(:project, group: create(:group)) }
let(:error_message) { 'Not eligible to rotate token with access level higher than the user' }
subject(:response) { described_class.new(current_user, token, project).execute }
shared_examples_for 'rotates token succesfully' do
it "rotates user's own token", :freeze_time do
expect(response).to be_success
new_token = response.payload[:personal_access_token]
expect(new_token.token).not_to eq(token.token)
expect(new_token.expires_at).to eq(Date.today + 1.week)
expect(new_token.user).to eq(token.user)
end
end
context 'when user tries to rotate token with different access level' do
before do
project.add_guest(token.user)
end
context 'when current user is an owner' do
before do
project.add_owner(current_user)
end
it_behaves_like "rotates token succesfully"
context 'when creating the new token fails' do
let(:error_message) { 'boom!' }
before do
allow_next_instance_of(PersonalAccessToken) do |token|
allow(token).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return(error_message)
allow(token).to receive_message_chain(:errors, :clear)
allow(token).to receive_message_chain(:errors, :empty?).and_return(false)
end
end
it 'returns an error' do
expect(response).to be_error
expect(response.message).to eq(error_message)
end
it 'reverts the changes' do
expect { response }.not_to change { token.reload.revoked? }.from(false)
end
end
end
context 'when current user is not an owner' do
context 'when current user is maintainer' do
before do
project.add_maintainer(current_user)
end
context 'when access level is not owner' do
it_behaves_like "rotates token succesfully"
end
context 'when access level is owner' do
before do
project.add_owner(token.user)
end
it "does not rotate token with higher priviledge" do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
context 'when current user is not maintainer' do
before do
project.add_developer(current_user)
end
it 'does not rotate the token' do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
context 'when current user is admin' do
let(:current_user) { create(:admin) }
context 'when admin mode enabled', :enable_admin_mode do
it_behaves_like "rotates token succesfully"
end
context 'when admin mode not enabled' do
it 'does not rotate the token' do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
context 'when nested membership' do
let_it_be(:project_bot) { create(:user, :project_bot) }
let(:token) { create(:personal_access_token, user: project_bot) }
let(:top_level_group) { create(:group) }
let(:sub_group) { create(:group, parent: top_level_group) }
let(:project) { create(:project, group: sub_group) }
before do
project.add_maintainer(project_bot)
end
context 'when current user is an owner' do
before do
project.add_owner(current_user)
end
it_behaves_like "rotates token succesfully"
context 'when its a bot user' do
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:bot_user_membership) do
create(:project_member, :developer, user: bot_user, project: create(:project))
end
let_it_be(:token, reload: true) { create(:personal_access_token, user: bot_user) }
it 'updates membership expires at' do
response
new_token = response.payload[:personal_access_token]
expect(bot_user_membership.reload.expires_at).to eq(new_token.expires_at)
end
end
end
context 'when current user is not an owner' do
context 'when current user is maintainer' do
before do
project.add_maintainer(current_user)
end
context 'when access level is not owner' do
it_behaves_like "rotates token succesfully"
end
context 'when access level is owner' do
before do
project.add_owner(token.user)
end
it "does not rotate token with higher priviledge" do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
context 'when current user is not maintainer' do
before do
project.add_developer(current_user)
end
it 'does not rotate the token' do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
end
end
end
end

View File

@ -0,0 +1,157 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ResourceAccessTokens::RotateService, feature_category: :system_access do
shared_examples_for 'rotates token successfully' do
it "rotates user's own token", :freeze_time do
expect(response).to be_success
new_token = response.payload[:personal_access_token]
expect(new_token.token).not_to eq(token.token)
expect(new_token.expires_at).to eq(Date.today + 1.week)
expect(new_token.user).to eq(token.user)
end
it 'updates membership expires_at' do
response
new_token = response.payload[:personal_access_token]
expect(bot_user.members.first.reload.expires_at).to eq(new_token.expires_at)
end
end
shared_examples 'token rotation access level check' do |source_type|
before do
resource.add_guest(token.user)
end
context 'when current user is an owner' do
before do
resource.add_owner(current_user)
end
it_behaves_like "rotates token successfully"
context 'when creating the new token fails' do
let(:error_message) { 'boom!' }
before do
allow_next_instance_of(PersonalAccessToken) do |token|
allow(token).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return(error_message)
allow(token).to receive_message_chain(:errors, :clear)
allow(token).to receive_message_chain(:errors, :empty?).and_return(false)
end
end
it 'returns an error' do
expect(response).to be_error
expect(response.message).to eq(error_message)
end
it 'reverts the changes' do
expect { response }.not_to change { token.reload.revoked? }.from(false)
end
end
end
context 'when current user is maintainer' do
before do
resource.add_maintainer(current_user)
end
context 'and token user is not owner' do
if source_type == 'project'
it_behaves_like "rotates token successfully"
elsif source_type == 'group'
it 'cannot rotate the token' do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
context 'and token user is owner' do
before do
resource.add_owner(token.user)
end
it "cannot rotate token with higher privilege" do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
context 'when current user is neither owner or maintainer' do
before do
resource.add_developer(current_user)
end
it 'cannot rotate the token' do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
context 'when current user is admin' do
let(:current_user) { create(:admin) }
context 'with admin mode', :enable_admin_mode do
it_behaves_like "rotates token successfully"
end
context 'without admin mode' do
it 'cannot rotate the token' do
response
expect(response).to be_error
expect(response.message).to eq(error_message)
end
end
end
end
describe '#execute' do
subject(:response) { described_class.new(current_user, token, resource).execute }
let(:current_user) { create(:user) }
let(:error_message) { 'Not eligible to rotate token with access level higher than the user' }
let(:bot_user) { create(:user, :project_bot) }
let(:token) { create(:personal_access_token, user: bot_user) }
context 'for project' do
let_it_be(:resource) { create(:project, group: create(:group)) }
it_behaves_like 'token rotation access level check', 'project'
context 'with a nested membership' do
let(:top_level_group) { create(:group) }
let(:sub_group) { create(:group, parent: top_level_group) }
let(:resource) { create(:project, group: sub_group) }
it_behaves_like 'token rotation access level check', 'project'
end
end
context 'for group' do
let(:resource) { create(:group) }
it_behaves_like 'token rotation access level check', 'group'
context 'with a nested membership' do
let(:top_level_group) { create(:group) }
let(:resource) { create(:group, parent: top_level_group) }
it_behaves_like 'token rotation access level check', 'group'
end
end
end
end

View File

@ -45,6 +45,32 @@ RSpec.describe AvatarUploader do
expect(uploader.absolute_path.scan(storage_path).size).to eq(1)
expect(uploader.absolute_path).to eq(absolute_path)
end
describe "avatar cache" do
subject(:user) { create(:user) }
let(:file_path) do
File.join("spec", "fixtures", "rails_sample.png")
end
let(:file) { fixture_file_upload(file_path) }
it "clears the cache on upload" do
expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails).once
user.avatar = file
user.save!
end
it "clears the cache on removal" do
user.avatar = file
user.save!
expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails).once
user.avatar.remove!
end
end
end
context 'accept allowlist file content type' do