Commit dd552d06 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '31591-project-deploy-tokens-to-allow-permanent-access' into 'master'

Create Project Deploy Tokens to allow permanent access to repo and registry

Closes #31591

See merge request gitlab-org/gitlab-ce!17894
parents 671e93dc b38439a3
import initForm from '../form';
document.addEventListener('DOMContentLoaded', initForm);
/* eslint-disable no-new */
import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import DueDateSelectors from '~/due_date_select';
export default () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
new DueDateSelectors();
};
/* eslint-disable no-new */
import initForm from '../form';
import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
});
document.addEventListener('DOMContentLoaded', initForm);
......@@ -284,3 +284,23 @@
.deprecated-service {
cursor: default;
}
.personal-access-tokens-never-expires-label {
color: $note-disabled-comment-color;
}
.created-deploy-token-container {
.deploy-token-field {
width: 90%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
.deploy-token-help-block {
display: block;
margin-bottom: 0;
}
}
......@@ -25,8 +25,7 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
if @authentication_result.failed? ||
(@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User))
if @authentication_result.failed?
render_unauthorized
end
end
......
class Projects::DeployTokensController < Projects::ApplicationController
before_action :authorize_admin_project!
def revoke
@token = @project.deploy_tokens.find(params[:id])
@token.revoke!
redirect_to project_settings_repository_path(project)
end
end
......@@ -4,13 +4,31 @@ module Projects
before_action :authorize_admin_project!
def show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
render_show
end
define_protected_refs
def create_deploy_token
@new_deploy_token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
if @new_deploy_token.persisted?
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
end
render_show
end
private
def render_show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
@deploy_tokens = @project.deploy_tokens.active
define_deploy_token
define_protected_refs
render 'show'
end
def define_protected_refs
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
......@@ -51,6 +69,14 @@ module Projects
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
end
def define_deploy_token
@new_deploy_token ||= DeployToken.new
end
def deploy_token_params
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry)
end
end
end
end
module DeployTokensHelper
def expand_deploy_tokens_section?(deploy_token)
deploy_token.persisted? ||
deploy_token.errors.present? ||
Rails.env.test?
end
def container_registry_enabled?(project)
Gitlab.config.registry.enabled &&
can?(current_user, :read_container_image, project)
end
end
class DeployToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token
has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
before_save :ensure_token
accepts_nested_attributes_for :project_deploy_tokens
scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
def revoke!
update!(revoked: true)
end
def active?
!revoked
end
def scopes
AVAILABLE_SCOPES.select { |token_scope| read_attribute(token_scope) }
end
def username
"gitlab+deploy-token-#{id}"
end
def has_access_to?(requested_project)
project == requested_project
end
# This is temporal. Currently we limit DeployToken
# to a single project, later we're going to extend
# that to be for multiple projects and namespaces.
def project
projects.first
end
def expires_at
expires_at = read_attribute(:expires_at)
expires_at != Forever.date ? expires_at : nil
end
def expires_at=(value)
write_attribute(:expires_at, value.presence || Forever.date)
end
private
def ensure_at_least_one_scope
errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
end
end
......@@ -222,6 +222,8 @@ class Project < ActiveRecord::Base
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
......
class ProjectDeployToken < ActiveRecord::Base
belongs_to :project
belongs_to :deploy_token, inverse_of: :project_deploy_tokens
validates :deploy_token, presence: true
validates :project, presence: true
validates :deploy_token_id, uniqueness: { scope: [:project_id] }
end
class DeployTokenPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:master) { @subject.project.team.master?(@user) }
rule { anonymous }.prevent_all
rule { master }.policy do
enable :create_deploy_token
enable :update_deploy_token
end
end
......@@ -143,7 +143,7 @@ class ProjectPolicy < BasePolicy
end
# These abilities are not allowed to admins that are not members of the project,
# that's why they are defined separatly.
# that's why they are defined separately.
rule { guest & can?(:download_code) }.enable :build_download_code
rule { guest & can?(:read_container_image) }.enable :build_read_container_image
......
......@@ -109,7 +109,7 @@ module Auth
case requested_action
when 'pull'
build_can_pull?(requested_project) || user_can_pull?(requested_project)
build_can_pull?(requested_project) || user_can_pull?(requested_project) || deploy_token_can_pull?(requested_project)
when 'push'
build_can_push?(requested_project) || user_can_push?(requested_project)
when '*'
......@@ -123,22 +123,33 @@ module Auth
Gitlab.config.registry
end
def can_user?(ability, project)
user = current_user.is_a?(User) ? current_user : nil
can?(user, ability, project)
end
def build_can_pull?(requested_project)
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
has_authentication_ability?(:build_read_container_image) &&
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
(requested_project == project || can_user?(:build_read_container_image, requested_project))
end
def user_can_admin?(requested_project)
has_authentication_ability?(:admin_container_image) &&
can?(current_user, :admin_container_image, requested_project)
can_user?(:admin_container_image, requested_project)
end
def user_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
can_user?(:read_container_image, requested_project)
end
def deploy_token_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
current_user.is_a?(DeployToken) &&
current_user.has_access_to?(requested_project)
end
##
......@@ -154,7 +165,7 @@ module Auth
def user_can_push?(requested_project)
has_authentication_ability?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
can_user?(:create_container_image, requested_project)
end
def error(code, status:, message: '')
......
module DeployTokens
class CreateService < BaseService
def execute
@project.deploy_tokens.create(params)
end
end
end
......@@ -2,7 +2,6 @@
- page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
......
%p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
= form_for token, url: create_deploy_token_namespace_project_settings_repository_path(project.namespace, project), method: :post do |f|
= form_errors(token)
.form-group
= f.label :name, class: 'label-light'
= f.text_field :name, class: 'form-control', required: true
.form-group
= f.label :expires_at, class: 'label-light'
= f.text_field :expires_at, class: 'datepicker form-control', value: f.object.expires_at
.form-group
= f.label :scopes, class: 'label-light'
%fieldset
= f.check_box :read_repository
= label_tag ("deploy_token_read_repository"), 'read_repository'
%span= s_('DeployTokens|Allows read-only access to the repository')
- if container_registry_enabled?(project)
%fieldset
= f.check_box :read_registry
= label_tag ("deploy_token_read_registry"), 'read_registry'
%span= s_('DeployTokens|Allows read-only access to the registry images')
.prepend-top-default
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success'
- expanded = expand_deploy_tokens_section?(@new_deploy_token)
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('DeployTokens|Deploy Tokens')
%button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.')
.settings-content
- if @new_deploy_token.persisted?
= render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
- else
%h5.prepend-top-0
= s_('DeployTokens|Add a deploy token')
= render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
%hr
= render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
.created-deploy-token-container
%h5.prepend-top-0
= s_('DeployTokens|Your New Deploy Token')
.form-group
= text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
= clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
%span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
.form-group
= text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
= clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
%span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
%hr
.modal{ id: "revoke-modal-#{token.id}" }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.pull-left
= s_('DeployTokens|Revoke')
%b #{token.name}?
%button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' }
%span{ 'aria-hidden' => 'true' } &times;
.modal-body
%p
= s_('DeployTokens|You are about to revoke')
%b #{token.name}.
= s_('DeployTokens|This action cannot be undone.')
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel')
= link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_project_deploy_token_path(project, token), method: :put, class: 'btn btn-danger'
%h5= s_("DeployTokens|Active Deploy Tokens (%{active_tokens})") % { active_tokens: active_tokens.length }
- if active_tokens.present?
.table-responsive.deploy-tokens
%table.table
%thead
%tr
%th= s_('DeployTokens|Name')
%th= s_('DeployTokens|Username')
%th= s_('DeployTokens|Created')
%th= s_('DeployTokens|Expires')
%th= s_('DeployTokens|Scopes')
%th
%tbody
- active_tokens.each do |token|
%tr
%td= token.name
%td= token.username
%td= token.created_at.to_date.to_s(:medium)
%td
- if token.expires?
%span{ class: ('text-warning' if token.expires_soon?) }
In #{distance_of_time_in_words_to_now(token.expires_at)}
- else
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger pull-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
= render 'projects/deploy_tokens/revoke_modal', token: token, project: project
- else
.settings-message.text-center
= s_('DeployTokens|This project has no active Deploy Tokens.')
......@@ -28,6 +28,10 @@
%pre
docker login #{Gitlab.config.registry.host_port}
%br
%p
- deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
%pre
......
......@@ -9,3 +9,4 @@
= render "projects/protected_branches/index"
= render "projects/protected_tags/index"
= render @deploy_keys
= render "projects/deploy_tokens/index"
---
title: Create Deploy Tokens to allow permanent access to repository and registry
merge_request: 17894
author:
type: added
......@@ -88,6 +88,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :deploy_tokens, constraints: { id: /\d+/ }, only: [] do
member do
put :revoke
end
end
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
......@@ -426,7 +432,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :reset_cache
end
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository
resource :repository, only: [:show], controller: :repository do
post :create_deploy_token, path: 'deploy_token/create'
end
end
# Since both wiki and repository routing contains wildcard characters
......
class CreateDeployTokens < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :deploy_tokens do |t|
t.boolean :revoked, default: false
t.boolean :read_repository, null: false, default: false
t.boolean :read_registry, null: false, default: false
t.datetime_with_timezone :expires_at, null: false
t.datetime_with_timezone :created_at, null: false
t.string :name, null: false
t.string :token, index: { unique: true }, null: false
t.index [:token, :expires_at, :id], where: "(revoked IS FALSE)"
end
end
end
class CreateProjectDeployTokens < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :project_deploy_tokens do |t|
t.integer :project_id, null: false
t.integer :deploy_token_id, null: false
t.datetime_with_timezone :created_at, null: false
t.foreign_key :deploy_tokens, column: :deploy_token_id, on_delete: :cascade
t.foreign_key :projects, column: :project_id, on_delete: :cascade
t.index [:project_id, :deploy_token_id], unique: true
end
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180405101928) do
ActiveRecord::Schema.define(version: 20180405142733) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -683,6 +683,19 @@ ActiveRecord::Schema.define(version: 20180405101928) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
create_table "deploy_tokens", force: :cascade do |t|
t.boolean "revoked", default: false
t.boolean "read_repository", default: false, null: false
t.boolean "read_registry", default: false, null: false
t.datetime_with_timezone "expires_at", null: false
t.datetime_with_timezone "created_at", null: false
t.string "name", null: false
t.string "token", null: false
end
add_index "deploy_tokens", ["token", "expires_at", "id"], name: "index_deploy_tokens_on_token_and_expires_at_and_id", where: "(revoked IS FALSE)", using: :btree
add_index "deploy_tokens", ["token"], name: "index_deploy_tokens_on_token", unique: true, using: :btree
create_table "deployments", force: :cascade do |t|
t.integer "iid", null: false
t.integer "project_id", null: false
......@@ -1430,6 +1443,14 @@ ActiveRecord::Schema.define(version: 20180405101928) do
add_index "project_custom_attributes", ["key", "value"], name: "index_project_custom_attributes_on_key_and_value", using: :btree
add_index "project_custom_attributes", ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree
create_table "project_deploy_tokens", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "deploy_token_id", null: false
t.datetime_with_timezone "created_at", null: false
end
add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
......@@ -2137,6 +2158,8 @@ ActiveRecord::Schema.define(version: 20180405101928) do
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
......
......@@ -115,15 +115,16 @@ and [Using the GitLab Container Registry documentation](../../ci/docker/using_do
## Using with private projects
> [Introduced][ce-11845] in GitLab 9.3.
> Personal Access tokens were [introduced][ce-11845] in GitLab 9.3.
> Project Deploy Tokens were [introduced][ce-17894] in GitLab 10.7
If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using [personal access tokens][pat].
The minimal scope needed is `read_registry`.
The preferred way to do this, is either by using a [personal access tokens][pat] or a [project deploy token][pdt].
The minimal scope needed for both of them is `read_registry`.
Example of using a personal access token:
```
docker login registry.example.com -u <your_username> -p <your_personal_access_token>
docker login registry.example.com -u <your_username> -p <your_access_token>
```
## Troubleshooting the GitLab Container Registry
......@@ -270,5 +271,7 @@ Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
[ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
[pat]: ../profile/personal_access_tokens.md
[pdt]: ../project/deploy_tokens/index.md
# Deploy Tokens
> [Introduced][ce-17894] in GitLab 10.7.
Deploy tokens allow to download (through `git clone`), or read the container registry images of a project without the need of having a user and a password.
Please note, that the expiration of deploy tokens happens on the date you define,
at midnight UTC and that they can be only managed by [masters](https://docs.gitlab.com/ee/user/permissions.html).
## Creating a Deploy Token
You can create as many deploy tokens as you like from the settings of your project:
1. Log in to your GitLab account.
1. Go to the project you want to create Deploy Tokens for.
1. Go to **Settings** > **Repository**
1. Click on "Expand" on **Deploy Tokens** section
1. Choose a name and optionally an expiry date for the token.
1. Choose the [desired scopes](#limiting-scopes-of-a-deploy-token).
1. Click on **Create deploy token**.
1. Save the deploy token somewhere safe. Once you leave or refresh
the page, **you won't be able to access it again**.
![Personal access tokens page](img/deploy_tokens.png)
## Revoking a personal access token
At any time, you can revoke any deploy token by just clicking the
respective **Revoke** button under the 'Active deploy tokens' area.
## Limiting scopes of a deploy token
Deploy tokens can be created with two different scopes that allow various
actions that a given token can perform. The available scopes are depicted in
the following table.
| Scope | Description |
| ----- | ----------- |
| `read_repository` | Allows read-access to the repository through `git clone` |
| `read_registry` | Allows read-access to [container registry] images if a project is private and authorization is required. |
## Usage
### Git clone a repository
To download a repository using a Deploy Token, you just need to:
1. Create a Deploy Token with `read_repository` as a scope.
2. Take note of your `username` and `token`
3. `git clone` the project using the Deploy Token:
```bash
git clone http://<username>:<deploy_token>@gitlab.example.com/tanuki/awesome_project.git
```
Just replace `<username>` and `<deploy_token>` with the proper values
### Read container registry images
To read the container registry images, you'll need to:
1. Create a Deploy Token with `read_registry` as a scope.
2. Take note of your `username` and `token`
3. Log in to GitLab’s Container Registry using the deploy token:
```
docker login registry.example.com -u <username> -p <deploy_token>
```
Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply
pull images from your Container Registry.
[ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
[container registry]: ../container_registry.md
......@@ -27,6 +27,7 @@ integrated platform
- [Protected tags](protected_tags.md): Control over who has
permission to create tags, and prevent accidental update or deletion
- [Signing commits](gpg_signed_commits/index.md): use GPG to sign your commits
- [Deploy tokens](deploy_tokens/index.md): Manage project-based deploy tokens that allow permanent access to the repository and Container Registry.
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**Starter/Premium**): Ask for approval before
......
class Forever
POSTGRESQL_DATE = DateTime.new(3000, 1, 1)
MYSQL_DATE = DateTime.new(2038, 01, 19)
# MySQL timestamp has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC
def self.date
if Gitlab::Database.postgresql?
POSTGRESQL_DATE
else
MYSQL_DATE
end
end
end
......@@ -5,7 +5,7 @@ module Gitlab
REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
API_SCOPES = [:api, :read_user, :sudo].freeze
API_SCOPES = [:api, :read_user, :sudo, :read_repository].freeze
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
......@@ -26,6 +26,7 @@ module Gitlab
lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(password) ||
deploy_token_check(login, password) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
......@@ -163,7 +164,8 @@ module Gitlab
def abilities_for_scopes(scopes)
abilities_by_scope = {
api: full_authentication_abilities,
read_registry: [:read_container_image]
read_registry: [:read_container_image],
read_repository: [:download_code]
}
scopes.flat_map do |scope|
......@@ -171,6 +173,22 @@ module Gitlab
end.uniq
end
def deploy_token_check(login, password)
return unless password.present?
token =
DeployToken.active.find_by(token: password)
return unless token && login
return if login != token.username
scopes = abilities_for_scopes(token.scopes)
if valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes)
end
end
def lfs_token_check(login, password, project)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
......
......@@ -208,6 +208,7 @@ module Gitlab
def check_download_access!
passed = deploy_key? ||
deploy_token? ||
user_can_download_code? ||
build_can_download_code? ||
guest_can_download_code?
......@@ -274,6 +275,14 @@ module Gitlab
actor.is_a?(DeployKey)
end
def deploy_token
actor if deploy_token?
end
def deploy_token?
actor.is_a?(DeployToken)
end
def ci?
actor == :ci
end
......@@ -281,6 +290,8 @@ module Gitlab
def can_read_project?
if deploy_key?
deploy_key.has_access_to?(project)
elsif deploy_token?
deploy_token.has_access_to?(project)
elsif user
user.can?(:read_project, project)
elsif ci?
......
FactoryBot.define do
factory :deploy_token do
token { SecureRandom.hex(50) }
sequence(:name) { |n| "PDT #{n}" }
read_repository true
read_registry true
revoked false
expires_at { 5.days.from_now }
trait :revoked do
revoked true
end
end
end
FactoryBot.define do
factory :project_deploy_token do
project
deploy_token
end
end
......@@ -88,5 +88,32 @@ feature 'Repository settings' do
expect(page).not_to have_content(private_deploy_key.title)
end
end
context 'Deploy tokens' do
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
before do
stub_container_registry_config(enabled: true)
visit project_settings_repository_path(project)
end
scenario 'view deploy tokens' do
within('.deploy-tokens') do
expect(page).to have_content(deploy_token.name)
expect(page).to have_content('read_repository')
expect(page).to have_content('read_registry')
end
end
scenario 'add a new deploy token' do
fill_in 'deploy_token_name', with: 'new_deploy_key'
fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s
check 'deploy_token_read_repository'
check 'deploy_token_read_registry'
click_button 'Create deploy token'
expect(page).to have_content('Your new project deploy token has been created')
end
end
end
end
require 'spec_helper'
describe Forever do
describe '.date' do
subject { described_class.date }
context 'when using PostgreSQL' do
it 'should return Postgresql future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(subject).to eq(described_class::POSTGRESQL_DATE)
end
end
context 'when using MySQL' do
it 'should return MySQL future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(subject).to eq(described_class::MYSQL_DATE)
end
end
end
end
......@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
expect(subject::API_SCOPES).to eq %i[api read_user sudo]
expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repository]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
......@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid]
expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid]
end
context 'registry_scopes' do
......@@ -231,7 +231,7 @@ describe Gitlab::Auth do
.to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
it 'falls through oauth authentication when the username is oauth2' do
it 'fails through oauth authentication when the username is oauth2' do
user = create(
:user,
username: 'oauth2',
......@@ -255,6 +255,122 @@ describe Gitlab::Auth do
expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
context 'while using deploy tokens' do
let(:project) { create(:project) }
let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) }
context 'when the deploy token has read_repository as scope' do
let(:deploy_token) { create(:deploy_token, read_registry: false, projects: [project]) }
let(:login) { deploy_token.username }
it 'succeeds when login and token are valid' do
auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login)
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
.to eq(auth_success)
end
it 'fails when login is not valid' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login')
expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails when token is not valid' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails if token is nil' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, nil, project: project, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails if token is not related to project' do
another_deploy_token = create(:deploy_token)
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, another_deploy_token.token, project: project, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails if token has been revoked' do
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token')
expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip'))
.to eq(auth_failure)
end
end
context 'when the deploy token has read_registry as a scope' do
let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) }
let(:login) { deploy_token.username }
context 'when registry enabled' do
before do
stub_container_registry_config(enabled: true)
end
it 'succeeds when login and token are valid' do
auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: login)
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
.to eq(auth_success)
end
it 'fails when login is not valid' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login')
expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails when token is not valid' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails if token is nil' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, nil, project: nil, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails if token is not related to project' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, ip: 'ip'))
.to eq(auth_failure)
end
it 'fails if token has been revoked' do
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'deploy-token')
expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip'))
.to eq(auth_failure)
end
end
context 'when registry disabled' do
before do
stub_container_registry_config(enabled: false)
end
it 'fails when login and token are valid' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
.to eq(auth_failure)
end
end
end
end
end
describe 'find_with_user_password' do
......
......@@ -145,6 +145,33 @@ describe Gitlab::GitAccess do
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
end
end
context 'when actor is DeployToken' do
let(:actor) { create(:deploy_token, projects: [project]) }
context 'when DeployToken is active and belongs to project' do
it 'allows pull access' do
expect { pull_access_check }.not_to raise_error
end
it 'blocks the push' do
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
end
end
context 'when DeployToken does not belong to project' do
let(:another_project) { create(:project) }
let(:actor) { create(:deploy_token, projects: [another_project]) }
it 'blocks pull access' do
expect { pull_access_check }.to raise_not_found
end
it 'blocks the push' do
expect { push_access_check }.to raise_not_found
end
end
end
end
context 'when actor is nil' do
......@@ -594,6 +621,41 @@ describe Gitlab::GitAccess do
end
end
describe 'deploy token permissions' do
let(:deploy_token) { create(:deploy_token) }
let(:actor) { deploy_token }
context 'pull code' do
context 'when project is authorized' do
before do
deploy_token.projects << project
end
it { expect { pull_access_check }.not_to raise_error }
end
context 'when unauthorized' do
context 'from public project' do
let(:project) { create(:project, :public, :repository) }
it { expect { pull_access_check }.not_to raise_error }
end
context 'from internal project' do
let(:project) { create(:project, :internal, :repository) }
it { expect { pull_access_check }.to raise_not_found }
end
context 'from private project' do
let(:project) { create(:project, :private, :repository) }
it { expect { pull_access_check }.to raise_not_found }
end
end
end
end
describe 'build authentication_abilities permissions' do
let(:authentication_abilities) { build_authentication_abilities }
......
......@@ -145,6 +145,9 @@ pipeline_schedule:
- pipelines
pipeline_schedule_variables:
- pipeline_schedule
deploy_tokens:
- project_deploy_tokens
- projects
deploy_keys:
- user
- deploy_keys_projects
......@@ -281,6 +284,8 @@ project:
- project_badges
- source_of_merge_requests
- internal_ids
- project_deploy_tokens
- deploy_tokens
award_emoji:
- awardable
- user
......
require 'spec_helper'
describe DeployToken do
subject(:deploy_token) { create(:deploy_token) }
it { is_expected.to have_many :project_deploy_tokens }
it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
describe '#ensure_token' do
it 'should ensure a token' do
deploy_token.token = nil
deploy_token.save
expect(deploy_token.token).not_to be_empty
end
end
describe '#ensure_at_least_one_scope' do
context 'with at least one scope' do
it 'should be valid' do
is_expected.to be_valid
end
end
context 'with no scopes' do
it 'should be invalid' do
deploy_token = build(:deploy_token, read_repository: false, read_registry: false)
expect(deploy_token).not_to be_valid
expect(deploy_token.errors[:base].first).to eq("Scopes can't be blank")
end
end
end
describe '#scopes' do
context 'with all the scopes' do
it 'should return scopes assigned to DeployToken' do
expect(deploy_token.scopes).to eq([:read_repository, :read_registry])
end
end
context 'with only one scope' do
it 'should return scopes assigned to DeployToken' do
deploy_token = create(:deploy_token, read_registry: false)
expect(deploy_token.scopes).to eq([:read_repository])
end
end
end
describe '#revoke!' do
it 'should update revoke attribute' do
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
end
end
describe "#active?" do
context "when it has been revoked" do
it 'should return false' do
deploy_token.revoke!
expect(deploy_token.active?).to be_falsy
end
end
context "when it hasn't been revoked" do
it 'should return true' do
expect(deploy_token.active?).to be_truthy
end
end
end
describe '#username' do
it 'returns a harcoded username' do
expect(deploy_token.username).to eq("gitlab+deploy-token-#{deploy_token.id}")
end
end
describe '#has_access_to?' do
let(:project) { create(:project) }
subject(:deploy_token) { create(:deploy_token, projects: [project]) }
context 'when the deploy token has access to the project' do
it 'should return true' do
expect(deploy_token.has_access_to?(project)).to be_truthy
end
end
context 'when the deploy token does not have access to the project' do
it 'should return false' do
another_project = create(:project)
expect(deploy_token.has_access_to?(another_project)).to be_falsy
end
end
end
describe '#expires_at' do
context 'when using Forever.date' do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
it 'should return nil' do
expect(deploy_token.expires_at).to be_nil
end
end
context 'when using a personalized date' do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
it 'should return the personalized date' do
expect(deploy_token.expires_at).to eq(expires_at)
end
end
end
describe '#expires_at=' do
context 'when passing nil' do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
it 'should assign Forever.date' do
expect(deploy_token.read_attribute(:expires_at)).to eq(Forever.date)
end
end
context 'when passign a value' do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
it 'should respect the value' do
expect(deploy_token.read_attribute(:expires_at)).to eq(expires_at)
end
end
end
end
require 'rails_helper'
RSpec.describe ProjectDeployToken, type: :model do
let(:project) { create(:project) }
let(:deploy_token) { create(:deploy_token) }
subject(:project_deploy_token) { create(:project_deploy_token, project: project, deploy_token: deploy_token) }
it { is_expected.to belong_to :project }
it { is_expected.to belong_to :deploy_token }
it { is_expected.to validate_presence_of :deploy_token }
it { is_expected.to validate_presence_of :project }
it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:project_id) }
end
......@@ -84,6 +84,8 @@ describe Project do
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
it { is_expected.to have_many(:lfs_file_locks) }
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
context 'after initialized' do
it "has a project_feature" do
......
require 'spec_helper'
describe DeployTokenPolicy do
let(:current_user) { create(:user) }
let(:project) { create(:project) }
let(:deploy_token) { create(:deploy_token, projects: [project]) }
subject { described_class.new(current_user, deploy_token) }
describe 'creating a deploy key' do
context 'when user is master' do
before do
project.add_master(current_user)
end
it { is_expected.to be_allowed(:create_deploy_token) }
end
context 'when user is not master' do
before do
project.add_developer(current_user)
end
it { is_expected.to be_disallowed(:create_deploy_token) }
end
end
describe 'updating a deploy key' do
context 'when user is master' do
before do
project.add_master(current_user)
end
it { is_expected.to be_allowed(:update_deploy_token) }
end
context 'when user is not master' do
before do
project.add_developer(current_user)
end
it { is_expected.to be_disallowed(:update_deploy_token) }
end
end
end
require 'spec_helper'
describe DeployTokens::CreateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:deploy_token_params) { attributes_for(:deploy_token) }
describe '#execute' do
subject { described_class.new(project, user, deploy_token_params).execute }
context 'when the deploy token is valid' do
it 'should create a new DeployToken' do
expect { subject }.to change { DeployToken.count }.by(1)
end
it 'should create a new ProjectDeployToken' do
expect { subject }.to change { ProjectDeployToken.count }.by(1)
end
it 'returns a DeployToken' do
expect(subject).to be_an_instance_of DeployToken
end
end
context 'when expires at date is not passed' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
it 'should set Forever.date' do
expect(subject.read_attribute(:expires_at)).to eq(Forever.date)
end
end
context 'when the deploy token is invalid' do
let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) }
it 'should not create a new DeployToken' do
expect { subject }.not_to change { DeployToken.count }
end
it 'should not create a new ProjectDeployToken' do
expect { subject }.not_to change { ProjectDeployToken.count }
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