diff options
author | Sean McGivern <sean@mcgivern.me.uk> | 2017-11-02 15:25:42 +0000 |
---|---|---|
committer | Sean McGivern <sean@mcgivern.me.uk> | 2017-11-02 15:25:42 +0000 |
commit | 56dccc2e1089e2866d0442cac379b3f93c98a55f (patch) | |
tree | 7600acfcf7de21ab4a5470c3653ea3ddcd91f13c | |
parent | 983436375690348c88fa79e4974c5267afb5b0ce (diff) | |
parent | d0af6047bcaa336a829d04786496db6d263ea0a4 (diff) | |
download | gitlab-ce-56dccc2e1089e2866d0442cac379b3f93c98a55f.tar.gz |
Merge branch 'dm-remove-private-token' into 'master'
Remove Private Tokens
Closes #38595 and #38447
See merge request gitlab-org/gitlab-ce!14838
60 files changed, 587 insertions, 951 deletions
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index 07c8bf714fc..7a2c7234a1e 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 391a0519195..3be7aee69bc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 4bceb1d67a3..7d6fe6a0232 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -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', diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 4146deefa89..6d9873e38df 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -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) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 5d87037f012..dbf61a17724 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -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 diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 95d7a02e9e9..dd5e66f60e3 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -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}", diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index f89e60ad9f4..e8595b13d6d 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 9459b6d4fa4..6c9349ed9dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 9c00ea789ec..46e19230328 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -39,11 +39,8 @@ class AccessTokenValidationService token_scopes = token.scopes.map(&:to_sym) required_scopes.any? do |scope| - if scope.respond_to?(:sufficient?) - scope.sufficient?(token_scopes, request) - else - API::Scope.new(scope).sufficient?(token_scopes, request) - end + scope = API::Scope.new(scope) unless scope.is_a?(API::Scope) + scope.sufficient?(token_scopes, request) end end end diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml deleted file mode 100644 index c31a4a8ecd4..00000000000 --- a/app/views/profiles/accounts/_reset_token.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- 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' diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 7f79168dfb3..ced58dffcdc 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -9,22 +9,6 @@ .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 Two-Factor Authentication %p Increase your account's security by enabling Two-Factor Authentication (2FA). diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 06bb72b9f0d..26c2e4c5936 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -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. diff --git a/changelogs/unreleased/dm-add-sudo-scope.yml b/changelogs/unreleased/dm-add-sudo-scope.yml new file mode 100644 index 00000000000..a0c173ce781 --- /dev/null +++ b/changelogs/unreleased/dm-add-sudo-scope.yml @@ -0,0 +1,6 @@ +--- +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 diff --git a/changelogs/unreleased/dm-convert-private-tokens.yml b/changelogs/unreleased/dm-convert-private-tokens.yml new file mode 100644 index 00000000000..8f5145c897b --- /dev/null +++ b/changelogs/unreleased/dm-convert-private-tokens.yml @@ -0,0 +1,5 @@ +--- +title: Convert private tokens to Personal Access Tokens with sudo scope +merge_request: +author: +type: security diff --git a/changelogs/unreleased/dm-remove-private-token-from-interface.yml b/changelogs/unreleased/dm-remove-private-token-from-interface.yml new file mode 100644 index 00000000000..1b8996b08c3 --- /dev/null +++ b/changelogs/unreleased/dm-remove-private-token-from-interface.yml @@ -0,0 +1,5 @@ +--- +title: Remove private tokens from web interface and API +merge_request: +author: +type: security diff --git a/changelogs/unreleased/dm-remove-private-token.yml b/changelogs/unreleased/dm-remove-private-token.yml new file mode 100644 index 00000000000..d721495721a --- /dev/null +++ b/changelogs/unreleased/dm-remove-private-token.yml @@ -0,0 +1,5 @@ +--- +title: Remove Session API now that private tokens are removed from user API endpoints +merge_request: +author: +type: removed diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 14d49885fb3..0da6b14c29e 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -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: diff --git a/config/routes/profile.rb b/config/routes/profile.rb index ddc852f0132..bcfc17a5f66 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -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 diff --git a/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb new file mode 100644 index 00000000000..9a909644a44 --- /dev/null +++ b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb @@ -0,0 +1,78 @@ +# 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 diff --git a/db/post_migrate/20171012150314_remove_user_authentication_token.rb b/db/post_migrate/20171012150314_remove_user_authentication_token.rb new file mode 100644 index 00000000000..d0f3aa06e98 --- /dev/null +++ b/db/post_migrate/20171012150314_remove_user_authentication_token.rb @@ -0,0 +1,20 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index 530f08022be..be381751238 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index 6f1356ddf8f..be538ea250a 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -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: ``` diff --git a/doc/api/README.md b/doc/api/README.md index 89ffe9d7868..f226716c3b5 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -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 diff --git a/doc/api/session.md b/doc/api/session.md deleted file mode 100644 index b97e26f34a2..00000000000 --- a/doc/api/session.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 diff --git a/doc/api/users.md b/doc/api/users.md index 1643c584244..aa711090af1 100644 --- a/doc/api/users.md +++ b/doc/api/users.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 } ``` diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 798f40eef3d..0e4ffbd7910 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -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 | diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 3ae46019daf..5554a0c8b78 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -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 -``` diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 5561784ed0b..042cde3f01e 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -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 diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 5ebb88bf324..5fcc0501dc1 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -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) diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index f28c034e74c..9b4fdd65e2f 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -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 diff --git a/lib/api/api.rb b/lib/api/api.rb index 7db18e25a5f..c37e596eb9d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -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 diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 87b9db66efd..b9c7d443f6c 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -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 end - def find_user_by_authentication_token(token_string) - User.find_by_authentication_token(token_string) - end + private - def find_user_by_personal_access_token(token_string) - access_token = PersonalAccessToken.find_by_token(token_string) + 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 diff --git a/lib/api/entities.rb b/lib/api/entities.rb index efe874b2e6b..67cecb6a7ad 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -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 diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7a2ec865860..1c12166e434 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -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 + @current_user = sudoed_user end def sudo_identifier diff --git a/lib/api/session.rb b/lib/api/session.rb deleted file mode 100644 index 016415c3023..00000000000 --- a/lib/api/session.rb +++ /dev/null @@ -1,20 +0,0 @@ -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 diff --git a/lib/api/users.rb b/lib/api/users.rb index b6f97a1eac2..d80b364bd09 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -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 diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 87aeb76b66a..0ad9285c0ea 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,11 +1,11 @@ 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 diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake deleted file mode 100644 index 3a16ace60bd..00000000000 --- a/lib/tasks/gitlab/users.rake +++ /dev/null @@ -1,11 +0,0 @@ -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 diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake index ad1818ff1fa..693597afdf8 100644 --- a/lib/tasks/tokens.rake +++ b/lib/tasks/tokens.rake @@ -1,12 +1,7 @@ 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) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 6802b839eaa..b73ca0c2346 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -50,70 +50,36 @@ 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") + describe "#authenticate_user_from_personal_access_token!" do + controller(described_class) do + def index + render text: 'authenticated' end end - describe "authenticating a user from a personal access token" do - controller(described_class) do - def index - render text: 'authenticated' - end - end - - let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } - context "when the 'personal_access_token' param is populated with the personal access token" do - it "logs the user in" do - get :index, private_token: personal_access_token.token - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end + context "when the 'personal_access_token' param is populated with the personal access token" do + it "logs the user in" do + get :index, private_token: personal_access_token.token + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq('authenticated') end + end - context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do - it "logs the user in" do - @request.headers["PRIVATE-TOKEN"] = personal_access_token.token - get :index - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it "logs the user in" do + @request.headers["PRIVATE-TOKEN"] = personal_access_token.token + get :index + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq('authenticated') end + end - it "doesn't log the user in otherwise" do - get :index, private_token: "token" - expect(response.status).not_to eq(200) - expect(response.body).not_to eq('authenticated') - end + it "doesn't log the user in otherwise" do + get :index, private_token: "token" + expect(response.status).not_to eq(200) + expect(response.body).not_to eq('authenticated') end end @@ -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 diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 5aae2dbaf91..89c9d377003 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -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") diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 321c8a2a670..2c0c331b6db 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -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 diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 3eeb4d35131..4102ac0588a 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -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') diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 9ce687afb31..2b934d81674 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -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 diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 1cddd35fd8a..0166ab8be99 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -1,6 +1,6 @@ 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 diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index e6c1d9c9d84..aa066883c47 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -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" } } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index af1db2c3455..54a853c9ce3 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -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 diff --git a/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb new file mode 100644 index 00000000000..b4834705011 --- /dev/null +++ b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb @@ -0,0 +1,25 @@ +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 diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 882afeccfc6..dfb83578fce 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1c3c9068f12..fb03e320734 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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) diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index de7ce848a31..308134eba72 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -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 diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 9f3b5a809d7..6c0996c543d 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -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 diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb deleted file mode 100644 index 83d09878813..00000000000 --- a/spec/requests/api/session_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -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 diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4737f034f21..634c8dae0ba 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -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 diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 407d19c3b2a..609481603af 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -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 diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 01aca74274c..ac0c7a9b493 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -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 - '' - end + if query_string + full_path << (path.index('?') ? '&' : '?') + full_path << query_string + end + + full_path end # Temporary helper method for simplifying V3 exclusive API specs diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index 688175369ae..658ff5871b0 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -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 } diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index ce8dfe5ae75..658ff5871b0 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -14,7 +14,5 @@ "provider":null, "is_admin":false, "can_create_group":false, - "can_create_project":false, - "private_token":"Wvjy2Krpb7y8xi93owUz", - "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file + "can_create_project":false +} diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb deleted file mode 100644 index 972670e7f91..00000000000 --- a/spec/tasks/gitlab/users_rake_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -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 diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb index b84137eb365..51f7a536cbb 100644 --- a/spec/tasks/tokens_spec.rb +++ b/spec/tasks/tokens_spec.rb @@ -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 } |