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 ...@@ -50,7 +50,7 @@ module Projects
end end
def create_params 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 end
def set_index_vars def set_index_vars
......
...@@ -16,11 +16,12 @@ module ResourceAccessTokens ...@@ -16,11 +16,12 @@ module ResourceAccessTokens
return error(user.errors.full_messages.to_sentence) unless user.persisted? 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? unless member.persisted?
delete_failed_user(user) 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 end
token_response = create_personal_access_token(user) token_response = create_personal_access_token(user)
...@@ -102,8 +103,8 @@ module ResourceAccessTokens ...@@ -102,8 +103,8 @@ module ResourceAccessTokens
Gitlab::Auth.resource_bot_scopes Gitlab::Auth.resource_bot_scopes
end end
def create_membership(resource, user) def create_membership(resource, user, access_level)
resource.add_user(user, :maintainer, expires_at: params[:expires_at]) resource.add_user(user, access_level, expires_at: params[:expires_at])
end end
def log_event(token) def log_event(token)
......
...@@ -33,8 +33,11 @@ ...@@ -33,8 +33,11 @@
= render 'shared/access_tokens/form', = render 'shared/access_tokens/form',
type: type, type: type,
path: project_settings_access_tokens_path(@project), path: project_settings_access_tokens_path(@project),
project: @project,
token: @project_access_token, token: @project_access_token,
scopes: @scopes, scopes: @scopes,
access_levels: ProjectMember.access_level_roles,
default_access_level: Gitlab::Access::MAINTAINER,
prefix: :project_access_token, prefix: :project_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'limiting-scopes-of-a-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 }) - title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
- prefix = local_assigns.fetch(:prefix, :personal_access_token) - prefix = local_assigns.fetch(:prefix, :personal_access_token)
- help_path = local_assigns.fetch(:help_path) - 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 %h5.gl-mt-0
= title = title
...@@ -29,6 +32,14 @@ ...@@ -29,6 +32,14 @@
.js-access-tokens-expires-at .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' } = 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 .form-group
%b{ :'aria-describedby' => 'select_scope_help_text' } %b{ :'aria-describedby' => 'select_scope_help_text' }
= s_('Tokens|Select scopes') = s_('Tokens|Select scopes')
......
...@@ -59,12 +59,13 @@ POST projects/:id/access_tokens ...@@ -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) | | `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 | | `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) | | `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 | | `expires_at` | Date | no | The token expires at midnight UTC on that date |
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type:application/json" \ --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" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
``` ```
...@@ -82,7 +83,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ ...@@ -82,7 +83,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"id" : 58, "id" : 58,
"expires_at" : "2021-01-31", "expires_at" : "2021-01-31",
"token" : "D4y...Wzr", "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 ...@@ -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. Navigate to the project you would like to create an access token for.
1. In the **Settings** menu choose **Access Tokens**. 1. In the **Settings** menu choose **Access Tokens**.
1. Choose a name and optional expiry date for the token. 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. Choose the [desired scopes](#limiting-scopes-of-a-project-access-token).
1. Click the **Create project access token** button. 1. Click the **Create project access token** button.
1. Save the project access token somewhere safe. Once you leave or refresh 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 ...@@ -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. 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 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: For the bot:
......
...@@ -11,9 +11,17 @@ module EE ...@@ -11,9 +11,17 @@ module EE
private 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) def audit_event_service(token, response)
message = if response.success? message = if response.success?
"Created #{resource_type} access token with token_id: #{token.id} with scopes: #{token.scopes}" success_message(token)
else else
"Attempted to create #{resource_type} access token but failed with message: #{response.message}" "Attempted to create #{resource_type} access token but failed with message: #{response.message}"
end end
......
...@@ -86,7 +86,7 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -86,7 +86,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
audit_event = AuditEvent.where(author_id: user.id).last audit_event = AuditEvent.where(author_id: user.id).last
access_token = response.payload[:access_token] access_token = response.payload[:access_token]
custom_message = <<~MESSAGE.squish 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 MESSAGE
expect(audit_event.details).to include( expect(audit_event.details).to include(
......
...@@ -58,6 +58,7 @@ module API ...@@ -58,6 +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 :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
......
...@@ -61,6 +61,14 @@ RSpec.describe Projects::Settings::AccessTokensController do ...@@ -61,6 +61,14 @@ RSpec.describe Projects::Settings::AccessTokensController do
expect { subject }.not_to change { User.count } expect { subject }.not_to change { User.count }
end end
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 end
describe '#revoke', :sidekiq_inline do describe '#revoke', :sidekiq_inline do
......
...@@ -212,8 +212,9 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -212,8 +212,9 @@ RSpec.describe API::ResourceAccessTokens do
end end
describe "POST projects/:id/access_tokens" do 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(:expires_at) { 1.month.from_now }
let(:access_level) { 20 }
subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params } subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
...@@ -232,6 +233,7 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -232,6 +233,7 @@ RSpec.describe API::ResourceAccessTokens do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq("test") expect(json_response["name"]).to eq("test")
expect(json_response["scopes"]).to eq(["api"]) 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["expires_at"]).to eq(expires_at.to_date.iso8601)
expect(json_response["token"]).to be_present expect(json_response["token"]).to be_present
end end
...@@ -249,6 +251,21 @@ RSpec.describe API::ResourceAccessTokens do ...@@ -249,6 +251,21 @@ RSpec.describe API::ResourceAccessTokens do
expect(json_response["expires_at"]).to eq(nil) expect(json_response["expires_at"]).to eq(nil)
end end
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 end
context "with invalid params" do context "with invalid params" do
......
...@@ -88,12 +88,28 @@ RSpec.describe ResourceAccessTokens::CreateService do ...@@ -88,12 +88,28 @@ RSpec.describe ResourceAccessTokens::CreateService do
end end
end end
it 'adds the bot user as a maintainer in the resource' do context 'access level' do
response = subject context 'when user does not specify an access level' do
access_token = response.payload[:access_token] it 'adds the bot user as a maintainer in the resource' do
bot_user = access_token.user response = subject
access_token = response.payload[:access_token]
bot_user = access_token.user
expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
end
end
expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id) 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 end
context 'personal access token' do context 'personal access token' do
......
...@@ -44,11 +44,13 @@ RSpec.shared_examples 'project access tokens available #create' do ...@@ -44,11 +44,13 @@ RSpec.shared_examples 'project access tokens available #create' do
end end
it 'creates project access token' do it 'creates project access token' do
access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER
subject subject
expect(created_token.name).to eq(access_token_params[:name]) expect(created_token.name).to eq(access_token_params[:name])
expect(created_token.scopes).to eq(access_token_params[:scopes]) expect(created_token.scopes).to eq(access_token_params[:scopes])
expect(created_token.expires_at).to eq(access_token_params[:expires_at]) 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 end
it 'creates project bot user' do 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