Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-08 00:14:32 +00:00
parent f0b862a982
commit c00e3e49ef
55 changed files with 886 additions and 435 deletions

View File

@ -332,7 +332,7 @@ export default {
v-if="showCheckbox"
class="gl-align-self-center"
:checked="checkboxChecked"
@input="$emit('checked-input', $event)"
@change="$emit('checked-input', $event)"
>
<span class="gl-sr-only">{{ __('Select all') }}</span>
</gl-form-checkbox>

View File

@ -276,7 +276,8 @@ class GroupsController < Groups::ApplicationController
:resource_access_token_creation_allowed,
:prevent_sharing_groups_outside_hierarchy,
:setup_for_company,
:jobs_to_be_done
:jobs_to_be_done,
:crm_enabled
]
end

View File

@ -18,7 +18,8 @@ module Mutations
def resolve(project_path:, iid:, contact_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, project.group, default_enabled: :yaml)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?(project)
contact_ids = contact_ids.compact.map do |contact_id|
raise Gitlab::Graphql::Errors::ArgumentError, "Contact #{contact_id} is invalid." unless contact_id.respond_to?(:model_id)
@ -43,6 +44,12 @@ module Mutations
errors: response.errors
}
end
private
def feature_enabled?(project)
Feature.enabled?(:customer_relations, project.group, default_enabled: :yaml) && project.group&.crm_enabled?
end
end
end
end

View File

@ -144,36 +144,39 @@ module ApplicationSettingsHelper
end
def external_authorization_description
_("If enabled, access to projects will be validated on an external service"\
s_("ExternalAuthorization|Access to projects is validated on an external service"\
" using their classification label.")
end
def external_authorization_timeout_help_text
_("Time in seconds GitLab will wait for a response from the external "\
"service. When the service does not respond in time, access will be "\
"denied.")
s_("ExternalAuthorization|Period GitLab waits for a response from the external "\
"service. If there is no response, access is denied. Default: 0.5 seconds.")
end
def external_authorization_url_help_text
_("When leaving the URL blank, classification labels can still be "\
"specified without disabling cross project features or performing "\
"external authorization checks.")
s_("ExternalAuthorization|URL to which the projects make authorization requests. If the URL is blank, cross-project "\
"features are available and can still specify classification "\
"labels for projects.")
end
def external_authorization_client_certificate_help_text
_("The X509 Certificate to use when mutual TLS is required to communicate "\
"with the external authorization service. If left blank, the server "\
"certificate is still validated when accessing over HTTPS.")
s_("ExternalAuthorization|Certificate used to authenticate with the external authorization service. "\
"If blank, the server certificate is validated when accessing over HTTPS.")
end
def external_authorization_client_key_help_text
_("The private key to use when a client certificate is provided. This value "\
"is encrypted at rest.")
s_("ExternalAuthorization|Private key of client authentication certificate. "\
"Encrypted when stored.")
end
def external_authorization_client_pass_help_text
_("The passphrase required to decrypt the private key. This is optional "\
"and the value is encrypted at rest.")
s_("ExternalAuthorization|Passphrase required to decrypt the private key. "\
"Encrypted when stored.")
end
def external_authorization_client_url_help_text
s_("ExternalAuthorization|Classification label to use when requesting authorization if no specific "\
" label is defined on the project.")
end
def sidekiq_job_limiter_mode_help_text

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Groups
module CrmSettingsHelper
def crm_feature_flag_enabled?(group)
Feature.enabled?(:customer_relations, group)
end
end
end

View File

@ -634,11 +634,16 @@ class Group < Namespace
group_members.find_by(user_id: user)
end
end
alias_method :resource_member, :group_member
def highest_group_member(user)
GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
end
def bots
users.project_bot
end
def related_group_ids
[id,
*ancestors.pluck(:id),

View File

@ -1667,6 +1667,7 @@ class Project < ApplicationRecord
project_members.find_by(user_id: user)
end
end
alias_method :resource_member, :project_member
def membership_locked?
false

View File

@ -23,6 +23,9 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
desc "User is a project bot"
condition(:project_bot) { user.project_bot? && access_level >= GroupMember::GUEST }
condition(:has_projects) do
group_projects_for(user: @user, group: @subject).any?
end
@ -75,7 +78,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
with_scope :subject
condition(:has_project_with_service_desk_enabled) { @subject.has_project_with_service_desk_enabled? }
condition(:crm_enabled, score: 0, scope: :subject) { Feature.enabled?(:customer_relations, @subject) }
condition(:crm_enabled, score: 0, scope: :subject) { Feature.enabled?(:customer_relations, @subject) && @subject.crm_enabled? }
with_scope :subject
condition(:group_runner_registration_allowed, score: 0, scope: :subject) do
@ -250,6 +253,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :admin_dependency_proxy
end
rule { project_bot }.enable :project_bot_access
rule { can?(:admin_group) & resource_access_token_feature_available }.policy do
enable :read_resource_access_tokens
enable :destroy_resource_access_tokens
@ -260,6 +265,10 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :create_resource_access_tokens
end
rule { can?(:project_bot_access) }.policy do
prevent :create_resource_access_tokens
end
rule { support_bot & has_project_with_service_desk_enabled }.policy do
enable :read_label
end

View File

@ -107,6 +107,7 @@ module Groups
def handle_changes
handle_settings_update
handle_crm_settings_update unless params[:crm_enabled].nil?
end
def handle_settings_update
@ -116,6 +117,15 @@ module Groups
::NamespaceSettings::UpdateService.new(current_user, group, settings_params).execute
end
def handle_crm_settings_update
crm_enabled = params.delete(:crm_enabled)
return if group.crm_enabled? == crm_enabled
crm_settings = group.crm_settings || group.build_crm_settings
crm_settings.enabled = crm_enabled
crm_settings.save
end
def allowed_settings_params
SETTINGS_PARAMS
end

View File

@ -63,7 +63,7 @@ module ResourceAccessTokens
name: params[:name] || "#{resource.name.to_s.humanize} bot",
email: generate_email,
username: generate_username,
user_type: "#{resource_type}_bot".to_sym,
user_type: :project_bot,
skip_confirmation: true # Bot users should always have their emails confirmed.
}
end

View File

@ -1,11 +1,12 @@
%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('External authentication')
= s_('ExternalAuthorization|External authorization')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('External Classification Policy Authorization')
= s_('ExternalAuthorization|External classification policy authorization.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/external_authorization'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form', id: 'external-auth-settings' } do |f|
@ -16,35 +17,37 @@
.form-check
= f.check_box :external_authorization_service_enabled, class: 'form-check-input'
= f.label :external_authorization_service_enabled, class: 'form-check-label' do
= _('Enable classification control using an external service')
= s_('ExternalAuthorization|Enable classification control using an external service')
%span.form-text.text-muted
= external_authorization_description
= link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/external_authorization')
.form-group
= f.label :external_authorization_service_url, _('Service URL'), class: 'label-bold'
= f.label :external_authorization_service_url, s_('ExternalAuthorization|Service URL'), class: 'label-bold'
= f.text_field :external_authorization_service_url, class: 'form-control gl-form-input'
%span.form-text.text-muted
= external_authorization_url_help_text
.form-group
= f.label :external_authorization_service_timeout, _('External authorization request timeout'), class: 'label-bold'
= f.label :external_authorization_service_timeout, s_('ExternalAuthorization|External authorization request timeout (seconds)'), class: 'label-bold'
= f.number_field :external_authorization_service_timeout, class: 'form-control gl-form-input', min: 0.001, max: 10, step: 0.001
%span.form-text.text-muted
= external_authorization_timeout_help_text
= f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'label-bold'
.form-group
= f.label :external_auth_client_cert, s_('ExternalAuthorization|Client authorization certificate'), class: 'label-bold'
= f.text_area :external_auth_client_cert, class: 'form-control gl-form-input'
%span.form-text.text-muted
= external_authorization_client_certificate_help_text
.form-group
= f.label :external_auth_client_key, _('Client authentication key'), class: 'label-bold'
= f.label :external_auth_client_key, s_('ExternalAuthorization|Client authorization key'), class: 'label-bold'
= f.text_area :external_auth_client_key, class: 'form-control gl-form-input'
%span.form-text.text-muted
= external_authorization_client_key_help_text
.form-group
= f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'label-bold'
= f.label :external_auth_client_key_pass, s_('ExternalAuthorization|Client authorization key password (optional)'), class: 'label-bold'
= f.password_field :external_auth_client_key_pass, class: 'form-control gl-form-input'
%span.form-text.text-muted
= external_authorization_client_pass_help_text
.form-group
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold'
= f.label :external_authorization_service_default_label, s_('ExternalAuthorization|Default classification label'), class: 'label-bold'
= f.text_field :external_authorization_service_default_label, class: 'form-control gl-form-input'
%span.form-text.text-muted
= external_authorization_client_url_help_text
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"

View File

@ -20,11 +20,11 @@
%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded), data: { qa_selector: 'permission_lfs_2fa_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Permissions, LFS, 2FA')
= _('Permissions and group features')
%button.btn.gl-button.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Configure advanced permissions, Large File Storage, and two-factor authentication settings.')
= _('Configure advanced permissions, Large File Storage, two-factor authentication, and customer relations settings.')
.settings-content
= render 'groups/settings/permissions'

View File

@ -41,4 +41,13 @@
= render 'groups/settings/two_factor_auth', f: f, group: @group
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render 'groups/settings/membership', f: f, group: @group
- if crm_feature_flag_enabled?(@group)
%h5= _('Customer relations')
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :crm_enabled,
s_('GroupSettings|Enable customer relations'),
checkbox_options: { checked: @group.crm_enabled? },
help_text: s_('GroupSettings|Allows creating organizations and contacts and associating them with issues.')
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }

