Commit 49480aa1 authored by Serena Fang's avatar Serena Fang Committed by Mark Chao

Project access token API

Not working yet
parent 44f55ba6
......@@ -30,6 +30,9 @@ class ProjectPolicy < BasePolicy
desc "User has maintainer access"
condition(:maintainer) { team_access_level >= Gitlab::Access::MAINTAINER }
desc "User is a project bot"
condition(:project_bot) { user.project_bot? && team_member? }
desc "Project is public"
condition(:public_project, scope: :subject, score: 0) { project.public? }
......@@ -616,10 +619,14 @@ class ProjectPolicy < BasePolicy
prevent :read_project
end
rule { project_bot }.enable :project_bot_access
rule { resource_access_token_available & can?(:admin_project) }.policy do
enable :admin_resource_access_tokens
end
rule { can?(:project_bot_access) }.prevent :admin_resource_access_tokens
rule { user_defined_variables_allowed | can?(:maintainer_access) }.policy do
enable :set_pipeline_variables
end
......
---
title: Project access token management via API
merge_request: 52139
author:
type: added
......@@ -25,6 +25,7 @@ The following API resources are available in the project context:
| Resource | Available endpoints |
|:--------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) |
| [Access tokens](resource_access_tokens.md) | `/projects/:id/access_tokens` |
| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` |
| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` |
| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` |
......@@ -76,7 +77,7 @@ The following API resources are available in the project context:
| [Remote mirrors](remote_mirrors.md) | `/projects/:id/remote_mirrors` |
| [Repositories](repositories.md) | `/projects/:id/repository` |
| [Repository files](repository_files.md) | `/projects/:id/repository/files` |
| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` |
| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` |
| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` (also available for groups) |
| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) |
| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
......
---
stage: Manage
group: Access
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
---
# Project access tokens API
You can read more about [project access tokens](../user/project/settings/project_access_tokens.md).
## List project access tokens
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238991) in GitLab 13.9.
Get a list of project access tokens.
```plaintext
GET /:id/access_tokens
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID of the project |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
```
```json
[
{
"user_id" : 141,
"scopes" : [
"api"
],
"name" : "token",
"expires_at" : "2021-01-31",
"id" : 42,
"active" : true,
"created_at" : "2021-01-20T22:11:48.151Z",
"revoked" : false
}
]
```
## Create a project access token
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238991) in GitLab 13.9.
Create a project access token.
```plaintext
POST /:id/access_tokens
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `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) |
| `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" }' \
"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
```
```json
{
"scopes" : [
"api",
"read_repository"
],
"active" : true,
"name" : "test",
"revoked" : false,
"created_at" : "2021-01-21T19:35:37.921Z",
"user_id" : 166,
"id" : 58,
"expires_at" : "2021-01-31"
}
```
## Revoke a project access token
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238991) in GitLab 13.9.
Revoke a project access token.
```plaintext
DELETE /:id/access_tokens/:token_id
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID of the project |
| `token_id` | integer/string | yes | The ID of the project access token |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>"
```
### Responses
- `204: No Content` if successfully revoked.
- `400 Bad Request` or `404 Not Found` if not revoked successfully.
......@@ -106,7 +106,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
context "when access provisioning fails" do
let_it_be(:user) { create(:user, :project_bot) }
let_it_be(:user) { create(:user) }
let(:unpersisted_member) { build(:project_member, source: resource, user: user) }
before do
......
......@@ -268,6 +268,7 @@ module API
mount ::API::Release::Links
mount ::API::RemoteMirrors
mount ::API::Repositories
mount ::API::ResourceAccessTokens
mount ::API::Search
mount ::API::Services
mount ::API::Settings
......
# frozen_string_literal: true
module API
class ResourceAccessTokens < ::API::Base
include PaginationParams
before { authenticate! }
feature_category :authentication_and_authorization
%w[project].each do |source_type|
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get list of all access tokens for the specified resource' do
detail 'This feature was introduced in GitLab 13.9.'
end
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
get ":id/access_tokens" do
resource = find_source(source_type, params[:id])
next unauthorized! unless has_permission_to_read?(resource)
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute
present paginate(tokens), with: Entities::PersonalAccessToken
end
desc 'Revoke a resource access token' do
detail 'This feature was introduced in GitLab 13.9.'
end
params do
requires :id, type: String, desc: "The #{source_type} ID"
requires :token_id, type: String, desc: "The ID of the token"
end
delete ':id/access_tokens/:token_id' do
resource = find_source(source_type, params[:id])
token = find_token(resource, params[:token_id])
if token.nil?
next not_found!("Could not find #{source_type} access token with token_id: #{params[:token_id]}")
end
service = ::ResourceAccessTokens::RevokeService.new(
current_user,
resource,
token
).execute
service.success? ? no_content! : bad_request!(service.message)
end
desc 'Create a resource access token' do
detail 'This feature was introduced in GitLab 13.9.'
end
params do
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 :expires_at, type: Date, desc: "The expiration date of the token"
end
post ':id/access_tokens' do
resource = find_source(source_type, params[:id])
token_response = ::ResourceAccessTokens::CreateService.new(
current_user,
resource,
declared_params
).execute
if token_response.success?
present token_response.payload[:access_token], with: Entities::PersonalAccessToken
else
bad_request!(token_response.message)
end
end
end
end
helpers do
def find_source(source_type, id)
public_send("find_#{source_type}!", id) # rubocop:disable GitlabSecurity/PublicSend
end
def find_token(resource, token_id)
PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).find_by_id(token_id)
end
def has_permission_to_read?(resource)
can?(current_user, :project_bot_access, resource) || can?(current_user, :admin_resource_access_tokens, resource)
end
end
end
end
......@@ -468,6 +468,49 @@ RSpec.describe ProjectPolicy do
end
end
context "project bots" do
let(:project_bot) { create(:user, :project_bot) }
let(:user) { create(:user) }
context "project_bot_access" do
context "when regular user and part of the project" do
let(:current_user) { user }
before do
project.add_developer(user)
end
it { is_expected.not_to be_allowed(:project_bot_access)}
end
context "when project bot and not part of the project" do
let(:current_user) { project_bot }
it { is_expected.not_to be_allowed(:project_bot_access)}
end
context "when project bot and part of the project" do
let(:current_user) { project_bot }
before do
project.add_developer(project_bot)
end
it { is_expected.to be_allowed(:project_bot_access)}
end
end
context 'with resource access tokens' do
let(:current_user) { project_bot }
before do
project.add_maintainer(project_bot)
end
it { is_expected.not_to be_allowed(:admin_resource_access_tokens)}
end
end
describe 'read_prometheus_alerts' do
context 'with admin' do
let(:current_user) { admin }
......
# frozen_string_literal: true
require "spec_helper"
RSpec.describe API::ResourceAccessTokens do
context "when the resource is a project" do
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
let_it_be(:user) { create(:user) }
describe "GET projects/:id/access_tokens" do
subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) }
context "when the user has maintainer permissions" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
let_it_be(:project_id) { project.id }
before do
project.add_maintainer(user)
project.add_maintainer(project_bot)
end
it "gets a list of access tokens for the specified project" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(token_ids).to match_array(access_tokens.pluck(:id))
end
context "when using a project access token to GET other project access tokens" do
let_it_be(:token) { access_tokens.first }
it "gets a list of access tokens for the specified project" do
get api("/projects/#{project_id}/access_tokens", personal_access_token: token)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(token_ids).to match_array(access_tokens.pluck(:id))
end
end
context "when tokens belong to a different project" do
let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: bot) }
before do
other_project.add_maintainer(bot)
other_project.add_maintainer(user)
end
it "does not return tokens from a different project" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).not_to include(token.id)
end
end
context "when the project has no access tokens" do
let(:project_id) { other_project.id }
before do
other_project.add_maintainer(user)
end
it 'returns an empty array' do
get_tokens
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([])
end
end
context "when trying to get the tokens of a different project" do
let_it_be(:project_id) { other_project.id }
it "returns 404" do
get_tokens
expect(response).to have_gitlab_http_status(:not_found)
end
end
context "when the project does not exist" do
let(:project_id) { non_existing_record_id }
it "returns 404" do
get_tokens
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context "when the user does not have valid permissions" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
let_it_be(:project_id) { project.id }
before do
project.add_developer(user)
project.add_maintainer(project_bot)
end
it "returns 401" do
get_tokens
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do
subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
let_it_be(:project_id) { project.id }
let_it_be(:token_id) { token.id }
before do
project.add_maintainer(project_bot)
end
context "when the user has maintainer permissions" do
before do
project.add_maintainer(user)
end
it "deletes the project access token from the project" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
expect(User.exists?(project_bot.id)).to be_falsy
end
context "when attempting to delete a non-existent project access token" do
let_it_be(:token_id) { non_existing_record_id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
end
end
context "when attempting to delete a token that does not belong to the specified project" do
let_it_be(:project_id) { other_project.id }
before do
other_project.add_maintainer(user)
end
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
end
end
end
context "when the user does not have valid permissions" do
before do
project.add_developer(user)
end
it "does not delete the token, and returns 400", :aggregate_failures do
delete_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(User.exists?(project_bot.id)).to be_truthy
expect(response.body).to include("#{user.name} cannot delete #{token.user.name}")
end
end
end
describe "POST projects/:id/access_tokens" do
let_it_be(:params) { { name: "test", scopes: ["api"], expires_at: Date.today + 1.month } }
subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
context "when the user has maintainer permissions" do
let_it_be(:project_id) { project.id }
let_it_be(:expires_at) { 1.month.from_now }
before do
project.add_maintainer(user)
end
context "with valid params" do
context "with full params" do
it "creates a project access token with the params", :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["expires_at"]).to eq(expires_at.to_date.iso8601)
end
end
context "when 'expires_at' is not set" do
let_it_be(:params) { { name: "test", scopes: ["api"] } }
it "creates a project access token with the params", :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["expires_at"]).to eq(nil)
end
end
end
context "with invalid params" do
context "when missing the 'name' param" do
let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } }
it "does not create a project access token without 'name'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("name is missing")
end
end
context "when missing the 'scopes' param" do
let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } }
it "does not create a project access token without 'scopes'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("scopes is missing")
end
end
end
context "when trying to create a token in a different project" do
let_it_be(:project_id) { other_project.id }
it "does not create the token, and returns the project not found error" do
create_token
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to include("Project Not Found")
end
end
end
context "when the user does not have valid permissions" do
let_it_be(:project_id) { project.id }
context "when the user is a developer" do
before do
project.add_developer(user)
end
it "does not create the token, and returns the permission error" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("User does not have permission to create project access token")
end
end
context "when a project access token tries to create another project access token" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { project_bot }
before do
project.add_maintainer(user)
end
it "does not allow a project access token to create another project access token" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include("User does not have permission to create project access token")
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment