diff options
author | Simon Vocella <voxsim@gmail.com> | 2016-12-28 17:19:08 +0100 |
---|---|---|
committer | Tiago Botelho <tiagonbotelho@hotmail.com> | 2017-02-28 22:15:39 +0000 |
commit | a3dfb58e7f1b1a3df4a4c16b2d09e50831370a69 (patch) | |
tree | e97662cc1d8ec3691184316f71e942bb6476249c | |
parent | 81246e5649a8fb9e73369cbd117505a546d7e807 (diff) | |
download | gitlab-ce-a3dfb58e7f1b1a3df4a4c16b2d09e50831370a69.tar.gz |
add impersonation token
-rw-r--r-- | app/models/personal_access_token.rb | 14 | ||||
-rw-r--r-- | db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb | 18 | ||||
-rw-r--r-- | db/schema.rb | 1 | ||||
-rw-r--r-- | lib/api/entities.rb | 1 | ||||
-rw-r--r-- | lib/api/users.rb | 7 | ||||
-rw-r--r-- | lib/gitlab/auth.rb | 19 | ||||
-rw-r--r-- | spec/controllers/profiles/personal_access_tokens_spec.rb | 29 | ||||
-rw-r--r-- | spec/factories/personal_access_tokens.rb | 5 | ||||
-rw-r--r-- | spec/lib/gitlab/auth_spec.rb | 29 | ||||
-rw-r--r-- | spec/requests/api/users_spec.rb | 28 |
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 |