diff options
author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-04-07 09:17:59 +0000 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-04-07 09:17:59 +0000 |
commit | dd552d06f6e39d5e6138a33bd7c1bffb2d3dbb1d (patch) | |
tree | e2cf4b8714c7dbb2bfc08c34e21b14cd5f9fa4e5 /app | |
parent | 671e93dc38365f3b05cb3cfe719e64713196be31 (diff) | |
parent | b38439a3ae3c7ea1675b7037e4882213bdc58fdf (diff) | |
download | gitlab-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
Diffstat (limited to 'app')
23 files changed, 315 insertions, 27 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' } × + .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’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" |