Commit 72a7f202 authored by James Lopez's avatar James Lopez

Merge branch '300346-project-access-token-level' into 'master'

Allow specifying an access level for Project Access Tokens

See merge request gitlab-org/gitlab!63725
parents 338fef9e 962c6b39
......@@ -50,7 +50,7 @@ module Projects
end
def create_params
params.require(:project_access_token).permit(:name, :expires_at, scopes: [])
params.require(:project_access_token).permit(:name, :expires_at, :access_level, scopes: [])
end
def set_index_vars
......
......@@ -16,11 +16,12 @@ module ResourceAccessTokens
return error(user.errors.full_messages.to_sentence) unless user.persisted?
member = create_membership(resource, user)
access_level = params[:access_level] || Gitlab::Access::MAINTAINER
member = create_membership(resource, user, access_level)
unless member.persisted?
delete_failed_user(user)
return error("Could not provision maintainer access to project access token")
return error("Could not provision #{Gitlab::Access.human_access(access_level).downcase} access to project access token")
end
token_response = create_personal_access_token(user)
......@@ -102,8 +103,8 @@ module ResourceAccessTokens
Gitlab::Auth.resource_bot_scopes
end
def create_membership(resource, user)
resource.add_user(user, :maintainer, expires_at: params[:expires_at])
def create_membership(resource, user, access_level)
resource.add_user(user, access_level, expires_at: params[:expires_at])
end
def log_event(token)
......
......@@ -33,8 +33,11 @@
= render 'shared/access_tokens/form',
type: type,
path: project_settings_access_tokens_path(@project),
project: @project,
token: @project_access_token,
scopes: @scopes,
access_levels: ProjectMember.access_level_roles,
default_access_level: Gitlab::Access::MAINTAINER,
prefix: :project_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'limiting-scopes-of-a-project-access-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)
- access_levels = local_assigns.fetch(:access_levels, false)
- default_access_level = local_assigns.fetch(:default_access_level, false)
%h5.gl-mt-0
= title
......@@ -29,6 +32,14 @@
.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
.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' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
%b{ :'aria-describedby' => 'select_scope_help_text' }
= s_('Tokens|Select scopes')
......
......@@ -59,12 +59,13 @@ POST projects/:id/access_tokens
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
| `name` | String | yes | The name of the project access token |
| `scopes` | `Array[String]` | yes | [List of scopes](../user/project/settings/project_access_tokens.md#limiting-scopes-of-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" }' \
--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31", "access_level": 30 }' \
"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
```
......@@ -82,7 +83,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"id" : 58,
"expires_at" : "2021-01-31",
"token" : "D4y...Wzr",
"access_level": 40
"access_level": 30
}
```
......
......@@ -29,6 +29,7 @@ For examples of how you can use a project access token to authenticate with the
1. Navigate to the project you would like to create an access token for.
1. In the **Settings** menu choose **Access Tokens**.
1. Choose a name and optional expiry date for the token.
1. Choose the access level the token should have in the project.
1. Choose the [desired scopes](#limiting-scopes-of-a-project-access-token).
1. Click the **Create project access token** button.
1. Save the project access token somewhere safe. Once you leave or refresh
......@@ -42,7 +43,7 @@ For examples of how you can use a project access token to authenticate with the
Project bot users are [GitLab-created service accounts](../../../subscriptions/self_managed/index.md#billable-users) and do not count as licensed seats.
For each project access token created, a bot user is created and added to the project with
[Maintainer level permissions](../../permissions.md#project-members-permissions).
the [specified level permissions](../../permissions.md#project-members-permissions).
For the bot:
......
......@@ -11,9 +11,17 @@ module EE
private
def success_message(token)
if resource_type == 'project'
"Created project access token with token_id: #{token.id} with scopes: #{token.scopes} and #{resource.project_member(token.user).human_access} access level."
else
"Created #{resource_type} token with token_id: #{token.id} with scopes: #{token.scopes}."
end
end
def audit_event_service(token, response)
message = if response.success?
"Created #{resource_type} access token with token_id: #{token.id} with scopes: #{token.scopes}"
success_message(token)
else
"Attempted to create #{resource_type} access token but failed with message: #{response.message}"
end
......
......@@ -86,7 +86,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
audit_event = AuditEvent.where(author_id: user.id).last
access_token = response.payload[:access_token]
custom_message = <<~MESSAGE.squish
Created project access token with token_id: #{access_token.id} with scopes: #{access_token.scopes}
Created project access token with token_id: #{access_token.id} with scopes: #{access_token.scopes} and Maintainer access level.
MESSAGE
expect(audit_event.details).to include(
......
......@@ -58,6 +58,7 @@ module API
requires :id, type: String, desc: "The #{source_type} ID"
requires :name, type: String, desc: "Resource access token name"
requires :scopes, type: Array[String], desc: "The permissions of the token"
optional :access_level, type: Integer, desc: "The access level of the token in the project"
optional :expires_at, type: Date, desc: "The expiration date of the token"
end
post ':id/access_tokens' do
......
......@@ -61,6 +61,14 @@ RSpec.describe Projects::Settings::AccessTokensController 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 :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
it_behaves_like 'project access tokens available #create'
end
end
describe '#revoke', :sidekiq_inline do
......
......@@ -212,8 +212,9 @@ RSpec.describe API::ResourceAccessTokens do
end
describe "POST projects/:id/access_tokens" do
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at } }
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at, access_level: access_level } }
let(:expires_at) { 1.month.from_now }
let(:access_level) { 20 }
subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
......@@ -232,6 +233,7 @@ RSpec.describe API::ResourceAccessTokens do
expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq("test")
expect(json_response["scopes"]).to eq(["api"])
expect(json_response["access_level"]).to eq(20)
expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601)
expect(json_response["token"]).to be_present
end
......@@ -249,6 +251,21 @@ RSpec.describe API::ResourceAccessTokens do
expect(json_response["expires_at"]).to eq(nil)
end
end
context "when 'access_level' is not set" do
let(:access_level) { nil }
it 'creates a project access token with the default access level', :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq("test")
expect(json_response["scopes"]).to eq(["api"])
expect(json_response["access_level"]).to eq(40)
expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601)
expect(json_response["token"]).to be_present
end
end
end
context "with invalid params" do
......
......@@ -88,6 +88,8 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
context 'access level' do
context 'when user does not specify an access level' do
it 'adds the bot user as a maintainer in the resource' do
response = subject
access_token = response.payload[:access_token]
......@@ -95,6 +97,20 @@ RSpec.describe ResourceAccessTokens::CreateService do
expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
end
end
context 'when user specifies an access level' do
let_it_be(:params) { { access_level: Gitlab::Access::DEVELOPER } }
it 'adds the bot user with the specified access level in the resource' do
response = subject
access_token = response.payload[:access_token]
bot_user = access_token.user
expect(resource.members.developers.map(&:user_id)).to include(bot_user.id)
end
end
end
context 'personal access token' do
it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
......
......@@ -44,11 +44,13 @@ RSpec.shared_examples 'project access tokens available #create' do
end
it 'creates project 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.project_member(created_token.user).access_level).to eq(access_level)
end
it 'creates project bot user' do
......
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