diff options
author | Simon Vocella <voxsim@gmail.com> | 2017-01-06 17:00:46 +0100 |
---|---|---|
committer | Tiago Botelho <tiagonbotelho@hotmail.com> | 2017-02-28 22:15:39 +0000 |
commit | c2b1cdef7e8cdaec35bd0844301ce8f06ed742b7 (patch) | |
tree | 072d178375afe0875fe2f4342e4f167848213939 | |
parent | 09dd6a7ead97122385f13265ea147ab689994244 (diff) | |
download | gitlab-ce-c2b1cdef7e8cdaec35bd0844301ce8f06ed742b7.tar.gz |
add admin panel for personal access tokens
-rw-r--r-- | app/assets/stylesheets/pages/settings.scss | 11 | ||||
-rw-r--r-- | app/controllers/admin/personal_access_tokens_controller.rb | 48 | ||||
-rw-r--r-- | app/views/admin/personal_access_tokens/_form.html.haml | 28 | ||||
-rw-r--r-- | app/views/admin/personal_access_tokens/index.html.haml | 80 | ||||
-rw-r--r-- | app/views/admin/users/_head.html.haml | 2 | ||||
-rw-r--r-- | config/routes/admin.rb | 5 | ||||
-rw-r--r-- | spec/features/admin/admin_users_personal_access_tokens_spec.rb | 95 |
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 |