Commit 56dccc2e authored by Sean McGivern's avatar Sean McGivern

Merge branch 'dm-remove-private-token' into 'master'

Remove Private Tokens

Closes #38595 and #38447

See merge request gitlab-org/gitlab-ce!14838
parents 98343637 d0af6047
...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController ...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build @impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute @inactive_impersonation_tokens = finder(state: 'inactive').execute
......
...@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base ...@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_personal_access_token!
before_action :authenticate_user_from_rss_token! before_action :authenticate_user_from_rss_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
...@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base ...@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user) return try(:authenticated_user)
end end
# This filter handles both private tokens and personal access tokens def authenticate_user_from_personal_access_token!
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
return unless token.present? return unless token.present?
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) user = User.find_by_personal_access_token(token)
sessionless_sign_in(user) sessionless_sign_in(user)
end end
......
...@@ -30,11 +30,11 @@ class JwtController < ApplicationController ...@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized render_unauthorized
end end
end end
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_token render_missing_personal_access_token
end end
def render_missing_personal_token def render_missing_personal_access_token
render json: { render json: {
errors: [ errors: [
{ code: 'UNAUTHORIZED', { code: 'UNAUTHORIZED',
......
...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth.available_scopes @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
......
...@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
end end
end end
def reset_private_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_authentication_token!
end
flash[:notice] = "Private token was successfully reset"
redirect_to profile_account_path
end
def reset_incoming_email_token def reset_incoming_email_token
Users::UpdateService.new(current_user, user: @user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token! user.reset_incoming_email_token!
...@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset" flash[:notice] = "Incoming email token was successfully reset"
redirect_to profile_account_path redirect_to profile_personal_access_tokens_path
end end
def reset_rss_token def reset_rss_token
...@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset" flash[:notice] = "RSS token was successfully reset"
redirect_to profile_account_path redirect_to profile_personal_access_tokens_path
end end
def audit_log def audit_log
......
...@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401 render plain: "HTTP Basic: Access denied\n", status: 401
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_token render_missing_personal_access_token
end end
def basic_auth_provided? def basic_auth_provided?
...@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end end
def render_missing_personal_token def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \ render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \ "You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}", "You can generate one at #{profile_personal_access_tokens_url}",
......
...@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken ...@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User' belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
alias_method :user, :resource_owner alias_attribute :user, :resource_owner
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
else
super
end
end
end end
...@@ -21,8 +21,8 @@ class User < ActiveRecord::Base ...@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email ignore_column :external_email
ignore_column :email_provider ignore_column :email_provider
ignore_column :authentication_token
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token add_authentication_token_field :rss_token
...@@ -163,7 +163,7 @@ class User < ActiveRecord::Base ...@@ -163,7 +163,7 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed? before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed? before_validation :set_public_email, if: :public_email_changed?
before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed? before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
...@@ -185,8 +185,6 @@ class User < ActiveRecord::Base ...@@ -185,8 +185,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files] enum project_view: [:readme, :activity, :files]
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do state_machine :state, initial: :active do
......
...@@ -39,11 +39,8 @@ class AccessTokenValidationService ...@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym) token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope| required_scopes.any? do |scope|
if scope.respond_to?(:sufficient?) scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
scope.sufficient?(token_scopes, request) scope.sufficient?(token_scopes, request)
else
API::Scope.new(scope).sufficient?(token_scopes, request)
end
end end
end end
end end
......
- name = label.parameterize
- attribute = name.underscore
.reset-action
%p.cgray
= label_tag name, label, class: "label-light"
= text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
= help_text
.prepend-top-default
= link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
...@@ -6,22 +6,6 @@ ...@@ -6,22 +6,6 @@
.alert.alert-info .alert.alert-info
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Private Tokens
%p
Keep these tokens secret, anyone with access to them can interact with
GitLab as if they were you.
.col-lg-8.private-tokens-reset
= render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
= render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
- if incoming_email_token_enabled?
= render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
%hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
......
...@@ -30,3 +30,40 @@ ...@@ -30,3 +30,40 @@
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
RSS token
%p
Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
%p
It cannot be used to access any other data.
.col-lg-8.rss-token-reset
= label_tag :rss_token, 'RSS token', class: "label-light"
= text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
You should
= link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
if that ever happens.
- if incoming_email_token_enabled?
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Incoming email token
%p
Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
%p
It cannot be used to access any other data.
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
You should
= link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
if that ever happens.
---
title: Add sudo scope for OAuth and Personal Access Tokens to be used by admins to
impersonate other users on the API
merge_request:
author:
type: added
---
title: Convert private tokens to Personal Access Tokens with sudo scope
merge_request:
author:
type: security
---
title: Remove private tokens from web interface and API
merge_request:
author:
type: security
---
title: Remove Session API now that private tokens are removed from user API endpoints
merge_request:
author:
type: removed
...@@ -58,9 +58,10 @@ en: ...@@ -58,9 +58,10 @@ en:
expired: "The access token expired" expired: "The access token expired"
unknown: "The access token is invalid" unknown: "The access token is invalid"
scopes: scopes:
api: Access your API api: Access the authenticated user's API
read_user: Read user information read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
flash: flash:
applications: applications:
......
...@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do ...@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do
get :audit_log get :audit_log
get :applications, to: 'oauth/applications#index' get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :reset_incoming_email_token put :reset_incoming_email_token
put :reset_rss_token put :reset_rss_token
put :update_username put :update_username
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MigrateUserAuthenticationTokenToPersonalAccessToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# disable_ddl_transaction!
TOKEN_NAME = 'Private Token'.freeze
def up
execute <<~SQL
INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api].to_yaml}'
FROM users
WHERE authentication_token IS NOT NULL
AND admin = FALSE
AND NOT EXISTS (
SELECT true
FROM personal_access_tokens
WHERE user_id = users.id
AND token = users.authentication_token
)
SQL
# Admins also need the `sudo` scope
execute <<~SQL
INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api sudo].to_yaml}'
FROM users
WHERE authentication_token IS NOT NULL
AND admin = TRUE
AND NOT EXISTS (
SELECT true
FROM personal_access_tokens
WHERE user_id = users.id
AND token = users.authentication_token
)
SQL
end
def down
if Gitlab::Database.postgresql?
execute <<~SQL
UPDATE users
SET authentication_token = pats.token
FROM (
SELECT user_id, token
FROM personal_access_tokens
WHERE name = '#{TOKEN_NAME}'
) AS pats
WHERE id = pats.user_id
SQL
else
execute <<~SQL
UPDATE users
INNER JOIN personal_access_tokens AS pats
ON users.id = pats.user_id
SET authentication_token = pats.token
WHERE pats.name = '#{TOKEN_NAME}'
SQL
end
execute <<~SQL
DELETE FROM personal_access_tokens
WHERE name = '#{TOKEN_NAME}'
AND EXISTS (
SELECT true
FROM users
WHERE id = personal_access_tokens.user_id
AND authentication_token = personal_access_tokens.token
)
SQL
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveUserAuthenticationToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_column :users, :authentication_token
end
def down
add_column :users, :authentication_token, :string
add_concurrent_index :users, :authentication_token, unique: true
end
end
...@@ -1670,7 +1670,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do ...@@ -1670,7 +1670,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.string "skype", default: "", null: false t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false t.string "twitter", default: "", null: false
t.string "authentication_token"
t.string "bio" t.string "bio"
t.integer "failed_attempts", default: 0 t.integer "failed_attempts", default: 0
t.datetime "locked_at" t.datetime "locked_at"
...@@ -1720,7 +1719,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do ...@@ -1720,7 +1719,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
end end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
......
...@@ -141,7 +141,7 @@ separate Rails process to debug the issue: ...@@ -141,7 +141,7 @@ separate Rails process to debug the issue:
1. Log in to your GitLab account. 1. Log in to your GitLab account.
1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC). 1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC).
1. Obtain the private token for your user (Profile Settings -> Account). 1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens).
1. Bring up the GitLab Rails console. For omnibus users, run: 1. Bring up the GitLab Rails console. For omnibus users, run:
``` ```
......
...@@ -50,7 +50,6 @@ following locations: ...@@ -50,7 +50,6 @@ following locations:
- [Repository Files](repository_files.md) - [Repository Files](repository_files.md)
- [Runners](runners.md) - [Runners](runners.md)
- [Services](services.md) - [Services](services.md)
- [Session](session.md)
- [Settings](settings.md) - [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md) - [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md) - [System Hooks](system_hooks.md)
...@@ -86,27 +85,10 @@ API requests should be prefixed with `api` and the API version. The API version ...@@ -86,27 +85,10 @@ API requests should be prefixed with `api` and the API version. The API version
is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API
is at `/api/v4`. is at `/api/v4`.
For endpoints that require [authentication](#authentication), you need to pass Example of a valid API request using cURL:
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).
Example of a valid API request:
```
GET /projects?private_token=9koXpg98eAheJpvBs5tK
```
Example of a valid API request using cURL and authentication via header:
```shell ```shell
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" curl "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API request using cURL and authentication via a query string:
```shell
curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK"
``` ```
The API uses JSON to serialize data. You don't need to specify `.json` at the The API uses JSON to serialize data. You don't need to specify `.json` at the
...@@ -114,15 +96,20 @@ end of an API URL. ...@@ -114,15 +96,20 @@ end of an API URL.
## Authentication ## Authentication
Most API requests require authentication via a session cookie or token. For Most API requests require authentication, or will only return public data when
authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation those cases where it is not required, this will be mentioned in the documentation
for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
There are three types of access tokens available: There are three ways to authenticate with the GitLab API:
1. [OAuth2 tokens](#oauth2-tokens) 1. [OAuth2 tokens](#oauth2-tokens)
1. [Private tokens](#private-tokens)
1. [Personal access tokens](#personal-access-tokens) 1. [Personal access tokens](#personal-access-tokens)
1. [Session cookie](#session-cookie)
For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available:
1. [Impersonation tokens](#impersonation-tokens)
2. [Sudo](#sudo)
If authentication information is invalid or omitted, an error message will be If authentication information is invalid or omitted, an error message will be
returned with status code `401`: returned with status code `401`:
...@@ -133,74 +120,84 @@ returned with status code `401`: ...@@ -133,74 +120,84 @@ returned with status code `401`:
} }
``` ```
### Session cookie ### OAuth2 tokens
When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the
set. The API will use this cookie for authentication if it is present, but using `access_token` parameter or the `Authorization` header.
the API to generate a new session cookie is currently not supported.
### OAuth2 tokens Example of using the OAuth2 token in a parameter:
You can use an OAuth 2 token to authenticate with the API by passing it either in the ```shell
`access_token` parameter or in the `Authorization` header. curl https://gitlab.example.com/api/v4/projects?access_token=OAUTH-TOKEN
```
Example of using the OAuth2 token in the header: Example of using the OAuth2 token in a header:
```shell ```shell
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects
``` ```
Read more about [GitLab as an OAuth2 client](oauth2.md). Read more about [GitLab as an OAuth2 provider](oauth2.md).
### Private tokens ### Personal access tokens
Private tokens provide full access to the GitLab API. Anyone with access to You can use a [personal access token][pat] to authenticate with the API by passing it in either the
them can interact with GitLab as if they were you. You can find or reset your `private_token` parameter or the `Private-Token` header.
private token in your account page (`/profile/account`).
For examples of usage, [read the basic usage section](#basic-usage). Example of using the personal access token in a parameter:
### Personal access tokens ```shell
curl https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
```
Example of using the personal access token in a header:
Instead of using your private token which grants full access to your account, ```shell
personal access tokens could be a better fit because of their granular curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects
permissions. ```
Once you have your token, pass it to the API using either the `private_token` Read more about [personal access tokens][pat].
parameter or the `PRIVATE-TOKEN` header. For examples of usage,
[read the basic usage section](#basic-usage). ### Session cookie
When signing in to the main GitLab application, a `_gitlab_session` cookie is
set. The API will use this cookie for authentication if it is present, but using
the API to generate a new session cookie is currently not supported.
[Read more about personal access tokens.][pat] The primary user of this authentication method is the web frontend of GitLab itself,
which can use the API as the authenticated user to get a list of their projects,
for example, without needing to explicitly pass an access token.
### Impersonation tokens ### Impersonation tokens
> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. > [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
Impersonation tokens are a type of [personal access token][pat] Impersonation tokens are a type of [personal access token][pat]
that can only be created by an admin for a specific user. that can only be created by an admin for a specific user. They are a great fit
if you want to build applications or scripts that authenticate with the API as a specific user.
They are a better alternative to using the user's password/private token They are an alternative to directly using the user's password or one of their
or using the [Sudo](#sudo) feature which also requires the admin's password personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo)
or private token, since the password/token can change over time. Impersonation password/token may not be known or may change over time.
tokens are a great fit if you want to build applications or tools which
authenticate with the API as a specific user.
For more information, refer to the For more information, refer to the
[users API](users.md#retrieve-user-impersonation-tokens) docs. [users API](users.md#retrieve-user-impersonation-tokens) docs.
For examples of usage, [read the basic usage section](#basic-usage). Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
`private_token` parameter or the `Private-Token` header.
### Sudo ### Sudo
> Needs admin permissions. > Needs admin permissions.
All API requests support performing an API call as if you were another user, All API requests support performing an API call as if you were another user,
provided your private token is from an administrator account. You need to pass provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope.
the `sudo` parameter either via query string or a header with an ID/username of
You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the the user you want to perform the operation as. If passed as a header, the
header name must be `SUDO` (uppercase). header name must be `Sudo`.
If a non administrative `private_token` is provided, then an error message will If a non administrative access token is provided, an error message will
be returned with status code `403`: be returned with status code `403`:
```json ```json
...@@ -209,12 +206,23 @@ be returned with status code `403`: ...@@ -209,12 +206,23 @@ be returned with status code `403`:
} }
``` ```
If an access token without the `sudo` scope is provided, an error message will
be returned with status code `403`:
```json
{
"error": "insufficient_scope",
"error_description": "The request requires higher privileges than provided by the access token.",
"scope": "sudo"
}
```
If the sudo user ID or username cannot be found, an error message will be If the sudo user ID or username cannot be found, an error message will be
returned with status code `404`: returned with status code `404`:
```json ```json
{ {
"message": "404 Not Found: No user id or username for: <id/username>" "message": "404 User with ID or username '123' Not Found"
} }
``` ```
...@@ -228,7 +236,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username ...@@ -228,7 +236,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
``` ```
```shell ```shell
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects" curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects"
``` ```
Example of a valid API call and a request using cURL with sudo request, Example of a valid API call and a request using cURL with sudo request,
...@@ -239,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 ...@@ -239,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
``` ```
```shell ```shell
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects"
``` ```
## Status codes ## Status codes
......
# Session API
>**Deprecation notice:**
Starting in GitLab 8.11, this feature has been **disabled** for users with
[two-factor authentication][2fa] turned on. These users can access the API
using [personal access tokens] instead.
You can login with both GitLab and LDAP credentials in order to obtain the
private token.
```
POST /session
```
| Attribute | Type | Required | Description |
| ---------- | ------- | -------- | -------- |
| `login` | string | yes | The username of the user|
| `email` | string | yes if login is not provided | The email of the user |
| `password` | string | yes | The password of the user |
```bash
curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd"
```
Example response:
```json
{
"name": "John Smith",
"username": "john_smith",
"id": 32,
"state": "active",
"avatar_url": null,
"created_at": "2015-01-29T21:07:19.440Z",
"is_admin": true,
"bio": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"email": "john@example.com",
"theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
"identities": [],
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": false,
"private_token": "9koXpg98eAheJpvBs5tK"
}
```
[2fa]: ../user/profile/account/two_factor_authentication.md
[personal access tokens]: ../user/profile/personal_access_tokens.md
...@@ -410,8 +410,7 @@ GET /user ...@@ -410,8 +410,7 @@ GET /user
"can_create_group": true, "can_create_group": true,
"can_create_project": true, "can_create_project": true,
"two_factor_enabled": true, "two_factor_enabled": true,
"external": false, "external": false
"private_token": "dd34asd13as"
} }
``` ```
......
...@@ -459,11 +459,11 @@ Rendered example: ...@@ -459,11 +459,11 @@ Rendered example:
### cURL commands ### cURL commands
- Use `https://gitlab.example.com/api/v4/` as an endpoint. - Use `https://gitlab.example.com/api/v4/` as an endpoint.
- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`. - Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`.
- Always put the request first. `GET` is the default so you don't have to - Always put the request first. `GET` is the default so you don't have to
include it. include it.
- Use double quotes to the URL when it includes additional parameters. - Use double quotes to the URL when it includes additional parameters.
- Prefer to use examples using the private token and don't pass data of - Prefer to use examples using the personal access token and don't pass data of
username and password. username and password.
| Methods | Description | | Methods | Description |
......
...@@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml ...@@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start sudo /etc/init.d/gitlab start
``` ```
## Clear authentication tokens for all users. Important! Data loss!
Clear authentication tokens for all users in the GitLab database. This
task is useful if your users' authentication tokens might have been exposed in
any way. All the existing tokens will become invalid, and new tokens are
automatically generated upon sign-in or user modification.
```
# omnibus-gitlab
sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
# installation from source
bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
```
...@@ -517,7 +517,7 @@ Feature.get(:auto_devops_banner_disabled).enable ...@@ -517,7 +517,7 @@ Feature.get(:auto_devops_banner_disabled).enable
Or through the HTTP API with an admin access token: Or through the HTTP API with an admin access token:
```sh ```sh
curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
``` ```
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
......
...@@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b ...@@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b
From there, you can: From there, you can:
- Update your personal information - Update your personal information
- Manage [private tokens](../../api/README.md#private-tokens), email tokens, [2FA](account/two_factor_authentication.md) - Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md) - Change your username and [delete your account](account/delete_account.md)
- Manage applications that can - Manage applications that can
[use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth) [use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
......
...@@ -2,17 +2,15 @@ ...@@ -2,17 +2,15 @@
> [Introduced][ce-3749] in GitLab 8.8. > [Introduced][ce-3749] in GitLab 8.8.
Personal access tokens are useful if you need access to the [GitLab API][api]. Personal access tokens are the preferred way for third party applications and scripts to
Instead of using your private token which grants full access to your account, authenticate with the [GitLab API][api], if using [OAuth2](../../api/oauth2.md) is not practical.
personal access tokens could be a better fit because of their
[granular permissions](#limiting-scopes-of-a-personal-access-token).
You can also use them to authenticate against Git over HTTP. They are the only You can also use them to authenticate against Git over HTTP. They are the only
accepted method of authentication when you have accepted method of authentication when you have
[Two-Factor Authentication (2FA)][2fa] enabled. [Two-Factor Authentication (2FA)][2fa] enabled.
Once you have your token, [pass it to the API][usage] using either the Once you have your token, [pass it to the API][usage] using either the
`private_token` parameter or the `PRIVATE-TOKEN` header. `private_token` parameter or the `Private-Token` header.
The expiration of personal access tokens happens on the date you define, The expiration of personal access tokens happens on the date you define,
at midnight UTC. at midnight UTC.
...@@ -49,12 +47,14 @@ the following table. ...@@ -49,12 +47,14 @@ the following table.
|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). | |`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). |
| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. | | `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. |
| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). | | `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). |
| `sudo` | Allows performing API actions as any user in the system (if the authenticated user is an admin) ([introduced][ce-14838] in GitLab 10.2). |
[2fa]: ../account/two_factor_authentication.md [2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md [api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 [ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951 [ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 [ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838
[container registry]: ../project/container_registry.md [container registry]: ../project/container_registry.md
[users]: ../../api/users.md [users]: ../../api/users.md
[usage]: ../../api/README.md#basic-usage [usage]: ../../api/README.md#personal-access-tokens
...@@ -142,7 +142,6 @@ module API ...@@ -142,7 +142,6 @@ module API
mount ::API::Runner mount ::API::Runner
mount ::API::Runners mount ::API::Runners
mount ::API::Services mount ::API::Services
mount ::API::Session
mount ::API::Settings mount ::API::Settings
mount ::API::SidekiqMetrics mount ::API::SidekiqMetrics
mount ::API::Snippets mount ::API::Snippets
......
...@@ -42,72 +42,42 @@ module API ...@@ -42,72 +42,42 @@ module API
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods module HelperMethods
def find_current_user def find_current_user!
user = user = find_user_from_access_token || find_user_from_warden
find_user_from_private_token || return unless user
find_user_from_oauth_token ||
find_user_from_warden
return nil unless user forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user user
end end
def private_token def access_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] return @access_token if defined?(@access_token)
end
private
def find_user_from_private_token
token_string = private_token.to_s
return nil unless token_string.present?
user = @access_token = find_oauth_access_token || find_personal_access_token
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string)
raise UnauthorizedError unless user
user
end end
# Invokes the doorkeeper guard. def validate_access_token!(scopes: [])
#
# If token is presented and valid, then it sets @current_user.
#
# If the token does not have sufficient scopes to cover the requred scopes,
# then it raises InsufficientScopeError.
#
# If the token is expired, then it raises ExpiredError.
#
# If the token is revoked, then it raises RevokedError.
#
# If the token is not found (nil), then it returns nil
#
# Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
def find_user_from_oauth_token
access_token = find_oauth_access_token
return unless access_token return unless access_token
find_user_by_access_token(access_token) case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
end
end end
def find_user_by_authentication_token(token_string) private
User.find_by_authentication_token(token_string)
end
def find_user_by_personal_access_token(token_string) def find_user_from_access_token
access_token = PersonalAccessToken.find_by_token(token_string)
return unless access_token return unless access_token
find_user_by_access_token(access_token) validate_access_token!
access_token.user || raise(UnauthorizedError)
end end
# Check the Rails session for valid authentication details # Check the Rails session for valid authentication details
...@@ -125,34 +95,26 @@ module API ...@@ -125,34 +95,26 @@ module API
end end
def find_oauth_access_token def find_oauth_access_token
return @oauth_access_token if defined?(@oauth_access_token)
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @oauth_access_token = nil unless token return unless token
@oauth_access_token = OauthAccessToken.by_token(token) # Expiration, revocation and scopes are verified in `find_user_by_access_token`
raise UnauthorizedError unless @oauth_access_token access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless access_token
@oauth_access_token.revoke_previous_refresh_token! access_token.revoke_previous_refresh_token!
@oauth_access_token access_token
end end
def find_user_by_access_token(access_token) def find_personal_access_token
scopes = scopes_registered_for_endpoint token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
return unless token.present?
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) # Expiration, revocation and scopes are verified in `find_user_by_access_token`
when AccessTokenValidationService::INSUFFICIENT_SCOPE access_token = PersonalAccessToken.find_by(token: token)
raise InsufficientScopeError.new(scopes) raise UnauthorizedError unless access_token
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED access_token
raise RevokedError
when AccessTokenValidationService::VALID
access_token.user
end
end end
def doorkeeper_request def doorkeeper_request
...@@ -236,7 +198,7 @@ module API ...@@ -236,7 +198,7 @@ module API
class InsufficientScopeError < StandardError class InsufficientScopeError < StandardError
attr_reader :scopes attr_reader :scopes
def initialize(scopes) def initialize(scopes)
@scopes = scopes @scopes = scopes.map { |s| s.try(:name) || s }
end end
end end
end end
......
...@@ -57,10 +57,6 @@ module API ...@@ -57,10 +57,6 @@ module API
expose :admin?, as: :is_admin expose :admin?, as: :is_admin
end end
class UserWithPrivateDetails < UserWithAdmin
expose :private_token
end
class Email < Grape::Entity class Email < Grape::Entity
expose :id, :email expose :id, :email
end end
......
...@@ -41,6 +41,8 @@ module API ...@@ -41,6 +41,8 @@ module API
sudo! sudo!
validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
@current_user @current_user
end end
...@@ -385,7 +387,7 @@ module API ...@@ -385,7 +387,7 @@ module API
return @initial_current_user if defined?(@initial_current_user) return @initial_current_user if defined?(@initial_current_user)
begin begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user } @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
rescue APIGuard::UnauthorizedError rescue APIGuard::UnauthorizedError
unauthorized! unauthorized!
end end
...@@ -393,24 +395,23 @@ module API ...@@ -393,24 +395,23 @@ module API
def sudo! def sudo!
return unless sudo_identifier return unless sudo_identifier
return unless initial_current_user
unauthorized! unless initial_current_user
unless initial_current_user.admin? unless initial_current_user.admin?
forbidden!('Must be admin to use sudo') forbidden!('Must be admin to use sudo')
end end
# Only private tokens should be used for the SUDO feature unless access_token
unless private_token == initial_current_user.private_token forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
forbidden!('Private token must be specified in order to use sudo')
end end
validate_access_token!(scopes: [:sudo])
sudoed_user = find_user(sudo_identifier) sudoed_user = find_user(sudo_identifier)
not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
if sudoed_user @current_user = sudoed_user
@current_user = sudoed_user
else
not_found!("No user id or username for: #{sudo_identifier}")
end
end end
def sudo_identifier def sudo_identifier
......
module API
class Session < Grape::API
desc 'Login to get token' do
success Entities::UserWithPrivateDetails
end
params do
optional :login, type: String, desc: 'The username'
optional :email, type: String, desc: 'The email of the user'
requires :password, type: String, desc: 'The password of the user'
at_least_one_of :login, :email
end
post "/session" do
user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
return unauthorized! unless user
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
present user, with: Entities::UserWithPrivateDetails
end
end
end
...@@ -507,9 +507,7 @@ module API ...@@ -507,9 +507,7 @@ module API
end end
get do get do
entity = entity =
if sudo? if current_user.admin?
Entities::UserWithPrivateDetails
elsif current_user.admin?
Entities::UserWithAdmin Entities::UserWithAdmin
else else
Entities::UserPublic Entities::UserPublic
......
module Gitlab module Gitlab
module Auth module Auth
MissingPersonalTokenError = Class.new(StandardError) MissingPersonalAccessTokenError = Class.new(StandardError)
REGISTRY_SCOPES = [:read_registry].freeze REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access # Scopes used for GitLab API access
API_SCOPES = [:api, :read_user].freeze API_SCOPES = [:api, :read_user, :sudo].freeze
# Scopes used for OpenID Connect # Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze OPENID_SCOPES = [:openid].freeze
...@@ -38,7 +38,7 @@ module Gitlab ...@@ -38,7 +38,7 @@ module Gitlab
# If sign-in is disabled and LDAP is not configured, recommend a # If sign-in is disabled and LDAP is not configured, recommend a
# personal access token on failed auth attempts # personal access token on failed auth attempts
raise Gitlab::Auth::MissingPersonalTokenError raise Gitlab::Auth::MissingPersonalAccessTokenError
end end
def find_with_user_password(login, password) def find_with_user_password(login, password)
...@@ -106,7 +106,7 @@ module Gitlab ...@@ -106,7 +106,7 @@ module Gitlab
user = find_with_user_password(login, password) user = find_with_user_password(login, password)
return unless user return unless user
raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled? raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities) Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end end
...@@ -128,7 +128,7 @@ module Gitlab ...@@ -128,7 +128,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
if token && valid_scoped_token?(token, available_scopes) if token && valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scope(token.scopes))
end end
end end
...@@ -226,8 +226,10 @@ module Gitlab ...@@ -226,8 +226,10 @@ module Gitlab
[] []
end end
def available_scopes def available_scopes(current_user = nil)
API_SCOPES + registry_scopes scopes = API_SCOPES + registry_scopes
scopes.delete(:sudo) if current_user && !current_user.admin?
scopes
end end
# Other available scopes # Other available scopes
......
namespace :gitlab do
namespace :users do
desc "GitLab | Clear the authentication token for all users"
task clear_all_authentication_tokens: :environment do |t, args|
# Do small batched updates because these updates will be slow and locking
User.select(:id).find_in_batches(batch_size: 100) do |batch|
User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
end
end
end
end
require_relative '../../app/models/concerns/token_authenticatable.rb' require_relative '../../app/models/concerns/token_authenticatable.rb'
namespace :tokens do namespace :tokens do
desc "Reset all GitLab user auth tokens" desc "Reset all GitLab incoming email tokens"
task reset_all_auth: :environment do
reset_all_users_token(:reset_authentication_token!)
end
desc "Reset all GitLab email tokens"
task reset_all_email: :environment do task reset_all_email: :environment do
reset_all_users_token(:reset_incoming_email_token!) reset_all_users_token(:reset_incoming_email_token!)
end end
...@@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base ...@@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users' self.table_name = 'users'
def reset_authentication_token!
write_new_token(:authentication_token)
save!(validate: false)
end
def reset_incoming_email_token! def reset_incoming_email_token!
write_new_token(:incoming_email_token) write_new_token(:incoming_email_token)
save!(validate: false) save!(validate: false)
......
...@@ -50,70 +50,36 @@ describe ApplicationController do ...@@ -50,70 +50,36 @@ describe ApplicationController do
end end
end end
describe "#authenticate_user_from_token!" do describe "#authenticate_user_from_personal_access_token!" do
describe "authenticating a user from a private token" do controller(described_class) do
controller(described_class) do def index
def index render text: 'authenticated'
render text: "authenticated"
end
end
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).to have_gitlab_http_status(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).to have_gitlab_http_status(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
end end
describe "authenticating a user from a personal access token" do let(:personal_access_token) { create(:personal_access_token, user: user) }
controller(described_class) do
def index
render text: 'authenticated'
end
end
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 context "when the 'personal_access_token' param is populated with the personal access token" do
it "logs the user in" do it "logs the user in" do
get :index, private_token: personal_access_token.token get :index, private_token: personal_access_token.token
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.body).to eq('authenticated') expect(response.body).to eq('authenticated')
end
end end
end
context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
it "logs the user in" do it "logs the user in" do
@request.headers["PRIVATE-TOKEN"] = personal_access_token.token @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
get :index get :index
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.body).to eq('authenticated') expect(response.body).to eq('authenticated')
end
end end
end
it "doesn't log the user in otherwise" do it "doesn't log the user in otherwise" do
get :index, private_token: "token" get :index, private_token: "token"
expect(response.status).not_to eq(200) expect(response.status).not_to eq(200)
expect(response.body).not_to eq('authenticated') expect(response.body).not_to eq('authenticated')
end
end end
end end
...@@ -152,11 +118,15 @@ describe ApplicationController do ...@@ -152,11 +118,15 @@ describe ApplicationController do
end end
end end
before do
sign_in user
end
context 'when format is handled' do context 'when format is handled' do
let(:requested_format) { :json } let(:requested_format) { :json }
it 'returns 200 response' do it 'returns 200 response' do
get :index, private_token: user.private_token, format: requested_format get :index, format: requested_format
expect(response).to have_gitlab_http_status 200 expect(response).to have_gitlab_http_status 200
end end
...@@ -164,7 +134,7 @@ describe ApplicationController do ...@@ -164,7 +134,7 @@ describe ApplicationController do
context 'when format is not handled' do context 'when format is not handled' do
it 'returns 404 response' do it 'returns 404 response' do
get :index, private_token: user.private_token get :index
expect(response).to have_gitlab_http_status 404 expect(response).to have_gitlab_http_status 404
end end
......
...@@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do ...@@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do
end end
describe "atom feed" do describe "atom feed" do
it "renders atom feed via private token" do it "renders atom feed via personal access token" do
visit issues_dashboard_path(:atom, private_token: user.private_token) personal_access_token = create(:personal_access_token, user: user)
visit issues_dashboard_path(:atom, private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues") expect(body).to have_selector('title', text: "#{user.name} issues")
......
...@@ -4,9 +4,11 @@ describe "Dashboard Feed" do ...@@ -4,9 +4,11 @@ describe "Dashboard Feed" do
describe "GET /" do describe "GET /" do
let!(:user) { create(:user, name: "Jonh") } let!(:user) { create(:user, name: "Jonh") }
context "projects atom feed via private token" do context "projects atom feed via personal access token" do
it "renders projects atom feed" do it "renders projects atom feed" do
visit dashboard_projects_path(:atom, private_token: user.private_token) personal_access_token = create(:personal_access_token, user: user)
visit dashboard_projects_path(:atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title') expect(body).to have_selector('feed title')
end end
end end
......
...@@ -28,10 +28,12 @@ describe 'Issues Feed' do ...@@ -28,10 +28,12 @@ describe 'Issues Feed' do
end end
end end
context 'when authenticated via private token' do context 'when authenticated via personal access token' do
it 'renders atom feed' do it 'renders atom feed' do
personal_access_token = create(:personal_access_token, user: user)
visit project_issues_path(project, :atom, visit project_issues_path(project, :atom,
private_token: user.private_token) private_token: personal_access_token.token)
expect(response_headers['Content-Type']) expect(response_headers['Content-Type'])
.to have_content('application/atom+xml') .to have_content('application/atom+xml')
......
...@@ -4,9 +4,11 @@ describe "User Feed" do ...@@ -4,9 +4,11 @@ describe "User Feed" do
describe "GET /" do describe "GET /" do
let!(:user) { create(:user) } let!(:user) { create(:user) }
context 'user atom feed via private token' do context 'user atom feed via personal access token' do
it "renders user atom feed" do it "renders user atom feed" do
visit user_path(user, :atom, private_token: user.private_token) personal_access_token = create(:personal_access_token, user: user)
visit user_path(user, :atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title') expect(body).to have_selector('feed title')
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe 'Profile account page' do describe 'Profile account page', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
...@@ -56,47 +56,38 @@ describe 'Profile account page' do ...@@ -56,47 +56,38 @@ describe 'Profile account page' do
end end
end end
describe 'when I reset private token' do
before do
visit profile_account_path
end
it 'resets private token' do
previous_token = find("#private-token").value
click_link('Reset private token')
expect(find('#private-token').value).not_to eq(previous_token)
end
end
describe 'when I reset RSS token' do describe 'when I reset RSS token' do
before do before do
visit profile_account_path visit profile_personal_access_tokens_path
end end
it 'resets RSS token' do it 'resets RSS token' do
previous_token = find("#rss-token").value within('.rss-token-reset') do
previous_token = find("#rss_token").value
click_link('Reset RSS token') click_link('reset it')
expect(find('#rss_token').value).not_to eq(previous_token)
end
expect(page).to have_content 'RSS token was successfully reset' expect(page).to have_content 'RSS token was successfully reset'
expect(find('#rss-token').value).not_to eq(previous_token)
end end
end end
describe 'when I reset incoming email token' do describe 'when I reset incoming email token' do
before do before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
visit profile_account_path visit profile_personal_access_tokens_path
end end
it 'resets incoming email token' do it 'resets incoming email token' do
previous_token = find('#incoming-email-token').value within('.incoming-email-token-reset') do
previous_token = find('#incoming_email_token').value
click_link('Reset incoming email token') click_link('reset it')
expect(find('#incoming-email-token').value).not_to eq(previous_token) expect(find('#incoming_email_token').value).not_to eq(previous_token)
end
end end
end end
......
...@@ -27,11 +27,9 @@ ...@@ -27,11 +27,9 @@
"can_create_group", "can_create_group",
"can_create_project", "can_create_project",
"two_factor_enabled", "two_factor_enabled",
"external", "external"
"private_token"
], ],
"properties": { "properties": {
"$ref": "full.json", "$ref": "full.json"
"private_token": { "type": "string" }
} }
} }
...@@ -5,7 +5,7 @@ describe Gitlab::Auth do ...@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' do describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do it 'API_SCOPES contains all scopes for API access' do
expect(subject::API_SCOPES).to eq [:api, :read_user] expect(subject::API_SCOPES).to eq %i[api read_user sudo]
end end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
...@@ -19,7 +19,7 @@ describe Gitlab::Auth do ...@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
expect(subject.optional_scopes).to eq %i[read_user read_registry openid] expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid]
end end
context 'registry_scopes' do context 'registry_scopes' do
...@@ -164,7 +164,7 @@ describe Gitlab::Auth do ...@@ -164,7 +164,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['api']) personal_access_token = create(:personal_access_token, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') 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)) 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_access_token, full_authentication_abilities))
end end
context 'when registry is enabled' do context 'when registry is enabled' do
...@@ -176,7 +176,7 @@ describe Gitlab::Auth do ...@@ -176,7 +176,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['read_registry']) personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') 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, [:read_container_image])) 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_access_token, [:read_container_image]))
end end
end end
...@@ -184,14 +184,14 @@ describe Gitlab::Auth do ...@@ -184,14 +184,14 @@ describe Gitlab::Auth do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities)) expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities))
end end
it 'limits abilities based on scope' do it 'limits abilities based on scope' do
personal_access_token = create(:personal_access_token, scopes: ['read_user']) personal_access_token = create(:personal_access_token, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') 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, [])) 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_access_token, []))
end end
it 'fails if password is nil' do it 'fails if password is nil' do
...@@ -234,7 +234,7 @@ describe Gitlab::Auth do ...@@ -234,7 +234,7 @@ describe Gitlab::Auth do
it 'throws an error suggesting user create a PAT when internal auth is disabled' do it 'throws an error suggesting user create a PAT when internal auth is disabled' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false } allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false }
expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError) expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end end
end end
......
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20171012125712_migrate_user_authentication_token_to_personal_access_token.rb')
describe MigrateUserAuthenticationTokenToPersonalAccessToken, :migration do
let(:users) { table(:users) }
let(:personal_access_tokens) { table(:personal_access_tokens) }
let!(:user) { users.create!(id: 1, email: 'user@example.com', authentication_token: 'user-token', admin: false) }
let!(:admin) { users.create!(id: 2, email: 'admin@example.com', authentication_token: 'admin-token', admin: true) }
it 'migrates private tokens to Personal Access Tokens' do
migrate!
expect(personal_access_tokens.count).to eq(2)
user_token = personal_access_tokens.find_by(user_id: user.id)
admin_token = personal_access_tokens.find_by(user_id: admin.id)
expect(user_token.token).to eq('user-token')
expect(admin_token.token).to eq('admin-token')
expect(user_token.scopes).to eq(%w[api].to_yaml)
expect(admin_token.scopes).to eq(%w[api sudo].to_yaml)
end
end
...@@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do ...@@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do
end end
describe User, 'TokenAuthenticatable' do describe User, 'TokenAuthenticatable' do
let(:token_field) { :authentication_token } let(:token_field) { :rss_token }
it_behaves_like 'TokenAuthenticatable' it_behaves_like 'TokenAuthenticatable'
describe 'ensures authentication token' do describe 'ensures authentication token' do
......
...@@ -346,7 +346,6 @@ describe User do ...@@ -346,7 +346,6 @@ describe User do
describe "Respond to" do describe "Respond to" do
it { is_expected.to respond_to(:admin?) } it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) } it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) } it { is_expected.to respond_to(:external?) }
end end
...@@ -526,14 +525,6 @@ describe User do ...@@ -526,14 +525,6 @@ describe User do
end end
end end
describe 'authentication token' do
it "has authentication token" do
user = create(:user)
expect(user.authentication_token).not_to be_blank
end
end
describe 'ensure incoming email token' do describe 'ensure incoming email token' do
it 'has incoming email token' do it 'has incoming email token' do
user = create(:user) user = create(:user)
......
...@@ -25,7 +25,7 @@ describe 'doorkeeper access' do ...@@ -25,7 +25,7 @@ describe 'doorkeeper access' do
end end
end end
describe "authorization by private token" do describe "authorization by OAuth token" do
it "returns authentication success" do it "returns authentication success" do
get api("/user", user) get api("/user", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
...@@ -39,20 +39,20 @@ describe 'doorkeeper access' do ...@@ -39,20 +39,20 @@ describe 'doorkeeper access' do
end end
describe "when user is blocked" do describe "when user is blocked" do
it "returns authentication error" do it "returns authorization error" do
user.block user.block
get api("/user"), access_token: token.token get api("/user"), access_token: token.token
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(403)
end end
end end
describe "when user is ldap_blocked" do describe "when user is ldap_blocked" do
it "returns authentication error" do it "returns authorization error" do
user.ldap_block user.ldap_block
get api("/user"), access_token: token.token get api("/user"), access_token: token.token
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(403)
end end
end end
end end
...@@ -28,39 +28,11 @@ describe API::Helpers do ...@@ -28,39 +28,11 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:options).and_return({}) allow_any_instance_of(self.class).to receive(:options).and_return({})
end end
def set_env(user_or_token, identifier)
clear_env
clear_param
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
env[API::Helpers::SUDO_HEADER] = identifier.to_s
end
def set_param(user_or_token, identifier)
clear_env
clear_param
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
params[API::Helpers::SUDO_PARAM] = identifier.to_s
end
def clear_env
env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
env.delete(API::Helpers::SUDO_HEADER)
end
def clear_param
params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
params.delete(API::Helpers::SUDO_PARAM)
end
def warden_authenticate_returns(value) def warden_authenticate_returns(value)
warden = double("warden", authenticate: value) warden = double("warden", authenticate: value)
env['warden'] = warden env['warden'] = warden
end end
def doorkeeper_guard_returns(value)
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
end
def error!(message, status, header) def error!(message, status, header)
raise Exception.new("#{status} - #{message}") raise Exception.new("#{status} - #{message}")
end end
...@@ -69,10 +41,6 @@ describe API::Helpers do ...@@ -69,10 +41,6 @@ describe API::Helpers do
subject { current_user } subject { current_user }
describe "Warden authentication", :allow_forgery_protection do describe "Warden authentication", :allow_forgery_protection do
before do
doorkeeper_guard_returns false
end
context "with invalid credentials" do context "with invalid credentials" do
context "GET request" do context "GET request" do
before do before do
...@@ -160,75 +128,32 @@ describe API::Helpers do ...@@ -160,75 +128,32 @@ describe API::Helpers do
end end
end end
describe "when authenticating using a user's private token" do
it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
expect { current_user }.to raise_error /401/
end
it "returns a 401 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect { current_user }.to raise_error /401/
end
it 'returns a 401 response for a user who is blocked' do
user.block!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
expect { current_user }.to raise_error /401/
end
it "leaves user as is when sudo not specified" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
expect(current_user).to eq(user)
clear_env
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
expect(current_user).to eq(user)
end
end
describe "when authenticating using a user's personal access tokens" do describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
end
it "returns a 401 response for an invalid token" do it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error /401/
end end
it "returns a 401 response for a user without access" do it "returns a 403 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error /403/
end end
it 'returns a 401 response for a user who is blocked' do it 'returns a 403 response for a user who is blocked' do
user.block! user.block!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error /403/
end end
it "leaves user as is when sudo not specified" do it "sets current_user" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user) expect(current_user).to eq(user)
clear_env
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
expect(current_user).to eq(user)
end end
it "does not allow tokens without the appropriate scope" do it "does not allow tokens without the appropriate scope" do
...@@ -252,210 +177,6 @@ describe API::Helpers do ...@@ -252,210 +177,6 @@ describe API::Helpers do
expect { current_user }.to raise_error API::APIGuard::ExpiredError expect { current_user }.to raise_error API::APIGuard::ExpiredError
end end
end end
context 'sudo usage' do
context 'with admin' do
context 'with header' do
context 'with id' do
it 'changes current_user to sudo' do
set_env(admin, user.id)
expect(current_user).to eq(user)
end
it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
set_env(admin, user.id)
expect(current_user).to eq(user)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do
set_env(admin, admin.id)
expect(current_user).to eq(admin)
end
it 'throws an error when user cannot be found' do
id = user.id + admin.id
expect(user.id).not_to eq(id)
expect(admin.id).not_to eq(id)
set_env(admin, id)
expect { current_user }.to raise_error(Exception)
end
end
context 'with username' do
it 'changes current_user to sudo' do
set_env(admin, user.username)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do
set_env(admin, admin.username)
expect(current_user).to eq(admin)
end
it "throws an error when the user cannot be found for a given username" do
username = "#{user.username}#{admin.username}"
expect(user.username).not_to eq(username)
expect(admin.username).not_to eq(username)
set_env(admin, username)
expect { current_user }.to raise_error(Exception)
end
end
end
context 'with param' do
context 'with id' do
it 'changes current_user to sudo' do
set_param(admin, user.id)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do
set_param(admin, admin.id)
expect(current_user).to eq(admin)
end
it 'handles sudo to oneself using string' do
set_env(admin, user.id.to_s)
expect(current_user).to eq(user)
end
it 'throws an error when user cannot be found' do
id = user.id + admin.id
expect(user.id).not_to eq(id)
expect(admin.id).not_to eq(id)
set_param(admin, id)
expect { current_user }.to raise_error(Exception)
end
end
context 'with username' do
it 'changes current_user to sudo' do
set_param(admin, user.username)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do
set_param(admin, admin.username)
expect(current_user).to eq(admin)
end
it "throws an error when the user cannot be found for a given username" do
username = "#{user.username}#{admin.username}"
expect(user.username).not_to eq(username)
expect(admin.username).not_to eq(username)
set_param(admin, username)
expect { current_user }.to raise_error(Exception)
end
end
end
context 'when user is blocked' do
before do
user.block!
end
it 'changes current_user to sudo' do
set_env(admin, user.id)
expect(current_user).to eq(user)
end
end
end
context 'with regular user' do
context 'with env' do
it 'changes current_user to sudo when admin and user id' do
set_env(user, admin.id)
expect { current_user }.to raise_error(Exception)
end
it 'changes current_user to sudo when admin and user username' do
set_env(user, admin.username)
expect { current_user }.to raise_error(Exception)
end
end
context 'with params' do
it 'changes current_user to sudo when admin and user id' do
set_param(user, admin.id)
expect { current_user }.to raise_error(Exception)
end
it 'changes current_user to sudo when admin and user username' do
set_param(user, admin.username)
expect { current_user }.to raise_error(Exception)
end
end
end
end
end
describe '.sudo?' do
context 'when no sudo env or param is passed' do
before do
doorkeeper_guard_returns(nil)
end
it 'returns false' do
expect(sudo?).to be_falsy
end
end
context 'when sudo env or param is passed', 'user is not an admin' do
before do
set_env(user, '123')
end
it 'returns an 403 Forbidden' do
expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
end
end
context 'when sudo env or param is passed', 'user is admin' do
context 'personal access token is used' do
before do
personal_access_token = create(:personal_access_token, user: admin)
set_env(personal_access_token.token, user.id)
end
it 'returns an 403 Forbidden' do
expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
end
end
context 'private access token is used' do
before do
set_env(admin.private_token, user.id)
end
it 'returns true' do
expect(sudo?).to be_truthy
end
end
end
end end
describe '.handle_api_exception' do describe '.handle_api_exception' do
...@@ -582,4 +303,147 @@ describe API::Helpers do ...@@ -582,4 +303,147 @@ describe API::Helpers do
end end
end end
end end
context 'sudo' do
shared_examples 'successful sudo' do
it 'sets current_user' do
expect(current_user).to eq(user)
end
it 'sets sudo?' do
expect(sudo?).to be_truthy
end
end
shared_examples 'sudo' do
context 'when admin' do
before do
token.user = admin
token.save!
end
context 'when token has sudo scope' do
before do
token.scopes = %w[sudo]
token.save!
end
context 'when user exists' do
context 'when using header' do
context 'when providing username' do
before do
env[API::Helpers::SUDO_HEADER] = user.username
end
it_behaves_like 'successful sudo'
end
context 'when providing user ID' do
before do
env[API::Helpers::SUDO_HEADER] = user.id.to_s
end
it_behaves_like 'successful sudo'
end
end
context 'when using param' do
context 'when providing username' do
before do
params[API::Helpers::SUDO_PARAM] = user.username
end
it_behaves_like 'successful sudo'
end
context 'when providing user ID' do
before do
params[API::Helpers::SUDO_PARAM] = user.id.to_s
end
it_behaves_like 'successful sudo'
end
end
end
context 'when user does not exist' do
before do
params[API::Helpers::SUDO_PARAM] = 'nonexistent'
end
it 'raises an error' do
expect { current_user }.to raise_error /User with ID or username 'nonexistent' Not Found/
end
end
end
context 'when token does not have sudo scope' do
before do
token.scopes = %w[api]
token.save!
params[API::Helpers::SUDO_PARAM] = user.id.to_s
end
it 'raises an error' do
expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
end
end
end
context 'when not admin' do
before do
token.user = user
token.save!
params[API::Helpers::SUDO_PARAM] = user.id.to_s
end
it 'raises an error' do
expect { current_user }.to raise_error /Must be admin to use sudo/
end
end
end
context 'using an OAuth token' do
let(:token) { create(:oauth_access_token) }
before do
env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
end
it_behaves_like 'sudo'
end
context 'using a personal access token' do
let(:token) { create(:personal_access_token) }
context 'passed as param' do
before do
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = token.token
end
it_behaves_like 'sudo'
end
context 'passed as header' do
before do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = token.token
end
it_behaves_like 'sudo'
end
end
context 'using warden authentication' do
before do
warden_authenticate_returns admin
env[API::Helpers::SUDO_HEADER] = user.username
end
it 'raises an error' do
expect { current_user }.to raise_error /Must be authenticated using an OAuth or Personal Access Token to use sudo/
end
end
end
end end
require 'spec_helper'
describe API::Session do
let(:user) { create(:user) }
describe "POST /session" do
context "when valid password" do
it "returns private token" do
post api("/session"), email: user.email, password: '12345678'
expect(response).to have_gitlab_http_status(201)
expect(json_response['email']).to eq(user.email)
expect(json_response['private_token']).to eq(user.private_token)
expect(json_response['is_admin']).to eq(user.admin?)
expect(json_response['can_create_project']).to eq(user.can_create_project?)
expect(json_response['can_create_group']).to eq(user.can_create_group?)
end
context 'with 2FA enabled' do
it 'rejects sign in attempts' do
user = create(:user, :two_factor)
post api('/session'), email: user.email, password: user.password
expect(response).to have_gitlab_http_status(401)
expect(response.body).to include('You have 2FA enabled.')
end
end
end
context 'when email has case-typo and password is valid' do
it 'returns private token' do
post api('/session'), email: user.email.upcase, password: '12345678'
expect(response.status).to eq 201
expect(json_response['email']).to eq user.email
expect(json_response['private_token']).to eq user.private_token
expect(json_response['is_admin']).to eq user.admin?
expect(json_response['can_create_project']).to eq user.can_create_project?
expect(json_response['can_create_group']).to eq user.can_create_group?
end
end
context 'when login has case-typo and password is valid' do
it 'returns private token' do
post api('/session'), login: user.username.upcase, password: '12345678'
expect(response.status).to eq 201
expect(json_response['email']).to eq user.email
expect(json_response['private_token']).to eq user.private_token
expect(json_response['is_admin']).to eq user.admin?
expect(json_response['can_create_project']).to eq user.can_create_project?
expect(json_response['can_create_group']).to eq user.can_create_group?
end
end
context "when invalid password" do
it "returns authentication error" do
post api("/session"), email: user.email, password: '123'
expect(response).to have_gitlab_http_status(401)
expect(json_response['email']).to be_nil
expect(json_response['private_token']).to be_nil
end
end
context "when empty password" do
it "returns authentication error with email" do
post api("/session"), email: user.email
expect(response).to have_gitlab_http_status(400)
end
it "returns authentication error with username" do
post api("/session"), email: user.username
expect(response).to have_gitlab_http_status(400)
end
end
context "when empty name" do
it "returns authentication error" do
post api("/session"), password: user.password
expect(response).to have_gitlab_http_status(400)
end
end
context "when user is blocked" do
it "returns authentication error" do
user.block
post api("/session"), email: user.username, password: user.password
expect(response).to have_gitlab_http_status(401)
end
end
context "when user is ldap_blocked" do
it "returns authentication error" do
user.ldap_block
post api("/session"), email: user.username, password: user.password
expect(response).to have_gitlab_http_status(401)
end
end
end
end
...@@ -127,8 +127,8 @@ describe API::Users do ...@@ -127,8 +127,8 @@ describe API::Users do
context "when admin" do context "when admin" do
context 'when sudo is defined' do context 'when sudo is defined' do
it 'does not return 500' do it 'does not return 500' do
admin_personal_access_token = create(:personal_access_token, user: admin).token admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin) get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
end end
...@@ -1097,14 +1097,6 @@ describe API::Users do ...@@ -1097,14 +1097,6 @@ describe API::Users do
end end
end end
context 'with private token' do
it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{user.private_token}&sudo=123")
expect(response).to have_gitlab_http_status(403)
end
end
it 'returns current user without private token when sudo not defined' do it 'returns current user without private token when sudo not defined' do
get api("/user", user) get api("/user", user)
...@@ -1139,24 +1131,6 @@ describe API::Users do ...@@ -1139,24 +1131,6 @@ describe API::Users do
expect(json_response['id']).to eq(admin.id) expect(json_response['id']).to eq(admin.id)
end end
end end
context 'with private token' do
it 'returns sudoed user with private token when sudo defined' do
get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/user/login')
expect(json_response['id']).to eq(user.id)
end
it 'returns initial current user without private token but with is_admin when sudo not defined' do
get api("/user?private_token=#{admin.private_token}")
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['id']).to eq(admin.id)
end
end
end end
context 'with unauthenticated user' do context 'with unauthenticated user' do
......
...@@ -135,7 +135,6 @@ end ...@@ -135,7 +135,6 @@ end
# profile_history GET /profile/history(.:format) profile#history # profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update # profile_password PUT /profile/password(.:format) profile#password_update
# profile_token GET /profile/token(.:format) profile#token # profile_token GET /profile/token(.:format) profile#token
# profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token
# profile GET /profile(.:format) profile#show # profile GET /profile(.:format) profile#show
# profile_update PUT /profile/update(.:format) profile#update # profile_update PUT /profile/update(.:format) profile#update
describe ProfilesController, "routing" do describe ProfilesController, "routing" do
...@@ -147,10 +146,6 @@ describe ProfilesController, "routing" do ...@@ -147,10 +146,6 @@ describe ProfilesController, "routing" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log') expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end end
it "to #reset_private_token" do
expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token')
end
it "to #reset_rss_token" do it "to #reset_rss_token" do
expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token') expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
end end
......
...@@ -18,21 +18,23 @@ module ApiHelpers ...@@ -18,21 +18,23 @@ module ApiHelpers
# #
# Returns the relative path to the requested API resource # Returns the relative path to the requested API resource
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil) def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil)
"/api/#{version}#{path}" + full_path = "/api/#{version}#{path}"
# Normalize query string if oauth_access_token
(path.index('?') ? '' : '?') + query_string = "access_token=#{oauth_access_token.token}"
elsif personal_access_token
query_string = "private_token=#{personal_access_token.token}"
elsif user
personal_access_token = create(:personal_access_token, user: user)
query_string = "private_token=#{personal_access_token.token}"
end
if personal_access_token.present? if query_string
"&private_token=#{personal_access_token.token}" full_path << (path.index('?') ? '&' : '?')
elsif oauth_access_token.present? full_path << query_string
"&access_token=#{oauth_access_token.token}" end
# Append private_token if given a User object
elsif user.respond_to?(:private_token) full_path
"&private_token=#{user.private_token}"
else
''
end
end end
# Temporary helper method for simplifying V3 exclusive API specs # Temporary helper method for simplifying V3 exclusive API specs
......
...@@ -14,7 +14,5 @@ ...@@ -14,7 +14,5 @@
"provider":null, "provider":null,
"is_admin":false, "is_admin":false,
"can_create_group":false, "can_create_group":false,
"can_create_project":false, "can_create_project":false
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
} }
...@@ -14,7 +14,5 @@ ...@@ -14,7 +14,5 @@
"provider":null, "provider":null,
"is_admin":false, "is_admin":false,
"can_create_group":false, "can_create_group":false,
"can_create_project":false, "can_create_project":false
"private_token":"Wvjy2Krpb7y8xi93owUz", }
"access_token":"Wvjy2Krpb7y8xi93owUz"
}
\ No newline at end of file
require 'spec_helper'
require 'rake'
describe 'gitlab:users namespace rake task' do
let(:enable_registry) { true }
before :all do
Rake.application.rake_require 'tasks/gitlab/helpers'
Rake.application.rake_require 'tasks/gitlab/users'
# empty task as env is already loaded
Rake::Task.define_task :environment
end
def run_rake_task(task_name)
Rake::Task[task_name].reenable
Rake.application.invoke_task task_name
end
describe 'clear_all_authentication_tokens' do
before do
# avoid writing task output to spec progress
allow($stdout).to receive :write
end
context 'gitlab version' do
it 'clears the authentication token for all users' do
create_list(:user, 2)
expect(User.pluck(:authentication_token)).to all(be_present)
run_rake_task('gitlab:users:clear_all_authentication_tokens')
expect(User.pluck(:authentication_token)).to all(be_nil)
end
end
end
end
...@@ -7,12 +7,6 @@ describe 'tokens rake tasks' do ...@@ -7,12 +7,6 @@ describe 'tokens rake tasks' do
Rake.application.rake_require 'tasks/tokens' Rake.application.rake_require 'tasks/tokens'
end end
describe 'reset_all task' do
it 'invokes create_hooks task' do
expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token }
end
end
describe 'reset_all_email task' do describe 'reset_all_email task' do
it 'invokes create_hooks task' do it 'invokes create_hooks task' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token } expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment