manage personal_access_tokens through api

This commit is contained in:
Simon Vocella 2016-12-27 17:26:57 +01:00 committed by Tiago Botelho
parent 4c4810b35b
commit 81246e5649
12 changed files with 427 additions and 3 deletions

View file

@ -1,4 +1,5 @@
class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
add_authentication_token_field :token
@ -19,4 +20,8 @@ class PersonalAccessToken < ActiveRecord::Base
self.revoked = true
self.save
end
def active?
!revoked? && !expired?
end
end

View file

@ -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 ChangeExpiresAtToDateInPersonalAccessTokens < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = true
DOWNTIME_REASON = 'This migration requires downtime because it alters expires_at column from datetime to date'
def up
change_column :personal_access_tokens, :expires_at, :date
end
def down
change_column :personal_access_tokens, :expires_at, :datetime
end
end

View file

@ -879,7 +879,7 @@ ActiveRecord::Schema.define(version: 20170216141440) do
t.string "token", null: false
t.string "name", null: false
t.boolean "revoked", default: false
t.datetime "expires_at"
t.date "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "scopes", default: "--- []\n", null: false

View file

@ -93,6 +93,7 @@ module API
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::PersonalAccessTokens
mount ::API::Pipelines
mount ::API::ProjectHooks
mount ::API::Projects

View file

@ -696,5 +696,17 @@ module API
expose :id, :message, :starts_at, :ends_at, :color, :font
expose :active?, as: :active
end
class BasicPersonalAccessToken < Grape::Entity
expose :id, :name, :revoked, :created_at, :scopes
expose :active?, as: :active
expose :expires_at do |personal_access_token|
personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
end
end
class PersonalAccessToken < BasicPersonalAccessToken
expose :token
end
end
end

View file

@ -0,0 +1,56 @@
module API
class PersonalAccessTokens < Grape::API
before { authenticate! }
resource :personal_access_tokens do
desc 'Retrieve personal access tokens'
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
desc 'Create a personal access token'
params do
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'
end
post do
parameters = declared_params(include_missing: false)
parameters[:user_id] = current_user.id
personal_access_token = PersonalAccessToken.generate(parameters)
if personal_access_token.save
present personal_access_token, with: Entities::PersonalAccessToken
else
render_validation_error!(personal_access_token)
end
end
desc 'Revoke a personal access token'
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.revoke!
present personal_access_token, with: Entities::BasicPersonalAccessToken
end
end
end
end

View file

@ -362,6 +362,70 @@ module API
present paginate(events), with: Entities::Event
end
desc 'Retrieve personal access tokens. Available only for admins.'
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'
end
get ':user_id/personal_access_tokens' do
authenticated_as_admin!
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
personal_access_tokens = 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::PersonalAccessToken
end
desc 'Create a personal access token. Available only for admins.'
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
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'
end
post ':user_id/personal_access_tokens' do
authenticated_as_admin!
user = User.find_by(id: params[:user_id])
not_found!('User') unless user
personal_access_token = PersonalAccessToken.generate(declared_params(include_missing: false))
if personal_access_token.save
present personal_access_token, with: Entities::PersonalAccessToken
else
render_validation_error!(personal_access_token)
end
end
desc 'Revoke a personal access token. Available only for admins.'
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
requires :personal_access_token_id, type: Integer, desc: 'The ID of the personal access token'
end
delete ':user_id/personal_access_tokens/:personal_access_token_id' do
authenticated_as_admin!
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])
not_found!('PersonalAccessToken') unless personal_access_token
personal_access_token.revoke!
present personal_access_token, with: Entities::PersonalAccessToken
end
end
resource :user do

View file

@ -22,12 +22,12 @@ describe Profiles::PersonalAccessTokensController do
end
it "allows creation of a token with an expiry date" do
expires_at = 5.days.from_now
expires_at = 5.days.from_now.to_date
post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
expect(created_token).not_to be_nil
expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
expect(created_token.expires_at).to eq(expires_at)
end
context "scopes" do

View file

@ -6,5 +6,13 @@ FactoryGirl.define do
revoked false
expires_at { 5.days.from_now }
scopes ['api']
factory :revoked_personal_access_token do
revoked true
end
factory :expired_personal_access_token do
expires_at { 1.day.ago }
end
end
end

View file

@ -12,4 +12,22 @@ describe PersonalAccessToken, models: true do
expect(personal_access_token).not_to be_persisted
end
end
describe ".active?" do
let(:active_personal_access_token) { build(:personal_access_token) }
let(:revoked_personal_access_token) { build(:revoked_personal_access_token) }
let(:expired_personal_access_token) { build(:expired_personal_access_token) }
it "returns false if the personal_access_token is revoked" do
expect(revoked_personal_access_token).not_to be_active
end
it "returns false if the personal_access_token is expired" do
expect(expired_personal_access_token).not_to be_active
end
it "returns true if the personal_access_token is not revoked and not expired" do
expect(active_personal_access_token).to be_active
end
end
end

View file

@ -0,0 +1,107 @@
require 'spec_helper'
describe API::PersonalAccessTokens, api: true do
include ApiHelpers
let(:user) { create(:user) }
describe "GET /personal_access_tokens" 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) }
it 'returns an array of personal access tokens without exposing the token' do
get api("/personal_access_tokens", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
json_personal_access_token = json_response.detect do |personal_access_token|
personal_access_token['id'] == active_personal_access_token.id
end
expect(json_personal_access_token['name']).to eq(active_personal_access_token.name)
expect(json_personal_access_token['token']).not_to be_present
end
it 'returns an array of active personal access tokens if active is set to true' do
get api("/personal_access_tokens?state=active", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to all(include('active' => true))
end
it 'returns an array of inactive personal access tokens if active is set to false' do
get api("/personal_access_tokens?state=inactive", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to all(include('active' => false))
end
end
describe 'POST /personal_access_tokens' do
let(:name) { 'my new pat' }
let(:expires_at) { '2016-12-28' }
let(:scopes) { ['api', 'read_user'] }
it 'returns validation error if personal access token miss some attributes' do
post api("/personal_access_tokens", user)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
it 'creates a personal access token' do
post api("/personal_access_tokens", user),
name: name,
expires_at: expires_at,
scopes: scopes
expect(response).to have_http_status(201)
personal_access_token_id = json_response['id']
expect(json_response['name']).to eq(name)
expect(json_response['scopes']).to eq(scopes)
expect(json_response['expires_at']).to eq(expires_at)
expect(json_response['id']).to be_present
expect(json_response['created_at']).to be_present
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)
end
end
describe 'DELETE /personal_access_tokens/:personal_access_token_id' do
let!(:personal_access_token) { create(:personal_access_token, user: user, revoked: false) }
let!(:personal_access_token_of_another_user) { create(:personal_access_token, revoked: false) }
it 'returns a 404 error if personal access token not found' do
delete api("/personal_access_tokens/42", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 PersonalAccessToken Not Found')
end
it 'returns a 404 error if personal access token exists but it is a personal access tokens of another user' do
delete api("/personal_access_tokens/#{personal_access_token_of_another_user.id}", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 PersonalAccessToken Not Found')
end
it 'revokes a personal access token and does not expose token in the json response' do
delete api("/personal_access_tokens/#{personal_access_token.id}", user)
expect(response).to have_http_status(200)
expect(personal_access_token.revoked).to eq(false)
expect(personal_access_token.reload.revoked).to eq(true)
expect(json_response['revoked']).to eq(true)
expect(json_response['token']).not_to be_present
end
end
end

View file

@ -10,6 +10,7 @@ describe API::Users, api: true do
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 }
describe "GET /users" do
context "when unauthenticated" do
@ -1155,4 +1156,138 @@ describe API::Users, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
end
describe 'GET /users/:user_id/personal_access_tokens' 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) }
it 'returns a 404 error if user not found' do
get api("/users/#{not_existing_user_id}/personal_access_tokens", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
get api("/users/#{not_existing_user_id}/personal_access_tokens", user)
expect(response).to have_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'returns an array of personal access tokens' do
get api("/users/#{user.id}/personal_access_tokens", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
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)
end
it 'returns an array of active personal access tokens if active is set to true' do
get api("/users/#{user.id}/personal_access_tokens?state=active", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to all(include('active' => true))
end
it 'returns an array of inactive personal access tokens if active is set to false' do
get api("/users/#{user.id}/personal_access_tokens?state=inactive", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to all(include('active' => false))
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'] }
it 'returns validation error if personal access token miss some attributes' do
post api("/users/#{user.id}/personal_access_tokens", admin)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
it 'returns a 404 error if user not found' do
post api("/users/#{not_existing_user_id}/personal_access_tokens", admin),
name: name,
expires_at: expires_at
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
post api("/users/#{user.id}/personal_access_tokens", user),
name: name,
expires_at: expires_at
expect(response).to have_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'creates a personal access token' do
post api("/users/#{user.id}/personal_access_tokens", admin),
name: name,
expires_at: expires_at,
scopes: scopes
expect(response).to have_http_status(201)
personal_access_token_id = json_response['id']
expect(json_response['name']).to eq(name)
expect(json_response['scopes']).to eq(scopes)
expect(json_response['expires_at']).to eq(expires_at)
expect(json_response['id']).to be_present
expect(json_response['created_at']).to be_present
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)
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) }
it 'returns a 404 error if user not found' do
delete api("/users/#{not_existing_user_id}/personal_access_tokens/1", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 404 error if personal access token not found' do
delete api("/users/#{user.id}/personal_access_tokens/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 PersonalAccessToken Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
delete api("/users/#{user.id}/personal_access_tokens/#{personal_access_token.id}", user)
expect(response).to have_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'revokes a personal access token' do
delete api("/users/#{user.id}/personal_access_tokens/#{personal_access_token.id}", admin)
expect(response).to have_http_status(200)
expect(personal_access_token.revoked).to eq(false)
expect(personal_access_token.reload.revoked).to eq(true)
expect(json_response['revoked']).to eq(true)
expect(json_response['token']).to be_present
end
end
end