View File

@ -25,7 +25,7 @@ The following API resources are available in the project context:
| Resource | Available endpoints |
|:------------------------------------------------------------------------|:--------------------|
| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) |
| [Access tokens](resource_access_tokens.md) | `/projects/:id/access_tokens` |
| [Access tokens](resource_access_tokens.md) | `/projects/:id/access_tokens` (also available for groups) |
| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` |
| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` |
| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` |
@ -100,6 +100,7 @@ The following API resources are available in the group context:
| Resource | Available endpoints |
|:-----------------------------------------------------------------|:--------------------|
| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) |
| [Access tokens](group_access_tokens.md) | `/groups/:id/access_tokens` (also available for projects) |
| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) |
| [Debian distributions](packages/debian_group_distributions.md) | `/groups/:id/-/packages/debian` (also available for projects) |
| [Deploy tokens](deploy_tokens.md) | `/groups/:id/deploy_tokens` (also available for projects and standalone) |

View File

@ -0,0 +1,112 @@
---
stage: Manage
group: Authentication & Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Group access tokens API **(FREE)**
You can read more about [group access tokens](../user/project/settings/project_access_tokens.md#group-access-tokens).
## List group access tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236) in GitLab 14.7.
Get a list of [group access tokens](../user/project/settings/project_access_tokens.md#group-access-tokens).
```plaintext
GET groups/:id/access_tokens
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer or string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/<group_id>/access_tokens"
```
```json
[
{
"user_id" : 141,
"scopes" : [
"api"
],
"name" : "token",
"expires_at" : "2021-01-31",
"id" : 42,
"active" : true,
"created_at" : "2021-01-20T22:11:48.151Z",
"revoked" : false,
"access_level": 40
}
]
```
## Create a group access token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236) in GitLab 14.7.
Create a [group access token](../user/project/settings/project_access_tokens.md#group-access-tokens).
```plaintext
POST groups/:id/access_tokens
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer or string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `name` | String | yes | The name of the group access token |
| `scopes` | `Array[String]` | yes | [List of scopes](../user/project/settings/project_access_tokens.md#scopes-for-a-project-access-token) |
| `access_level` | Integer | no | A valid access level. Default value is 40 (Maintainer). Other allowed values are 10 (Guest), 20 (Reporter), and 30 (Developer). |
| `expires_at` | Date | no | The token expires at midnight UTC on that date |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type:application/json" \
--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31", "access_level": 30 }' \
"https://gitlab.example.com/api/v4/groups/<group_id>/access_tokens"
```
```json
{
"scopes" : [
"api",
"read_repository"
],
"active" : true,
"name" : "test",
"revoked" : false,
"created_at" : "2021-01-21T19:35:37.921Z",
"user_id" : 166,
"id" : 58,
"expires_at" : "2021-01-31",
"token" : "D4y...Wzr",
"access_level": 30
}
```
## Revoke a group access token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236) in GitLab 14.7.
Revoke a [group access token](../user/project/settings/project_access_tokens.md#group-access-tokens).
```plaintext
DELETE groups/:id/access_tokens/:token_id
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer or string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `token_id` | integer or string | yes | The ID of the group access token |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/<group_id>/access_tokens/<token_id>"
```
### Responses
- `204: No Content` if successfully revoked.
- `400 Bad Request` or `404 Not Found` if not revoked successfully.

View File

@ -8,8 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## List project repository tags
Get a list of repository tags from a project, sorted by name in reverse
alphabetical order. This endpoint can be accessed without authentication if the
Get a list of repository tags from a project, sorted by update date and time in descending order. This endpoint can be accessed without authentication if the
repository is publicly accessible.
```plaintext

View File

