Commit 33517ed1 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 31fc1197 98ee5d63
...@@ -332,7 +332,7 @@ export default { ...@@ -332,7 +332,7 @@ export default {
v-if="showCheckbox" v-if="showCheckbox"
class="gl-align-self-center" class="gl-align-self-center"
:checked="checkboxChecked" :checked="checkboxChecked"
@input="$emit('checked-input', $event)" @change="$emit('checked-input', $event)"
> >
<span class="gl-sr-only">{{ __('Select all') }}</span> <span class="gl-sr-only">{{ __('Select all') }}</span>
</gl-form-checkbox> </gl-form-checkbox>
......
...@@ -144,36 +144,39 @@ module ApplicationSettingsHelper ...@@ -144,36 +144,39 @@ module ApplicationSettingsHelper
end end
def external_authorization_description 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.") " using their classification label.")
end end
def external_authorization_timeout_help_text def external_authorization_timeout_help_text
_("Time in seconds GitLab will wait for a response from the external "\ s_("ExternalAuthorization|Period GitLab waits for a response from the external "\
"service. When the service does not respond in time, access will be "\ "service. If there is no response, access is denied. Default: 0.5 seconds.")
"denied.")
end end
def external_authorization_url_help_text def external_authorization_url_help_text
_("When leaving the URL blank, classification labels can still be "\ s_("ExternalAuthorization|URL to which the projects make authorization requests. If the URL is blank, cross-project "\
"specified without disabling cross project features or performing "\ "features are available and can still specify classification "\
"external authorization checks.") "labels for projects.")
end end
def external_authorization_client_certificate_help_text def external_authorization_client_certificate_help_text
_("The X509 Certificate to use when mutual TLS is required to communicate "\ s_("ExternalAuthorization|Certificate used to authenticate with the external authorization service. "\
"with the external authorization service. If left blank, the server "\ "If blank, the server certificate is validated when accessing over HTTPS.")
"certificate is still validated when accessing over HTTPS.")
end end
def external_authorization_client_key_help_text def external_authorization_client_key_help_text
_("The private key to use when a client certificate is provided. This value "\ s_("ExternalAuthorization|Private key of client authentication certificate. "\
"is encrypted at rest.") "Encrypted when stored.")
end end
def external_authorization_client_pass_help_text def external_authorization_client_pass_help_text
_("The passphrase required to decrypt the private key. This is optional "\ s_("ExternalAuthorization|Passphrase required to decrypt the private key. "\
"and the value is encrypted at rest.") "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 end
def sidekiq_job_limiter_mode_help_text def sidekiq_job_limiter_mode_help_text
......
...@@ -634,11 +634,16 @@ class Group < Namespace ...@@ -634,11 +634,16 @@ class Group < Namespace
group_members.find_by(user_id: user) group_members.find_by(user_id: user)
end end
end end
alias_method :resource_member, :group_member
def highest_group_member(user) def highest_group_member(user)
GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
end end
def bots
users.project_bot
end
def related_group_ids def related_group_ids
[id, [id,
*ancestors.pluck(:id), *ancestors.pluck(:id),
......
...@@ -1667,6 +1667,7 @@ class Project < ApplicationRecord ...@@ -1667,6 +1667,7 @@ class Project < ApplicationRecord
project_members.find_by(user_id: user) project_members.find_by(user_id: user)
end end
end end
alias_method :resource_member, :project_member
def membership_locked? def membership_locked?
false false
......
...@@ -23,6 +23,9 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy ...@@ -23,6 +23,9 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? } 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) } 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 condition(:has_projects) do
group_projects_for(user: @user, group: @subject).any? group_projects_for(user: @user, group: @subject).any?
end end
...@@ -250,6 +253,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy ...@@ -250,6 +253,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :admin_dependency_proxy enable :admin_dependency_proxy
end end
rule { project_bot }.enable :project_bot_access
rule { can?(:admin_group) & resource_access_token_feature_available }.policy do rule { can?(:admin_group) & resource_access_token_feature_available }.policy do
enable :read_resource_access_tokens enable :read_resource_access_tokens
enable :destroy_resource_access_tokens enable :destroy_resource_access_tokens
...@@ -260,6 +265,10 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy ...@@ -260,6 +265,10 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :create_resource_access_tokens enable :create_resource_access_tokens
end 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 rule { support_bot & has_project_with_service_desk_enabled }.policy do
enable :read_label enable :read_label
end end
......
...@@ -63,7 +63,7 @@ module ResourceAccessTokens ...@@ -63,7 +63,7 @@ module ResourceAccessTokens
name: params[:name] || "#{resource.name.to_s.humanize} bot", name: params[:name] || "#{resource.name.to_s.humanize} bot",
email: generate_email, email: generate_email,
username: generate_username, 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. skip_confirmation: true # Bot users should always have their emails confirmed.
} }
end end
......
%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) } %section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _('External authentication') = s_('ExternalAuthorization|External authorization')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %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 .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| = 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 @@ ...@@ -16,35 +17,37 @@
.form-check .form-check
= f.check_box :external_authorization_service_enabled, class: 'form-check-input' = f.check_box :external_authorization_service_enabled, class: 'form-check-input'
= f.label :external_authorization_service_enabled, class: 'form-check-label' do = 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 %span.form-text.text-muted
= external_authorization_description = external_authorization_description
= link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/external_authorization')
.form-group .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' = f.text_field :external_authorization_service_url, class: 'form-control gl-form-input'
%span.form-text.text-muted %span.form-text.text-muted
= external_authorization_url_help_text = external_authorization_url_help_text
.form-group .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 = 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 %span.form-text.text-muted
= external_authorization_timeout_help_text = 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' = f.text_area :external_auth_client_cert, class: 'form-control gl-form-input'
%span.form-text.text-muted %span.form-text.text-muted
= external_authorization_client_certificate_help_text = external_authorization_client_certificate_help_text
.form-group .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' = f.text_area :external_auth_client_key, class: 'form-control gl-form-input'
%span.form-text.text-muted %span.form-text.text-muted
= external_authorization_client_key_help_text = external_authorization_client_key_help_text
.form-group .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' = f.password_field :external_auth_client_key_pass, class: 'form-control gl-form-input'
%span.form-text.text-muted %span.form-text.text-muted
= external_authorization_client_pass_help_text = external_authorization_client_pass_help_text
.form-group .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' = 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" = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
...@@ -25,7 +25,7 @@ The following API resources are available in the project context: ...@@ -25,7 +25,7 @@ The following API resources are available in the project context:
| Resource | Available endpoints | | Resource | Available endpoints |
|:------------------------------------------------------------------------|:--------------------| |:------------------------------------------------------------------------|:--------------------|
| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) | | [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` | | [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` | | [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` |
| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | | [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` |
...@@ -100,6 +100,7 @@ The following API resources are available in the group context: ...@@ -100,6 +100,7 @@ The following API resources are available in the group context:
| Resource | Available endpoints | | Resource | Available endpoints |
|:-----------------------------------------------------------------|:--------------------| |:-----------------------------------------------------------------|:--------------------|
| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) | | [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) | | [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) | | [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) | | [Deploy tokens](deploy_tokens.md) | `/groups/:id/deploy_tokens` (also available for projects and standalone) |
......
---
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.
...@@ -29,39 +29,13 @@ functionality that render cross-project data. That includes: ...@@ -29,39 +29,13 @@ functionality that render cross-project data. That includes:
Labels, Milestones, Merge requests). Labels, Milestones, Merge requests).
- Global and Group search are disabled. - 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. authorization service.
Whenever access is granted or denied this is logged in a log file called 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 `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). 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 When using TLS Authentication with a self signed certificate, the CA certificate
needs to be trusted by the OpenSSL installation. When using GitLab installed needs to be trusted by the OpenSSL installation. When using GitLab installed
using Omnibus, learn to install a custom CA in the using Omnibus, learn to install a custom CA in the
...@@ -69,6 +43,16 @@ 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 Alternatively, learn where to install custom certificates by using
`openssl version -d`. `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 ## How it works
When GitLab requests access, it sends a JSON POST request to the external When GitLab requests access, it sends a JSON POST request to the external
......
...@@ -4,7 +4,7 @@ module API ...@@ -4,7 +4,7 @@ module API
module Entities module Entities
class ResourceAccessToken < Entities::PersonalAccessToken class ResourceAccessToken < Entities::PersonalAccessToken
expose :access_level do |token, options| 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 end
end end
......
...@@ -8,7 +8,7 @@ module API ...@@ -8,7 +8,7 @@ module API
feature_category :authentication_and_authorization 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 resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get list of all access tokens for the specified resource' do desc 'Get list of all access tokens for the specified resource' do
detail 'This feature was introduced in GitLab 13.9.' detail 'This feature was introduced in GitLab 13.9.'
...@@ -23,8 +23,8 @@ module API ...@@ -23,8 +23,8 @@ module API
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute.preload_users tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute.preload_users
resource.project_members.load resource.members.load
present paginate(tokens), with: Entities::ResourceAccessToken, project: resource present paginate(tokens), with: Entities::ResourceAccessToken, resource: resource
end end
desc 'Revoke a resource access token' do desc 'Revoke a resource access token' do
...@@ -58,7 +58,7 @@ module API ...@@ -58,7 +58,7 @@ module API
requires :id, type: String, desc: "The #{source_type} ID" requires :id, type: String, desc: "The #{source_type} ID"
requires :name, type: String, desc: "Resource access token name" requires :name, type: String, desc: "Resource access token name"
requires :scopes, type: Array[String], desc: "The permissions of the token" 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" optional :expires_at, type: Date, desc: "The expiration date of the token"
end end
post ':id/access_tokens' do post ':id/access_tokens' do
...@@ -71,7 +71,7 @@ module API ...@@ -71,7 +71,7 @@ module API
).execute ).execute
if token_response.success? 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 else
bad_request!(token_response.message) bad_request!(token_response.message)
end end
......
...@@ -7386,15 +7386,6 @@ msgstr "" ...@@ -7386,15 +7386,6 @@ msgstr ""
msgid "Click to reveal" msgid "Click to reveal"
msgstr "" msgstr ""
msgid "Client authentication certificate"
msgstr ""
msgid "Client authentication key"
msgstr ""
msgid "Client authentication key password"
msgstr ""
msgid "Client request timeout" msgid "Client request timeout"
msgstr "" msgstr ""
...@@ -11335,9 +11326,6 @@ msgstr "" ...@@ -11335,9 +11326,6 @@ msgstr ""
msgid "Default branch protection" msgid "Default branch protection"
msgstr "" msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default delayed project deletion" msgid "Default delayed project deletion"
msgstr "" msgstr ""
...@@ -13207,9 +13195,6 @@ msgstr "" ...@@ -13207,9 +13195,6 @@ msgstr ""
msgid "Enable automatic repository housekeeping" msgid "Enable automatic repository housekeeping"
msgstr "" 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." msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7."
msgstr "" msgstr ""
...@@ -14492,9 +14477,6 @@ msgstr "" ...@@ -14492,9 +14477,6 @@ msgstr ""
msgid "Exported requirements" msgid "Exported requirements"
msgstr "" msgstr ""
msgid "External Classification Policy Authorization"
msgstr ""
msgid "External ID" msgid "External ID"
msgstr "" msgstr ""
...@@ -14504,15 +14486,9 @@ msgstr "" ...@@ -14504,15 +14486,9 @@ msgstr ""
msgid "External User:" msgid "External User:"
msgstr "" msgstr ""
msgid "External authentication"
msgstr ""
msgid "External authorization denied access to this project" msgid "External authorization denied access to this project"
msgstr "" msgstr ""
msgid "External authorization request timeout"
msgstr ""
msgid "External storage URL" msgid "External storage URL"
msgstr "" msgstr ""
...@@ -14528,6 +14504,54 @@ msgstr "" ...@@ -14528,6 +14504,54 @@ msgstr ""
msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used." msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
msgstr "" 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}." 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 "" msgstr ""
...@@ -17829,9 +17853,6 @@ msgstr "" ...@@ -17829,9 +17853,6 @@ msgstr ""
msgid "If enabled, GitLab will handle Object Storage replication using Geo. %{linkStart}Learn more%{linkEnd}" msgid "If enabled, GitLab will handle Object Storage replication using Geo. %{linkStart}Learn more%{linkEnd}"
msgstr "" 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." msgid "If enabled, only protected branches will be mirrored."
msgstr "" msgstr ""
...@@ -32383,9 +32404,6 @@ msgstr "" ...@@ -32383,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." 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 "" msgstr ""
msgid "Service URL"
msgstr ""
msgid "Service account generated successfully" msgid "Service account generated successfully"
msgstr "" msgstr ""
...@@ -35274,9 +35292,6 @@ msgstr "" ...@@ -35274,9 +35292,6 @@ msgstr ""
msgid "The URLs for connecting to Elasticsearch. For clustering, add the URLs separated by commas." msgid "The URLs for connecting to Elasticsearch. For clustering, add the URLs separated by commas."
msgstr "" 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." 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 "" msgstr ""
...@@ -35552,9 +35567,6 @@ msgstr "" ...@@ -35552,9 +35567,6 @@ msgstr ""
msgid "The parent epic is confidential and can only contain confidential epics and issues" msgid "The parent epic is confidential and can only contain confidential epics and issues"
msgstr "" 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." msgid "The password for the Jenkins server."
msgstr "" msgstr ""
...@@ -35570,9 +35582,6 @@ msgstr "" ...@@ -35570,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." 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 "" 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." msgid "The project can be accessed by any logged in user except external users."
msgstr "" msgstr ""
...@@ -36791,9 +36800,6 @@ msgstr "" ...@@ -36791,9 +36800,6 @@ msgstr ""
msgid "Time in seconds" msgid "Time in seconds"
msgstr "" 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}" msgid "Time of import: %{importTime}"
msgstr "" msgstr ""
...@@ -40034,9 +40040,6 @@ msgstr "" ...@@ -40034,9 +40040,6 @@ msgstr ""
msgid "When inactive, an external authentication provider must be used." msgid "When inactive, an external authentication provider must be used."
msgstr "" 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." msgid "When merge requests and commits in the default branch close, any issues they reference also close."
msgstr "" msgstr ""
......
...@@ -104,6 +104,26 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do ...@@ -104,6 +104,26 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
end end
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 def create_closed
create(:issue, project: project, state: :closed) create(:issue, project: project, state: :closed)
end end
......
...@@ -2086,6 +2086,23 @@ RSpec.describe Group do ...@@ -2086,6 +2086,23 @@ RSpec.describe Group do
end end
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 describe '#related_group_ids' do
let(:nested_group) { create(:group, parent: group) } let(:nested_group) { create(:group, parent: group) }
let(:shared_with_group) { create(:group, parent: group) } let(:shared_with_group) { create(:group, parent: group) }
......
...@@ -975,7 +975,7 @@ RSpec.describe GroupPolicy do ...@@ -975,7 +975,7 @@ RSpec.describe GroupPolicy do
it { expect_disallowed(:read_label) } it { expect_disallowed(:read_label) }
context 'when group hierarchy has a project with service desk enabled' do 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) } let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) }
it { expect_allowed(:read_label) } it { expect_allowed(:read_label) }
...@@ -983,6 +983,49 @@ RSpec.describe GroupPolicy do ...@@ -983,6 +983,49 @@ RSpec.describe GroupPolicy do
end end
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 describe 'update_runners_registration_token' do
context 'admin' do context 'admin' do
let(:current_user) { admin } let(:current_user) { admin }
......
...@@ -3,25 +3,27 @@ ...@@ -3,25 +3,27 @@
require "spec_helper" require "spec_helper"
RSpec.describe API::ResourceAccessTokens do 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 shared_examples 'resource access token API' do |source_type|
subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) } 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(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, 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 before do
project.add_maintainer(user) if source_type == 'project'
project.add_maintainer(project_bot) resource.add_maintainer(project_bot)
else
resource.add_owner(project_bot)
end
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 get_tokens
token_ids = json_response.map { |token| token['id'] } token_ids = json_response.map { |token| token['id'] }
...@@ -38,16 +40,22 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -38,16 +40,22 @@ RSpec.describe API::ResourceAccessTokens do
expect(api_get_token["name"]).to eq(token.name) expect(api_get_token["name"]).to eq(token.name)
expect(api_get_token["scopes"]).to eq(token.scopes) 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["expires_at"]).to eq(token.expires_at.to_date.iso8601)
expect(api_get_token).not_to have_key('token') expect(api_get_token).not_to have_key('token')
end 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 } let_it_be(:token) { access_tokens.first }
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 api("/projects/#{project_id}/access_tokens", personal_access_token: token) get api("/#{source_type}s/#{resource_id}/access_tokens", personal_access_token: token)
token_ids = json_response.map { |token| token['id'] } token_ids = json_response.map { |token| token['id'] }
...@@ -56,16 +64,15 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -56,16 +64,15 @@ RSpec.describe API::ResourceAccessTokens do
end end
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(:bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: bot) } let_it_be(:token) { create(:personal_access_token, user: bot) }
before do before do
other_project.add_maintainer(bot) other_resource.add_maintainer(bot)
other_project.add_maintainer(user)
end end
it "does not return tokens from a different project" do it "does not return tokens from a different #{source_type}" do
get_tokens get_tokens
token_ids = json_response.map { |token| token['id'] } token_ids = json_response.map { |token| token['id'] }
...@@ -74,12 +81,8 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -74,12 +81,8 @@ RSpec.describe API::ResourceAccessTokens do
end end
end end
context "when the project has no access tokens" do context "when the #{source_type} has no access tokens" do
let(:project_id) { other_project.id } let(:resource_id) { other_resource.id }
before do
other_project.add_maintainer(user)
end
it 'returns an empty array' do it 'returns an empty array' do
get_tokens get_tokens
...@@ -89,8 +92,8 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -89,8 +92,8 @@ RSpec.describe API::ResourceAccessTokens do
end end
end end
context "when trying to get the tokens of a different project" do context "when trying to get the tokens of a different #{source_type}" do
let_it_be(:project_id) { other_project.id } let_it_be(:resource_id) { unknown_resource.id }
it "returns 404" do it "returns 404" do
get_tokens get_tokens
...@@ -99,8 +102,8 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -99,8 +102,8 @@ RSpec.describe API::ResourceAccessTokens do
end end
end end
context "when the project does not exist" do context "when the #{source_type} does not exist" do
let(:project_id) { non_existing_record_id } let(:resource_id) { non_existing_record_id }
it "returns 404" do it "returns 404" do
get_tokens get_tokens
...@@ -111,13 +114,13 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -111,13 +114,13 @@ RSpec.describe API::ResourceAccessTokens do
end end
context "when the user does not have valid permissions" do 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(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, 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 before do
project.add_developer(user) resource.add_maintainer(project_bot)
project.add_maintainer(project_bot)
end end
it "returns 401" do it "returns 401" do
...@@ -128,40 +131,36 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -128,40 +131,36 @@ RSpec.describe API::ResourceAccessTokens do
end end
end end
describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do context "DELETE #{source_type}s/:id/access_tokens/:token_id", :sidekiq_inline do
subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) } 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(:project_bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, 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 } let_it_be(:token_id) { token.id }
before do 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 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 delete_token
expect(response).to have_gitlab_http_status(:no_content) expect(response).to have_gitlab_http_status(:no_content)
expect(User.exists?(project_bot.id)).to be_falsy expect(User.exists?(project_bot.id)).to be_falsy
end 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_project_bot) { create(:user, :project_bot) }
let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) } let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
let_it_be(:token_id) { other_token.id } let_it_be(:token_id) { other_token.id }
before do before do
project.add_maintainer(other_project_bot) resource.add_maintainer(other_project_bot)
end 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 delete_token
expect(response).to have_gitlab_http_status(:no_content) expect(response).to have_gitlab_http_status(:no_content)
...@@ -169,37 +168,31 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -169,37 +168,31 @@ RSpec.describe API::ResourceAccessTokens do
end end
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 } let_it_be(:token_id) { non_existing_record_id }
it "does not delete the token, and returns 404" do it "does not delete the token, and returns 404" do
delete_token delete_token
expect(response).to have_gitlab_http_status(:not_found) 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 end
context "when attempting to delete a token that does not belong to the specified project" do context "when attempting to delete a token that does not belong to the specified #{source_type}" do
let_it_be(:project_id) { other_project.id } let_it_be(:resource_id) { other_resource.id }
before do
other_project.add_maintainer(user)
end
it "does not delete the token, and returns 404" do it "does not delete the token, and returns 404" do
delete_token delete_token
expect(response).to have_gitlab_http_status(:not_found) 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 end
end end
context "when the user does not have valid permissions" do context "when the user does not have valid permissions" do
before do let_it_be(:user) { user_non_priviledged }
project.add_developer(user)
end
it "does not delete the token, and returns 400", :aggregate_failures do it "does not delete the token, and returns 400", :aggregate_failures do
delete_token delete_token
...@@ -211,23 +204,19 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -211,23 +204,19 @@ RSpec.describe API::ResourceAccessTokens do
end end
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(:params) { { name: "test", scopes: ["api"], expires_at: expires_at, access_level: access_level } }
let(:expires_at) { 1.month.from_now } let(:expires_at) { 1.month.from_now }
let(:access_level) { 20 } 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 context "when the user has valid permissions" do
let_it_be(:project_id) { project.id } let_it_be(:resource_id) { resource.id }
before do
project.add_maintainer(user)
end
context "with valid params" do context "with valid params" do
context "with full 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 create_token
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
...@@ -242,7 +231,7 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -242,7 +231,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'expires_at' is not set" do context "when 'expires_at' is not set" do
let(:expires_at) { nil } 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 create_token
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
...@@ -255,7 +244,7 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -255,7 +244,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'access_level' is not set" do context "when 'access_level' is not set" do
let(:access_level) { nil } 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 create_token
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
...@@ -272,7 +261,7 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -272,7 +261,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'name' param" do context "when missing the 'name' param" do
let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } } 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 create_token
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
...@@ -283,7 +272,7 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -283,7 +272,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'scopes' param" do context "when missing the 'scopes' param" do
let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } } 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 create_token
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
...@@ -292,50 +281,80 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -292,50 +281,80 @@ RSpec.describe API::ResourceAccessTokens do
end end
end end
context "when trying to create a token in a different project" do context "when trying to create a token in a different #{source_type}" do
let_it_be(:project_id) { other_project.id } 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 create_token
expect(response).to have_gitlab_http_status(:not_found) 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 end
end end
context "when the user does not have valid permissions" do 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 context "when the user role is too low" do
before do let_it_be(:user) { user_non_priviledged }
project.add_developer(user)
end
it "does not create the token, and returns the permission error" do it "does not create the token, and returns the permission error" do
create_token create_token
expect(response).to have_gitlab_http_status(:bad_request) 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
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(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { project_bot } let_it_be(:user) { project_bot }
before do before do
project.add_maintainer(user) if source_type == 'project'
resource.add_maintainer(project_bot)
else
resource.add_owner(project_bot)
end
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 create_token
expect(response).to have_gitlab_http_status(:bad_request) 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
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
end end
...@@ -7,10 +7,10 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -7,10 +7,10 @@ RSpec.describe ResourceAccessTokens::CreateService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) } let_it_be(:project) { create(:project, :private) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:params) { {} } let_it_be(:params) { {} }
describe '#execute' do 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 shared_examples 'token creation fails' do
let(:resource) { create(:project)} let(:resource) { create(:project)}
...@@ -31,7 +31,7 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -31,7 +31,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token] 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) expect(access_token.user.created_by_id).to eq(user.id)
end end
...@@ -112,10 +112,8 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -112,10 +112,8 @@ RSpec.describe ResourceAccessTokens::CreateService do
end end
context 'when user is external' do context 'when user is external' do
let(:user) { create(:user, :external) }
before do before do
project.add_maintainer(user) user.update!(external: true)
end end
it 'creates resource bot user with external status' do it 'creates resource bot user with external status' do
...@@ -162,7 +160,7 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -162,7 +160,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token] access_token = response.payload[:access_token]
project_bot = access_token.user 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 end
end end
...@@ -183,7 +181,7 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -183,7 +181,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token] access_token = response.payload[:access_token]
project_bot = access_token.user 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 end
end end
...@@ -234,11 +232,7 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -234,11 +232,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
end end
end end
context 'when resource is a project' do shared_examples 'when user does not have permission to create a resource bot' 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_behaves_like 'token creation fails'
it 'returns the permission error message' do it 'returns the permission error message' do
...@@ -249,6 +243,12 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -249,6 +243,12 @@ RSpec.describe ResourceAccessTokens::CreateService do
end end
end end
context 'when resource is a project' do
let_it_be(:resource_type) { 'project' }
let_it_be(:resource) { project }
it_behaves_like 'when user does not have permission to create a resource bot'
context 'user with valid permission' do context 'user with valid permission' do
before_all do before_all do
resource.add_maintainer(user) resource.add_maintainer(user)
...@@ -257,5 +257,20 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -257,5 +257,20 @@ RSpec.describe ResourceAccessTokens::CreateService do
it_behaves_like 'allows creation of bot with valid params' it_behaves_like 'allows creation of bot with valid params'
end end
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
end end
...@@ -6,11 +6,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -6,11 +6,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do
subject { described_class.new(user, resource, access_token).execute } subject { described_class.new(user, resource, access_token).execute }
let_it_be(:user) { create(:user) } 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) } let(:access_token) { create(:personal_access_token, user: resource_bot) }
describe '#execute', :sidekiq_inline do 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 shared_examples 'revokes access token' do
it { expect(subject.success?).to be true } it { expect(subject.success?).to be true }
...@@ -79,22 +80,10 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -79,22 +80,10 @@ RSpec.describe ResourceAccessTokens::RevokeService do
end end
end end
context 'when resource is a project' do shared_examples 'revoke fails' do |resource_type|
let_it_be(:resource) { create(:project, :private) }
let(:resource_bot) { create(:user, :project_bot) }
before do
resource.add_maintainer(user)
resource.add_maintainer(resource_bot)
end
it_behaves_like 'revokes access token'
context 'revoke fails' do
let_it_be(:other_user) { create(:user) } let_it_be(:other_user) { create(:user) }
context 'when access token does not belong to this project' do context "when access token does not belong to this #{resource_type}" do
it 'does not find the bot' do it 'does not find the bot' do
other_access_token = create(:personal_access_token, user: other_user) other_access_token = create(:personal_access_token, user: other_user)
...@@ -107,7 +96,7 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -107,7 +96,7 @@ RSpec.describe ResourceAccessTokens::RevokeService do
end end
context 'when user does not have permission to destroy bot' do context 'when user does not have permission to destroy bot' do
context 'when non-project member tries to delete project bot' do context "when non-#{resource_type} member tries to delete project bot" do
it 'does not allow other user to delete bot' do it 'does not allow other user to delete bot' do
response = described_class.new(other_user, resource, access_token).execute response = described_class.new(other_user, resource, access_token).execute
...@@ -117,18 +106,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -117,18 +106,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do
end end
end end
context 'when non-maintainer project member tries to delete project bot' do context "when non-priviledged #{resource_type} 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 it 'does not allow developer to delete bot' do
response = described_class.new(developer, resource, access_token).execute response = described_class.new(user_non_priviledged, resource, access_token).execute
expect(response.success?).to be false expect(response.success?).to be false
expect(response.message).to eq("#{developer.name} cannot delete #{access_token.user.name}") expect(response.message).to eq("#{user_non_priviledged.name} cannot delete #{access_token.user.name}")
expect(access_token.reload.revoked?).to be false expect(access_token.reload.revoked?).to be false
end end
end end
...@@ -144,6 +127,33 @@ RSpec.describe ResourceAccessTokens::RevokeService do ...@@ -144,6 +127,33 @@ RSpec.describe ResourceAccessTokens::RevokeService do
it_behaves_like 'rollback revoke steps' it_behaves_like 'rollback revoke steps'
end end
end end
context 'when resource is a project' do
let_it_be(:resource) { create(:project, :private) }
before do
resource.add_maintainer(user)
resource.add_developer(user_non_priviledged)
resource.add_maintainer(resource_bot)
end
it_behaves_like 'revokes access token'
it_behaves_like 'revoke fails', 'project'
end
context 'when resource is a group' do
let_it_be(:resource) { create(:group, :private) }
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 end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment