summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG1
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/profile.scss8
-rw-r--r--app/controllers/application_controller.rb25
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb40
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/user.rb1
-rw-r--r--app/views/layouts/nav/_profile.html.haml5
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml113
-rw-r--r--config/routes.rb7
-rw-r--r--db/migrate/20160415062917_create_personal_access_tokens.rb13
-rw-r--r--db/schema.rb14
-rw-r--r--doc/api/README.md61
-rw-r--r--lib/api/helpers.rb18
-rw-r--r--spec/controllers/application_controller_spec.rb71
-rw-r--r--spec/factories/personal_access_tokens.rb9
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb93
-rw-r--r--spec/models/personal_access_token_spec.rb15
-rw-r--r--spec/requests/api/api_helpers_spec.rb76
19 files changed, 540 insertions, 55 deletions
diff --git a/CHANGELOG b/CHANGELOG
index b80980fbf81..ddc087162bd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -36,6 +36,7 @@ v 8.9.0 (unreleased)
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
- Pipelines can be canceled only when there are running builds
+ - Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d8ea07559ab..8db50063233 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -259,3 +259,8 @@ $calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
+
+/*
+ * Personal Access Tokens
+ */
+$personal-access-tokens-disabled-label-color: #bbb;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 167ab40d881..88e062d156f 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -192,6 +192,14 @@
}
}
+.personal-access-tokens-never-expires-label {
+ color: $personal-access-tokens-disabled-label-color;
+}
+
+.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
+ text-align: center;
+}
+
.user-profile {
@media (max-width: $screen-xs-max) {
.cover-block {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 62f63701799..e71273e695d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -63,18 +63,8 @@ class ApplicationController < ActionController::Base
end
end
- # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
- # https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
def authenticate_user_from_token!
- user_token = if params[:authenticity_token].presence
- params[:authenticity_token].presence
- elsif params[:private_token].presence
- params[:private_token].presence
- elsif request.headers['PRIVATE-TOKEN'].present?
- request.headers['PRIVATE-TOKEN']
- end
- user = user_token && User.find_by_authentication_token(user_token.to_s)
-
+ user = get_user_from_private_token || get_user_from_personal_access_token
if user
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
@@ -385,4 +375,17 @@ class ApplicationController < ActionController::Base
(controller_name == 'groups' && action_name == page_type) ||
(controller_name == 'dashboard' && action_name == page_type)
end
+
+ # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
+ # https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
+ def get_user_from_private_token
+ user_token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+ User.find_by_authentication_token(user_token.to_s) if user_token
+ end
+
+ def get_user_from_personal_access_token
+ token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+ personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
+ personal_access_token.user if personal_access_token
+ end
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
new file mode 100644
index 00000000000..86e08fed8e2
--- /dev/null
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -0,0 +1,40 @@
+class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
+ before_action :load_personal_access_tokens, only: :index
+
+ def index
+ @personal_access_token = current_user.personal_access_tokens.build
+ end
+
+ def create
+ @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
+
+ if @personal_access_token.save
+ flash[:personal_access_token] = @personal_access_token.token
+ redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
+ else
+ load_personal_access_tokens
+ render :index
+ end
+ end
+
+ def revoke
+ @personal_access_token = current_user.personal_access_tokens.find(params[:id])
+
+ if @personal_access_token.revoke!
+ redirect_to profile_personal_access_tokens_path, notice: "Revoked personal access token #{@personal_access_token.name}!"
+ else
+ redirect_to profile_personal_access_tokens_path, alert: "Could not revoke personal access token #{@personal_access_token.name}."
+ end
+ end
+
+ private
+
+ def personal_access_token_params
+ params.require(:personal_access_token).permit(:name, :expires_at)
+ end
+
+ def load_personal_access_tokens
+ @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
+ @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
+ end
+end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
new file mode 100644
index 00000000000..c4b095e0c04
--- /dev/null
+++ b/app/models/personal_access_token.rb
@@ -0,0 +1,20 @@
+class PersonalAccessToken < ActiveRecord::Base
+ include TokenAuthenticatable
+ add_authentication_token_field :token
+
+ belongs_to :user
+
+ scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
+ scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+
+ def self.generate(params)
+ personal_access_token = self.new(params)
+ personal_access_token.ensure_token
+ personal_access_token
+ end
+
+ def revoke!
+ self.revoked = true
+ self.save
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index e0987e07e1f..3449544c170 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -49,6 +49,7 @@ class User < ActiveRecord::Base
# Profile
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
+ has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 09d9f0184be..a0509f786d1 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -16,6 +16,11 @@
= icon('cloud fw')
%span
Applications
+ = nav_link(controller: :personal_access_tokens) do
+ = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
+ = icon('ticket fw')
+ %span
+ Personal Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
= icon('envelope-o fw')
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
new file mode 100644
index 00000000000..4ad07df42dc
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -0,0 +1,113 @@
+- page_title "Personal Access Tokens"
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can generate a personal access token for each application you use that needs access to the GitLab API.
+ .col-lg-9
+
+ - if flash[:personal_access_token]
+ .created-personal-access-token
+ %h5.prepend-top-0
+ Your New Personal Access Token
+ .form-group
+ .input-group
+ = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
+ .input-group-addon= clipboard_button(clipboard_text: flash[:personal_access_token])
+ %span#created-personal-access-token-help-block.help-block Make sure you save it - you won't be able to access it again.
+
+ %hr
+
+ %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.
+ = form_for [:profile, @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.hidden_field :expires_at, class: "form-control", required: false
+ .datepicker.personal-access-tokens-expires-at
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
+
+ %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
+ %tbody
+ - @active_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires_at.present?
+ = token.expires_at.to_date.to_s(:medium)
+ - else
+ %span.personal-access-tokens-never-expires-label Never
+ %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this certificate? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ You don't have any active tokens yet.
+
+ %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
+ There are no inactive tokens.
+
+
+:javascript
+ var date = $('#personal_access_token_expires_at').val();
+
+ var datepicker = $(".datepicker").datepicker({
+ altFormat: "yy-mm-dd",
+ altField: "#personal_access_token_expires_at",
+ minDate: 0
+ });
+
+ if (date) {
+ datepicker.datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', date));
+ }
+ else {
+ datepicker.datepicker("setDate", null);
+ }
+
+ $("#created-personal-access-token").click(function() {
+ this.select();
+ });
diff --git a/config/routes.rb b/config/routes.rb
index 4191ec3598c..f2be5d502b5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -343,6 +343,13 @@ Rails.application.routes.draw do
resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
+
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
diff --git a/db/migrate/20160415062917_create_personal_access_tokens.rb b/db/migrate/20160415062917_create_personal_access_tokens.rb
new file mode 100644
index 00000000000..2c9f773e308
--- /dev/null
+++ b/db/migrate/20160415062917_create_personal_access_tokens.rb
@@ -0,0 +1,13 @@
+class CreatePersonalAccessTokens < ActiveRecord::Migration
+ def change
+ create_table :personal_access_tokens do |t|
+ t.references :user, index: true, foreign_key: true, null: false
+ t.string :token, index: {unique: true}, null: false
+ t.string :name, null: false
+ t.boolean :revoked, default: false
+ t.datetime :expires_at
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b7adf48fdb4..b4f76374598 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -724,6 +724,19 @@ ActiveRecord::Schema.define(version: 20160608155312) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "personal_access_tokens", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "token", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "revoked", default: false
+ t.datetime "expires_at"
+ end
+
+ add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
@@ -1064,5 +1077,6 @@ ActiveRecord::Schema.define(version: 20160608155312) do
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_foreign_key "personal_access_tokens", "users"
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/api/README.md b/doc/api/README.md
index 27c5962decf..1b3f58fbab9 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -37,13 +37,11 @@ following locations:
## Authentication
-All API requests require authentication. You need to pass a `private_token`
-parameter via query string or header. If passed as a header, the header name
-must be `PRIVATE-TOKEN` (uppercase and with a dash instead of an underscore).
-You can find or reset your private token in your account page (`/profile/account`).
+All API requests require authentication via a token. There are three types of tokens
+available: private tokens, OAuth 2 tokens, and personal access tokens.
-If `private_token` is invalid or omitted, then an error message will be
-returned with status code `401`:
+If a token is invalid or omitted, an error message will be returned with
+status code `401`:
```json
{
@@ -51,42 +49,56 @@ returned with status code `401`:
}
```
-API requests should be prefixed with `api` and the API version. The API version
-is defined in [`lib/api.rb`][lib-api-url].
+### Private Tokens
-Example of a valid API request:
+You need to pass a `private_token` parameter via query string or header. If passed as a
+header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
+an underscore). You can find or reset your private token in your account page
+(`/profile/account`).
-```shell
-GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
-```
+### OAuth 2 Tokens
-Example of a valid API request using cURL and authentication via header:
+You can use an OAuth 2 token to authenticate with the API by passing it either in the
+`access_token` parameter or in the `Authorization` header.
+
+Example of using the OAuth2 token in the header:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
```
-The API uses JSON to serialize data. You don't need to specify `.json` at the
-end of an API URL.
+Read more about [GitLab as an OAuth2 client](oauth2.md).
+
+### Personal Access Tokens
-## Authentication with OAuth2 token
+> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8
-Instead of the `private_token` you can transmit the OAuth2 access token as a
-header or as a parameter.
+You can create as many personal access tokens as you like from your GitLab
+profile (`/profile/personal_access_tokens`); perhaps one for each application
+that needs access to the GitLab API.
-Example of OAuth2 token as a parameter:
+Once you have your token, pass it to the API using either the `private_token`
+parameter or the `PRIVATE-TOKEN` header.
+
+## Basic Usage
+
+API requests should be prefixed with `api` and the API version. The API version
+is defined in [`lib/api.rb`][lib-api-url].
+
+Example of a valid API request:
```shell
-curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
```
-Example of OAuth2 token as a header:
+Example of a valid API request using cURL and authentication via header:
```shell
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+The API uses JSON to serialize data. You don't need to specify `.json` at the
+end of an API URL.
## Status codes
@@ -323,3 +335,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API"
[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
+[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 2aaa0557ea3..a179fe9f2f9 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -4,14 +4,26 @@ module API
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
+ PERSONAL_ACCESS_TOKEN_PARAM = PRIVATE_TOKEN_PARAM
+ PERSONAL_ACCESS_TOKEN_HEADER = PRIVATE_TOKEN_HEADER
def parse_boolean(value)
[ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
end
- def current_user
+ def find_user_by_private_token
private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
- @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
+ User.find_by_authentication_token(private_token)
+ end
+
+ def find_user_by_personal_access_token
+ personal_access_token_string = (params[PERSONAL_ACCESS_TOKEN_PARAM] || env[PERSONAL_ACCESS_TOKEN_HEADER]).to_s
+ personal_access_token = PersonalAccessToken.active.find_by_token(personal_access_token_string)
+ personal_access_token.user if personal_access_token
+ end
+
+ def current_user
+ @current_user ||= (find_user_by_private_token || find_user_by_personal_access_token || doorkeeper_guard)
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
return nil
@@ -33,7 +45,7 @@ module API
identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
# Regex for integers
- if !!(identifier =~ /^[0-9]+$/)
+ if !!(identifier =~ /\A[0-9]+\z/)
identifier.to_i
else
identifier
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 186239d3096..ff5b3916273 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -30,4 +30,75 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
end
+
+ describe "#authenticate_user_from_token!" do
+ describe "authenticating a user from a private token" do
+ controller(ApplicationController) do
+ def index
+ render text: "authenticated"
+ end
+ end
+
+ let(:user) { create(:user) }
+
+ context "when the 'private_token' param is populated with the private token" do
+ it "logs the user in" do
+ get :index, private_token: user.private_token
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+
+ context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
+ it "logs the user in" do
+ @request.headers['PRIVATE-TOKEN'] = user.private_token
+ get :index
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ @request.headers['PRIVATE-TOKEN'] = "token"
+ get :index, private_token: "token", authenticity_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq("authenticated")
+ end
+ end
+
+ describe "authenticating a user from a personal access token" do
+ controller(ApplicationController) do
+ def index
+ render text: 'authenticated'
+ end
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
+ end
+ end
+ end
end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
new file mode 100644
index 00000000000..da4c72bcb5b
--- /dev/null
+++ b/spec/factories/personal_access_tokens.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :personal_access_token do
+ user
+ token { SecureRandom.hex(50) }
+ name { FFaker::Product.brand }
+ revoked false
+ expires_at { 5.days.from_now }
+ end
+end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..105c5dae34e
--- /dev/null
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe 'Profile > Personal Access Tokens', feature: true, js: true do
+ 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 input").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(user)
+ end
+
+ describe "token creation" do
+ it "allows creation of a token" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text("Never")
+ end
+
+ it "allows creation of a token with an expiry date" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ # Set date to 1st of next month
+ find("a[title='Next']").click
+ click_on "1"
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ end
+
+ context "when creation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+ 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 profile_personal_access_tokens_path
+ 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 profile_personal_access_tokens_path
+
+ 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 profile_personal_access_tokens_path
+
+ expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+ expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(page).to have_content("Could not revoke")
+ end
+ end
+ end
+end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
new file mode 100644
index 00000000000..46eb71cef14
--- /dev/null
+++ b/spec/models/personal_access_token_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe PersonalAccessToken, models: true do
+ describe ".generate" do
+ it "generates a random token" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token.token).to be_present
+ end
+
+ it "doesn't save the record" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 0c19094ec54..06997147b09 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
-describe API, api: true do
+describe API::Helpers, api: true do
+
include API::Helpers
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -39,24 +41,64 @@ describe API, api: true do
end
describe ".current_user" do
- it "should return nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it "should return nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
+ describe "when authenticating using a user's private token" do
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ expect(current_user).to eq(user)
+ end
end
- it "should leave user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- expect(current_user).to eq(user)
- clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
- expect(current_user).to eq(user)
+ describe "when authenticating using a user's personal access tokens" do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PERSONAL_ACCESS_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PERSONAL_ACCESS_TOKEN_HEADER] = personal_access_token.token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PERSONAL_ACCESS_TOKEN_HEADER] = personal_access_token.token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PERSONAL_ACCESS_TOKEN_PARAM] = personal_access_token.token
+ expect(current_user).to eq(user)
+ end
+
+ it 'does not allow revoked tokens' do
+ personal_access_token.revoke!
+ env[API::Helpers::PERSONAL_ACCESS_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it 'does not allow expired tokens' do
+ personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ env[API::Helpers::PERSONAL_ACCESS_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
end
it "should change current user to sudo when admin" do