summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Vocella <voxsim@gmail.com>2016-12-28 17:19:08 +0100
committerTiago Botelho <tiagonbotelho@hotmail.com>2017-02-28 22:15:39 +0000
commita3dfb58e7f1b1a3df4a4c16b2d09e50831370a69 (patch)
treee97662cc1d8ec3691184316f71e942bb6476249c
parent81246e5649a8fb9e73369cbd117505a546d7e807 (diff)
downloadgitlab-ce-a3dfb58e7f1b1a3df4a4c16b2d09e50831370a69.tar.gz
add impersonation token
-rw-r--r--app/models/personal_access_token.rb14
-rw-r--r--db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb18
-rw-r--r--db/schema.rb1
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/users.rb7
-rw-r--r--lib/gitlab/auth.rb19
-rw-r--r--spec/controllers/profiles/personal_access_tokens_spec.rb29
-rw-r--r--spec/factories/personal_access_tokens.rb5
-rw-r--r--spec/lib/gitlab/auth_spec.rb29
-rw-r--r--spec/requests/api/users_spec.rb28
10 files changed, 116 insertions, 35 deletions
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 5da98d7126a..cf94b01c33e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -7,13 +7,19 @@ class PersonalAccessToken < ActiveRecord::Base
belongs_to :user
+ 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) }
- def self.generate(params)
- personal_access_token = self.new(params)
- personal_access_token.ensure_token
- personal_access_token
+ 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
def revoke!
diff --git a/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..c16d508e141
--- /dev/null
+++ b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddImpersonationToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :personal_access_tokens, :impersonation, :boolean, default: false
+ end
+
+ def down
+ remove_column :personal_access_tokens, :impersonation
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 82b4a931069..ec2b09f9b7b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -883,6 +883,7 @@ ActiveRecord::Schema.define(version: 20170216141440) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "scopes", default: "--- []\n", null: false
+ t.boolean "impersonation", default: false, null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 211353ef2a9..4e8d2410496 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -706,6 +706,7 @@ module API
end
class PersonalAccessToken < BasicPersonalAccessToken
+ expose :impersonation
expose :token
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 450d678061e..2b48da6ea99 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -367,6 +367,7 @@ module API
params do
requires :user_id, type: Integer
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_token'
end
get ':user_id/personal_access_tokens' do
authenticated_as_admin!
@@ -374,7 +375,8 @@ module API
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
- personal_access_tokens = user.personal_access_tokens
+ 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"
@@ -392,6 +394,7 @@ module API
requires :name, type: String, desc: 'The name of the personal access token'
optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
optional :scopes, type: Array, desc: 'The array of scopes of the personal access token'
+ optional :impersonation, type: Boolean, default: false, desc: 'The impersonation flag of the personal access token'
end
post ':user_id/personal_access_tokens' do
authenticated_as_admin!
@@ -419,7 +422,7 @@ module API
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
- personal_access_token = PersonalAccessToken.find_by(id: params[:personal_access_token_id])
+ 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.revoke!
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 89db6c3da46..e48462a4bd6 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -18,8 +18,8 @@ module Gitlab
build_access_token_check(login, password) ||
lfs_token_check(login, password) ||
oauth_access_token_check(login, password) ||
- personal_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
+ personal_access_token_check(password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
@@ -102,14 +102,13 @@ module Gitlab
end
end
- def personal_access_token_check(login, password)
- if login && password
- token = PersonalAccessToken.active.find_by_token(password)
- validation = User.by_login(login)
+ def personal_access_token_check(password)
+ return unless password.present?
- if valid_personal_access_token?(token, validation)
- Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
- end
+ token = PersonalAccessToken.and_impersonation_tokens.active.find_by_token(password)
+
+ if token && (valid_api_token?(token) || token.impersonation)
+ Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities)
end
end
@@ -117,10 +116,6 @@ module Gitlab
token && token.accessible? && valid_api_token?(token)
end
- def valid_personal_access_token?(token, user)
- token && token.user == user && valid_api_token?(token)
- end
-
def valid_api_token?(token)
AccessTokenValidationService.new(token).include_any_scope?(['api'])
end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
index 96b3b7d1b24..ac330d15e95 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -3,13 +3,13 @@ require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
+ before { sign_in(user) }
+
describe '#create' do
def created_token
PersonalAccessToken.order(:created_at).last
end
- before { sign_in(user) }
-
it "allows creation of a token" do
name = FFaker::Product.brand
@@ -46,4 +46,29 @@ describe Profiles::PersonalAccessTokensController do
end
end
end
+
+ describe '#index' do
+ let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:inactive_personal_access_token) { create(:revoked_personal_access_token, user: user) }
+ let!(:impersonation_personal_access_token) { create(:impersonation_personal_access_token, user: user) }
+
+ it "retrieves active personal access tokens" do
+ get :index
+
+ expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token)
+ end
+
+ it "retrieves inactive personal access tokens" do
+ get :index
+
+ expect(assigns(:inactive_personal_access_tokens)).to include(inactive_personal_access_token)
+ end
+
+ it "does not retrieve impersonation personal access tokens" do
+ get :index
+
+ expect(assigns(:active_personal_access_tokens)).not_to include(impersonation_personal_access_token)
+ expect(assigns(:inactive_personal_access_tokens)).not_to include(impersonation_personal_access_token)
+ end
+ end
end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index 3464d2d5d89..1905b22935b 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -6,6 +6,7 @@ FactoryGirl.define do
revoked false
expires_at { 5.days.from_now }
scopes ['api']
+ impersonation false
factory :revoked_personal_access_token do
revoked true
@@ -14,5 +15,9 @@ FactoryGirl.define do
factory :expired_personal_access_token do
expires_at { 1.day.ago }
end
+
+ factory :impersonation_personal_access_token do
+ impersonation true
+ end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b234de4c772..f9be51302d9 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -110,25 +110,30 @@ describe Gitlab::Auth, lib: true do
end
context 'while using personal access tokens as passwords' do
- let(:user) { create(:user) }
- let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) }
-
it 'succeeds for personal access tokens with the `api` scope' do
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+ personal_access_token = create(:personal_access_token, scopes: ['api'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
end
- it 'fails for personal access tokens with other scopes' do
- personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+ it 'succeeds if it is an impersonation token' do
+ personal_access_token = create(:personal_access_token, impersonation: true, scopes: [])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
end
- it 'does not try password auth before personal access tokens' do
- expect(gl_auth).not_to receive(:find_with_user_password)
+ it 'fails for personal access tokens with other scopes' do
+ personal_access_token = create(:personal_access_token, scopes: ['read_user'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ end
- gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')
+ it 'fails if password is nil' do
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+ expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 5ed6adc09bc..8aa975b19fb 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1161,6 +1161,7 @@ describe API::Users, api: true do
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
let!(:revoked_personal_access_token) { create(:revoked_personal_access_token, user: user) }
let!(:expired_personal_access_token) { create(:expired_personal_access_token, user: user) }
+ let!(:impersonation_personal_access_token) { create(:impersonation_personal_access_token, user: user) }
it 'returns a 404 error if user not found' do
get api("/users/#{not_existing_user_id}/personal_access_tokens", admin)
@@ -1181,7 +1182,7 @@ describe API::Users, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to eq(3)
+ expect(json_response.size).to eq(4)
expect(json_response.detect do |personal_access_token|
personal_access_token['id'] == active_personal_access_token.id
end['token']).to eq(active_personal_access_token.token)
@@ -1202,12 +1203,21 @@ describe API::Users, api: true do
expect(json_response).to be_an Array
expect(json_response).to all(include('active' => false))
end
+
+ it 'returns an array of impersonation personal access tokens if impersonation is set to true' do
+ get api("/users/#{user.id}/personal_access_tokens?impersonation=true", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to all(include('impersonation' => true))
+ end
end
describe 'POST /users/:user_id/personal_access_tokens' do
let(:name) { 'my new pat' }
let(:expires_at) { '2016-12-28' }
let(:scopes) { ['api', 'read_user'] }
+ let(:impersonation) { true }
it 'returns validation error if personal access token miss some attributes' do
post api("/users/#{user.id}/personal_access_tokens", admin)
@@ -1238,7 +1248,8 @@ describe API::Users, api: true do
post api("/users/#{user.id}/personal_access_tokens", admin),
name: name,
expires_at: expires_at,
- scopes: scopes
+ scopes: scopes,
+ impersonation: impersonation
expect(response).to have_http_status(201)
@@ -1252,12 +1263,14 @@ describe API::Users, api: true do
expect(json_response['active']).to eq(false)
expect(json_response['revoked']).to eq(false)
expect(json_response['token']).to be_present
- expect(PersonalAccessToken.find(personal_access_token_id)).not_to eq(nil)
+ expect(json_response['impersonation']).to eq(impersonation)
+ expect(PersonalAccessToken.and_impersonation_tokens.find(personal_access_token_id)).not_to eq(nil)
end
end
describe 'DELETE /users/:id/personal_access_tokens/:personal_access_token_id' do
let!(:personal_access_token) { create(:personal_access_token, user: user, revoked: false) }
+ let!(:impersonation_token) { create(:impersonation_personal_access_token, user: user, revoked: false) }
it 'returns a 404 error if user not found' do
delete api("/users/#{not_existing_user_id}/personal_access_tokens/1", admin)
@@ -1289,5 +1302,14 @@ describe API::Users, api: true do
expect(json_response['revoked']).to eq(true)
expect(json_response['token']).to be_present
end
+
+ it 'revokes an impersonation token' do
+ delete api("/users/#{user.id}/personal_access_tokens/#{impersonation_token.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(impersonation_token.revoked).to eq(false)
+ expect(impersonation_token.reload.revoked).to eq(true)
+ expect(json_response['revoked']).to eq(true)
+ end
end
end