Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js2
-rw-r--r--app/assets/stylesheets/pages/settings.scss19
-rw-r--r--app/controllers/projects/deploy_tokens_controller.rb34
-rw-r--r--app/controllers/projects/settings/repository_controller.rb10
-rw-r--r--app/models/deploy_token.rb25
-rw-r--r--app/models/project.rb1
-rw-r--r--app/presenters/projects/settings/deploy_tokens_presenter.rb48
-rw-r--r--app/services/deploy_tokens/create_service.rb24
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml1
-rw-r--r--app/views/projects/deploy_tokens/_form.html.haml21
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml19
-rw-r--r--app/views/projects/deploy_tokens/_new_deploy_token.html.haml9
-rw-r--r--app/views/projects/deploy_tokens/_revoke_modal.html.haml15
-rw-r--r--app/views/projects/deploy_tokens/_scope_form.html.haml4
-rw-r--r--app/views/projects/deploy_tokens/_table.html.haml29
-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.rb6
-rw-r--r--db/migrate/20180319190020_create_deploy_tokens.rb16
-rw-r--r--db/schema.rb15
-rw-r--r--spec/controllers/projects/deploy_tokens_controller_spec.rb55
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb26
-rw-r--r--spec/factories/deploy_tokens.rb22
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/deploy_token_spec.rb38
-rw-r--r--spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb46
-rw-r--r--spec/services/deploy_tokens/create_service_spec.rb45
27 files changed, 536 insertions, 1 deletions
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..5bcdfc26a4b 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -6,6 +6,7 @@ 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';
document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
@@ -14,4 +15,5 @@ document.addEventListener('DOMContentLoaded', () => {
initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
+ new DueDateSelectors();
});
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a6ca8ed5016..9db6386b86a 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -284,3 +284,22 @@
.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;
+ }
+
+ .help-block {
+ margin-top: 4px;
+ }
+}
diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb
new file mode 100644
index 00000000000..ecc6db50f2f
--- /dev/null
+++ b/app/controllers/projects/deploy_tokens_controller.rb
@@ -0,0 +1,34 @@
+class Projects::DeployTokensController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+
+ def create
+ @token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
+ token_params = {}
+
+ if @token.valid?
+ flash[:notice] = 'Your new project deploy token has been created.'
+ else
+ token_params = @token.attributes.slice("name", "scopes", "expires_at")
+ flash[:alert] = @token.errors.full_messages.join(', ').html_safe
+ end
+
+ redirect_to project_settings_repository_path(project, deploy_token: token_params)
+ end
+
+ def revoke
+ @token = @project.deploy_tokens.find(params[:id])
+ @token.revoke!
+
+ redirect_to project_settings_repository_path(project)
+ end
+
+ private
+
+ def deploy_token_params
+ params.require(:deploy_token).permit(:name, :expires_at, scopes: [])
+ end
+
+ def authorize_admin_project!
+ return render_404 unless can?(current_user, :admin_project, @project)
+ end
+end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index dd9e4a2af3e..28897cc5946 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -5,7 +5,9 @@ module Projects
def show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
+ @deploy_tokens = DeployTokensPresenter.new(@project.deploy_tokens.active, current_user: current_user, project: project)
+ define_deploy_token
define_protected_refs
end
@@ -51,6 +53,14 @@ module Projects
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
end
+
+ def define_deploy_token
+ @deploy_token = @project.deploy_tokens.build(deploy_token_attributes)
+ end
+
+ def deploy_token_attributes
+ params.fetch(:deploy_token, {}).permit(:name, :expires_at, scopes: [])
+ end
end
end
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
new file mode 100644
index 00000000000..185bd806b18
--- /dev/null
+++ b/app/models/deploy_token.rb
@@ -0,0 +1,25 @@
+class DeployToken < ActiveRecord::Base
+ include Expirable
+ include TokenAuthenticatable
+ add_authentication_token_field :token
+
+ AVAILABLE_SCOPES = %w(read_repo read_registry).freeze
+
+ serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
+
+ validates :scopes, presence: true
+
+ belongs_to :project
+
+ before_save :ensure_token
+
+ scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
+
+ def revoke!
+ update!(revoked: true)
+ end
+
+ def self.redis_shared_state_key(user_id)
+ "gitlab:personal_access_token:#{user_id}"
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 96907f3b23d..3cfb163abf4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -222,6 +222,7 @@ class Project < ActiveRecord::Base
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
+ has_many :deploy_tokens
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
diff --git a/app/presenters/projects/settings/deploy_tokens_presenter.rb b/app/presenters/projects/settings/deploy_tokens_presenter.rb
new file mode 100644
index 00000000000..cbad35431b3
--- /dev/null
+++ b/app/presenters/projects/settings/deploy_tokens_presenter.rb
@@ -0,0 +1,48 @@
+module Projects
+ module Settings
+ class DeployTokensPresenter < Gitlab::View::Presenter::Simple
+ include Enumerable
+
+ presents :deploy_tokens
+
+ def available_scopes
+ DeployToken::AVAILABLE_SCOPES
+ end
+
+ def length
+ deploy_tokens.length
+ end
+
+ def scope_description(scope)
+ scope_descriptions[scope]
+ end
+
+ def each
+ deploy_tokens.each do |deploy_token|
+ yield deploy_token
+ end
+ end
+
+ def new_deploy_token
+ @new_deploy_token ||= Gitlab::Redis::SharedState.with do |redis|
+ token = redis.get(deploy_token_key)
+ redis.del(deploy_token_key)
+ token
+ end
+ end
+
+ private
+
+ def scope_descriptions
+ {
+ 'read_repo' => 'Allows read-only access to the repository',
+ 'read_registry' => 'Allows read-only access to the registry images'
+ }
+ end
+
+ def deploy_token_key
+ DeployToken.redis_shared_state_key(current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/services/deploy_tokens/create_service.rb b/app/services/deploy_tokens/create_service.rb
new file mode 100644
index 00000000000..e93c021557e
--- /dev/null
+++ b/app/services/deploy_tokens/create_service.rb
@@ -0,0 +1,24 @@
+module DeployTokens
+ class CreateService < BaseService
+ REDIS_EXPIRY_TIME = 3.minutes
+
+ def execute
+ @deploy_token = @project.deploy_tokens.create(params)
+ store_in_redis if @deploy_token.persisted?
+
+ @deploy_token
+ end
+
+ private
+
+ def store_in_redis
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(deploy_token_key, @deploy_token.token, ex: REDIS_EXPIRY_TIME)
+ end
+ end
+
+ def deploy_token_key
+ DeployToken.redis_shared_state_key(current_user.id)
+ 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..6f1c892bce0
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_form.html.haml
@@ -0,0 +1,21 @@
+%p.profile-settings-content
+ Pick a name for the application, and we'll give you a unique deploy token.
+
+= form_for token, url: project_deploy_tokens_path(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"
+
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ - presenter.available_scopes.each do |scope|
+ = render 'projects/deploy_tokens/scope_form', token: token, scope: scope, presenter: presenter
+
+ .prepend-top-default
+ = f.submit "Create deploy token", class: "btn btn-create"
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..7d2bfe291c3
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_index.html.haml
@@ -0,0 +1,19 @@
+- expanded = Rails.env.test?
+
+%section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ Deploy Tokens
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Deploy tokens allow read-only access to your repository and registry images.
+ .settings-content
+ - if @deploy_tokens.new_deploy_token
+ = render 'projects/deploy_tokens/new_deploy_token', new_token: @deploy_tokens.new_deploy_token
+
+ %h5.prepend-top-0
+ Add a deploy token
+ = render 'projects/deploy_tokens/form', project: @project, token: @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..8f5e669c815
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
@@ -0,0 +1,9 @@
+.created-deploy-token-container
+ %h5.prepend-top-0
+ Your New Deploy Token
+ .form-group
+ = text_field_tag 'deploy-token', new_token, readonly: true, class: "deploy-token-field form-control js-select-on-focus", 'aria-describedby' => "deploy-token-help-block"
+ = clipboard_button(text: new_token, title: "Copy deploy token to clipboard", placement: "left")
+ %span.deploy-token.help-block.text-danger 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..a859aac015d
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
@@ -0,0 +1,15 @@
+.modal{ id: "revoke-modal-#{token.id}" }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h4.modal-title.pull-left
+ Revoke
+ %b #{token.name}?
+ %button.close.pull-right{ "aria-label" => "Close", data: { dismiss: "modal"} }
+ %span{ "aria-hidden" => "true" } ×
+ .modal-body
+ %p
+ Are you sure you want to revoke this Deploy Token? This action cannot be undone
+ .modal-footer
+ %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
+ = link_to "Revoke #{token.name}", revoke_project_deploy_token_path(project, token), method: :put, class: 'btn btn-danger'
diff --git a/app/views/projects/deploy_tokens/_scope_form.html.haml b/app/views/projects/deploy_tokens/_scope_form.html.haml
new file mode 100644
index 00000000000..f67701c8ee1
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_scope_form.html.haml
@@ -0,0 +1,4 @@
+%fieldset
+ = check_box_tag "deploy_token[scopes][]", scope, token.scopes.include?(scope), id: "deploy_token_scopes_#{scope}"
+ = label_tag ("deploy_token_scopes_#{scope}"), scope
+ %span= presenter.scope_description(scope)
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..5ea9e86020f
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_table.html.haml
@@ -0,0 +1,29 @@
+%h5 Active Deploy Tokens (#{active_tokens.length})
+
+- if active_tokens.present?
+ .table-responsive.deploy-tokens
+ %table.table
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %th Expires
+ %th Scopes
+ %th
+ %tbody
+ - active_tokens.each do |token|
+ %tr
+ %td= token.name
+ %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 "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
+ This project has no active Deploy Tokens.
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..b9b19eb20ad
--- /dev/null
+++ b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
@@ -0,0 +1,5 @@
+---
+title: Creates Deploy Tokens to allow permanent access to repo and registry
+merge_request: 17894
+author:
+type: added
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 618c7897060..27d3569829f 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: :create do
+ member do
+ put :revoke
+ end
+ end
+
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
diff --git a/db/migrate/20180319190020_create_deploy_tokens.rb b/db/migrate/20180319190020_create_deploy_tokens.rb
new file mode 100644
index 00000000000..53808300fc1
--- /dev/null
+++ b/db/migrate/20180319190020_create_deploy_tokens.rb
@@ -0,0 +1,16 @@
+class CreateDeployTokens < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :deploy_tokens do |t|
+ t.references :project, index: true, foreign_key: true, null: false
+ t.string :name, null: false
+ t.string :token, index: { unique: true }, null: false
+ t.string :scopes
+ t.boolean :revoked, default: false
+ t.datetime :expires_at
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2cd51b200b3..333baa245b7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -683,6 +683,20 @@ 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.integer "project_id", null: false
+ t.string "name", null: false
+ t.string "token", null: false
+ t.string "scopes"
+ t.boolean "revoked", default: false
+ t.datetime "expires_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "deploy_tokens", ["project_id"], name: "index_deploy_tokens_on_project_id", 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
@@ -2072,6 +2086,7 @@ ActiveRecord::Schema.define(version: 20180405101928) do
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
+ add_foreign_key "deploy_tokens", "projects"
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
diff --git a/spec/controllers/projects/deploy_tokens_controller_spec.rb b/spec/controllers/projects/deploy_tokens_controller_spec.rb
new file mode 100644
index 00000000000..0ade61f4380
--- /dev/null
+++ b/spec/controllers/projects/deploy_tokens_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Projects::DeployTokensController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let!(:member) { project.add_master(user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'POST #create' do
+ let(:deploy_token_params) { attributes_for(:deploy_token) }
+ subject do
+ post :create,
+ namespace_id: project.namespace,
+ project_id: project,
+ deploy_token: deploy_token_params
+ end
+
+ context 'with valid params' do
+ it 'should create a new DeployToken' do
+ expect { subject }.to change(DeployToken, :count).by(1)
+ end
+
+ it 'should include a flash notice' do
+ subject
+ expect(flash[:notice]).to eq('Your new project deploy token has been created.')
+ end
+ end
+
+ context 'with invalid params' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) }
+
+ it 'should not create a new DeployToken' do
+ expect { subject }.not_to change(DeployToken, :count)
+ end
+
+ it 'should include a flash alert with the error message' do
+ subject
+ expect(flash[:alert]).to eq("Scopes can't be blank")
+ end
+ end
+
+ context 'when user does not have enough permissions' do
+ let!(:member) { project.add_developer(user) }
+
+ it 'responds with status 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 3a4014b7768..03867661483 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -16,5 +16,31 @@ describe Projects::Settings::RepositoryController do
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show)
end
+
+ context 'with no deploy token params' do
+ it 'should build an empty instance of DeployToken' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ deploy_token = assigns(:deploy_token)
+ expect(deploy_token).to be_an_instance_of(DeployToken)
+ expect(deploy_token.name).to be_nil
+ expect(deploy_token.expires_at).to be_nil
+ expect(deploy_token.scopes).to eq([])
+ end
+ end
+
+ context 'when rendering an invalid deploy token' do
+ let(:deploy_token_attributes) { attributes_for(:deploy_token, project_id: project.id) }
+
+ it 'should build an instance of DeployToken' do
+ get :show, namespace_id: project.namespace, project_id: project, deploy_token: deploy_token_attributes
+
+ deploy_token = assigns(:deploy_token)
+ expect(deploy_token).to be_an_instance_of(DeployToken)
+ expect(deploy_token.name).to eq(deploy_token_attributes[:name])
+ expect(deploy_token.expires_at.to_date).to eq(deploy_token_attributes[:expires_at].to_date)
+ expect(deploy_token.scopes).to match_array(deploy_token_attributes[:scopes])
+ end
+ end
end
end
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
new file mode 100644
index 00000000000..fa349e10ddc
--- /dev/null
+++ b/spec/factories/deploy_tokens.rb
@@ -0,0 +1,22 @@
+FactoryBot.define do
+ factory :deploy_token do
+ project
+ token { SecureRandom.hex(50) }
+ sequence(:name) { |n| "PDT #{n}" }
+ revoked false
+ expires_at { 5.days.from_now }
+ scopes %w(read_repo read_registry)
+
+ trait :revoked do
+ revoked true
+ end
+
+ trait :read_repo do
+ scopes ['read_repo']
+ end
+
+ trait :read_registry do
+ scopes ['read_registry']
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index b675d5dc031..d38e665436f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -281,6 +281,7 @@ project:
- project_badges
- source_of_merge_requests
- internal_ids
+- 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..bd27da63dfe
--- /dev/null
+++ b/spec/models/deploy_token_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe DeployToken do
+ it { is_expected.to belong_to :project }
+
+ describe 'validations' do
+ let(:project_deploy_token) { build(:deploy_token) }
+
+ context 'with no scopes defined' do
+ it 'should not be valid' do
+ project_deploy_token.scopes = []
+
+ expect(project_deploy_token).not_to be_valid
+ expect(project_deploy_token.errors[:scopes].first).to eq("can't be blank")
+ end
+ end
+ end
+
+ describe '#ensure_token' do
+ let(:project_deploy_token) { build(:deploy_token) }
+
+ it 'should ensure a token' do
+ project_deploy_token.token = nil
+ project_deploy_token.save
+
+ expect(project_deploy_token.token).not_to be_empty
+ end
+ end
+
+ describe '#revoke!' do
+ subject { create(:deploy_token) }
+
+ it 'should update revoke attribute' do
+ subject.revoke!
+ expect(subject.revoked?).to be_truthy
+ end
+ end
+end
diff --git a/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb
new file mode 100644
index 00000000000..d3210439b05
--- /dev/null
+++ b/spec/presenters/projects/settings/deploy_tokens_presenter_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Projects::Settings::DeployTokensPresenter do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:deploy_tokens) { create_list(:deploy_token, 3, project: project) }
+
+ subject(:presenter) { described_class.new(deploy_tokens, current_user: user, project: project) }
+
+ describe '#available_scopes' do
+ it 'returns the all the deploy token scopes' do
+ expect(presenter.available_scopes).to match_array(%w(read_repo read_registry))
+ end
+ end
+
+ describe '#scope_description' do
+ let(:deploy_token) { create(:deploy_token, project: project, scopes: [:read_registry]) }
+
+ it 'returns the description for a given scope' do
+ description = 'Allows read-only access to the registry images'
+ expect(presenter.scope_description('read_registry')).to eq(description)
+ end
+ end
+
+ describe '#length' do
+ it 'returns the size of deploy tokens presented' do
+ expect(presenter.length).to eq(3)
+ end
+ end
+
+ describe '#new_deploy_token' do
+ context 'when a deploy token has been created recently' do
+ it 'returns the token of the deploy' do
+ deploy_token = ::DeployTokens::CreateService.new(project, user, attributes_for(:deploy_token)).execute
+
+ expect(presenter.new_deploy_token).to eq(deploy_token.token)
+ end
+ end
+
+ context 'when a deploy token has not been created recently' do
+ it 'does returns nil' do
+ expect(presenter.new_deploy_token).to be_nil
+ end
+ 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..84aa17971d6
--- /dev/null
+++ b/spec/services/deploy_tokens/create_service_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe DeployTokens::CreateService, :clean_gitlab_redis_shared_state 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) }
+
+ context 'when the deploy token is valid' do
+ it 'should create a new DeployToken' do
+ expect { subject.execute }.to change { DeployToken.count }.by(1)
+ end
+
+ it 'should assign the DeployToken to the project' do
+ subject.execute
+
+ expect(subject.project).to eq(project)
+ end
+
+ it 'should store the token on redis' do
+ subject.execute
+ redis_key = DeployToken.redis_shared_state_key(user.id)
+
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).not_to be_nil
+ end
+ end
+
+ context 'when the deploy token is invalid' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, scopes: []) }
+
+ it 'it should not create a new DeployToken' do
+ expect { subject.execute }.not_to change { DeployToken.count }
+ end
+
+ it 'should not store the token on redis' do
+ subject.execute
+ redis_key = DeployToken.redis_shared_state_key(user.id)
+
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get(redis_key) }).to be_nil
+ end
+ end
+ end
+end