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
end
def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES
@scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
......
......@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
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!
before_action :validate_user_service_ticket!
......@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user)
end
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
def authenticate_user_from_personal_access_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
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)
end
......
......@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized
end
end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
def render_missing_personal_token
def render_missing_personal_access_token
render json: {
errors: [
{ code: 'UNAUTHORIZED',
......
......@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
@scopes = Gitlab::Auth.available_scopes
@scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
......
......@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
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
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token!
......@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset"
redirect_to profile_account_path
redirect_to profile_personal_access_tokens_path
end
def reset_rss_token
......@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset"
redirect_to profile_account_path
redirect_to profile_personal_access_tokens_path
end
def audit_log
......
......@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
def basic_auth_provided?
......@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end
def render_missing_personal_token
def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\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}",
......
......@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
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
......@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email
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 :rss_token
......@@ -163,7 +163,7 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :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 :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? }
......@@ -185,8 +185,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files]
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do
......
......@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym)
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)
else
API::Scope.new(scope).sufficient?(token_scopes, request)
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 @@
.alert.alert-info
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
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
......
......@@ -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_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:
expired: "The access token expired"
unknown: "The access token is invalid"
scopes:
api: Access your API
read_user: Read user information
api: Access the authenticated user's API
read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
flash:
applications:
......
......@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do
get :audit_log
get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :reset_incoming_email_token
put :reset_rss_token
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
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
t.string "authentication_token"
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
......@@ -1720,7 +1719,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
end
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", ["created_at"], name: "index_users_on_created_at", 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:
1. Log in to your GitLab account.
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:
```
......
......@@ -50,7 +50,6 @@ following locations:
- [Repository Files](repository_files.md)
- [Runners](runners.md)
- [Services](services.md)
- [Session](session.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
......@@ -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 at `/api/v4`.
For endpoints that require [authentication](#authentication), you need to pass
a `private_token` parameter via query string or header. If passed as a header,
the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
an underscore).
Example of a valid API request:
```
GET /projects?private_token=9koXpg98eAheJpvBs5tK
```
Example of a valid API request using cURL and authentication via header:
Example of a valid API request using cURL:
```shell
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "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"
curl "https://gitlab.example.com/api/v4/projects"
```
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.
## 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
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. [Private tokens](#private-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
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
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.
You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the
`access_token` parameter or the `Authorization` header.
### 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
`access_token` parameter or in the `Authorization` header.
```shell
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
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
them can interact with GitLab as if they were you. You can find or reset your
private token in your account page (`/profile/account`).
You can use a [personal access token][pat] to authenticate with the API by passing it in either the
`private_token` parameter or the `Private-Token` header.
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,
personal access tokens could be a better fit because of their granular
permissions.
```shell
curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects
```
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header. For examples of usage,
[read the basic usage section](#basic-usage).
Read more about [personal access tokens][pat].
### 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
> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
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
or using the [Sudo](#sudo) feature which also requires the admin's password
or private token, since the password/token can change over time. Impersonation
tokens are a great fit if you want to build applications or tools which
authenticate with the API as a specific user.
They are an alternative to directly using the user's password or one of their
personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo)
password/token may not be known or may change over time.
For more information, refer to the
[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
> Needs admin permissions.
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
the `sudo` parameter either via query string or a header with an ID/username of
provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope.
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
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`:
```json
......@@ -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
returned with status code `404`:
```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
```
```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,
......@@ -239,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
```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
......
# 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
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
"private_token": "dd34asd13as"
"external": false
}
```
......
......@@ -459,11 +459,11 @@ Rendered example:
### cURL commands
- 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
include it.
- 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.
| Methods | Description |
......
......@@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml
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
Or through the HTTP API with an admin access token:
```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
......
......@@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b
From there, you can:
- 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)
- Manage applications that can
[use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
......
......@@ -2,17 +2,15 @@
> [Introduced][ce-3749] in GitLab 8.8.
Personal access tokens are useful if you need access to the [GitLab API][api].
Instead of using your private token which grants full access to your account,
personal access tokens could be a better fit because of their
[granular permissions](#limiting-scopes-of-a-personal-access-token).
Personal access tokens are the preferred way for third party applications and scripts to
authenticate with the [GitLab API][api], if using [OAuth2](../../api/oauth2.md) is not practical.
You can also use them to authenticate against Git over HTTP. They are the only
accepted method of authentication when you have
[Two-Factor Authentication (2FA)][2fa] enabled.
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,
at midnight UTC.
......@@ -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). |
| `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). |
| `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
[api]: ../../api/README.md
[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-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
[users]: ../../api/users.md
[usage]: ../../api/README.md#basic-usage
[usage]: ../../api/README.md#personal-access-tokens
......@@ -142,7 +142,6 @@ module API
mount ::API::Runner
mount ::API::Runners
mount ::API::Services
mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
......
......@@ -42,72 +42,42 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
def find_current_user
user =
find_user_from_private_token ||
find_user_from_oauth_token ||
find_user_from_warden
def find_current_user!
user = find_user_from_access_token || find_user_from_warden
return unless user
return nil unless user
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
private
def find_user_from_private_token
token_string = private_token.to_s
return nil unless token_string.present?
def access_token
return @access_token if defined?(@access_token)
user =
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string)
raise UnauthorizedError unless user
user
@access_token = find_oauth_access_token || find_personal_access_token
end
# Invokes the doorkeeper guard.
#
# 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
def validate_access_token!(scopes: [])
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
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end
def find_user_by_personal_access_token(token_string)
access_token = PersonalAccessToken.find_by_token(token_string)
private
def find_user_from_access_token
return unless access_token
find_user_by_access_token(access_token)
validate_access_token!
access_token.user || raise(UnauthorizedError)
end
# Check the Rails session for valid authentication details
......@@ -125,34 +95,26 @@ module API
end
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)
return @oauth_access_token = nil unless token
return unless token
@oauth_access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless @oauth_access_token
# Expiration, revocation and scopes are verified in `find_user_by_access_token`
access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless access_token
@oauth_access_token.revoke_previous_refresh_token!
@oauth_access_token
access_token.revoke_previous_refresh_token!
access_token
end
def find_user_by_access_token(access_token)
scopes = scopes_registered_for_endpoint
def find_personal_access_token
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)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
# Expiration, revocation and scopes are verified in `find_user_by_access_token`
access_token = PersonalAccessToken.find_by(token: token)
raise UnauthorizedError unless access_token
when AccessTokenValidationService::REVOKED
raise RevokedError
when AccessTokenValidationService::VALID
access_token.user
end
access_token
end
def doorkeeper_request
......@@ -236,7 +198,7 @@ module API
class InsufficientScopeError < StandardError
attr_reader :scopes
def initialize(scopes)
@scopes = scopes
@scopes = scopes.map { |s| s.try(:name) || s }
end
end
end
......
......@@ -57,10 +57,6 @@ module API
expose :admin?, as: :is_admin
end
class UserWithPrivateDetails < UserWithAdmin
expose :private_token
end
class Email < Grape::Entity
expose :id, :email
end
......
......@@ -41,6 +41,8 @@ module API
sudo!
validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
@current_user
end
......@@ -385,7 +387,7 @@ module API
return @initial_current_user if defined?(@initial_current_user)
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
unauthorized!
end
......@@ -393,24 +395,23 @@ module API
def sudo!
return unless sudo_identifier
return unless initial_current_user
unauthorized! unless initial_current_user
unless initial_current_user.admin?
forbidden!('Must be admin to use sudo')
end
# Only private tokens should be used for the SUDO feature
unless private_token == initial_current_user.private_token
forbidden!('Private token must be specified in order to use sudo')
unless access_token
forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
end
validate_access_token!(scopes: [:sudo])
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
else
not_found!("No user id or username for: #{sudo_identifier}")
end
end
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
end
get do
entity =
if sudo?
Entities::UserWithPrivateDetails
elsif current_user.admin?
if current_user.admin?
Entities::UserWithAdmin
else
Entities::UserPublic
......
module Gitlab
module Auth
MissingPersonalTokenError = Class.new(StandardError)
MissingPersonalAccessTokenError = Class.new(StandardError)
REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
API_SCOPES = [:api, :read_user].freeze
API_SCOPES = [:api, :read_user, :sudo].freeze
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
......@@ -38,7 +38,7 @@ module Gitlab
# If sign-in is disabled and LDAP is not configured, recommend a
# personal access token on failed auth attempts
raise Gitlab::Auth::MissingPersonalTokenError
raise Gitlab::Auth::MissingPersonalAccessTokenError
end
def find_with_user_password(login, password)
......@@ -106,7 +106,7 @@ module Gitlab
user = find_with_user_password(login, password)
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)
end
......@@ -128,7 +128,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
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
......@@ -226,8 +226,10 @@ module Gitlab
[]
end
def available_scopes
API_SCOPES + registry_scopes
def available_scopes(current_user = nil)
scopes = API_SCOPES + registry_scopes
scopes.delete(:sudo) if current_user && !current_user.admin?
scopes
end
# 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'
namespace :tokens do
desc "Reset all GitLab user auth tokens"
task reset_all_auth: :environment do
reset_all_users_token(:reset_authentication_token!)
end
desc "Reset all GitLab email tokens"
desc "Reset all GitLab incoming email tokens"
task reset_all_email: :environment do
reset_all_users_token(:reset_incoming_email_token!)
end
......@@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
def reset_authentication_token!
write_new_token(:authentication_token)
save!(validate: false)
end
def reset_incoming_email_token!
write_new_token(:incoming_email_token)
save!(validate: false)
......
......@@ -50,40 +50,7 @@ describe ApplicationController do
end
end
describe "#authenticate_user_from_token!" do
describe "authenticating a user from a private token" do
controller(described_class) do
def index
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
describe "authenticating a user from a personal access token" do
describe "#authenticate_user_from_personal_access_token!" do
controller(described_class) do
def index
render text: 'authenticated'
......@@ -115,7 +82,6 @@ describe ApplicationController do
expect(response.body).not_to eq('authenticated')
end
end
end
describe 'rescue from Gitlab::Git::Storage::Inaccessible' do
controller(described_class) do
......@@ -152,11 +118,15 @@ describe ApplicationController do
end
end
before do
sign_in user
end
context 'when format is handled' do
let(:requested_format) { :json }
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
end
......@@ -164,7 +134,7 @@ describe ApplicationController do
context 'when format is not handled' do
it 'returns 404 response' do
get :index, private_token: user.private_token
get :index
expect(response).to have_gitlab_http_status 404
end
......
......@@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do
end
describe "atom feed" do
it "renders atom feed via private token" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
it "renders atom feed via personal access token" do
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(body).to have_selector('title', text: "#{user.name} issues")
......
......@@ -4,9 +4,11 @@ describe "Dashboard Feed" do
describe "GET /" do
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
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')
end
end
......
......@@ -28,10 +28,12 @@ describe 'Issues Feed' do
end
end
context 'when authenticated via private token' do
context 'when authenticated via personal access token' do
it 'renders atom feed' do
personal_access_token = create(:personal_access_token, user: user)
visit project_issues_path(project, :atom,
private_token: user.private_token)
private_token: personal_access_token.token)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
......
......@@ -4,9 +4,11 @@ describe "User Feed" do
describe "GET /" do
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
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')
end
end
......
require 'spec_helper'
describe 'Profile account page' do
describe 'Profile account page', :js do
let(:user) { create(:user) }
before do
......@@ -56,47 +56,38 @@ describe 'Profile account page' do
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
before do
visit profile_account_path
visit profile_personal_access_tokens_path
end
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(find('#rss-token').value).not_to eq(previous_token)
end
end
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
visit profile_account_path
visit profile_personal_access_tokens_path
end
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
......
......@@ -27,11 +27,9 @@
"can_create_group",
"can_create_project",
"two_factor_enabled",
"external",
"private_token"
"external"
],
"properties": {
"$ref": "full.json",
"private_token": { "type": "string" }
"$ref": "full.json"
}
}
......@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' 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
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
......@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
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
context 'registry_scopes' do
......@@ -164,7 +164,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
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
context 'when registry is enabled' do
......@@ -176,7 +176,7 @@ describe Gitlab::Auth do
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.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
......@@ -184,14 +184,14 @@ describe Gitlab::Auth do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
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
it 'limits abilities based on scope' do
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.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
it 'fails if password is nil' do
......@@ -234,7 +234,7 @@ describe Gitlab::Auth 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 }
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
......
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
end
describe User, 'TokenAuthenticatable' do
let(:token_field) { :authentication_token }
let(:token_field) { :rss_token }
it_behaves_like 'TokenAuthenticatable'
describe 'ensures authentication token' do
......
......@@ -346,7 +346,6 @@ describe User do
describe "Respond to" do
it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) }
end
......@@ -526,14 +525,6 @@ describe User do
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
it 'has incoming email token' do
user = create(:user)
......
......@@ -25,7 +25,7 @@ describe 'doorkeeper access' do
end
end
describe "authorization by private token" do
describe "authorization by OAuth token" do
it "returns authentication success" do
get api("/user", user)
expect(response).to have_gitlab_http_status(200)
......@@ -39,20 +39,20 @@ describe 'doorkeeper access' do
end
describe "when user is blocked" do
it "returns authentication error" do
it "returns authorization error" do
user.block
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
describe "when user is ldap_blocked" do
it "returns authentication error" do
it "returns authorization error" do
user.ldap_block
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
......@@ -28,39 +28,11 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:options).and_return({})
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)
warden = double("warden", authenticate: value)
env['warden'] = warden
end
def doorkeeper_guard_returns(value)
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
end
def error!(message, status, header)
raise Exception.new("#{status} - #{message}")
end
......@@ -69,10 +41,6 @@ describe API::Helpers do
subject { current_user }
describe "Warden authentication", :allow_forgery_protection do
before do
doorkeeper_guard_returns false
end
context "with invalid credentials" do
context "GET request" do
before do
......@@ -160,75 +128,32 @@ describe API::Helpers do
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
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
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
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
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
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!
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
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
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
it "does not allow tokens without the appropriate scope" do
......@@ -252,210 +177,6 @@ describe API::Helpers do
expect { current_user }.to raise_error API::APIGuard::ExpiredError
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
describe '.handle_api_exception' do
......@@ -582,4 +303,147 @@ describe API::Helpers do
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
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
context "when admin" do
context 'when sudo is defined' do
it 'does not return 500' do
admin_personal_access_token = create(:personal_access_token, user: admin).token
get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin)
admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
expect(response).to have_gitlab_http_status(:success)
end
......@@ -1097,14 +1097,6 @@ describe API::Users do
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
get api("/user", user)
......@@ -1139,24 +1131,6 @@ describe API::Users do
expect(json_response['id']).to eq(admin.id)
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
context 'with unauthenticated user' do
......
......@@ -135,7 +135,6 @@ end
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
# 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_update PUT /profile/update(.:format) profile#update
describe ProfilesController, "routing" do
......@@ -147,10 +146,6 @@ describe ProfilesController, "routing" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
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
expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
end
......
......@@ -18,21 +18,23 @@ module ApiHelpers
#
# 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)
"/api/#{version}#{path}" +
full_path = "/api/#{version}#{path}"
# Normalize query string
(path.index('?') ? '' : '?') +
if oauth_access_token
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?
"&private_token=#{personal_access_token.token}"
elsif oauth_access_token.present?
"&access_token=#{oauth_access_token.token}"
# Append private_token if given a User object
elsif user.respond_to?(:private_token)
"&private_token=#{user.private_token}"
else
''
if query_string
full_path << (path.index('?') ? '&' : '?')
full_path << query_string
end
full_path
end
# Temporary helper method for simplifying V3 exclusive API specs
......
......@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
"can_create_project":false
}
......@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
"can_create_project":false
}
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
Rake.application.rake_require 'tasks/tokens'
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
it 'invokes create_hooks task' do
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