Commit 0d60b072 authored by Devin Christensen's avatar Devin Christensen Committed by Jan Provaznik

Add "active" filter to deploy tokens API

parent aff5ebaf
# frozen_string_literal: true
# Arguments:
# current_user: The currently logged in user.
# scope: A Project or Group to scope deploy tokens to (or :all for all tokens).
# params:
# active: Boolean - When true, only return active deployments.
module DeployTokens
class TokensFinder
attr_reader :current_user, :params, :scope
def initialize(current_user, scope, params = {})
@current_user = current_user
@scope = scope
@params = params
end
def execute
by_active(init_collection)
end
private
def init_collection
case scope
when Group, Project
raise Gitlab::Access::AccessDeniedError unless current_user.can?(:read_deploy_token, scope)
scope.deploy_tokens
when :all
raise Gitlab::Access::AccessDeniedError unless current_user.can_read_all_resources?
DeployToken.all
else
raise ArgumentError, "Scope must be a Group, a Project, or the :all symbol."
end
end
def by_active(items)
params[:active] ? items.active : items
end
end
end
---
title: Add "active" filter to deploy tokens API
merge_request: 59582
author: Devin Christensen
type: added
......@@ -16,6 +16,12 @@ Get a list of all deploy tokens across the GitLab instance. This endpoint requir
GET /deploy_tokens
```
Parameters:
| Attribute | Type | Required | Description |
|-----------|----------|------------------------|-------------|
| `active` | boolean | **{dotted-circle}** No | Limit by active status. |
Example request:
```shell
......@@ -31,6 +37,8 @@ Example response:
"name": "MyToken",
"username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z",
"revoked": false,
"expired": false,
"scopes": [
"read_repository",
"read_registry"
......@@ -55,9 +63,10 @@ GET /projects/:id/deploy_tokens
Parameters:
| Attribute | Type | Required | Description |
|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| Attribute | Type | Required | Description |
|:---------------|:---------------|:-----------------------|:------------|
| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `active` | boolean | **{dotted-circle}** No | Limit by active status. |
Example request:
......@@ -74,6 +83,8 @@ Example response:
"name": "MyToken",
"username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z",
"revoked": false,
"expired": false,
"scopes": [
"read_repository",
"read_registry"
......@@ -92,13 +103,17 @@ Creates a new deploy token for a project.
POST /projects/:id/deploy_tokens
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Parameters:
| Attribute | Type | Required | Description |
| ------------ | ---------------- | ---------------------- | ----------- |
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | **{check-circle}** Yes | New deploy token's name |
| `expires_at` | datetime | **{dotted-circle}** No | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `username` | string | **{dotted-circle}** No | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
| `scopes` | array of strings | **{check-circle}** Yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "My deploy token", "expires_at": "2021-01-01", "username": "custom-user", "scopes": ["read_repository"]}' "https://gitlab.example.com/api/v4/projects/5/deploy_tokens/"
......@@ -113,6 +128,8 @@ Example response:
"username": "custom-user",
"expires_at": "2021-01-01T00:00:00.000Z",
"token": "jMRvtPNxrn3crTAGukpZ",
"revoked": false,
"expired": false,
"scopes": [
"read_repository"
]
......@@ -129,10 +146,12 @@ Removes a deploy token from the project.
DELETE /projects/:id/deploy_tokens/:token_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `token_id` | integer | yes | The ID of the deploy token |
Parameters:
| Attribute | Type | Required | Description |
| ---------- | -------------- | ---------------------- | ----------- |
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `token_id` | integer | **{check-circle}** Yes | The ID of the deploy token |
Example request:
......@@ -157,9 +176,10 @@ GET /groups/:id/deploy_tokens
Parameters:
| Attribute | Type | Required | Description |
|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| Attribute | Type | Required | Description |
|:---------------|:---------------|:-----------------------|:------------|
| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `active` | boolean | **{dotted-circle}** No | Limit by active status. |
Example request:
......@@ -176,6 +196,8 @@ Example response:
"name": "MyToken",
"username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z",
"revoked": false,
"expired": false,
"scopes": [
"read_repository",
"read_registry"
......@@ -194,13 +216,15 @@ Creates a new deploy token for a group.
POST /groups/:id/deploy_tokens
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Parameters:
| Attribute | Type | Required | Description |
| ------------ | ---- | --------- | ----------- |
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | **{check-circle}** Yes | New deploy token's name |
| `expires_at` | datetime | **{dotted-circle}** No | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `username` | string | **{dotted-circle}** No | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
| `scopes` | array of strings | **{check-circle}** Yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Example request:
......@@ -217,6 +241,8 @@ Example response:
"username": "custom-user",
"expires_at": "2021-01-01T00:00:00.000Z",
"token": "jMRvtPNxrn3crTAGukpZ",
"revoked": false,
"expired": false,
"scopes": [
"read_registry"
]
......@@ -233,10 +259,12 @@ Removes a deploy token from the group.
DELETE /groups/:id/deploy_tokens/:token_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `token_id` | integer | yes | The ID of the deploy token |
Parameters:
| Attribute | Type | Required | Description |
| ----------- | -------------- | ---------------------- | ----------- |
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `token_id` | integer | **{check-circle}** Yes | The ID of the deploy token |
Example request:
......
......@@ -18,6 +18,10 @@ module API
result_hash[:read_repository] = scopes.include?('read_repository')
result_hash
end
params :filter_params do
optional :active, type: Boolean, desc: 'Limit by active status'
end
end
desc 'Return all deploy tokens' do
......@@ -26,11 +30,18 @@ module API
end
params do
use :pagination
use :filter_params
end
get 'deploy_tokens' do
authenticated_as_admin!
present paginate(DeployToken.all), with: Entities::DeployToken
deploy_tokens = ::DeployTokens::TokensFinder.new(
current_user,
:all,
declared_params
).execute
present paginate(deploy_tokens), with: Entities::DeployToken
end
params do
......@@ -39,6 +50,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
use :pagination
use :filter_params
end
desc 'List deploy tokens for a project' do
detail 'This feature was introduced in GitLab 12.9'
......@@ -47,7 +59,13 @@ module API
get ':id/deploy_tokens' do
authorize!(:read_deploy_token, user_project)
present paginate(user_project.deploy_tokens), with: Entities::DeployToken
deploy_tokens = ::DeployTokens::TokensFinder.new(
current_user,
user_project,
declared_params
).execute
present paginate(deploy_tokens), with: Entities::DeployToken
end
params do
......@@ -98,6 +116,7 @@ module API
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
use :pagination
use :filter_params
end
desc 'List deploy tokens for a group' do
detail 'This feature was introduced in GitLab 12.9'
......@@ -106,7 +125,13 @@ module API
get ':id/deploy_tokens' do
authorize!(:read_deploy_token, user_group)
present paginate(user_group.deploy_tokens), with: Entities::DeployToken
deploy_tokens = ::DeployTokens::TokensFinder.new(
current_user,
user_group,
declared_params
).execute
present paginate(deploy_tokens), with: Entities::DeployToken
end
params do
......
......@@ -4,7 +4,8 @@ module API
module Entities
class DeployToken < Grape::Entity
# exposing :token is a security risk and should be avoided
expose :id, :name, :username, :expires_at, :scopes
expose :id, :name, :username, :expires_at, :scopes, :revoked
expose :expired?, as: :expired
end
end
end
......@@ -59,6 +59,8 @@ RSpec.describe Groups::Settings::RepositoryController do
'username' => deploy_token_params[:username],
'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'expired' => false,
'revoked' => false,
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
......
......@@ -78,6 +78,8 @@ RSpec.describe Projects::Settings::RepositoryController do
'username' => deploy_token_params[:username],
'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'expired' => false,
'revoked' => false,
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DeployTokens::TokensFinder do
include AdminModeHelper
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:project) { create(:project, creator_id: user.id) }
let_it_be(:group) { create(:group) }
let!(:project_deploy_token) { create(:deploy_token, projects: [project]) }
let!(:revoked_project_deploy_token) { create(:deploy_token, projects: [project], revoked: true) }
let!(:expired_project_deploy_token) { create(:deploy_token, projects: [project], expires_at: '1988-01-11T04:33:04-0600') }
let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
let!(:revoked_group_deploy_token) { create(:deploy_token, :group, groups: [group], revoked: true) }
let!(:expired_group_deploy_token) { create(:deploy_token, :group, groups: [group], expires_at: '1988-01-11T04:33:04-0600') }
describe "#execute" do
let(:params) { {} }
context 'when scope is :all' do
subject { described_class.new(admin, :all, params).execute }
before do
enable_admin_mode!(admin)
end
it 'returns all deploy tokens' do
expect(subject.size).to eq(6)
is_expected.to match_array([
project_deploy_token,
revoked_project_deploy_token,
expired_project_deploy_token,
group_deploy_token,
revoked_group_deploy_token,
expired_group_deploy_token
])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens' do
is_expected.to match_array([
project_deploy_token,
group_deploy_token
])
end
end
context 'but user is not an admin' do
subject { described_class.new(user, :all, params).execute }
it 'raises Gitlab::Access::AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'when scope is a Project' do
subject { described_class.new(user, project, params).execute }
before do
project.add_maintainer(user)
end
it 'returns all deploy tokens for the project' do
is_expected.to match_array([
project_deploy_token,
revoked_project_deploy_token,
expired_project_deploy_token
])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens for the project' do
is_expected.to match_array([project_deploy_token])
end
end
context 'but user is not a member' do
subject { described_class.new(other_user, :all, params).execute }
it 'raises Gitlab::Access::AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'when scope is a Group' do
subject { described_class.new(user, group, params).execute }
before do
group.add_maintainer(user)
end
it 'returns all deploy tokens for the group' do
is_expected.to match_array([
group_deploy_token,
revoked_group_deploy_token,
expired_group_deploy_token
])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens for the group' do
is_expected.to match_array([group_deploy_token])
end
end
context 'but user is not a member' do
subject { described_class.new(other_user, :all, params).execute }
it 'raises Gitlab::Access::AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'when scope is nil' do
subject { described_class.new(user, nil, params).execute }
it 'raises ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
end
end
end
end
......@@ -8,7 +8,11 @@ RSpec.describe API::DeployTokens do
let_it_be(:project) { create(:project, creator_id: creator.id) }
let_it_be(:group) { create(:group) }
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
let!(:revoked_deploy_token) { create(:deploy_token, projects: [project], revoked: true) }
let!(:expired_deploy_token) { create(:deploy_token, projects: [project], expires_at: '1988-01-11T04:33:04-0600') }
let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
let!(:revoked_group_deploy_token) { create(:deploy_token, :group, groups: [group], revoked: true) }
let!(:expired_group_deploy_token) { create(:deploy_token, :group, groups: [group], expires_at: '1988-01-11T04:33:04-0600') }
describe 'GET /deploy_tokens' do
subject do
......@@ -36,8 +40,31 @@ RSpec.describe API::DeployTokens do
it 'returns all deploy tokens' do
subject
token_ids = json_response.map { |token| token['id'] }
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/deploy_tokens')
expect(token_ids).to match_array([
deploy_token.id,
revoked_deploy_token.id,
expired_deploy_token.id,
group_deploy_token.id,
revoked_group_deploy_token.id,
expired_group_deploy_token.id
])
end
context 'and active=true' do
it 'only returns active deploy tokens' do
get api('/deploy_tokens?active=true', user)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(token_ids).to match_array([
deploy_token.id,
group_deploy_token.id
])
end
end
end
end
......@@ -82,7 +109,22 @@ RSpec.describe API::DeployTokens do
subject
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).not_to include(other_deploy_token.id)
expect(token_ids).to match_array([
deploy_token.id,
expired_deploy_token.id,
revoked_deploy_token.id
])
end
context 'and active=true' do
it 'only returns active deploy tokens for the project' do
get api("/projects/#{project.id}/deploy_tokens?active=true", user)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(token_ids).to match_array([deploy_token.id])
end
end
end
end
......@@ -119,8 +161,10 @@ RSpec.describe API::DeployTokens do
it 'returns all deploy tokens for the group' do
subject
token_ids = json_response.map { |token| token['id'] }
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/deploy_tokens')
expect(token_ids.length).to be(3)
end
it 'does not return deploy tokens for other groups' do
......@@ -129,6 +173,17 @@ RSpec.describe API::DeployTokens do
token_ids = json_response.map { |token| token['id'] }
expect(token_ids).not_to include(other_deploy_token.id)
end
context 'and active=true' do
it 'only returns active deploy tokens for the group' do
get api("/groups/#{group.id}/deploy_tokens?active=true", user)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(token_ids).to eql([group_deploy_token.id])
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