Commit 9544f4a4 by Tiago

applies relevant changes to the code and code structure

parent efef8827
......@@ -25,8 +25,8 @@
padding-top: 0;
}
.personal-access-token-token-container {
#personal-access-token-token {
.impersonation-token-token-container {
#impersonation-token-token {
width: 80%;
display: inline;
}
......
class Admin::ImpersonationTokensController < Admin::ApplicationController
before_action :user
def index
set_index_vars
end
def create
# We never want to non-impersonate a user
@impersonation_token = user.personal_access_tokens.build(impersonation_token_params.merge(impersonation: true))
if @impersonation_token.save
flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
render :index
end
end
def revoke
@impersonation_token = user.personal_access_tokens.impersonation.find(params[:id])
if @impersonation_token.revoke!
flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!"
else
flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}."
end
redirect_to admin_user_impersonation_tokens_path
end
private
def user
@user ||= User.find_by!(username: params[:user_id])
end
def impersonation_token_params
params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
end
def set_index_vars
@impersonation_token ||= user.personal_access_tokens.build
@scopes = Gitlab::Auth::SCOPES
@active_impersonation_tokens = user.personal_access_tokens.impersonation.active.order(:expires_at)
@inactive_impersonation_tokens = user.personal_access_tokens.impersonation.inactive
end
end
class Admin::PersonalAccessTokensController < Admin::ApplicationController
before_action :user
def index
set_index_vars
end
def create
# We never want to non-impersonate a user
@personal_access_token = user.personal_access_tokens.generate(personal_access_token_params.merge(impersonation: true))
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
......@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def create
@personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
@personal_access_token = current_user.personal_access_tokens.build(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
......
class PersonalAccessTokensFinder
def initialize(user, params = {})
@user = user
@params = params
end
def execute
pat_id = token_id?
personal_access_tokens = @user.personal_access_tokens
personal_access_tokens = personal_access_tokens.impersonation if impersonation?
return find_token_by_id(personal_access_tokens, pat_id) if pat_id
case state?
when 'active'
personal_access_tokens.active
when 'inactive'
personal_access_tokens.inactive
else
personal_access_tokens
end
end
private
def state?
@params[:state].presence
end
def impersonation?
@params[:impersonation].presence
end
def token_id?
@params[:personal_access_token_id].presence
end
def find_token_by_id(personal_access_tokens, pat_id)
personal_access_tokens.find_by(id: pat_id)
end
end
......@@ -7,20 +7,13 @@ class PersonalAccessToken < ActiveRecord::Base
belongs_to :user
before_save :ensure_token
default_scope { where(impersonation: false) }
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :impersonation, -> { where(impersonation: true) }
class << self
alias_method :and_impersonation_tokens, :unscoped
def generate(params)
personal_access_token = self.new(params)
personal_access_token.ensure_token
personal_access_token
end
end
scope :impersonation, -> { unscoped.where(impersonation: true) }
scope :with_impersonation_tokens, -> { unscoped }
def revoke!
self.revoked = true
......
- page_title "Impersonation Tokens", @user.name, "Users"
= render 'admin/users/head'
.row.prepend-top-default
.col-lg-12
%h5.prepend-top-0
Add a Impersonation Token
%p.profile-settings-content
Pick a name for the application, and we'll give the respective user a unique token.
= render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, personal_access_token: @impersonation_token, scopes: @scopes
%hr
%h5 Active Impersonation Tokens (#{@active_impersonation_tokens.length})
%p.profile-settings-content
To see all the user's personal access tokens you must impersonate first
- if @active_impersonation_tokens.present?
.table-responsive
%table.table.active-impersonation-tokens
%thead
%tr
%th Name
%th Created
%th Expires
%th Scopes
%th Token
%th
%tbody
- @active_impersonation_tokens.each do |impersonation_token|
%tr
%td= impersonation_token.name
%td= impersonation_token.created_at.to_date.to_s(:medium)
%td
- if impersonation_token.expires?
%span{ class: ('text-warning' if impersonation_token.expires_soon?) }
In #{distance_of_time_in_words_to_now(impersonation_token.expires_at)}
- else
%span.impersonation_tokens-never-expires-label Never
%td= impersonation_token.scopes.present? ? impersonation_token.scopes.join(", ") : "<no scopes selected>"
%td.impersonation-token-token-container
= text_field_tag 'impersonation-token-token', impersonation_token.token, readonly: true, class: "form-control"
= clipboard_button(clipboard_text: impersonation_token.token)
%td= link_to "Revoke", revoke_admin_user_impersonation_token_path(id: impersonation_token.id, user_id: impersonation_token.user.username), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this impersonation token? This action cannot be undone." }
- else
.settings-message.text-center
This user has no active impersonation tokens.
%hr
%h5 Inactive Impersonation Tokens (#{@inactive_impersonation_tokens.length})
- if @inactive_impersonation_tokens.present?
.table-responsive
%table.table.inactive-impersonation-tokens
%thead
%tr
%th Name
%th Created
%tbody
- @inactive_impersonation_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 impersonation tokens.
- 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 "profiles/personal_access_tokens/form", user: :admin_user, 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 $dateField = $('#personal_access_token_expires_at');
var date = $dateField.val();
new Pikaday({
field: $dateField.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
minDate: new Date(),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
......@@ -21,6 +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)
= nav_link(controller: :impersonation_tokens) do
= link_to "Access Tokens", admin_user_impersonation_tokens_path(@user)
.append-bottom-default
......@@ -29,7 +29,7 @@
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
= render "form", user: :profile, personal_access_token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, personal_access_token: @personal_access_token, scopes: @scopes
%hr
......
- impersonation = impersonation || false
- personal_access_token = local_assigns.fetch(:personal_access_token)
- scopes = local_assigns.fetch(:scopes)
= form_for [user, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
= form_for personal_access_token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
= form_errors(personal_access_token)
......@@ -18,4 +19,19 @@
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
.prepend-top-default
= f.submit 'Create Personal Access Token', class: "btn btn-create"
- type = impersonation ? "Impersonation" : "Personal Access"
= f.submit "Create #{type} Token", class: "btn btn-create"
:javascript
var $dateField = $('.datepicker');
var date = $dateField.val();
new Pikaday({
field: $dateField.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
minDate: new Date(),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
---
title: Manage user personal access tokens through api and add impersonation tokens
merge_request: 8350
author: Simon Vocella @voxsim
merge_request: 9099
author: Simon Vocella
......@@ -2,7 +2,7 @@ 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
resources :impersonation_tokens, only: [:index, :create] do
member do
put :revoke
end
......
......@@ -9,7 +9,7 @@ class AddImpersonationToPersonalAccessTokens < ActiveRecord::Migration
DOWNTIME = false
def up
add_column_with_default :personal_access_tokens, :impersonation, :boolean, default: false
add_column_with_default :personal_access_tokens, :impersonation, :boolean, default: false, null: false
end
def down
......
......@@ -52,6 +52,8 @@ Parameters:
POST /personal_access_tokens
```
It responds with the new personal access token for the current user.
Parameters:
| Attribute | Type | Required | Description |
......
......@@ -697,7 +697,7 @@ module API
expose :active?, as: :active
end
class BasicPersonalAccessToken < Grape::Entity
class PersonalAccessToken < Grape::Entity
expose :id, :name, :revoked, :created_at, :scopes
expose :active?, as: :active
expose :expires_at do |personal_access_token|
......@@ -705,9 +705,12 @@ module API
end
end
class PersonalAccessToken < BasicPersonalAccessToken
expose :impersonation
class PersonalAccessTokenWithToken < PersonalAccessToken
expose :token
end
class ImpersonationToken < PersonalAccessTokenWithToken
expose :impersonation
end
end
end
......@@ -5,41 +5,30 @@ module API
resource :personal_access_tokens do
desc 'Retrieve personal access tokens' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::BasicPersonalAccessToken
success Entities::PersonalAccessToken
end
params do
optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) personal_access_tokens'
end
get do
personal_access_tokens = current_user.personal_access_tokens
case params[:state]
when "active"
personal_access_tokens = personal_access_tokens.active
when "inactive"
personal_access_tokens = personal_access_tokens.inactive
end
present personal_access_tokens, with: Entities::BasicPersonalAccessToken
end
get { present PersonalAccessTokensFinder.new(current_user, params).execute, with: Entities::PersonalAccessToken }
desc 'Retrieve personal access token' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::BasicPersonalAccessToken
success Entities::PersonalAccessToken
end
params do
requires :personal_access_token_id, type: Integer, desc: 'The ID of the personal access token'
end
get ':personal_access_token_id' do
personal_access_token = PersonalAccessToken.find_by(id: params[:personal_access_token_id], user_id: current_user.id)
not_found!('PersonalAccessToken') unless personal_access_token
personal_access_token = PersonalAccessTokensFinder.new(current_user, declared_params(include_missing: false)).execute
not_found!('Personal Access Token') unless personal_access_token
present personal_access_token, with: Entities::BasicPersonalAccessToken
present personal_access_token, with: Entities::PersonalAccessToken
end
desc 'Create a personal access token' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::BasicPersonalAccessToken
success Entities::PersonalAccessTokenWithToken
end
params do
requires :name, type: String, desc: 'The name of the personal access token'
......@@ -47,13 +36,10 @@ module API
optional :scopes, type: Array, desc: 'The array of scopes of the personal access token'
end
post do
parameters = declared_params(include_missing: false)
parameters[:user_id] = current_user.id
personal_access_token = PersonalAccessToken.generate(parameters)
personal_access_token = current_user.personal_access_tokens.build(declared_params(include_missing: false))
if personal_access_token.save
present personal_access_token, with: Entities::PersonalAccessToken
present personal_access_token, with: Entities::PersonalAccessTokenWithToken
else
render_validation_error!(personal_access_token)
end
......@@ -61,14 +47,13 @@ module API
desc 'Revoke a personal access token' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::BasicPersonalAccessToken
end
params do
requires :personal_access_token_id, type: Integer, desc: 'The ID of the personal access token'
end
delete ':personal_access_token_id' do
personal_access_token = PersonalAccessToken.find_by(id: params[:personal_access_token_id], user_id: current_user.id)
not_found!('PersonalAccessToken') unless personal_access_token
personal_access_token = PersonalAccessTokensFinder.new(current_user, declared_params(include_missing: false)).execute
not_found!('Personal Access Token') unless personal_access_token
personal_access_token.revoke!
......
......@@ -9,6 +9,11 @@ module API
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
def find_user(params)
user = User.find_by(id: params[:id])
user ? user : not_found!('User')
end
params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username'
......@@ -364,40 +369,28 @@ module API
end
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
requires :id, type: Integer, desc: 'The ID of the user'
end
segment ':user_id' do
segment ':id' do
resource :personal_access_tokens do
before { authenticated_as_admin! }
desc 'Retrieve personal access tokens. Available only for admins.' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::PersonalAccessToken
success Entities::ImpersonationToken
end
params do
optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) personal_access_tokens'
optional :impersonation, type: Boolean, default: false, desc: 'Filters only impersonation personal_access_tokens'
end
get do
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
personal_access_tokens = PersonalAccessToken.and_impersonation_tokens.where(user_id: user.id)
personal_access_tokens = personal_access_tokens.impersonation if params[:impersonation]
case params[:state]
when "active"
personal_access_tokens = personal_access_tokens.active
when "inactive"
personal_access_tokens = personal_access_tokens.inactive
end
present personal_access_tokens, with: Entities::PersonalAccessToken
user = find_user(params)
present PersonalAccessTokensFinder.new(user, params).execute, with: Entities::ImpersonationToken
end
desc 'Create a personal access token. Available only for admins.' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::PersonalAccessToken
success Entities::ImpersonationToken
end
params do
requires :name, type: String, desc: 'The name of the personal access token'
......@@ -406,13 +399,11 @@ module API
optional :impersonation, type: Boolean, default: false, desc: 'The impersonation flag of the personal access token'
end
post do
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
personal_access_token = PersonalAccessToken.generate(declared_params(include_missing: false, include_parent_namespaces: true))
user = find_user(params)
personal_access_token = PersonalAccessTokensFinder.new(user).execute.build(declared_params(include_missing: false))
if personal_access_token.save
present personal_access_token, with: Entities::PersonalAccessToken
present personal_access_token, with: Entities::ImpersonationToken
else
render_validation_error!(personal_access_token)
end
......@@ -420,34 +411,33 @@ module API
desc 'Retrieve personal access token. Available only for admins.' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::PersonalAccessToken
success Entities::ImpersonationToken
end
params do
requires :personal_access_token_id, type: Integer, desc: 'The ID of the personal access token'
optional :impersonation, type: Boolean, default: false, desc: 'The impersonation flag of the personal access token'
end
get '/:personal_access_token_id' do
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
get ':personal_access_token_id' do
user = find_user(params)
personal_access_token = PersonalAccessToken.and_impersonation_tokens.find_by(user_id: user.id, id: params[:personal_access_token_id])
not_found!('PersonalAccessToken') unless personal_access_token
personal_access_token = PersonalAccessTokensFinder.new(user, declared_params(include_missing: false)).execute
not_found!('Personal Access Token') unless personal_access_token
present personal_access_token, with: Entities::PersonalAccessToken
present personal_access_token, with: Entities::ImpersonationToken
end
desc 'Revoke a personal access token. Available only for admins.' do
detail 'This feature was introduced in GitLab 9.0'
success Entities::PersonalAccessToken
end
params do
requires :personal_access_token_id, type: Integer, desc: 'The ID of the personal access token'
optional :impersonation, type: Boolean, default: false, desc: 'The impersonation flag of the personal access token'
end
delete '/:personal_access_token_id' do
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
delete ':personal_access_token_id' do
user = find_user(params)
personal_access_token = PersonalAccessToken.and_impersonation_tokens.find_by(user_id: user.id, id: params[:personal_access_token_id])
not_found!('PersonalAccessToken') unless personal_access_token
personal_access_token = PersonalAccessTokensFinder.new(user, declared_params(include_missing: false)).execute
not_found!('Personal Access Token') unless personal_access_token
personal_access_token.revoke!
......
......@@ -105,9 +105,9 @@ module Gitlab
def personal_access_token_check(password)
return unless password.present?
token = PersonalAccessToken.and_impersonation_tokens.active.find_by_token(password)
token = PersonalAccessToken.with_impersonation_tokens.active.find_by_token(password)
if token && (valid_api_token?(token) || token.impersonation)
if token && valid_api_token?(token)
Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities)
end
end
......
require 'spec_helper'
describe 'Admin > Users > Personal Access Tokens', feature: true, js: true do
describe 'Admin > Users > Impersonation 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")
find(".table.active-impersonation-tokens")
end
def inactive_personal_access_tokens
find(".table.inactive-personal-access-tokens")
find(".table.inactive-impersonation-tokens")
end
def created_personal_access_token
find("#created-personal-access-token").value
find("#created-impersonation-token").value
end
def disallow_personal_access_token_saves!
......@@ -28,7 +28,7 @@ describe 'Admin > Users > Personal Access Tokens', feature: true, js: true do
it "allows creation of a token" do
name = FFaker::Product.brand
visit admin_user_personal_access_tokens_path(user_id: user.username)
visit admin_user_impersonation_tokens_path(user_id: user.username)
fill_in "Name", with: name
# Set date to 1st of next month
......@@ -40,31 +40,20 @@ describe 'Admin > Users > Personal Access Tokens', feature: true, js: true do
check "api"
check "read_user"
click_on "Create Personal Access Token"
expect { click_on "Create Impersonation Token" }.to change { PersonalAccessToken.impersonation.count }
expect(active_personal_access_tokens).to have_text(name)
expect(active_personal_access_tokens).to have_text('In')