summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Vocella <voxsim@gmail.com>2017-01-06 17:00:46 +0100
committerTiago Botelho <tiagonbotelho@hotmail.com>2017-02-28 22:15:39 +0000
commitc2b1cdef7e8cdaec35bd0844301ce8f06ed742b7 (patch)
tree072d178375afe0875fe2f4342e4f167848213939
parent09dd6a7ead97122385f13265ea147ab689994244 (diff)
downloadgitlab-ce-c2b1cdef7e8cdaec35bd0844301ce8f06ed742b7.tar.gz
add admin panel for personal access tokens
-rw-r--r--app/assets/stylesheets/pages/settings.scss11
-rw-r--r--app/controllers/admin/personal_access_tokens_controller.rb48
-rw-r--r--app/views/admin/personal_access_tokens/_form.html.haml28
-rw-r--r--app/views/admin/personal_access_tokens/index.html.haml80
-rw-r--r--app/views/admin/users/_head.html.haml2
-rw-r--r--config/routes/admin.rb5
-rw-r--r--spec/features/admin/admin_users_personal_access_tokens_spec.rb95
7 files changed, 269 insertions, 0 deletions
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a28a87ed4f8..905ecbff57c 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -24,3 +24,14 @@
.service-settings .control-label {
padding-top: 0;
}
+
+.personal-access-token-token-container {
+ #personal-access-token-token {
+ width: 80%;
+ display: inline;
+ }
+
+ .btn-clipboard {
+ margin-left: 5px;
+ }
+}
diff --git a/app/controllers/admin/personal_access_tokens_controller.rb b/app/controllers/admin/personal_access_tokens_controller.rb
new file mode 100644
index 00000000000..7202d80ce1b
--- /dev/null
+++ b/app/controllers/admin/personal_access_tokens_controller.rb
@@ -0,0 +1,48 @@
+class Admin::PersonalAccessTokensController < Admin::ApplicationController
+ before_action :user
+
+ def index
+ set_index_vars
+ end
+
+ def create
+ @personal_access_token = user.personal_access_tokens.generate(personal_access_token_params)
+
+ if @personal_access_token.save
+ flash[:personal_access_token] = @personal_access_token.token
+ redirect_to admin_user_personal_access_tokens_path, notice: "A new personal access token has been created."
+ else
+ set_index_vars
+ render :index
+ end
+ end
+
+ def revoke
+ @personal_access_token = user.personal_access_tokens.find(params[:id])
+
+ if @personal_access_token.revoke!
+ flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+ else
+ flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+ end
+
+ redirect_to admin_user_personal_access_tokens_path
+ end
+
+ private
+
+ def user
+ @user ||= User.find_by!(username: params[:user_id])
+ end
+
+ def personal_access_token_params
+ params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
+ end
+
+ def set_index_vars
+ @personal_access_token ||= user.personal_access_tokens.build
+ @scopes = Gitlab::Auth::SCOPES
+ @active_personal_access_tokens = PersonalAccessToken.and_impersonation_tokens.where(user_id: user.id).active.order(:expires_at)
+ @inactive_personal_access_tokens = PersonalAccessToken.and_impersonation_tokens.where(user_id: user.id).inactive
+ end
+end
diff --git a/app/views/admin/personal_access_tokens/_form.html.haml b/app/views/admin/personal_access_tokens/_form.html.haml
new file mode 100644
index 00000000000..d194a0fd511
--- /dev/null
+++ b/app/views/admin/personal_access_tokens/_form.html.haml
@@ -0,0 +1,28 @@
+- personal_access_token = local_assigns.fetch(:personal_access_token)
+- scopes = local_assigns.fetch(:scopes)
+
+= form_for [:admin_user, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(personal_access_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'
+ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
+
+ .form-group
+ = f.label :impersonation, class: 'label-light'
+ %fieldset
+ = f.check_box :impersonation
+ = f.label 'impersonation', 'You can impersonate the user'
+ %span= "(Normal users will not see this type of token)"
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/admin/personal_access_tokens/index.html.haml b/app/views/admin/personal_access_tokens/index.html.haml
new file mode 100644
index 00000000000..90aade17e1b
--- /dev/null
+++ b/app/views/admin/personal_access_tokens/index.html.haml
@@ -0,0 +1,80 @@
+- page_title "Personal Access Tokens"
+= render 'admin/users/head'
+
+.row.prepend-top-default
+ .col-lg-12
+
+ %h5.prepend-top-0
+ Add a Personal Access Token
+ %p.profile-settings-content
+ Pick a name for the application, and we'll give you a unique token.
+
+ = render "form", personal_access_token: @personal_access_token, scopes: @scopes
+
+ %hr
+
+ %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
+
+ - if @active_personal_access_tokens.present?
+ .table-responsive
+ %table.table.active-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %th Expires
+ %th Scopes
+ %th Token
+ %th Impersonation
+ %th
+ %tbody
+ - @active_personal_access_tokens.each do |personal_access_token|
+ %tr
+ %td= personal_access_token.name
+ %td= personal_access_token.created_at.to_date.to_s(:medium)
+ %td
+ - if personal_access_token.expires?
+ %span{ class: ('text-warning' if personal_access_token.expires_soon?) }
+ In #{distance_of_time_in_words_to_now(personal_access_token.expires_at)}
+ - else
+ %span.personal-access-personal_access_tokens-never-expires-label Never
+ %td= personal_access_token.scopes.present? ? personal_access_token.scopes.join(", ") : "<no scopes selected>"
+ %td.personal-access-token-token-container
+ = text_field_tag 'personal-access-token-token', personal_access_token.token, readonly: true, class: "form-control"
+ = clipboard_button(clipboard_text: personal_access_token.token)
+ %td= personal_access_token.impersonation
+ %td= link_to "Revoke", revoke_admin_user_personal_access_token_path(id: personal_access_token.id, user_id: personal_access_token.user.username), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ This user has no active tokens.
+
+ %hr
+
+ %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
+
+ - if @inactive_personal_access_tokens.present?
+ .table-responsive
+ %table.table.inactive-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %tbody
+ - @inactive_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+
+ - else
+ .settings-message.text-center
+ This user has no inactive tokens.
+
+
+:javascript
+ var date = $('#personal_access_token_expires_at').val();
+
+ var datepicker = $(".datepicker").datepicker({
+ dateFormat: "yy-mm-dd",
+ minDate: 0
+ });
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 9984e733956..c95ae93b710 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -21,4 +21,6 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
+ = nav_link(controller: :personal_access_tokens) do
+ = link_to "Access Tokens", admin_user_personal_access_tokens_path(@user)
.append-bottom-default
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 8e99239f350..6d2748df386 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -2,6 +2,11 @@ namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
resources :identities, except: [:show]
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
member do
get :projects
diff --git a/spec/features/admin/admin_users_personal_access_tokens_spec.rb b/spec/features/admin/admin_users_personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..b7ec8c9fe86
--- /dev/null
+++ b/spec/features/admin/admin_users_personal_access_tokens_spec.rb
@@ -0,0 +1,95 @@
+require 'spec_helper'
+
+describe 'Admin > Users > Personal Access Tokens', feature: true, js: true do
+ let(:admin) { create(:admin) }
+ let!(:user) { create(:user) }
+
+ def active_personal_access_tokens
+ find(".table.active-personal-access-tokens")
+ end
+
+ def inactive_personal_access_tokens
+ find(".table.inactive-personal-access-tokens")
+ end
+
+ def created_personal_access_token
+ find("#created-personal-access-token").value
+ end
+
+ def disallow_personal_access_token_saves!
+ allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false)
+ errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
+ allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
+ end
+
+ before do
+ login_as(admin)
+ end
+
+ describe "token creation" do
+ it "allows creation of a token" do
+ name = FFaker::Product.brand
+
+ visit admin_user_personal_access_tokens_path(user_id: user.username)
+ fill_in "Name", with: name
+
+ # Set date to 1st of next month
+ find_field("Expires at").trigger('focus')
+ find("a[title='Next']").click
+ click_on "1"
+
+ # Scopes
+ check "api"
+ check "read_user"
+
+ check "You can impersonate the user"
+
+ click_on "Create Personal Access Token"
+ expect(active_personal_access_tokens).to have_text(name)
+ expect(active_personal_access_tokens).to have_text('In')
+ expect(active_personal_access_tokens).to have_text('api')
+ expect(active_personal_access_tokens).to have_text('read_user')
+ expect(active_personal_access_tokens).to have_text('true')
+ end
+
+ context "when creation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit admin_user_personal_access_tokens_path(user_id: user.username)
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count }
+ expect(page).to have_content("Name cannot be nil")
+ end
+ end
+ end
+
+ describe "inactive tokens" do
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "allows revocation of an active token" do
+ visit admin_user_personal_access_tokens_path(user_id: user.username)
+ click_on "Revoke"
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ it "moves expired tokens to the 'inactive' section" do
+ personal_access_token.update(expires_at: 5.days.ago)
+ visit admin_user_personal_access_tokens_path(user_id: user.username)
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ context "when revocation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit admin_user_personal_access_tokens_path(user_id: user.username)
+
+ click_on "Revoke"
+ expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(page).to have_content("Could not revoke")
+ end
+ end
+ end
+end