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
This commit is contained in:
commit
dd552d06f6
47 changed files with 1010 additions and 38 deletions
|
@ -0,0 +1,3 @@
|
|||
import initForm from '../form';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initForm);
|
|
@ -0,0 +1,19 @@
|
|||
/* 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();
|
||||
};
|
|
@ -1,17 +1,3 @@
|
|||
/* 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
|
||||
|
|
10
app/controllers/projects/deploy_tokens_controller.rb
Normal file
10
app/controllers/projects/deploy_tokens_controller.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
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
|
||||
|
|
12
app/helpers/deploy_tokens_helper.rb
Normal file
12
app/helpers/deploy_tokens_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
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
|
61
app/models/deploy_token.rb
Normal file
61
app/models/deploy_token.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
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'
|
||||
|
||||
|
|
8
app/models/project_deploy_token.rb
Normal file
8
app/models/project_deploy_token.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
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
|
11
app/policies/deploy_token_policy.rb
Normal file
11
app/policies/deploy_token_policy.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
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: '')
|
||||
|
|
7
app/services/deploy_tokens/create_service.rb
Normal file
7
app/services/deploy_tokens/create_service.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
|
|
29
app/views/projects/deploy_tokens/_form.html.haml
Normal file
29
app/views/projects/deploy_tokens/_form.html.haml
Normal file
|
@ -0,0 +1,29 @@
|
|||
%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'
|
18
app/views/projects/deploy_tokens/_index.html.haml
Normal file
18
app/views/projects/deploy_tokens/_index.html.haml
Normal file
|
@ -0,0 +1,18 @@
|
|||
- 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
|
14
app/views/projects/deploy_tokens/_new_deploy_token.html.haml
Normal file
14
app/views/projects/deploy_tokens/_new_deploy_token.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
|||
.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
|
17
app/views/projects/deploy_tokens/_revoke_modal.html.haml
Normal file
17
app/views/projects/deploy_tokens/_revoke_modal.html.haml
Normal file
|
@ -0,0 +1,17 @@
|
|||
.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' } ×
|
||||
.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'
|
31
app/views/projects/deploy_tokens/_table.html.haml
Normal file
31
app/views/projects/deploy_tokens/_table.html.haml
Normal file
|
@ -0,0 +1,31 @@
|
|||
%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’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"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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
|
||||
|
|
19
db/migrate/20180319190020_create_deploy_tokens.rb
Normal file
19
db/migrate/20180319190020_create_deploy_tokens.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
|
16
db/migrate/20180405142733_create_project_deploy_tokens.rb
Normal file
16
db/migrate/20180405142733_create_project_deploy_tokens.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
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
|
25
db/schema.rb
25
db/schema.rb
|
@ -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
|
||||
|
|
BIN
doc/user/project/deploy_tokens/img/deploy_tokens.png
Normal file
BIN
doc/user/project/deploy_tokens/img/deploy_tokens.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
76
doc/user/project/deploy_tokens/index.md
Normal file
76
doc/user/project/deploy_tokens/index.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# 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
|
||||
|
|
13
lib/forever.rb
Normal file
13
lib/forever.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
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?
|
||||
|
|
14
spec/factories/deploy_tokens.rb
Normal file
14
spec/factories/deploy_tokens.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
6
spec/factories/project_deploy_tokens.rb
Normal file
6
spec/factories/project_deploy_tokens.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
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
|
||||
|
|
21
spec/lib/forever_spec.rb
Normal file
21
spec/lib/forever_spec.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
|
|
134
spec/models/deploy_token_spec.rb
Normal file
134
spec/models/deploy_token_spec.rb
Normal file
|
@ -0,0 +1,134 @@
|
|||
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
|
14
spec/models/project_deploy_token_spec.rb
Normal file
14
spec/models/project_deploy_token_spec.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
|
|
45
spec/policies/deploy_token_policy_spec.rb
Normal file
45
spec/policies/deploy_token_policy_spec.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
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
|
45
spec/services/deploy_tokens/create_service_spec.rb
Normal file
45
spec/services/deploy_tokens/create_service_spec.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
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
|
Loading…
Reference in a new issue