Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
23a4cb9200
commit
3737826152
2
Gemfile
2
Gemfile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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**:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue