Commit a1a5cb34 authored by Fabio Huser's avatar Fabio Huser

Add group level access token UI

This commit is a follow up to
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236
and adds the group level counterpart to the existing
Project Access Token UI. Group Access Tokens can already be created
by instance administrators via rails console or users via GitLab
REST API but not yet on the UI itself. The new page allows group
owners to list, create and delete said tokens.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/214045

Changelog: added
parent 802314d3
import { initExpiresAtField } from '~/access_tokens';
initExpiresAtField();
# frozen_string_literal: true
module AccessTokensActions
extend ActiveSupport::Concern
included do
before_action -> { check_permission(:read_resource_access_tokens) }, only: [:index]
before_action -> { check_permission(:destroy_resource_access_tokens) }, only: [:revoke]
before_action -> { check_permission(:create_resource_access_tokens) }, only: [:create]
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def index
@resource_access_token = PersonalAccessToken.new
set_index_vars
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def create
token_response = ResourceAccessTokens::CreateService.new(current_user, resource, create_params).execute
if token_response.success?
@resource_access_token = token_response.payload[:access_token]
PersonalAccessToken.redis_store!(key_identity, @resource_access_token.token)
redirect_to resource_access_tokens_path, notice: _("Your new access token has been created.")
else
redirect_to resource_access_tokens_path, alert: _("Failed to create new access token: %{token_response_message}") % { token_response_message: token_response.message }
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def revoke
@resource_access_token = finder.find(params[:id])
revoked_response = ResourceAccessTokens::RevokeService.new(current_user, resource, @resource_access_token).execute
if revoked_response.success?
flash[:notice] = _("Revoked access token %{access_token_name}!") % { access_token_name: @resource_access_token.name }
else
flash[:alert] = _("Could not revoke access token %{access_token_name}.") % { access_token_name: @resource_access_token.name }
end
redirect_to resource_access_tokens_path
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
private
def check_permission(action)
render_404 unless can?(current_user, action, resource)
end
def create_params
params.require(:resource_access_token).permit(:name, :expires_at, :access_level, scopes: [])
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_index_vars
# Loading resource members so that we can fetch access level of the bot
# user in the resource without multiple queries.
resource.members.load
@scopes = Gitlab::Auth.resource_bot_scopes
@active_resource_access_tokens = finder(state: 'active').execute.preload_users
@inactive_resource_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
@new_resource_access_token = PersonalAccessToken.redis_getdel(key_identity)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def finder(options = {})
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
end
def bot_users
resource.bots
end
def key_identity
"#{current_user.id}:#{resource.id}"
end
end
# frozen_string_literal: true
module Groups
module Settings
class AccessTokensController < Groups::ApplicationController
include AccessTokensActions
layout 'group_settings'
feature_category :authentication_and_authorization
alias_method :resource, :group
def resource_access_tokens_path
group_settings_access_tokens_path
end
end
end
end
......@@ -3,77 +3,15 @@
module Projects
module Settings
class AccessTokensController < Projects::ApplicationController
include ProjectsHelper
include AccessTokensActions
layout 'project_settings'
before_action -> { check_permission(:read_resource_access_tokens) }, only: [:index]
before_action -> { check_permission(:destroy_resource_access_tokens) }, only: [:revoke]
before_action -> { check_permission(:create_resource_access_tokens) }, only: [:create]
feature_category :authentication_and_authorization
def index
@project_access_token = PersonalAccessToken.new
set_index_vars
end
def create
token_response = ResourceAccessTokens::CreateService.new(current_user, @project, create_params).execute
if token_response.success?
@project_access_token = token_response.payload[:access_token]
PersonalAccessToken.redis_store!(key_identity, @project_access_token.token)
redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
else
redirect_to namespace_project_settings_access_tokens_path, alert: _("Failed to create new project access token: %{token_response_message}") % { token_response_message: token_response.message }
end
end
def revoke
@project_access_token = finder.find(params[:id])
revoked_response = ResourceAccessTokens::RevokeService.new(current_user, @project, @project_access_token).execute
if revoked_response.success?
flash[:notice] = _("Revoked project access token %{project_access_token_name}!") % { project_access_token_name: @project_access_token.name }
else
flash[:alert] = _("Could not revoke project access token %{project_access_token_name}.") % { project_access_token_name: @project_access_token.name }
end
redirect_to namespace_project_settings_access_tokens_path
end
private
def check_permission(action)
render_404 unless can?(current_user, action, @project)
end
def create_params
params.require(:project_access_token).permit(:name, :expires_at, :access_level, scopes: [])
end
def set_index_vars
# Loading project members so that we can fetch access level of the bot
# user in the project without multiple queries.
@project.project_members.load
@scopes = Gitlab::Auth.resource_bot_scopes
@active_project_access_tokens = finder(state: 'active').execute.preload_users
@inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
@new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
end
def finder(options = {})
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
end
def bot_users
@project.bots
end
alias_method :resource, :project
def key_identity
"#{current_user.id}:#{@project.id}"
def resource_access_tokens_path
namespace_project_settings_access_tokens_path
end
end
end
......
......@@ -5,6 +5,7 @@ class GroupMemberPolicy < BasePolicy
with_scope :subject
condition(:last_owner) { @subject.group.member_last_owner?(@subject) || @subject.group.member_last_blocked_owner?(@subject) }
condition(:project_bot) { @subject.user&.project_bot? && @subject.group.member?(@subject.user) }
desc "Membership is users' own"
with_score 0
......@@ -20,11 +21,13 @@ class GroupMemberPolicy < BasePolicy
prevent :destroy_group_member
end
rule { can?(:admin_group_member) }.policy do
rule { ~project_bot & can?(:admin_group_member) }.policy do
enable :update_group_member
enable :destroy_group_member
end
rule { project_bot & can?(:admin_group_member) }.enable :destroy_project_bot_member
rule { is_target_user }.policy do
enable :destroy_group_member
end
......
......@@ -29,7 +29,7 @@
checkbox_options: { checked: @group.mentions_disabled? },
help_text: s_('GroupSettings|Prevents group members from being notified if the group is mentioned.')
= render 'groups/settings/project_access_token_creation', f: f, group: @group
= render 'groups/settings/resource_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
= render 'groups/settings/ip_restriction_registration_features_cta', f: f
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
......
......@@ -2,8 +2,10 @@
.form-group.gl-mb-3
- project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
- group_access_tokens_link = help_page_path('user/group/settings/group_access_tokens')
- link_start_project = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
- link_start_group = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_access_tokens_link }
= f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,
s_('GroupSettings|Allow project access token creation'),
s_('GroupSettings|Allow project and group access token creation'),
checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed?, data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } },
help_text: s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
help_text: s_('GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group.').html_safe % { link_start_project: link_start_project, link_start_group: link_start_group, link_end: '</a>'.html_safe }
- breadcrumb_title s_('AccessTokens|Access Tokens')
- page_title _('Group Access Tokens')
- type = _('group access token')
- type_plural = _('group access tokens')
- @content_class = 'limit-container-width' unless fluid_layout
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
= page_title
%p
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/settings/group_access_tokens') }
- if current_user.can?(:create_resource_access_tokens, @group)
= _('Generate group access tokens scoped to this group for your applications that need access to the GitLab API.')
%p
= _('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- else
= _('Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%p
- root_group = @group.root_ancestor
- if current_user.can?(:admin_group, root_group)
- group_settings_link = edit_group_path(root_group)
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link }
= _('You can enable group access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
- if @new_resource_access_token
= render 'shared/access_tokens/created_container',
type: type,
new_token_value: @new_resource_access_token
- if current_user.can?(:create_resource_access_tokens, @group)
= render 'shared/access_tokens/form',
type: type,
path: group_settings_access_tokens_path(@group),
resource: @group,
token: @resource_access_token,
scopes: @scopes,
access_levels: GroupMember.access_level_roles,
default_access_level: Gitlab::Access::MAINTAINER,
prefix: :resource_access_token,
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
= render 'shared/access_tokens/table',
active_tokens: @active_resource_access_tokens,
resource: @group,
type: type,
type_plural: type_plural,
revoke_route_helper: ->(token) { revoke_group_settings_access_token_path(id: token) },
no_active_tokens_message: _('This group has no active access tokens.')
......@@ -5,7 +5,7 @@
- @content_class = 'limit-container-width' unless fluid_layout
.row.gl-mt-3
.col-lg-4.profile-settings-sidebar
.col-lg-4
%h4.gl-mt-0
= page_title
%p
......@@ -24,26 +24,26 @@
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
- if @new_project_access_token
- if @new_resource_access_token
= render 'shared/access_tokens/created_container',
type: type,
new_token_value: @new_project_access_token
new_token_value: @new_resource_access_token
- if current_user.can?(:create_resource_access_tokens, @project)
= render 'shared/access_tokens/form',
type: type,
path: project_settings_access_tokens_path(@project),
project: @project,
token: @project_access_token,
resource: @project,
token: @resource_access_token,
scopes: @scopes,
access_levels: ProjectMember.access_level_roles,
default_access_level: Gitlab::Access::MAINTAINER,
prefix: :project_access_token,
prefix: :resource_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
= render 'shared/access_tokens/table',
active_tokens: @active_project_access_tokens,
project: @project,
active_tokens: @active_resource_access_tokens,
resource: @project,
type: type,
type_plural: type_plural,
revoke_route_helper: ->(token) { revoke_namespace_project_settings_access_token_path(id: token) },
......
- title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
- prefix = local_assigns.fetch(:prefix, :personal_access_token)
- help_path = local_assigns.fetch(:help_path)
- project = local_assigns.fetch(:project, false)
- resource = local_assigns.fetch(:resource, false)
- access_levels = local_assigns.fetch(:access_levels, false)
- default_access_level = local_assigns.fetch(:default_access_level, false)
......@@ -32,12 +32,12 @@
.js-access-tokens-expires-at
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
- if project
- if resource
.row
.form-group.col-md-6
= label_tag :access_level, _("Select a role"), class: "label-bold"
.select-wrapper
= select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control", data: { qa_selector: 'access_token_access_level' }
= select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control", data: { qa_selector: 'access_token_access_level' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
......
- no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural })
- impersonation = local_assigns.fetch(:impersonation, false)
- project = local_assigns.fetch(:project, false)
- personal = !impersonation && !project
- resource = local_assigns.fetch(:resource, false)
- personal = !impersonation && !resource
%hr
......@@ -30,7 +30,7 @@
= _('Last Used')
= link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'view-the-last-time-a-token-was-used'), target: '_blank', rel: 'noopener noreferrer'
%th= _('Expires')
- if project
- if resource
%th= _('Role')
%th
%tbody
......@@ -54,8 +54,8 @@
= time_ago_with_tooltip(token.expires_at)
- else
%span.token-never-expires-label= _('Never')
- if project
%td= project.member(token.user).human_access
- if resource
%td= resource.member(token.user).human_access
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right qa-revoke-button #{'btn-danger-secondary' unless token.expires?}", data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } }
- else
.settings-message.text-center
......
......@@ -43,6 +43,12 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :create_deploy_token, path: 'deploy_token/create'
end
resources :access_tokens, only: [:index, :create] do
member do
put :revoke
end
end
resources :integrations, only: [:index, :edit, :update] do
member do
put :test
......
......@@ -6,13 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Group access tokens API **(FREE)**
You can read more about [group access tokens](../user/project/settings/project_access_tokens.md#group-access-tokens).
You can read more about [group access tokens](../user/group/settings/group_access_tokens.md).
## 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).
Get a list of [group access tokens](../user/group/settings/group_access_tokens.md).
```plaintext
GET groups/:id/access_tokens
......@@ -48,7 +48,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
> [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).
Create a [group access token](../user/group/settings/group_access_tokens.md).
```plaintext
POST groups/:id/access_tokens
......@@ -91,7 +91,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_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).
Revoke a [group access token](../user/group/settings/group_access_tokens.md).
```plaintext
DELETE groups/:id/access_tokens/:token_id
......
......@@ -169,24 +169,24 @@ for examples requesting a new access token using a refresh token.
A default refresh setting of two hours is tracked in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336598).
### Personal/project access tokens
### Personal/project/group access tokens
You can use access tokens to authenticate with the API by passing it in either
the `private_token` parameter or the `PRIVATE-TOKEN` header.
Example of using the personal or project access token in a parameter:
Example of using the personal, project, or group access token in a parameter:
```shell
curl "https://gitlab.example.com/api/v4/projects?private_token=<your_access_token>"
```
Example of using the personal or project access token in a header:
Example of using the personal, project, or group access token in a header:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects"
```
You can also use personal or project access tokens with OAuth-compliant headers:
You can also use personal, project, or group access tokens with OAuth-compliant headers:
```shell
curl --header "Authorization: Bearer <your_access_token>" "https://gitlab.example.com/api/v4/projects"
......
......@@ -93,17 +93,19 @@ This table shows available scopes per token. Scopes can be limited further on to
| | API access | Registry access | Repository access |
|-----------------------------|------------|-----------------|-------------------|
| Personal access token | ✅ | ✅ | ✅ |
| OAuth2 token | ✅ | 🚫 | ✅ |
| Impersonation token | ✅ | ✅ | ✅ |
| Project access token | ✅(1) | ✅(1) | ✅(1) |
| Deploy token | 🚫 | ✅ | ✅ |
| Deploy key | 🚫 | 🚫 | ✅ |
| Runner registration token | 🚫 | 🚫 | ✴️(2) |
| Runner authentication token | 🚫 | 🚫 | ✴️(2) |
| Job token | ✴️(3) | 🚫 | ✅ |
| Personal access token | ✅ | ✅ | ✅ |
| OAuth2 token | ✅ | 🚫 | ✅ |
| Impersonation token | ✅ | ✅ | ✅ |
| Project access token | ✅(1) | ✅(1) | ✅(1) |
| Group access token | ✅(2) | ✅(2) | ✅(2) |
| Deploy token | 🚫 | ✅ | ✅ |
| Deploy key | 🚫 | 🚫 | ✅ |
| Runner registration token | 🚫 | 🚫 | ✴️(3) |
| Runner authentication token | 🚫 | 🚫 | ✴️(3) |
| Job token | ✴️(4) | 🚫 | ✅ |
1. Limited to the one project.
1. Limited to the one group.
1. Runner registration and authentication token don't provide direct access to repositories, but can be used to register and authenticate a new runner that may execute jobs which do have access to the repository
1. Limited to certain [endpoints](../ci/jobs/ci_job_token.md).
......@@ -113,7 +115,7 @@ Access tokens should be treated like passwords and kept secure.
Adding them to URLs is a security risk. This is especially true when cloning or adding a remote, as Git then writes the URL to its `.git/config` file in plain text. URLs are also generally logged by proxies and application servers, which makes those credentials visible to system administrators.
Instead, API calls can be passed an access token using headers, like [the `Private-Token` header](../api/index.md#personalproject-access-tokens).
Instead, API calls can be passed an access token using headers, like [the `Private-Token` header](../api/index.md#personalprojectgroup-access-tokens).
Tokens can also be stored using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
......
......@@ -40,8 +40,9 @@ This page gathers all the resources for the topic **Authentication** within GitL
## API
- [OAuth 2 Tokens](../../api/index.md#oauth2-tokens)
- [Personal access tokens](../../api/index.md#personalproject-access-tokens)
- [Project access tokens](../../api/index.md#personalproject-access-tokens)
- [Personal access tokens](../../api/index.md#personalprojectgroup-access-tokens)
- [Project access tokens](../../api/index.md#personalprojectgroup-access-tokens)
- [Group access tokens](../../api/index.md#personalprojectgroup-access-tokens)
- [Impersonation tokens](../../api/index.md#impersonation-tokens)
- [OAuth 2.0 identity provider API](../../api/oauth2.md)
......
---
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"
type: reference, howto
---
# Group access tokens
You can use a group access token to authenticate:
- With the [GitLab API](../../../api/index.md#personalprojectgroup-access-tokens).
- With Git, when using HTTP Basic Authentication.
After you configure a group access token, you don't need a password when you authenticate.
Instead, you can enter any non-blank value.
Group access tokens are similar to [project access tokens](../../project/settings/project_access_tokens.md)
and [personal access tokens](../../profile/personal_access_tokens.md), except they are
associated with a group rather than a project or user.
You can use group access tokens:
- On GitLab SaaS if you have the Premium license tier or higher. Group access tokens are not available with a [trial license](https://about.gitlab.com/free-trial/).
- On self-managed instances of GitLab, with any license tier. If you have the Free tier:
- Review your security and compliance policies around
[user self-enrollment](../../admin_area/settings/sign_up_restrictions.md#disable-new-sign-ups).
- Consider [disabling group access tokens](#enable-or-disable-group-access-token-creation) to
lower potential abuse.
Group access tokens inherit the [default prefix setting](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix)
configured for personal access tokens.
## Create a group access token using UI
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214045) in GitLab 14.7.
To create a group access token:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Access Tokens**.
1. Enter a name.
1. Optional. Enter an expiry date for the token. The token will expire on that date at midnight UTC.
1. Select a role for the token.
1. Select the [desired scopes](#scopes-for-a-group-access-token).
1. Select **Create group access token**.
A group access token is displayed. Save the group access token somewhere safe. After you leave or refresh the page, you can't view it again.
## Create a group access token using Rails console
GitLab 14.6 and earlier doesn't support creating group access tokens using the UI
or API. However, administrators can use a workaround:
1. Run the following commands in a [Rails console](../../../administration/operations/rails_console.md):
```ruby
# Set the GitLab administration user to use. If user ID 1 is not available or is not an administrator, use 'admin = User.admins.first' instead to select an administrator.
admin = User.find(1)
# Set the group group you want to create a token for. For example, group with ID 109.
group = Group.find(109)
# Create the group bot user. For further group access tokens, the username should be group_#{group.id}_bot#{bot_count}. For example, group_109_bot2 and email address group_109_bot2@example.com.
bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot@example.com", user_type: :project_bot }).execute
# Confirm the group bot.
bot.confirm
# Add the bot to the group with the required role.
group.add_user(bot, :maintainer)
# Give the bot a personal access token.
token = bot.personal_access_tokens.create(scopes:[:api, :write_repository], name: 'group_token')
# Get the token value.
gtoken = token.token
```
1. Test if the generated group access token works:
1. Use the group access token in the `PRIVATE-TOKEN` header with GitLab REST APIs. For example:
- [Create an epic](../../../api/epics.md#new-epic) in the group.
- [Create a project pipeline](../../../api/pipelines.md#create-a-new-pipeline) in one of the group's projects.
- [Create an issue](../../../api/issues.md#new-issue) in one of the group's projects.
1. Use the group token to [clone a group's project](../../../gitlab-basics/start-using-git.md#clone-with-https)
using HTTPS.
## Revoke a group access token using the UI
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214045) in GitLab 14.7.
To revoke a group access token:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Access Tokens**.
1. Next to the group access token to revoke, select **Revoke**.
## Revoke a group access token using Rails console
GitLab 14.6 and earlier doesn't support revoking group access tokens using the UI
or API. However, administrators can use a workaround.
To revoke a group access token, run the following command in a [Rails console](../../../administration/operations/rails_console.md):
```ruby
bot = User.find_by(username: 'group_109_bot') # the owner of the token you want to revoke
token = bot.personal_access_tokens.last # the token you want to revoke
token.revoke!
```
## Scopes for a group access token
The scope determines the actions you can perform when you authenticate with a group access token.
| Scope | Description |
|:-------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `api` | Grants complete read and write access to the scoped group and related project API, including the [Package Registry](../../packages/package_registry/index.md). |
| `read_api` | Grants read access to the scoped group and related project API, including the [Package Registry](../../packages/package_registry/index.md). |
| `read_registry` | Allows read access (pull) to the [Container Registry](../../packages/container_registry/index.md) images if any project within a group is private and authorization is required. |
| `write_registry` | Allows write access (push) to the [Container Registry](../../packages/container_registry/index.md). |
| `read_repository` | Allows read access (pull) to all repositories within a group. |
| `write_repository` | Allows read and write access (pull and push) to all repositories within a group. |
## Enable or disable group access token creation
To enable or disable group access token creation for all sub-groups in a top-level group:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Permissions and group features**.
1. Under **Permissions**, turn on or off **Allow project and group access token creation**.
Even when creation is disabled, you can still use and revoke existing group access tokens.
## Bot users
Each time you create a group access token, a bot user is created and added to the group.
These bot users are similar to [project bot users](../../project/settings/project_access_tokens.md#project-bot-users), but are added to groups instead of projects. For more information, see
[Project bot users](../../project/settings/project_access_tokens.md#project-bot-users).
......@@ -67,7 +67,7 @@ Creating a Debian package is documented [on the Debian Wiki](https://wiki.debian
To create a distribution, publish a package, or install a private package, you need one of the
following:
- [Personal access token](../../../api/index.md#personalproject-access-tokens)
- [Personal access token](../../../api/index.md#personalprojectgroup-access-tokens)
- [CI/CD job token](../../../ci/jobs/ci_job_token.md)
- [Deploy token](../../project/deploy_tokens/index.md)
......
......@@ -17,13 +17,13 @@ Publish generic files, like release binaries, in your project's Package Registry
## Authenticate to the Package Registry
To authenticate to the Package Registry, you need either a [personal access token](../../../api/index.md#personalproject-access-tokens),
To authenticate to the Package Registry, you need either a [personal access token](../../../api/index.md#personalprojectgroup-access-tokens),
[CI/CD job token](../../../ci/jobs/ci_job_token.md), or [deploy token](../../project/deploy_tokens/index.md).
In addition to the standard API authentication mechanisms, the generic package
API allows authentication with HTTP Basic authentication for use with tools that
do not support the other available mechanisms. The `user-id` is not checked and
may be any value, and the `password` must be either a [personal access token](../../../api/index.md#personalproject-access-tokens),
may be any value, and the `password` must be either a [personal access token](../../../api/index.md#personalprojectgroup-access-tokens),
a [CI/CD job token](../../../ci/jobs/ci_job_token.md), or a [deploy token](../../project/deploy_tokens/index.md).
## Publish a package file
......
......@@ -30,7 +30,7 @@ Read more in the Helm documentation about these topics:
To authenticate to the Helm repository, you need either:
- A [personal access token](../../../api/index.md#personalproject-access-tokens) with the scope set to `api`.
- A [personal access token](../../../api/index.md#personalprojectgroup-access-tokens) with the scope set to `api`.
- A [deploy token](../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
- A [CI/CD job token](../../../ci/jobs/ci_job_token.md).
......
......@@ -15,7 +15,7 @@ as a Terraform module registry.
To authenticate to the Terraform module registry, you need either:
- A [personal access token](../../../api/index.md#personalproject-access-tokens) with at least `read_api` rights.
- A [personal access token](../../../api/index.md#personalprojectgroup-access-tokens) with at least `read_api` rights.
- A [CI/CD job token](../../../ci/jobs/ci_job_token.md).
## Publish a Terraform Module
......
......@@ -14,7 +14,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Personal access tokens can be an alternative to [OAuth2](../../api/oauth2.md) and used to:
- Authenticate with the [GitLab API](../../api/index.md#personalproject-access-tokens).
- Authenticate with the [GitLab API](../../api/index.md#personalprojectgroup-access-tokens).
- Authenticate with Git using HTTP Basic Authentication.
In both cases, you authenticate with a personal access token in place of your password.
......@@ -33,7 +33,7 @@ Though required, GitLab usernames are ignored when authenticating with a persona
There is an [issue for tracking](https://gitlab.com/gitlab-org/gitlab/-/issues/212953) to make GitLab
use the username.
For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/index.md#personalproject-access-tokens).
For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/index.md#personalprojectgroup-access-tokens).
Alternately, GitLab administrators can use the API to create [impersonation tokens](../../api/index.md#impersonation-tokens).
Use impersonation tokens to automate authentication as a specific user.
......
......@@ -14,18 +14,19 @@ type: reference, howto
You can use a project access token to authenticate:
- With the [GitLab API](../../../api/index.md#personalproject-access-tokens).
- With the [GitLab API](../../../api/index.md#personalprojectgroup-access-tokens).
- With Git, when using HTTP Basic Authentication.
After you configure a project access token, you don't need a password when you authenticate.
Instead, you can enter any non-blank value.
Project access tokens are similar to [personal access tokens](../../profile/personal_access_tokens.md),
except they are associated with a project rather than a user.
Project access tokens are similar to [group access tokens](../../group/settings/group_access_tokens.md)
and [personal access tokens](../../profile/personal_access_tokens.md), except they are
associated with a project rather than a group or user.
You can use project access tokens:
- On GitLab SaaS if you have the Premium license tier or higher. Personal access tokens are not available with a [trial license](https://about.gitlab.com/free-trial/).
- On GitLab SaaS if you have the Premium license tier or higher. Project access tokens are not available with a [trial license](https://about.gitlab.com/free-trial/).
- On self-managed instances of GitLab, with any license tier. If you have the Free tier:
- Review your security and compliance policies around
[user self-enrollment](../../admin_area/settings/sign_up_restrictions.md#disable-new-sign-ups).
......@@ -79,7 +80,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 and group features**.
1. Under **Permissions**, turn on or off **Allow project access token creation**.
1. Under **Permissions**, turn on or off **Allow project and group access token creation**.
Even when creation is disabled, you can still use and revoke existing project access tokens.
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::AccessTokensController, :saas do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group_with_plan, plan: :bronze_plan) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:resource) { create(:project, group: group) }
let_it_be(:bot_user) { create(:user, :project_bot) }
before do
......@@ -15,49 +15,58 @@ RSpec.describe Projects::Settings::AccessTokensController, :saas do
end
before_all do
project.add_maintainer(bot_user)
project.add_maintainer(user)
resource.add_maintainer(bot_user)
resource.add_maintainer(user)
end
shared_examples 'feature unavailable' do
context 'with a free plan' do
let(:group) { create(:group_with_plan, plan: :free_plan) }
let(:project) { create(:project, group: group) }
let(:resource) { create(:project, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when user is not a maintainer with a paid group plan' do
before do
project.add_developer(user)
resource.add_developer(user)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
describe '#index' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
describe 'GET /:namespace/:project/-/settings/access_tokens' do
subject do
get project_settings_access_tokens_path(resource)
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'project access tokens available #index'
it_behaves_like 'GET resource access tokens available'
end
describe '#create' do
describe 'POST /:namespace/:project/-/settings/access_tokens' do
let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
subject do
post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'project access tokens available #create'
it_behaves_like 'POST resource access tokens available'
end
describe '#revoke', :sidekiq_inline do
let(:project_access_token) { create(:personal_access_token, user: bot_user) }
describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
subject do
put revoke_project_settings_access_token_path(resource, resource_access_token)
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'project access tokens available #revoke'
it_behaves_like 'PUT resource access tokens available'
end
end
......@@ -10,6 +10,7 @@ module Sidebars
add_item(general_menu_item)
add_item(integrations_menu_item)
add_item(access_tokens_menu_item)
add_item(group_projects_menu_item)
add_item(repository_menu_item)
add_item(ci_cd_menu_item)
......@@ -56,6 +57,19 @@ module Sidebars
)
end
def access_tokens_menu_item
unless can?(context.current_user, :read_resource_access_tokens, context.group)
return ::Sidebars::NilMenuItem.new(item_id: :access_tokens)
end
::Sidebars::MenuItem.new(
title: _('Access Tokens'),
link: group_settings_access_tokens_path(context.group),
active_routes: { path: 'access_tokens#index' },
item_id: :access_tokens
)
end
def group_projects_menu_item
::Sidebars::MenuItem.new(
title: _('Projects'),
......
......@@ -9898,13 +9898,13 @@ msgstr ""
msgid "Could not restore the group"
msgstr ""
msgid "Could not revoke impersonation token %{token_name}."
msgid "Could not revoke access token %{access_token_name}."
msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
msgid "Could not revoke project access token %{project_access_token_name}."
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
msgid "Could not save configuration. Please refresh the page, or try again later."
......@@ -14638,7 +14638,7 @@ msgstr ""
msgid "Failed to create merge request. Please try again."
msgstr ""
msgid "Failed to create new project access token: %{token_response_message}"
msgid "Failed to create new access token: %{token_response_message}"
msgstr ""
msgid "Failed to create repository"
......@@ -15578,6 +15578,9 @@ msgstr ""
msgid "Generate a default set of labels"
msgstr ""
msgid "Generate group access tokens scoped to this group for your applications that need access to the GitLab API."
msgstr ""
msgid "Generate key"
msgstr ""
......@@ -16676,6 +16679,9 @@ msgstr ""
msgid "Group %{group_name} was successfully created."
msgstr ""
msgid "Group Access Tokens"
msgstr ""
msgid "Group Git LFS status:"
msgstr ""
......@@ -16694,6 +16700,9 @@ msgstr ""
msgid "Group URL"
msgstr ""
msgid "Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Group application: %{name}"
msgstr ""
......@@ -17105,7 +17114,7 @@ msgstr ""
msgid "GroupSelect|Select a group"
msgstr ""
msgid "GroupSettings|Allow project access token creation"
msgid "GroupSettings|Allow project and group access token creation"
msgstr ""
msgid "GroupSettings|Allows creating organizations and contacts and associating them with issues."
......@@ -17252,7 +17261,7 @@ msgstr ""
msgid "GroupSettings|Transfer group"
msgstr ""
msgid "GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group."
msgid "GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group."
msgstr ""
msgid "GroupSettings|What are badges?"
......@@ -30519,13 +30528,13 @@ msgstr ""
msgid "Revoked"
msgstr ""
msgid "Revoked impersonation token %{token_name}!"
msgid "Revoked access token %{access_token_name}!"
msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgid "Revoked impersonation token %{token_name}!"
msgstr ""
msgid "Revoked project access token %{project_access_token_name}!"
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
msgid "RightSidebar|Copy email address"
......@@ -36380,6 +36389,9 @@ msgstr ""
msgid "This group has been scheduled for permanent removal on %{date}"
msgstr ""
msgid "This group has no active access tokens."
msgstr ""
msgid "This group is linked to a subscription"
msgstr ""
......@@ -40642,6 +40654,9 @@ msgstr ""
msgid "You can also upload existing files from your computer using the instructions below."
msgstr ""
msgid "You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
msgstr ""
......@@ -40687,6 +40702,9 @@ msgstr ""
msgid "You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service."
msgstr ""
msgid "You can enable group access token creation in %{link_start}group settings%{link_end}."
msgstr ""
msgid "You can enable project access token creation in %{link_start}group settings%{link_end}."
msgstr ""
......@@ -41352,13 +41370,13 @@ msgstr ""
msgid "Your new SCIM token"
msgstr ""
msgid "Your new comment"
msgid "Your new access token has been created."
msgstr ""
msgid "Your new personal access token has been created."
msgid "Your new comment"
msgstr ""
msgid "Your new project access token has been created."
msgid "Your new personal access token has been created."
msgstr ""
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
......@@ -42226,6 +42244,12 @@ msgstr ""
msgid "group"
msgstr ""
msgid "group access token"
msgstr ""
msgid "group access tokens"
msgstr ""
msgid "group members"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Group > Settings > Access Tokens', :js do
let_it_be(:user) { create(:user) }
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:group) { create(:group) }
let_it_be(:resource_settings_access_tokens_path) { group_settings_access_tokens_path(group) }
before_all do
group.add_owner(user)
end
before do
stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
def create_resource_access_token
group.add_maintainer(bot_user)
create(:personal_access_token, user: bot_user)
end
context 'when user is not a group owner' do
before do
group.add_maintainer(user)
end
it_behaves_like 'resource access tokens missing access rights'
end
describe 'token creation' do
it_behaves_like 'resource access tokens creation', 'group'
context 'when token creation is not allowed' do
it_behaves_like 'resource access tokens creation disallowed', 'Group access token creation is disabled in this group. You can still use and manage existing tokens.'
end
end
describe 'active tokens' do
let!(:resource_access_token) { create_resource_access_token }
it_behaves_like 'active resource access tokens'
end
describe 'inactive tokens' do
let!(:resource_access_token) { create_resource_access_token }
it_behaves_like 'inactive resource access tokens', 'This group has no active access tokens.'
end
end
......@@ -7,6 +7,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:resource_settings_access_tokens_path) { project_settings_access_tokens_path(project) }
before_all do
project.add_maintainer(user)
......@@ -17,78 +18,25 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
sign_in(user)
end
def create_project_access_token
def create_resource_access_token
project.add_maintainer(bot_user)
create(:personal_access_token, user: bot_user)
end
def active_project_access_tokens
find('.table.active-tokens')
end
def no_project_access_tokens_message
find('.settings-message')
end
def created_project_access_token
find('#created-personal-access-token').value
end
context 'when user is not a project maintainer' do
before do
project.add_developer(user)
end
it 'does not show project access token page' do
visit project_settings_access_tokens_path(project)
expect(page).to have_content("Page Not Found")
end
it_behaves_like 'resource access tokens missing access rights'
end
describe 'token creation' do
it 'allows creation of a project access token' do
name = 'My project access token'
visit project_settings_access_tokens_path(project)
fill_in 'Token name', with: name
# Set date to 1st of next month
find_field('Expiration date').click
find('.pika-next').click
click_on '1'
# Scopes
check 'api'
check 'read_api'
click_on 'Create project access token'
expect(active_project_access_tokens).to have_text(name)
expect(active_project_access_tokens).to have_text('in')
expect(active_project_access_tokens).to have_text('api')
expect(active_project_access_tokens).to have_text('read_api')
expect(active_project_access_tokens).to have_text('Maintainer')
expect(created_project_access_token).not_to be_empty
end
it_behaves_like 'resource access tokens creation', 'project'
context 'when token creation is not allowed' do
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
it 'does not show project access token creation form' do
visit project_settings_access_tokens_path(project)
expect(page).not_to have_selector('#new_project_access_token')
end
it 'shows project access token creation disabled text' do
visit project_settings_access_tokens_path(project)
expect(page).to have_text('Project access token creation is disabled in this group. You can still use and manage existing tokens.')
end
it_behaves_like 'resource access tokens creation disallowed', 'Project access token creation is disabled in this group. You can still use and manage existing tokens.'
context 'with a project in a personal namespace' do
let(:personal_project) { create(:project) }
......@@ -97,113 +45,25 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
personal_project.add_maintainer(user)
end
it 'shows project access token creation form and text' do
it 'shows access token creation form and text' do
visit project_settings_access_tokens_path(personal_project)
expect(page).to have_selector('#new_project_access_token')
expect(page).to have_selector('#new_resource_access_token')
expect(page).to have_text('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.')
end
end
context 'group settings link' do
context 'when user is not a group owner' do
before do
group.add_developer(user)
end
it 'does not show group settings link' do
visit project_settings_access_tokens_path(project)
expect(page).not_to have_link('group settings', href: edit_group_path(group))
end
end
context 'with nested groups' do
let(:subgroup) { create(:group, parent: group) }
context 'when user is not a top level group owner' do
before do
subgroup.add_owner(user)
end
it 'does not show group settings link' do
visit project_settings_access_tokens_path(project)
expect(page).not_to have_link('group settings', href: edit_group_path(group))
end
end
end
context 'when user is a group owner' do
before do
group.add_owner(user)
end
it 'shows group settings link' do
visit project_settings_access_tokens_path(project)
expect(page).to have_link('group settings', href: edit_group_path(group))
end
end
end
end
end
describe 'active tokens' do
let!(:project_access_token) { create_project_access_token }
let!(:resource_access_token) { create_resource_access_token }
it 'shows active project access tokens' do
visit project_settings_access_tokens_path(project)
expect(active_project_access_tokens).to have_text(project_access_token.name)
end
context 'when User#time_display_relative is false' do
before do
user.update!(time_display_relative: false)
end
it 'shows absolute times for expires_at' do
visit project_settings_access_tokens_path(project)
expect(active_project_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
end
end
it_behaves_like 'active resource access tokens'
end
describe 'inactive tokens' do
let!(:project_access_token) { create_project_access_token }
no_active_tokens_text = 'This project has no active access tokens.'
let!(:resource_access_token) { create_resource_access_token }
it 'allows revocation of an active token' do
visit project_settings_access_tokens_path(project)
accept_confirm { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
end
it 'removes expired tokens from active section' do
project_access_token.update!(expires_at: 5.days.ago)
visit project_settings_access_tokens_path(project)
expect(page).to have_selector('.settings-message')
expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
end
context 'when resource access token creation is not allowed' do
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
it 'allows revocation of an active token' do
visit project_settings_access_tokens_path(project)
accept_confirm { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
end
end
it_behaves_like 'inactive resource access tokens', 'This project has no active access tokens.'
end
end
......@@ -56,6 +56,12 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu do
it_behaves_like 'access rights checks'
end
describe 'Access Tokens' do
let(:item_id) { :access_tokens }
it_behaves_like 'access rights checks'
end
describe 'Repository menu' do
let(:item_id) { :repository }
......
......@@ -83,6 +83,23 @@ RSpec.describe GroupMemberPolicy do
specify { expect_allowed(:read_group) }
end
context 'with bot user' do
let(:current_user) { create(:user, :project_bot) }
before do
group.add_owner(current_user)
end
specify { expect_allowed(:read_group, :destroy_project_bot_member) }
end
context 'with anonymous bot user' do
let(:current_user) { create(:user, :project_bot) }
let(:membership) { guest.members.first }
specify { expect_disallowed(:read_group, :destroy_project_bot_member) }
end
context 'with one owner' do
let(:current_user) { owner }
......@@ -106,6 +123,7 @@ RSpec.describe GroupMemberPolicy do
end
specify { expect_allowed(*member_related_permissions) }
specify { expect_disallowed(:destroy_project_bot_member) }
end
context 'with the group parent' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:resource) { create(:group) }
let_it_be(:bot_user) { create(:user, :project_bot) }
before_all do
resource.add_owner(user)
resource.add_maintainer(bot_user)
end
before do
sign_in(user)
end
shared_examples 'feature unavailable' do
context 'user is not a owner' do
before do
resource.add_maintainer(user)
end
it { expect(subject).to have_gitlab_http_status(:not_found) }
end
end
describe 'GET /:namespace/-/settings/access_tokens' do
subject do
get group_settings_access_tokens_path(resource)
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'GET resource access tokens available'
end
describe 'POST /:namespace/-/settings/access_tokens' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
subject do
post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'POST resource access tokens available'
context 'when group access token creation is disabled' do
before do
resource.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
it { expect(subject).to have_gitlab_http_status(:not_found) }
it 'does not create the token' do
expect { subject }.not_to change { PersonalAccessToken.count }
end
it 'does not add the project bot as a member' do
expect { subject }.not_to change { Member.count }
end
it 'does not create the project bot user' do
expect { subject }.not_to change { User.count }
end
end
context 'with custom access level' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
subject { post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
it_behaves_like 'POST resource access tokens available'
end
end
describe 'PUT /:namespace/-/settings/access_tokens/:id', :sidekiq_inline do
let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
subject do
put revoke_group_settings_access_token_path(resource, resource_access_token)
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'PUT resource access tokens available'
end
end
# frozen_string_literal: true
require('spec_helper')
require 'spec_helper'
RSpec.describe Projects::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:resource) { create(:project, group: group) }
let_it_be(:bot_user) { create(:user, :project_bot) }
before_all do
project.add_maintainer(user)
project.add_maintainer(bot_user)
resource.add_maintainer(user)
resource.add_maintainer(bot_user)
end
before do
......@@ -20,34 +20,40 @@ RSpec.describe Projects::Settings::AccessTokensController do
shared_examples 'feature unavailable' do
context 'user is not a maintainer' do
before do
project.add_developer(user)
resource.add_developer(user)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
it { expect(subject).to have_gitlab_http_status(:not_found) }
end
end
describe '#index' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
describe 'GET /:namespace/:project/-/settings/access_tokens' do
subject do
get project_settings_access_tokens_path(resource)
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'project access tokens available #index'
it_behaves_like 'GET resource access tokens available'
end
describe '#create' do
describe 'POST /:namespace/:project/-/settings/access_tokens' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
subject do
post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'project access tokens available #create'
it_behaves_like 'POST resource access tokens available'
context 'when project access token creation is disabled' do
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
it { expect(subject).to have_gitlab_http_status(:not_found) }
it 'does not create the token' do
expect { subject }.not_to change { PersonalAccessToken.count }
......@@ -65,18 +71,21 @@ RSpec.describe Projects::Settings::AccessTokensController do
context 'with custom access level' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
subject { post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
it_behaves_like 'project access tokens available #create'
it_behaves_like 'POST resource access tokens available'
end
end
describe '#revoke', :sidekiq_inline do
let(:project_access_token) { create(:personal_access_token, user: bot_user) }
describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
subject do
put revoke_project_settings_access_token_path(resource, resource_access_token)
response
end
it_behaves_like 'feature unavailable'
it_behaves_like 'project access tokens available #revoke'
it_behaves_like 'PUT resource access tokens available'
end
end
......@@ -142,6 +142,7 @@ RSpec.shared_context 'group navbar structure' do
nav_sub_items: [
_('General'),
_('Integrations'),
_('Access Tokens'),
_('Projects'),
_('Repository'),
_('CI/CD'),
......
# frozen_string_literal: true
RSpec.shared_examples 'resource access tokens missing access rights' do
it 'does not show access token page' do
visit resource_settings_access_tokens_path
expect(page).to have_content("Page Not Found")
end
end
RSpec.shared_examples 'resource access tokens creation' do |resource_type|
def active_resource_access_tokens
find('.table.active-tokens')
end
def created_resource_access_token
find('#created-personal-access-token').value
end
it 'allows creation of an access token', :aggregate_failures do
name = 'My access token'
visit resource_settings_access_tokens_path
fill_in 'Token name', with: name
# Set date to 1st of next month
find_field('Expiration date').click
find('.pika-next').click
click_on '1'
# Scopes
check 'api'
check 'read_api'
click_on "Create #{resource_type} access token"
expect(active_resource_access_tokens).to have_text(name)
expect(active_resource_access_tokens).to have_text('in')
expect(active_resource_access_tokens).to have_text('api')
expect(active_resource_access_tokens).to have_text('read_api')
expect(active_resource_access_tokens).to have_text('Maintainer')
expect(created_resource_access_token).not_to be_empty
end
end
RSpec.shared_examples 'resource access tokens creation disallowed' do |error_message|
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
it 'does not show access token creation form' do
visit resource_settings_access_tokens_path
expect(page).not_to have_selector('#new_resource_access_token')
end
it 'shows access token creation disabled text' do
visit resource_settings_access_tokens_path
expect(page).to have_text(error_message)
end
context 'group settings link' do
context 'when user is not a group owner' do
before do
group.add_developer(user)
end
it 'does not show group settings link' do
visit resource_settings_access_tokens_path
expect(page).not_to have_link('group settings', href: edit_group_path(group))
end
end
context 'with nested groups' do
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
context 'when user is not a top level group owner' do
before do
group.add_owner(user)
end
it 'does not show group settings link' do
visit resource_settings_access_tokens_path
expect(page).not_to have_link('group settings', href: edit_group_path(group))
end
end
end
context 'when user is a group owner' do
before do
group.add_owner(user)
end
it 'shows group settings link' do
visit resource_settings_access_tokens_path
expect(page).to have_link('group settings', href: edit_group_path(group))
end
end
end
end
RSpec.shared_examples 'active resource access tokens' do
def active_resource_access_tokens
find('.table.active-tokens')
end
it 'shows active access tokens' do
visit resource_settings_access_tokens_path
expect(active_resource_access_tokens).to have_text(resource_access_token.name)
end
context 'when User#time_display_relative is false' do
before do
user.update!(time_display_relative: false)
end
it 'shows absolute times for expires_at' do
visit resource_settings_access_tokens_path
expect(active_resource_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
end
end
end
RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text|
def no_resource_access_tokens_message
find('.settings-message')
end
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
accept_confirm { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
end
it 'removes expired tokens from active section' do
resource_access_token.update!(expires_at: 5.days.ago)
visit resource_settings_access_tokens_path
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
end
context 'when resource access token creation is not allowed' do
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
accept_confirm { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'project access tokens available #index' do
let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
RSpec.shared_examples 'GET resource access tokens available' do
let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) }
let_it_be(:inactive_resource_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
it 'retrieves active project access tokens' do
it 'retrieves active resource access tokens' do
subject
expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
expect(assigns(:active_resource_access_tokens)).to contain_exactly(active_resource_access_token)
end
it 'retrieves inactive project access tokens' do
it 'retrieves inactive resource access tokens' do
subject
expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
expect(assigns(:inactive_resource_access_tokens)).to contain_exactly(inactive_resource_access_token)
end
it 'lists all available scopes' do
......@@ -24,15 +24,15 @@ RSpec.shared_examples 'project access tokens available #index' do
it 'retrieves newly created personal access token value' do
token_value = 'random-value'
allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{resource.id}").and_return(token_value)
subject
expect(assigns(:new_project_access_token)).to eq(token_value)
expect(assigns(:new_resource_access_token)).to eq(token_value)
end
end
RSpec.shared_examples 'project access tokens available #create' do
RSpec.shared_examples 'POST resource access tokens available' do
def created_token
PersonalAccessToken.order(:created_at).last
end
......@@ -40,17 +40,17 @@ RSpec.shared_examples 'project access tokens available #create' do
it 'returns success message' do
subject
expect(controller).to set_flash[:notice].to match('Your new project access token has been created.')
expect(flash[:notice]).to match('Your new access token has been created.')
end
it 'creates project access token' do
it 'creates resource access token' do
access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER
subject
expect(created_token.name).to eq(access_token_params[:name])
expect(created_token.scopes).to eq(access_token_params[:scopes])
expect(created_token.expires_at).to eq(access_token_params[:expires_at])
expect(project.member(created_token.user).access_level).to eq(access_level)
expect(resource.member(created_token.user).access_level).to eq(access_level)
end
it 'creates project bot user' do
......@@ -90,12 +90,12 @@ RSpec.shared_examples 'project access tokens available #create' do
it 'shows a failure alert' do
subject
expect(controller).to set_flash[:alert].to match("Failed to create new project access token: Failed!")
expect(flash[:alert]).to match("Failed to create new access token: Failed!")
end
end
end
RSpec.shared_examples 'project access tokens available #revoke' do
RSpec.shared_examples 'PUT resource access tokens available' do
it 'calls delete user worker' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true)
......@@ -105,7 +105,7 @@ RSpec.shared_examples 'project access tokens available #revoke' do
it 'removes membership of bot user' do
subject
expect(project.reload.bots).not_to include(bot_user)
expect(resource.reload.bots).not_to include(bot_user)
end
it 'converts issuables of the bot user to ghost user' do
......@@ -121,4 +121,18 @@ RSpec.shared_examples 'project access tokens available #revoke' do
expect(User.exists?(bot_user.id)).to be_falsy
end
context 'when unsuccessful' do
before do
allow_next_instance_of(ResourceAccessTokens::RevokeService) do |service|
allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
end
end
it 'shows a failure alert' do
subject
expect(flash[:alert]).to include("Could not revoke access token")
end
end
end
......@@ -11,7 +11,7 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
let_it_be(:user) { create(:user) }
let_it_be(:tokens) { [create(:personal_access_token, user: user)] }
let_it_be(:project) { false }
let_it_be(:resource) { false }
before do
stub_licensed_features(enforce_personal_access_token_expiration: true)
......@@ -20,8 +20,8 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
allow(view).to receive(:personal_access_token_expiration_enforced?).and_return(token_expiry_enforced?)
allow(view).to receive(:show_profile_token_expiry_notification?).and_return(true)
if project
project.add_maintainer(user)
if resource
resource.add_maintainer(user)
end
# Forcibly removing scopes from one token as it's not possible to do with the current modal on creation
......@@ -34,7 +34,7 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
type: type,
type_plural: type_plural,
active_tokens: tokens,
project: project,
resource: resource,
impersonation: impersonation,
revoke_route_helper: ->(token) { 'path/' }
}
......@@ -80,8 +80,8 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
end
end
context 'if project' do
let_it_be(:project) { create(:project) }
context 'if resource is project' do
let_it_be(:resource) { create(:project) }
it 'shows the project content', :aggregate_failures do
expect(rendered).to have_selector 'th', text: 'Role'
......@@ -92,6 +92,18 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
end
end
context 'if resource is group' do
let_it_be(:resource) { create(:group) }
it 'shows the group content', :aggregate_failures do
expect(rendered).to have_selector 'th', text: 'Role'
expect(rendered).to have_selector 'td', text: 'Maintainer'
expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
end
end
context 'without tokens' do
let_it_be(:tokens) { [] }
......
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