@ -170,6 +170,13 @@ Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/), itself deployed with
See [Review Apps](../review_apps.md) for more details about Review Apps.
### Run tests in parallel
To run tests in parallel on CI, the [Knapsack](https://github.com/KnapsackPro/knapsack)
gem is used. Knapsack reports are generated automatically and stored in the `GCS` bucket
`knapsack-reports` in the `gitlab-qa-resources` project. The [`KnapsackReport`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/tools/knapsack_report.rb)
helper handles automated report generation and upload.
## Test metrics
For additional test health visibility, use a custom setup to export test execution

View File

@ -478,6 +478,8 @@ The following are some available Rake tasks:
| [`sudo gitlab-rake gitlab:elastic:mark_reindex_failed`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Mark the most recent re-index job as failed. |
| [`sudo gitlab-rake gitlab:elastic:list_pending_migrations`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | List pending migrations. Pending migrations include those that have not yet started, have started but not finished, and those that are halted. |
| [`sudo gitlab-rake gitlab:elastic:estimate_cluster_size`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Get an estimate of cluster size based on the total repository size. |
| [`sudo gitlab-rake gitlab:elastic:enable_search_with_elasticsearch`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enable advanced search with Elasticsearch. |
| [`sudo gitlab-rake gitlab:elastic:disable_search_with_elasticsearch`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Disables advanced search with Elasticsearch. |
### Environment variables

View File

@ -49,7 +49,7 @@ Gitlab::CurrentSettings.update!('require_two_factor_authentication': false)
To enforce 2FA only for certain groups:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select the **Require all users in this group to set up two-factor authentication** option.
You can also specify a grace period in the **Time before enforced** option.
@ -76,7 +76,7 @@ The following are important notes about 2FA:
groups) the shortest grace period is used.
- It is possible to disallow subgroups from setting up their own 2FA requirements:
1. Go to the top-level group's **Settings > General**.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Uncheck the **Allow subgroups to set up their own two-factor authentication rule** field.
This action causes all subgroups with 2FA requirements to stop requiring that from their members.

View File

@ -29,39 +29,13 @@ functionality that render cross-project data. That includes:
Labels, Milestones, Merge requests).
- Global and Group search are disabled.
This is to prevent performing to many requests at once to the external
This is to prevent performing too many requests at once to the external
authorization service.
Whenever access is granted or denied this is logged in a log file called
`external-policy-access-control.log`. Read more about the logs GitLab keeps in
the [Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/logs.html).
## Configuration
The external authorization service can be enabled by an administrator:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**:
![Enable external authorization service](img/external_authorization_service_settings.png)
The available required properties are:
- **Service URL**: The URL to make authorization requests to. When leaving the
URL blank, cross project features remain available while still being able
to specify classification labels for projects.
- **External authorization request timeout**: The timeout after which an
authorization request is aborted. When a request times out, access is denied
to the user.
- **Client authentication certificate**: The certificate to use to authenticate
with the external authorization service.
- **Client authentication key**: Private key for the certificate when
authentication is required for the external authorization service, this is
encrypted when stored.
- **Client authentication key password**: Passphrase to use for the private key
when authenticating with the external service this is encrypted when stored.
- **Default classification label**: The classification label to use when
requesting authorization if no specific label is defined on the project
When using TLS Authentication with a self signed certificate, the CA certificate
needs to be trusted by the OpenSSL installation. When using GitLab installed
using Omnibus, learn to install a custom CA in the
@ -69,6 +43,16 @@ using Omnibus, learn to install a custom CA in the
Alternatively, learn where to install custom certificates by using
`openssl version -d`.
## Configuration
The external authorization service can be enabled by an administrator:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand **External authorization**.
1. Complete the fields.
1. Select **Save changes**.
## How it works
When GitLab requests access, it sends a JSON POST request to the external

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -6,13 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Customer relations management (CRM) **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
On GitLab.com, this feature is not available.
You should not use this feature for production environments.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
With customer relations management (CRM) you can create a record of contacts
(individuals) and organizations (companies) and relate them to issues.
@ -20,6 +14,16 @@ With customer relations management (CRM) you can create a record of contacts
You can use contacts and organizations to tie work to customers for billing and reporting purposes.
To read more about what is planned for the future, see [issue 2256](https://gitlab.com/gitlab-org/gitlab/-/issues/2256).
## Enable customer relations management (CRM)
To enable customer relations management in a group:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Permissions and group features** section.
1. Select **Enable customer relations**.
1. Select **Save changes**.
## Contacts
### View contacts linked to a group

View File

@ -123,7 +123,7 @@ your group.
1. Select **Your Groups**.
1. Find the group and select it.
1. From the left menu, select **Settings > General**.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Clear the **Allow users to request access** checkbox.
1. Select **Save changes**.
@ -219,7 +219,7 @@ To change this setting for a specific group:
1. Select **Your Groups**.
1. Find the group and select it.
1. From the left menu, select **Settings > General**.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select the desired option in the **Default branch protection** dropdown list.
1. Select **Save changes**.
@ -250,7 +250,7 @@ To change this setting for a specific group:
1. Select **Your Groups**.
1. Find the group and select it.
1. From the left menu, select **Settings > General**.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select the desired option in the **Allowed to create projects** dropdown list.
1. Select **Save changes**.
@ -489,7 +489,7 @@ If you select this setting in the **Animals** group:
To prevent sharing outside of the group's hierarchy:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select **Prevent members from sending invitations to groups outside of `<group_name>` and its subgroups**.
1. Select **Save changes**.
@ -501,7 +501,7 @@ a project with another group](../project/members/share_project_with_groups.md) t
To prevent a project from being shared with other groups:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select **Prevent sharing a project within `<group_name>` with other groups**.
1. Select **Save changes**.
@ -523,7 +523,7 @@ The setting does not cascade. Projects in subgroups observe the subgroup configu
To prevent members from being added to projects in a group:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Under **Member lock**, select **Prevent adding new members to project membership within this group**.
1. Select **Save changes**.
@ -574,7 +574,7 @@ You should consider these security implications before configuring IP address re
To restrict group access by IP address:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. In the **Allow access to the following IP addresses** field, enter IP address ranges in CIDR notation.
1. Select **Save changes**.
@ -591,7 +591,7 @@ You can prevent users with email addresses in specific domains from being added
To restrict group access by domain:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. In the **Restrict membership by email** field, enter the domain names.
1. Select **Save changes**.
@ -645,7 +645,7 @@ You can disable all email notifications related to the group, which includes its
To disable email notifications:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select **Disable email notifications**.
1. Select **Save changes**.
@ -663,7 +663,7 @@ This is particularly helpful for groups with a large number of users.
To disable group mentions:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Select **Disable group mentions**.
1. Select **Save changes**.
@ -688,7 +688,7 @@ the default setting.
To enable delayed deletion of projects in a group:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Check **Enable delayed project deletion**.
1. Optional. To prevent subgroups from changing this setting, select **Enforce for all subgroups**.
1. Select **Save changes**.
@ -713,7 +713,7 @@ If even one is set to `true`, then the group does not allow outside forks.
To prevent projects from being forked outside the group:
1. Go to the top-level group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Check **Prevent project forking outside current group**.
1. Select **Save changes**.

View File

@ -113,7 +113,7 @@ on the lifetime of personal access tokens apply.
To set a limit on how long personal access tokens are valid for users in a group managed account:
1. Navigate to the **Settings > General** page in your group's sidebar.
1. Expand the **Permissions, LFS, 2FA** section.
1. Expand the **Permissions and group features** section.
1. Fill in the **Maximum allowable lifetime for personal access tokens (days)** field.
1. Click **Save changes**.

View File

@ -101,7 +101,7 @@ You can change this setting:
- As group owner:
1. Select the group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Permissions, LFS, 2FA**.
1. Expand **Permissions and group features**.
- As an administrator:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Overview > Groups**.

View File

@ -78,7 +78,7 @@ To enable or disable project access token creation for all projects in a top-lev
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Permissions, LFS, 2FA**.
1. Expand **Permissions and group features**.
1. Under **Permissions**, turn on or off **Allow project access token creation**.
Even when creation is disabled, you can still use and revoke existing project access tokens.

View File

@ -4,7 +4,7 @@ module API
module Entities
class ResourceAccessToken < Entities::PersonalAccessToken
expose :access_level do |token, options|
options[:project].project_member(token.user).access_level
options[:resource].resource_member(token.user).access_level
end
end
end

View File

@ -8,7 +8,7 @@ module API
feature_category :authentication_and_authorization
%w[project].each do |source_type|
%w[project group].each do |source_type|
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get list of all access tokens for the specified resource' do
detail 'This feature was introduced in GitLab 13.9.'
@ -23,8 +23,8 @@ module API
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute.preload_users
resource.project_members.load
present paginate(tokens), with: Entities::ResourceAccessToken, project: resource
resource.members.load
present paginate(tokens), with: Entities::ResourceAccessToken, resource: resource
end
desc 'Revoke a resource access token' do
@ -58,7 +58,7 @@ module API
requires :id, type: String, desc: "The #{source_type} ID"
requires :name, type: String, desc: "Resource access token name"
requires :scopes, type: Array[String], desc: "The permissions of the token"
optional :access_level, type: Integer, desc: "The access level of the token in the project"
optional :access_level, type: Integer, desc: "The access level of the token in the #{source_type}"
optional :expires_at, type: Date, desc: "The expiration date of the token"
end
post ':id/access_tokens' do
@ -71,7 +71,7 @@ module API
).execute
if token_response.success?
present token_response.payload[:access_token], with: Entities::ResourceAccessTokenWithToken, project: resource
present token_response.payload[:access_token], with: Entities::ResourceAccessTokenWithToken, resource: resource
else
bad_request!(token_response.message)
end

View File

@ -7386,15 +7386,6 @@ msgstr ""
msgid "Click to reveal"
msgstr ""
msgid "Client authentication certificate"
msgstr ""
msgid "Client authentication key"
msgstr ""
msgid "Client authentication key password"
msgstr ""
msgid "Client request timeout"
msgstr ""
@ -9034,7 +9025,7 @@ msgstr ""
msgid "Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
msgstr ""
msgid "Configure advanced permissions, Large File Storage, and two-factor authentication settings."
msgid "Configure advanced permissions, Large File Storage, two-factor authentication, and customer relations settings."
msgstr ""
msgid "Configure existing installation"
@ -11335,9 +11326,6 @@ msgstr ""
msgid "Default branch protection"
msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default delayed project deletion"
msgstr ""
@ -13207,9 +13195,6 @@ msgstr ""
msgid "Enable automatic repository housekeeping"
msgstr ""
msgid "Enable classification control using an external service"
msgstr ""
msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7."
msgstr ""
@ -14492,9 +14477,6 @@ msgstr ""
msgid "Exported requirements"
msgstr ""
msgid "External Classification Policy Authorization"
msgstr ""
msgid "External ID"
msgstr ""
@ -14504,15 +14486,9 @@ msgstr ""
msgid "External User:"
msgstr ""
msgid "External authentication"
msgstr ""
msgid "External authorization denied access to this project"
msgstr ""
msgid "External authorization request timeout"
msgstr ""
msgid "External storage URL"
msgstr ""
@ -14528,6 +14504,54 @@ msgstr ""
msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
msgstr ""
msgid "ExternalAuthorization|Access to projects is validated on an external service using their classification label."
msgstr ""
msgid "ExternalAuthorization|Certificate used to authenticate with the external authorization service. If blank, the server certificate is validated when accessing over HTTPS."
msgstr ""
msgid "ExternalAuthorization|Classification label to use when requesting authorization if no specific label is defined on the project."
msgstr ""
msgid "ExternalAuthorization|Client authorization certificate"
msgstr ""
msgid "ExternalAuthorization|Client authorization key"
msgstr ""
msgid "ExternalAuthorization|Client authorization key password (optional)"
msgstr ""
msgid "ExternalAuthorization|Default classification label"
msgstr ""
msgid "ExternalAuthorization|Enable classification control using an external service"
msgstr ""
msgid "ExternalAuthorization|External authorization"
msgstr ""
msgid "ExternalAuthorization|External authorization request timeout (seconds)"
msgstr ""
msgid "ExternalAuthorization|External classification policy authorization."
msgstr ""
msgid "ExternalAuthorization|Passphrase required to decrypt the private key. Encrypted when stored."
msgstr ""
msgid "ExternalAuthorization|Period GitLab waits for a response from the external service. If there is no response, access is denied. Default: 0.5 seconds."
msgstr ""
msgid "ExternalAuthorization|Private key of client authentication certificate. Encrypted when stored."
msgstr ""
msgid "ExternalAuthorization|Service URL"
msgstr ""
msgid "ExternalAuthorization|URL to which the projects make authorization requests. If the URL is blank, cross-project features are available and can still specify classification labels for projects."
msgstr ""
msgid "ExternalIssueIntegration|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}%{trackerName}%{linkEnd}."
msgstr ""
@ -17078,6 +17102,9 @@ msgstr ""
msgid "GroupSettings|Allow project access token creation"
msgstr ""
msgid "GroupSettings|Allows creating organizations and contacts and associating them with issues."
msgstr ""
msgid "GroupSettings|Applied to all subgroups unless overridden by a group owner. Groups already added to the project lose access."
msgstr ""
@ -17123,6 +17150,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
msgid "GroupSettings|Enable customer relations"
msgstr ""
msgid "GroupSettings|Enable delayed project deletion"
msgstr ""
@ -17823,9 +17853,6 @@ msgstr ""
msgid "If enabled, GitLab will handle Object Storage replication using Geo. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "If enabled, access to projects will be validated on an external service using their classification label."
msgstr ""
msgid "If enabled, only protected branches will be mirrored."
msgstr ""
@ -25810,7 +25837,7 @@ msgstr ""
msgid "Permissions Help"
msgstr ""
msgid "Permissions, LFS, 2FA"
msgid "Permissions and group features"
msgstr ""
msgid "Personal Access Token"
@ -32377,9 +32404,6 @@ msgstr ""
msgid "Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email."
msgstr ""
msgid "Service URL"
msgstr ""
msgid "Service account generated successfully"
msgstr ""
@ -35268,9 +35292,6 @@ msgstr ""
msgid "The URLs for connecting to Elasticsearch. For clustering, add the URLs separated by commas."
msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
msgstr ""
@ -35546,9 +35567,6 @@ msgstr ""
msgid "The parent epic is confidential and can only contain confidential epics and issues"
msgstr ""
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr ""
msgid "The password for the Jenkins server."
msgstr ""
@ -35564,9 +35582,6 @@ msgstr ""
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
msgstr ""
msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
msgstr ""
msgid "The project can be accessed by any logged in user except external users."
msgstr ""
@ -36785,9 +36800,6 @@ msgstr ""
msgid "Time in seconds"
msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
msgid "Time of import: %{importTime}"
msgstr ""
@ -40028,9 +40040,6 @@ msgstr ""
msgid "When inactive, an external authentication provider must be used."
msgstr ""
msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks."
msgstr ""
msgid "When merge requests and commits in the default branch close, any issues they reference also close."
msgstr ""

View File

@ -112,5 +112,11 @@ FactoryBot.define do
)
end
end
trait :crm_enabled do
after(:create) do |group|
create(:crm_settings, group: group, enabled: true)
end
end
end
end

View File

@ -9,7 +9,8 @@ RSpec.describe 'Group navbar' do
include_context 'group navbar structure'
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let(:group) { create(:group) }
before do
insert_package_nav(_('Kubernetes'))
@ -40,7 +41,9 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
context 'when customer_relations feature flag is enabled' do
context 'when customer_relations feature and flag is enabled' do
let(:group) { create(:group, :crm_enabled) }
before do
stub_feature_flags(customer_relations: true)

View File

@ -104,6 +104,26 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
end
end
describe 'select all issues' do
let!(:issue_2) { create(:issue, project: project) }
before do
stub_feature_flags(vue_issues_list: true)
end
it 'after selecting all issues, unchecking one issue only unselects that one issue' do
visit project_issues_path(project)
click_button 'Edit issues'
check 'Select all'
uncheck issue.title
expect(page).to have_unchecked_field 'Select all'
expect(page).to have_unchecked_field issue.title
expect(page).to have_checked_field issue_2.title
end
end
def create_closed
create(:issue, project: project, state: :closed)
end

View File

@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Contacts::Create do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let(:group) { create(:group, :crm_enabled) }
let(:not_found_or_does_not_belong) { 'The specified organization was not found or does not belong to this group' }
let(:valid_params) do
attributes_for(:contact,
@ -34,11 +34,11 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
end
context 'when the user has permission' do
before_all do
before do
group.add_developer(user)
end
context 'when the feature is disabled' do
context 'when the feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@ -49,6 +49,15 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
end
end
context 'when crm_enabled is false' do
let(:group) { create(:group) }
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
.with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
context 'when the params are invalid' do
it 'returns the validation error' do
valid_params[:first_name] = nil

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Contacts::Update do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let(:first_name) { 'Lionel' }
let(:last_name) { 'Smith' }

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Organizations::Create do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let(:valid_params) do
attributes_for(:organization,

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Organizations::Update do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let(:name) { 'GitLab' }
let(:default_rate) { 1000.to_f }
@ -56,7 +56,7 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
expect(resolve_mutation[:organization]).to have_attributes(attributes)
end
context 'when the feature is disabled' do
context 'when the feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@ -66,6 +66,15 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
.with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
context 'when the feature is disabled' do
let_it_be(:group) { create(:group) }
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
.with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::CrmSettingsHelper do
let_it_be(:group) { create(:group) }
describe '#crm_feature_flag_enabled?' do
subject do
helper.crm_feature_flag_enabled?(group)
end
context 'when feature flag is enabled' do
it { is_expected.to be_truthy }
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
it { is_expected.to be_falsy }
end
end
end

View File

@ -2086,6 +2086,23 @@ RSpec.describe Group do
end
end
describe '#bots' do
subject { group.bots }
let_it_be(:group) { create(:group) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { create(:user) }
before_all do
[project_bot, user].each do |member|
group.add_maintainer(member)
end
end
it { is_expected.to contain_exactly(project_bot) }
it { is_expected.not_to include(user) }
end
describe '#related_group_ids' do
let(:nested_group) { create(:group, parent: group) }
let(:shared_with_group) { create(:group, parent: group) }

View File

@ -6,7 +6,7 @@ RSpec.describe GroupPolicy do
include_context 'GroupPolicy context'
context 'public group with no user' do
let(:group) { create(:group, :public) }
let(:group) { create(:group, :public, :crm_enabled) }
let(:current_user) { nil }
it do
@ -975,7 +975,7 @@ RSpec.describe GroupPolicy do
it { expect_disallowed(:read_label) }
context 'when group hierarchy has a project with service desk enabled' do
let_it_be(:subgroup) { create(:group, :private, parent: group)}
let_it_be(:subgroup) { create(:group, :private, parent: group) }
let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) }
it { expect_allowed(:read_label) }
@ -983,6 +983,49 @@ RSpec.describe GroupPolicy do
end
end
context "project bots" do
let(:project_bot) { create(:user, :project_bot) }
let(:user) { create(:user) }
context "project_bot_access" do
context "when regular user and part of the group" do
let(:current_user) { user }
before do
group.add_developer(user)
end
it { is_expected.not_to be_allowed(:project_bot_access) }
end
context "when project bot and not part of the project" do
let(:current_user) { project_bot }
it { is_expected.not_to be_allowed(:project_bot_access) }
end
context "when project bot and part of the project" do
let(:current_user) { project_bot }
before do
group.add_developer(project_bot)
end
it { is_expected.to be_allowed(:project_bot_access) }
end
end
context 'with resource access tokens' do
let(:current_user) { project_bot }
before do
group.add_maintainer(project_bot)
end
it { is_expected.not_to be_allowed(:create_resource_access_tokens) }
end
end
describe 'update_runners_registration_token' do
context 'admin' do
let(:current_user) { admin }
@ -1113,7 +1156,7 @@ RSpec.describe GroupPolicy do
end
end
context 'with customer_relations feature flag disabled' do
context 'with customer relations feature flag disabled' do
let(:current_user) { owner }
before do
@ -1125,4 +1168,14 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_disallowed(:admin_crm_contact) }
it { is_expected.to be_disallowed(:admin_crm_organization) }
end
context 'when crm_enabled is false' do
let(:group) { create(:group) }
let(:current_user) { owner }
it { is_expected.to be_disallowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:read_crm_organization) }
it { is_expected.to be_disallowed(:admin_crm_contact) }
it { is_expected.to be_disallowed(:admin_crm_organization) }
end
end

View File

@ -6,7 +6,7 @@ RSpec.describe 'Setting issues crm contacts' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
@ -42,120 +42,134 @@ RSpec.describe 'Setting issues crm contacts' do
graphql_mutation_response(:issue_set_crm_contacts)
end
before do
create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
context 'when the feature is enabled' do
before do
create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
end
context 'when the user has no permission' do
it 'returns expected error' do
error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
post_graphql_mutation(mutation, current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => error))
end
end
context 'when the user has permission' do
before do
group.add_reporter(user)
end
context 'when the feature is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
it 'raises expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
end
end
context 'replace' do
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
.to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])])
end
end
context 'append' do
let(:contact_ids) { [global_id_of(contacts[3])] }
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
.to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])])
end
end
context 'remove' do
let(:contact_ids) { [global_id_of(contacts[0])] }
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
.to match_array([global_id_of(contacts[1])])
end
end
context 'when the contact does not exist' do
let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
.to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"])
end
end
context 'when the contact belongs to a different group' do
let(:group2) { create(:group, :crm_enabled) }
let(:contact) { create(:contact, group: group2) }
let(:contact_ids) { [global_id_of(contact)] }
before do
group2.add_reporter(user)
end
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
.to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"])
end
end
context 'when attempting to add more than 6' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
let(:gid) { global_id_of(contacts[0]) }
let(:contact_ids) { [gid, gid, gid, gid, gid, gid, gid] }
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
.to match_array(["You can only add up to 6 contacts at one time"])
end
end
context 'when trying to remove non-existent contact' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
it 'raises expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors)).to be_empty
end
end
end
end
context 'when the user has no permission' do
it 'returns expected error' do
error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
context 'when crm_enabled is false' do
let(:issue) { create(:issue) }
it 'raises expected error' do
issue.project.add_reporter(user)
post_graphql_mutation(mutation, current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => error))
end
end
context 'when the user has permission' do
before do
group.add_reporter(user)
end
context 'when the feature is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
it 'raises expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
end
end
context 'replace' do
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
.to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])])
end
end
context 'append' do
let(:contact_ids) { [global_id_of(contacts[3])] }
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
.to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])])
end
end
context 'remove' do
let(:contact_ids) { [global_id_of(contacts[0])] }
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
.to match_array([global_id_of(contacts[1])])
end
end
context 'when the contact does not exist' do
let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
.to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"])
end
end
context 'when the contact belongs to a different group' do
let(:group2) { create(:group) }
let(:contact) { create(:contact, group: group2) }
let(:contact_ids) { [global_id_of(contact)] }
before do
group2.add_reporter(user)
end
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
.to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"])
end
end
context 'when attempting to add more than 6' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
let(:gid) { global_id_of(contacts[0]) }
let(:contact_ids) { [gid, gid, gid, gid, gid, gid, gid] }
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
.to match_array(["You can only add up to 6 contacts at one time"])
end
end
context 'when trying to remove non-existent contact' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
it 'raises expected error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_data_at(:issue_set_crm_contacts, :errors)).to be_empty
end
expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
end
end
end

View File

@ -3,25 +3,27 @@
require "spec_helper"
RSpec.describe API::ResourceAccessTokens do
context "when the resource is a project" do
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let_it_be(:user_non_priviledged) { create(:user) }
describe "GET projects/:id/access_tokens" do
subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) }
shared_examples 'resource access token API' do |source_type|
context "GET #{source_type}s/:id/access_tokens" do
subject(:get_tokens) { get api("/#{source_type}s/#{resource_id}/access_tokens", user) }
context "when the user has maintainer permissions" do
context "when the user has valid permissions" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
let_it_be(:project_id) { project.id }
let_it_be(:resource_id) { resource.id }
before do
project.add_maintainer(user)
project.add_maintainer(project_bot)
if source_type == 'project'
resource.add_maintainer(project_bot)
else
resource.add_owner(project_bot)
end
end
it "gets a list of access tokens for the specified project" do
it "gets a list of access tokens for the specified #{source_type}" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
@ -38,16 +40,22 @@ RSpec.describe API::ResourceAccessTokens do
expect(api_get_token["name"]).to eq(token.name)
expect(api_get_token["scopes"]).to eq(token.scopes)
expect(api_get_token["access_level"]).to eq(project.team.max_member_access(token.user.id))
if source_type == 'project'
expect(api_get_token["access_level"]).to eq(resource.team.max_member_access(token.user.id))
else
expect(api_get_token["access_level"]).to eq(resource.max_member_access_for_user(token.user))
end
expect(api_get_token["expires_at"]).to eq(token.expires_at.to_date.iso8601)
expect(api_get_token).not_to have_key('token')
end
context "when using a project access token to GET other project access tokens" do
context "when using a #{source_type} access token to GET other #{source_type} access tokens" do
let_it_be(:token) { access_tokens.first }
it "gets a list of access tokens for the specified project" do
get api("/projects/#{project_id}/access_tokens", personal_access_token: token)
it "gets a list of access tokens for the specified #{source_type}" do
get api("/#{source_type}s/#{resource_id}/access_tokens", personal_access_token: token)
token_ids = json_response.map { |token| token['id'] }
@ -56,16 +64,15 @@ RSpec.describe API::ResourceAccessTokens do
end
end
context "when tokens belong to a different project" do
context "when tokens belong to a different #{source_type}" do
let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: bot) }
before do
other_project.add_maintainer(bot)
other_project.add_maintainer(user)
other_resource.add_maintainer(bot)
end
it "does not return tokens from a different project" do
it "does not return tokens from a different #{source_type}" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
@ -74,12 +81,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
context "when the project has no access tokens" do
let(:project_id) { other_project.id }
before do
other_project.add_maintainer(user)
end
context "when the #{source_type} has no access tokens" do
let(:resource_id) { other_resource.id }
it 'returns an empty array' do
get_tokens
@ -89,8 +92,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
context "when trying to get the tokens of a different project" do
let_it_be(:project_id) { other_project.id }
context "when trying to get the tokens of a different #{source_type}" do
let_it_be(:resource_id) { unknown_resource.id }
it "returns 404" do
get_tokens
@ -99,8 +102,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
context "when the project does not exist" do
let(:project_id) { non_existing_record_id }
context "when the #{source_type} does not exist" do
let(:resource_id) { non_existing_record_id }
it "returns 404" do
get_tokens
@ -111,13 +114,13 @@ RSpec.describe API::ResourceAccessTokens do
end
context "when the user does not have valid permissions" do
let_it_be(:user) { user_non_priviledged }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
let_it_be(:project_id) { project.id }
let_it_be(:resource_id) { resource.id }
before do
project.add_developer(user)
project.add_maintainer(project_bot)
resource.add_maintainer(project_bot)
end
it "returns 401" do
@ -128,40 +131,36 @@ RSpec.describe API::ResourceAccessTokens do
end
end
describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do
subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) }
context "DELETE #{source_type}s/:id/access_tokens/:token_id", :sidekiq_inline do
subject(:delete_token) { delete api("/#{source_type}s/#{resource_id}/access_tokens/#{token_id}", user) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
let_it_be(:project_id) { project.id }
let_it_be(:resource_id) { resource.id }
let_it_be(:token_id) { token.id }
before do
project.add_maintainer(project_bot)
resource.add_maintainer(project_bot)
end
context "when the user has maintainer permissions" do
before do
project.add_maintainer(user)
end
it "deletes the project access token from the project" do
context "when the user has valid permissions" do
it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
expect(User.exists?(project_bot.id)).to be_falsy
end
context "when using project access token to DELETE other project access token" do
context "when using #{source_type} access token to DELETE other #{source_type} access token" do
let_it_be(:other_project_bot) { create(:user, :project_bot) }
let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
let_it_be(:token_id) { other_token.id }
before do
project.add_maintainer(other_project_bot)
resource.add_maintainer(other_project_bot)
end
it "deletes the project access token from the project" do
it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
@ -169,37 +168,31 @@ RSpec.describe API::ResourceAccessTokens do
end
end
context "when attempting to delete a non-existent project access token" do
context "when attempting to delete a non-existent #{source_type} access token" do
let_it_be(:token_id) { non_existing_record_id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}")
end
end
context "when attempting to delete a token that does not belong to the specified project" do
let_it_be(:project_id) { other_project.id }
before do
other_project.add_maintainer(user)
end
context "when attempting to delete a token that does not belong to the specified #{source_type}" do
let_it_be(:resource_id) { other_resource.id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}")
end
end
end
context "when the user does not have valid permissions" do
before do
project.add_developer(user)
end
let_it_be(:user) { user_non_priviledged }
it "does not delete the token, and returns 400", :aggregate_failures do
delete_token
@ -211,23 +204,19 @@ RSpec.describe API::ResourceAccessTokens do
end
end
describe "POST projects/:id/access_tokens" do
context "POST #{source_type}s/:id/access_tokens" do
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at, access_level: access_level } }
let(:expires_at) { 1.month.from_now }
let(:access_level) { 20 }
subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
subject(:create_token) { post api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params }
context "when the user has maintainer permissions" do
let_it_be(:project_id) { project.id }
before do
project.add_maintainer(user)
end
context "when the user has valid permissions" do
let_it_be(:resource_id) { resource.id }
context "with valid params" do
context "with full params" do
it "creates a project access token with the params", :aggregate_failures do
it "creates a #{source_type} access token with the params", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@ -242,7 +231,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'expires_at' is not set" do
let(:expires_at) { nil }
it "creates a project access token with the params", :aggregate_failures do
it "creates a #{source_type} access token with the params", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@ -255,7 +244,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'access_level' is not set" do
let(:access_level) { nil }
it 'creates a project access token with the default access level', :aggregate_failures do
it "creates a #{source_type} access token with the default access level", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@ -272,7 +261,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'name' param" do
let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } }
it "does not create a project access token without 'name'" do
it "does not create a #{source_type} access token without 'name'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
@ -283,7 +272,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'scopes' param" do
let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } }
it "does not create a project access token without 'scopes'" do
it "does not create a #{source_type} access token without 'scopes'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
@ -292,50 +281,80 @@ RSpec.describe API::ResourceAccessTokens do
end
end
context "when trying to create a token in a different project" do
let_it_be(:project_id) { other_project.id }
context "when trying to create a token in a different #{source_type}" do
let_it_be(:resource_id) { unknown_resource.id }
it "does not create the token, and returns the project not found error" do
it "does not create the token, and returns the #{source_type} not found error" do
create_token
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to include("Project Not Found")
expect(response.body).to include("#{source_type.capitalize} Not Found")
end
end
end
context "when the user does not have valid permissions" do
let_it_be(:project_id) { project.id }
let_it_be(:resource_id) { resource.id }
context "when the user is a developer" do
before do
project.add_developer(user)
end
context "when the user role is too low" do
let_it_be(:user) { user_non_priviledged }
it "does not create the token, and returns the permission error" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("User does not have permission to create project access token")
expect(response.body).to include("User does not have permission to create #{source_type} access token")
end
end
context "when a project access token tries to create another project access token" do
context "when a #{source_type} access token tries to create another #{source_type} access token" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { project_bot }
before do
project.add_maintainer(user)
if source_type == 'project'
resource.add_maintainer(project_bot)
else
resource.add_owner(project_bot)
end
end
it "does not allow a project access token to create another project access token" do
it "does not allow a #{source_type} access token to create another #{source_type} access token" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("User does not have permission to create project access token")
expect(response.body).to include("User does not have permission to create #{source_type} access token")
end
end
end
end
end
context 'when the resource is a project' do
let_it_be(:resource) { create(:project) }
let_it_be(:other_resource) { create(:project) }
let_it_be(:unknown_resource) { create(:project) }
before_all do
resource.add_maintainer(user)
other_resource.add_maintainer(user)
resource.add_developer(user_non_priviledged)
end
it_behaves_like 'resource access token API', 'project'
end
context 'when the resource is a group' do
let_it_be(:resource) { create(:group) }
let_it_be(:other_resource) { create(:group) }
let_it_be(:unknown_resource) { create(:project) }
before_all do
resource.add_owner(user)
other_resource.add_owner(user)
resource.add_maintainer(user_non_priviledged)
end
it_behaves_like 'resource access token API', 'group'
end
end

View File

@ -24,7 +24,7 @@ RSpec.describe Groups::Crm::ContactsController do
shared_examples 'ok response with index template if authorized' do
context 'private group' do
let(:group) { create(:group, :private) }
let(:group) { create(:group, :private, :crm_enabled) }
context 'with authorized user' do
before do
@ -32,11 +32,17 @@ RSpec.describe Groups::Crm::ContactsController do
sign_in(user)
end
context 'when feature flag is enabled' do
context 'when crm_enabled is true' do
it_behaves_like 'ok response with index template'
end
context 'when feature flag is not enabled' do
context 'when crm_enabled is false' do
let(:group) { create(:group, :private) }
it_behaves_like 'response with 404 status'
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@ -64,7 +70,7 @@ RSpec.describe Groups::Crm::ContactsController do
end
context 'public group' do
let(:group) { create(:group, :public) }
let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do
it_behaves_like 'ok response with index template'

View File

@ -24,7 +24,7 @@ RSpec.describe Groups::Crm::OrganizationsController do
shared_examples 'ok response with index template if authorized' do
context 'private group' do
let(:group) { create(:group, :private) }
let(:group) { create(:group, :private, :crm_enabled) }
context 'with authorized user' do
before do
@ -32,11 +32,17 @@ RSpec.describe Groups::Crm::OrganizationsController do
sign_in(user)
end
context 'when feature flag is enabled' do
context 'when crm_enabled is true' do
it_behaves_like 'ok response with index template'
end
context 'when feature flag is not enabled' do
context 'when crm_enabled is false' do
let(:group) { create(:group, :private) }
it_behaves_like 'response with 404 status'
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@ -64,7 +70,7 @@ RSpec.describe Groups::Crm::OrganizationsController do
end
context 'public group' do
let(:group) { create(:group, :public) }
let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do
it_behaves_like 'ok response with index template'

View File

@ -12,7 +12,7 @@ RSpec.describe CustomerRelations::Contacts::CreateService do
subject(:response) { described_class.new(group: group, current_user: user, params: params).execute }
context 'when user does not have permission' do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_reporter(user)
@ -25,7 +25,7 @@ RSpec.describe CustomerRelations::Contacts::CreateService do
end
context 'when user has permission' do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_developer(user)

View File

@ -11,7 +11,7 @@ RSpec.describe CustomerRelations::Contacts::UpdateService do
describe '#execute' do
context 'when the user has no permission' do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let(:params) { { first_name: 'Gary' } }
@ -24,7 +24,7 @@ RSpec.describe CustomerRelations::Contacts::UpdateService do
end
context 'when user has permission' do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_developer(user)

View File

@ -6,7 +6,7 @@ RSpec.describe CustomerRelations::Organizations::CreateService do
describe '#execute' do
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
let(:group) { create(:group, :crm_enabled) }
let(:params) { attributes_for(:organization, group: group) }
subject(:response) { described_class.new(group: group, current_user: user, params: params).execute }

View File

@ -11,7 +11,7 @@ RSpec.describe CustomerRelations::Organizations::UpdateService do
describe '#execute' do
context 'when the user has no permission' do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let(:params) { { name: 'GitLab' } }
@ -24,7 +24,7 @@ RSpec.describe CustomerRelations::Organizations::UpdateService do
end
context 'when user has permission' do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_developer(user)

View File

@ -163,6 +163,70 @@ RSpec.describe Groups::UpdateService do
expect(updated_group.parent_id).to be_nil
end
end
context 'crm_enabled param' do
context 'when no existing crm_settings' do
it 'when param not present, leave crm disabled' do
params = {}
described_class.new(public_group, user, params).execute
updated_group = public_group.reload
expect(updated_group.crm_enabled?).to be_falsey
end
it 'when param set true, enables crm' do
params = { crm_enabled: true }
described_class.new(public_group, user, params).execute
updated_group = public_group.reload
expect(updated_group.crm_enabled?).to be_truthy
end
end
context 'with existing crm_settings' do
it 'when param set true, enables crm' do
params = { crm_enabled: true }
create(:crm_settings, group: public_group)
described_class.new(public_group, user, params).execute
updated_group = public_group.reload
expect(updated_group.crm_enabled?).to be_truthy
end
it 'when param set false, disables crm' do
params = { crm_enabled: false }
create(:crm_settings, group: public_group, enabled: true)
described_class.new(public_group, user, params).execute
updated_group = public_group.reload
expect(updated_group.crm_enabled?).to be_falsy
end
it 'when param not present, crm remains disabled' do
params = {}
create(:crm_settings, group: public_group)
described_class.new(public_group, user, params).execute
updated_group = public_group.reload
expect(updated_group.crm_enabled?).to be_falsy
end
it 'when param not present, crm remains enabled' do
params = {}
create(:crm_settings, group: public_group, enabled: true)
described_class.new(public_group, user, params).execute
updated_group = public_group.reload
expect(updated_group.crm_enabled?).to be_truthy
end
end
end
end
context "unauthorized visibility_level validation" do

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Issues::CreateService do
include AfterNextHelpers
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Issues::SetCrmContactsService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:contacts) { create_list(:contact, 4, group: group) }

