summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2018-04-07 09:17:59 +0000
committerKamil Trzciński <ayufan@ayufan.eu>2018-04-07 09:17:59 +0000
commitdd552d06f6e39d5e6138a33bd7c1bffb2d3dbb1d (patch)
treee2cf4b8714c7dbb2bfc08c34e21b14cd5f9fa4e5
parent671e93dc38365f3b05cb3cfe719e64713196be31 (diff)
parentb38439a3ae3c7ea1675b7037e4882213bdc58fdf (diff)
downloadgitlab-ce-dd552d06f6e39d5e6138a33bd7c1bffb2d3dbb1d.tar.gz
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
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js19
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js18
-rw-r--r--app/assets/stylesheets/pages/settings.scss20
-rw-r--r--app/controllers/jwt_controller.rb3
-rw-r--r--app/controllers/projects/deploy_tokens_controller.rb10
-rw-r--r--app/controllers/projects/settings/repository_controller.rb30
-rw-r--r--app/helpers/deploy_tokens_helper.rb12
-rw-r--r--app/models/deploy_token.rb61
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_deploy_token.rb8
-rw-r--r--app/policies/deploy_token_policy.rb11
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb21
-rw-r--r--app/services/deploy_tokens/create_service.rb7
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml1
-rw-r--r--app/views/projects/deploy_tokens/_form.html.haml29
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml18
-rw-r--r--app/views/projects/deploy_tokens/_new_deploy_token.html.haml14
-rw-r--r--app/views/projects/deploy_tokens/_revoke_modal.html.haml17
-rw-r--r--app/views/projects/deploy_tokens/_table.html.haml31
-rw-r--r--app/views/projects/registry/repositories/index.html.haml4
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml5
-rw-r--r--config/routes/project.rb10
-rw-r--r--db/migrate/20180319190020_create_deploy_tokens.rb19
-rw-r--r--db/migrate/20180405142733_create_project_deploy_tokens.rb16
-rw-r--r--db/schema.rb25
-rw-r--r--doc/user/project/container_registry.md11
-rw-r--r--doc/user/project/deploy_tokens/img/deploy_tokens.pngbin0 -> 75650 bytes
-rw-r--r--doc/user/project/deploy_tokens/index.md76
-rw-r--r--doc/user/project/index.md1
-rw-r--r--lib/forever.rb13
-rw-r--r--lib/gitlab/auth.rb22
-rw-r--r--lib/gitlab/git_access.rb11
-rw-r--r--spec/factories/deploy_tokens.rb14
-rw-r--r--spec/factories/project_deploy_tokens.rb6
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb27
-rw-r--r--spec/lib/forever_spec.rb21
-rw-r--r--spec/lib/gitlab/auth_spec.rb122
-rw-r--r--spec/lib/gitlab/git_access_spec.rb62
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/models/deploy_token_spec.rb134
-rw-r--r--spec/models/project_deploy_token_spec.rb14
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/policies/deploy_token_policy_spec.rb45
-rw-r--r--spec/services/deploy_tokens/create_service_spec.rb45
47 files changed, 1010 insertions, 38 deletions
diff --git a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
new file mode 100644
index 00000000000..ffc84dc106b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
@@ -0,0 +1,3 @@
+import initForm from '../form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
new file mode 100644
index 00000000000..a5c17ab322c
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -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();
+};
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 788d86d1192..ffc84dc106b 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -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);
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a6ca8ed5016..c410049bc0b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -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;
+ }
+}
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7d6fe6a0232..67057b5b126 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -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
diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb
new file mode 100644
index 00000000000..2f91b8f36de
--- /dev/null
+++ b/app/controllers/projects/deploy_tokens_controller.rb
@@ -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
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index dd9e4a2af3e..f17056f13e0 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -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
diff --git a/app/helpers/deploy_tokens_helper.rb b/app/helpers/deploy_tokens_helper.rb
new file mode 100644
index 00000000000..bd921322476
--- /dev/null
+++ b/app/helpers/deploy_tokens_helper.rb
@@ -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
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
new file mode 100644
index 00000000000..fe726b156d4
--- /dev/null
+++ b/app/models/deploy_token.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index 96907f3b23d..3f805dd1fc9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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'
diff --git a/app/models/project_deploy_token.rb b/app/models/project_deploy_token.rb
new file mode 100644
index 00000000000..ab4482f0c0b
--- /dev/null
+++ b/app/models/project_deploy_token.rb
@@ -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
diff --git a/app/policies/deploy_token_policy.rb b/app/policies/deploy_token_policy.rb
new file mode 100644
index 00000000000..7aa9106e8b1
--- /dev/null
+++ b/app/policies/deploy_token_policy.rb
@@ -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
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index b1ed034cd00..21bb0934dee 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -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
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 2b77f6be72a..8f050072f74 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -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: '')
diff --git a/app/services/deploy_tokens/create_service.rb b/app/services/deploy_tokens/create_service.rb
new file mode 100644
index 00000000000..52f545947af
--- /dev/null
+++ b/app/services/deploy_tokens/create_service.rb
@@ -0,0 +1,7 @@
+module DeployTokens
+ class CreateService < BaseService
+ def execute
+ @project.deploy_tokens.create(params)
+ end
+ end
+end
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index b96251cd982..9b87a7aaca8 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -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
diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/projects/deploy_tokens/_form.html.haml
new file mode 100644
index 00000000000..f8db30df7b4
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_form.html.haml
@@ -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'
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
new file mode 100644
index 00000000000..50e5950ced4
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_index.html.haml
@@ -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
diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
new file mode 100644
index 00000000000..1e715681e59
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
@@ -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
diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
new file mode 100644
index 00000000000..085964fe22e
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
@@ -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' } &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'
diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/projects/deploy_tokens/_table.html.haml
new file mode 100644
index 00000000000..5013a9b250d
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_table.html.haml
@@ -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.')
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 12d56e244ce..2c80f7c3fa3 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -29,6 +29,10 @@
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
:plain
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 6bef4d19434..f57590a908f 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -9,3 +9,4 @@
= render "projects/protected_branches/index"
= render "projects/protected_tags/index"
= render @deploy_keys
+= render "projects/deploy_tokens/index"
diff --git a/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
new file mode 100644
index 00000000000..5546d26d0fb
--- /dev/null
+++ b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
@@ -0,0 +1,5 @@
+---
+title: Create Deploy Tokens to allow permanent access to repository and registry
+merge_request: 17894
+author:
+type: added
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 618c7897060..e760a9d7ed2 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -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
diff --git a/db/migrate/20180319190020_create_deploy_tokens.rb b/db/migrate/20180319190020_create_deploy_tokens.rb
new file mode 100644
index 00000000000..d129459ea0a
--- /dev/null
+++ b/db/migrate/20180319190020_create_deploy_tokens.rb
@@ -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
diff --git a/db/migrate/20180405142733_create_project_deploy_tokens.rb b/db/migrate/20180405142733_create_project_deploy_tokens.rb
new file mode 100644
index 00000000000..9d8f89243a8
--- /dev/null
+++ b/db/migrate/20180405142733_create_project_deploy_tokens.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 2cd51b200b3..fd75b176318 100644
--- a/db/schema.rb
+++ b/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
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 394aa9209e4..9c5e3509046 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -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
diff --git a/doc/user/project/deploy_tokens/img/deploy_tokens.png b/doc/user/project/deploy_tokens/img/deploy_tokens.png
new file mode 100644
index 00000000000..7e2d67a3120
--- /dev/null
+++ b/doc/user/project/deploy_tokens/img/deploy_tokens.png
Binary files differ
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
new file mode 100644
index 00000000000..86fc58020e8
--- /dev/null
+++ b/doc/user/project/deploy_tokens/index.md
@@ -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
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index f94e93dd7d8..ba6651624f9 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.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
diff --git a/lib/forever.rb b/lib/forever.rb
new file mode 100644
index 00000000000..7df17912544
--- /dev/null
+++ b/lib/forever.rb
@@ -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
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 6af763faf10..2a44e11efb6 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -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/)
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 01f8b22b2b6..0d1ee73ca1a 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -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?
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
new file mode 100644
index 00000000000..5fea4a9d5a6
--- /dev/null
+++ b/spec/factories/deploy_tokens.rb
@@ -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
diff --git a/spec/factories/project_deploy_tokens.rb b/spec/factories/project_deploy_tokens.rb
new file mode 100644
index 00000000000..4866cb58d88
--- /dev/null
+++ b/spec/factories/project_deploy_tokens.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :project_deploy_token do
+ project
+ deploy_token
+ end
+end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 14670e91006..f2c371b7df5 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -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
diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb
new file mode 100644
index 00000000000..cf40c467c72
--- /dev/null
+++ b/spec/lib/forever_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 18cef8ec996..9ccd0b206cc 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c870997e274..6c625596605 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -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 }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index b675d5dc031..897a5984782 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -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
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
new file mode 100644
index 00000000000..5a15c23def4
--- /dev/null
+++ b/spec/models/deploy_token_spec.rb
@@ -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
diff --git a/spec/models/project_deploy_token_spec.rb b/spec/models/project_deploy_token_spec.rb
new file mode 100644
index 00000000000..9e2e40c2e8f
--- /dev/null
+++ b/spec/models/project_deploy_token_spec.rb
@@ -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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 7007f78e702..2675c2f52c1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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
diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb
new file mode 100644
index 00000000000..eea287d895e
--- /dev/null
+++ b/spec/policies/deploy_token_policy_spec.rb
@@ -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
diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb
new file mode 100644
index 00000000000..3a2bbf1ecd1
--- /dev/null
+++ b/spec/services/deploy_tokens/create_service_spec.rb
@@ -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