View File

@ -7,7 +7,7 @@ RSpec.describe Issues::UpdateService, :mailer do
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:group) { create(:group, :public, :crm_enabled) }
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe QuickActions::InterpretService do
let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:public_project) { create(:project, :public, group: group) }
let_it_be(:repository_project) { create(:project, :repository) }
let_it_be(:project) { public_project }

View File

@ -7,10 +7,10 @@ RSpec.describe ResourceAccessTokens::CreateService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:params) { {} }
describe '#execute' do
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'token creation fails' do
let(:resource) { create(:project)}
@ -31,7 +31,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token]
expect(access_token.user.reload.user_type).to eq("#{resource_type}_bot")
expect(access_token.user.reload.user_type).to eq("project_bot")
expect(access_token.user.created_by_id).to eq(user.id)
end
@ -112,10 +112,8 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
context 'when user is external' do
let(:user) { create(:user, :external) }
before do
project.add_maintainer(user)
user.update!(external: true)
end
it 'creates resource bot user with external status' do
@ -162,7 +160,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token]
project_bot = access_token.user
expect(project.members.find_by(user_id: project_bot.id).expires_at).to eq(nil)
expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq(nil)
end
end
end
@ -183,7 +181,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token]
project_bot = access_token.user
expect(project.members.find_by(user_id: project_bot.id).expires_at).to eq(params[:expires_at])
expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq(params[:expires_at])
end
end
end
@ -234,20 +232,22 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
shared_examples 'when user does not have permission to create a resource bot' do
it_behaves_like 'token creation fails'
it 'returns the permission error message' do
response = subject
expect(response.error?).to be true
expect(response.errors).to include("User does not have permission to create #{resource_type} access token")
end
end
context 'when resource is a project' do
let_it_be(:resource_type) { 'project' }
let_it_be(:resource) { project }
context 'when user does not have permission to create a resource bot' do
it_behaves_like 'token creation fails'
it 'returns the permission error message' do
response = subject
expect(response.error?).to be true
expect(response.errors).to include("User does not have permission to create #{resource_type} access token")
end
end
it_behaves_like 'when user does not have permission to create a resource bot'
context 'user with valid permission' do
before_all do
@ -257,5 +257,20 @@ RSpec.describe ResourceAccessTokens::CreateService do
it_behaves_like 'allows creation of bot with valid params'
end
end
context 'when resource is a project' do
let_it_be(:resource_type) { 'group' }
let_it_be(:resource) { group }
it_behaves_like 'when user does not have permission to create a resource bot'
context 'user with valid permission' do
before_all do
resource.add_owner(user)
end
it_behaves_like 'allows creation of bot with valid params'
end
end
end
end

View File

@ -6,11 +6,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do
subject { described_class.new(user, resource, access_token).execute }
let_it_be(:user) { create(:user) }
let_it_be(:user_non_priviledged) { create(:user) }
let_it_be(:resource_bot) { create(:user, :project_bot) }
let(:access_token) { create(:personal_access_token, user: resource_bot) }
describe '#execute', :sidekiq_inline do
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'revokes access token' do
it { expect(subject.success?).to be true }
@ -79,71 +80,80 @@ RSpec.describe ResourceAccessTokens::RevokeService do
end
end
shared_examples 'revoke fails' do |resource_type|
let_it_be(:other_user) { create(:user) }
context "when access token does not belong to this #{resource_type}" do
it 'does not find the bot' do
other_access_token = create(:personal_access_token, user: other_user)
response = described_class.new(user, resource, other_access_token).execute
expect(response.success?).to be false
expect(response.message).to eq("Failed to find bot user")
expect(access_token.reload.revoked?).to be false
end
end
context 'when user does not have permission to destroy bot' do
context "when non-#{resource_type} member tries to delete project bot" do
it 'does not allow other user to delete bot' do
response = described_class.new(other_user, resource, access_token).execute
expect(response.success?).to be false
expect(response.message).to eq("#{other_user.name} cannot delete #{access_token.user.name}")
expect(access_token.reload.revoked?).to be false
end
end
context "when non-priviledged #{resource_type} member tries to delete project bot" do
it 'does not allow developer to delete bot' do
response = described_class.new(user_non_priviledged, resource, access_token).execute
expect(response.success?).to be false
expect(response.message).to eq("#{user_non_priviledged.name} cannot delete #{access_token.user.name}")
expect(access_token.reload.revoked?).to be false
end
end
end
context 'when deletion of bot user fails' do
before do
allow_next_instance_of(::ResourceAccessTokens::RevokeService) do |service|
allow(service).to receive(:execute).and_return(false)
end
end
it_behaves_like 'rollback revoke steps'
end
end
context 'when resource is a project' do
let_it_be(:resource) { create(:project, :private) }
let(:resource_bot) { create(:user, :project_bot) }
before do
resource.add_maintainer(user)
resource.add_developer(user_non_priviledged)
resource.add_maintainer(resource_bot)
end
it_behaves_like 'revokes access token'
context 'revoke fails' do
let_it_be(:other_user) { create(:user) }
it_behaves_like 'revoke fails', 'project'
end
context 'when access token does not belong to this project' do
it 'does not find the bot' do
other_access_token = create(:personal_access_token, user: other_user)
context 'when resource is a group' do
let_it_be(:resource) { create(:group, :private) }
response = described_class.new(user, resource, other_access_token).execute
expect(response.success?).to be false
expect(response.message).to eq("Failed to find bot user")
expect(access_token.reload.revoked?).to be false
end
end
context 'when user does not have permission to destroy bot' do
context 'when non-project member tries to delete project bot' do
it 'does not allow other user to delete bot' do
response = described_class.new(other_user, resource, access_token).execute
expect(response.success?).to be false
expect(response.message).to eq("#{other_user.name} cannot delete #{access_token.user.name}")
expect(access_token.reload.revoked?).to be false
end
end
context 'when non-maintainer project member tries to delete project bot' do
let(:developer) { create(:user) }
before do
resource.add_developer(developer)
end
it 'does not allow developer to delete bot' do
response = described_class.new(developer, resource, access_token).execute
expect(response.success?).to be false
expect(response.message).to eq("#{developer.name} cannot delete #{access_token.user.name}")
expect(access_token.reload.revoked?).to be false
end
end
end
context 'when deletion of bot user fails' do
before do
allow_next_instance_of(::ResourceAccessTokens::RevokeService) do |service|
allow(service).to receive(:execute).and_return(false)
end
end
it_behaves_like 'rollback revoke steps'
end
before do
resource.add_owner(user)
resource.add_maintainer(user_non_priviledged)
resource.add_maintainer(resource_bot)
end
it_behaves_like 'revokes access token'
it_behaves_like 'revoke fails', 'group'
end
end
end

View File

@ -8,7 +8,7 @@ RSpec.shared_context 'GroupPolicy context' do
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:non_group_member) { create(:user) }
let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) }
let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only, :crm_enabled) }
let(:guest_permissions) do
%i[