Merge branch 'security-fix-pat-web-access' into 'master'

[master] Resolve "Personal access token with only `read_user` scope can be used to authenticate any web request"

See merge request gitlab/gitlabhq!2583
This commit is contained in:
Cindy Pallares 2018-11-28 19:06:02 +00:00
parent e122e14ac6
commit fe5f75930e
No known key found for this signature in database
GPG key ID: 8E13768AD1946B0C
28 changed files with 551 additions and 294 deletions

View file

@ -12,11 +12,11 @@ class ApplicationController < ActionController::Base
include WorkhorseHelper include WorkhorseHelper
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
include SessionlessAuthentication
# this can be removed after switching to rails 5 # this can be removed after switching to rails 5
# https://gitlab.com/gitlab-org/gitlab-ce/issues/51908 # https://gitlab.com/gitlab-org/gitlab-ce/issues/51908
include InvalidUTF8ErrorHandler unless Gitlab.rails5? include InvalidUTF8ErrorHandler unless Gitlab.rails5?
before_action :authenticate_sessionless_user!
before_action :authenticate_user! before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
@ -153,13 +153,6 @@ class ApplicationController < ActionController::Base
end end
end end
# This filter handles personal access tokens, and atom requests with rss tokens
def authenticate_sessionless_user!
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
sessionless_sign_in(user) if user
end
def log_exception(exception) def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled? Raven.capture_exception(exception) if sentry_enabled?
@ -426,25 +419,11 @@ class ApplicationController < ActionController::Base
Gitlab::I18n.with_user_locale(current_user, &block) Gitlab::I18n.with_user_locale(current_user, &block)
end end
def sessionless_sign_in(user)
if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in(user, store: false, message: :sessionless_sign_in)
end
end
def set_page_title_header def set_page_title_header
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8 # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab')) response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end end
def sessionless_user?
current_user && !session.keys.include?('warden.user.user.key')
end
def peek_request? def peek_request?
request.path.start_with?('/-/peek') request.path.start_with?('/-/peek')
end end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
# == SessionlessAuthentication
#
# Controller concern to handle PAT and RSS token authentication methods
#
module SessionlessAuthentication
# This filter handles personal access tokens, and atom requests with rss tokens
def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
sessionless_sign_in(user) if user
end
def sessionless_user?
current_user && !session.keys.include?('warden.user.user.key')
end
def sessionless_sign_in(user)
if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in(user, store: false, message: :sessionless_sign_in)
end
end
end

View file

@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility include ParamsBackwardCompatibility
include RendersMemberAccess include RendersMemberAccess
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :default_sorting before_action :default_sorting
skip_cross_project_access_check :index, :starred skip_cross_project_access_check :index, :starred

View file

@ -4,6 +4,9 @@ class DashboardController < Dashboard::ApplicationController
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
before_action :event_filter, only: :activity before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests] before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests] before_action :set_show_full_reference, only: [:issues, :merge_requests]

View file

@ -3,6 +3,7 @@
class GraphqlController < ApplicationController class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data # Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
prepend_before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
before_action :check_graphql_feature_flag! before_action :check_graphql_feature_flag!

View file

@ -9,6 +9,9 @@ class GroupsController < Groups::ApplicationController
respond_to :html respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
before_action :authenticate_user!, only: [:new, :create] before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create] before_action :group, except: [:index, :new, :create]

View file

@ -6,6 +6,7 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include RendersCommits include RendersCommits
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :whitelist_query_limiting, except: :commits_root before_action :whitelist_query_limiting, except: :commits_root
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root before_action :assign_ref_vars, except: :commits_root

View file

@ -9,10 +9,6 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuesCalendar include IssuesCalendar
include SpammableActions include SpammableActions
def self.authenticate_user_only_actions
%i[new]
end
def self.issue_except_actions def self.issue_except_actions
%i[index calendar new create bulk_update] %i[index calendar new create bulk_update]
end end
@ -21,7 +17,10 @@ class Projects::IssuesController < Projects::ApplicationController
%i[index calendar] %i[index calendar]
end end
prepend_before_action :authenticate_user!, only: authenticate_user_only_actions prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :authenticate_new_issue!, only: [:new]
prepend_before_action :store_uri, only: [:new, :show]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available! before_action :check_issues_available!
@ -232,16 +231,18 @@ class Projects::IssuesController < Projects::ApplicationController
] + [{ label_ids: [], assignee_ids: [] }] ] + [{ label_ids: [], assignee_ids: [] }]
end end
def authenticate_user! def authenticate_new_issue!
return if current_user return if current_user
notice = "Please sign in to create the new issue." notice = "Please sign in to create the new issue."
redirect_to new_user_session_path, notice: notice
end
def store_uri
if request.get? && !request.xhr? if request.get? && !request.xhr?
store_location_for :user, request.fullpath store_location_for :user, request.fullpath
end end
redirect_to new_user_session_path, notice: notice
end end
def serializer def serializer

View file

@ -3,6 +3,8 @@
class Projects::TagsController < Projects::ApplicationController class Projects::TagsController < Projects::ApplicationController
include SortingHelper include SortingHelper
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!

View file

@ -7,6 +7,8 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown include PreviewMarkdown
include SendFileUpload include SendFileUpload
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :whitelist_query_limiting, only: [:create] before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :redirect_git_extension, only: [:show] before_action :redirect_git_extension, only: [:show]

View file

@ -14,6 +14,7 @@ class UsersController < ApplicationController
calendar_activities: true calendar_activities: true
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists] before_action :user, except: [:exists]
before_action :authorize_read_user_profile!, before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets] only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]

View file

@ -0,0 +1,5 @@
---
title: Restrict Personal Access Tokens to API scope on web requests
merge_request:
author:
type: security

View file

@ -33,22 +33,22 @@ class Rack::Attack
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req| throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_api_enabled && Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
req.api_request? && req.api_request? &&
req.authenticated_user_id req.authenticated_user_id([:api])
end end
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_web_enabled && Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
req.web_request? && req.web_request? &&
req.authenticated_user_id req.authenticated_user_id([:api, :rss, :ics])
end end
class Request class Request
def unauthenticated? def unauthenticated?
!authenticated_user_id !authenticated_user_id([:api, :rss, :ics])
end end
def authenticated_user_id def authenticated_user_id(request_formats)
Gitlab::Auth::RequestAuthenticator.new(self).user&.id Gitlab::Auth::RequestAuthenticator.new(self).user(request_formats)&.id
end end
def api_request? def api_request?

View file

@ -13,12 +13,18 @@ module Gitlab
@request = request @request = request
end end
def user def user(request_formats)
find_sessionless_user || find_user_from_warden request_formats.each do |format|
user = find_sessionless_user(format)
return user if user
end
find_user_from_warden
end end
def find_sessionless_user def find_sessionless_user(request_format)
find_user_from_access_token || find_user_from_feed_token find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format)
rescue Gitlab::Auth::AuthenticationError rescue Gitlab::Auth::AuthenticationError
nil nil
end end

View file

@ -27,8 +27,8 @@ module Gitlab
current_request.env['warden']&.authenticate if verified_request? current_request.env['warden']&.authenticate if verified_request?
end end
def find_user_from_feed_token def find_user_from_feed_token(request_format)
return unless rss_request? || ics_request? return unless valid_rss_format?(request_format)
# NOTE: feed_token was renamed from rss_token but both needs to be supported because # NOTE: feed_token was renamed from rss_token but both needs to be supported because
# users might have already added the feed to their RSS reader before the rename # users might have already added the feed to their RSS reader before the rename
@ -38,6 +38,17 @@ module Gitlab
User.find_by_feed_token(token) || raise(UnauthorizedError) User.find_by_feed_token(token) || raise(UnauthorizedError)
end end
# We only allow Private Access Tokens with `api` scope to be used by web
# requests on RSS feeds or ICS files for backwards compatibility.
# It is also used by GraphQL/API requests.
def find_user_from_web_access_token(request_format)
return unless access_token && valid_web_access_format?(request_format)
validate_access_token!(scopes: [:api])
access_token.user || raise(UnauthorizedError)
end
def find_user_from_access_token def find_user_from_access_token
return unless access_token return unless access_token
@ -109,6 +120,26 @@ module Gitlab
@current_request ||= ensure_action_dispatch_request(request) @current_request ||= ensure_action_dispatch_request(request)
end end
def valid_web_access_format?(request_format)
case request_format
when :rss
rss_request?
when :ics
ics_request?
when :api
api_request?
end
end
def valid_rss_format?(request_format)
case request_format
when :rss
rss_request?
when :ics
ics_request?
end
end
def rss_request? def rss_request?
current_request.path.ends_with?('.atom') || current_request.format.atom? current_request.path.ends_with?('.atom') || current_request.format.atom?
end end
@ -116,6 +147,10 @@ module Gitlab
def ics_request? def ics_request?
current_request.path.ends_with?('.ics') || current_request.format.ics? current_request.path.ends_with?('.ics') || current_request.format.ics?
end end
def api_request?
current_request.path.starts_with?("/api/")
end
end end
end end
end end

View file

@ -107,59 +107,6 @@ describe ApplicationController do
end end
end end
describe "#authenticate_user_from_personal_access_token!" do
before do
stub_authentication_activity_metrics(debug: false)
end
controller(described_class) do
def index
render text: 'authenticated'
end
end
let(:personal_access_token) { create(:personal_access_token, user: user) }
context "when the 'personal_access_token' param is populated with the personal access token" do
it "logs the user in" do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
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
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
@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
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
get :index, private_token: "token"
expect(response.status).not_to eq(200)
expect(response.body).not_to eq('authenticated')
end
end
describe 'session expiration' do describe 'session expiration' do
controller(described_class) do controller(described_class) do
# The anonymous controller will report 401 and fail to run any actions. # The anonymous controller will report 401 and fail to run any actions.
@ -224,74 +171,6 @@ describe ApplicationController do
end end
end end
describe '#authenticate_sessionless_user!' do
before do
stub_authentication_activity_metrics(debug: false)
end
describe 'authenticating a user from a feed token' do
controller(described_class) do
def index
render text: 'authenticated'
end
end
context "when the 'feed_token' param is populated with the feed token" do
context 'when the request format is atom' do
it "logs the user in" do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
get :index, feed_token: user.feed_token, format: :atom
expect(response).to have_gitlab_http_status 200
expect(response.body).to eq 'authenticated'
end
end
context 'when the request format is ics' do
it "logs the user in" do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
get :index, feed_token: user.feed_token, format: :ics
expect(response).to have_gitlab_http_status 200
expect(response.body).to eq 'authenticated'
end
end
context 'when the request format is neither atom nor ics' do
it "doesn't log the user in" do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
get :index, feed_token: user.feed_token
expect(response.status).not_to have_gitlab_http_status 200
expect(response.body).not_to eq 'authenticated'
end
end
end
context "when the 'feed_token' param is populated with an invalid feed token" do
it "doesn't log the user" do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
get :index, feed_token: 'token', format: :atom
expect(response.status).not_to eq 200
expect(response.body).not_to eq 'authenticated'
end
end
end
end
describe '#route_not_found' do describe '#route_not_found' do
it 'renders 404 if authenticated' do it 'renders 404 if authenticated' do
allow(controller).to receive(:current_user).and_return(user) allow(controller).to receive(:current_user).and_return(user)
@ -557,36 +436,6 @@ describe ApplicationController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
context 'for sessionless users' do
render_views
before do
sign_out user
end
it 'renders a 403 when the sessionless user did not accept the terms' do
get :index, feed_token: user.feed_token, format: :atom
expect(response).to have_gitlab_http_status(403)
end
it 'renders the error message when the format was html' do
get :index,
private_token: create(:personal_access_token, user: user).token,
format: :html
expect(response.body).to have_content /accept the terms of service/i
end
it 'renders a 200 when the sessionless user accepted the terms' do
accept_terms(user)
get :index, feed_token: user.feed_token, format: :atom
expect(response).to have_gitlab_http_status(200)
end
end
end end
end end

View file

@ -0,0 +1,5 @@
require 'spec_helper'
describe Dashboard::ProjectsController do
it_behaves_like 'authenticates sessionless user', :index, :atom
end

View file

@ -1,21 +1,26 @@
require 'spec_helper' require 'spec_helper'
describe DashboardController do describe DashboardController do
let(:user) { create(:user) } context 'signed in' do
let(:project) { create(:project) } let(:user) { create(:user) }
let(:project) { create(:project) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end
describe 'GET issues' do
it_behaves_like 'issuables list meta-data', :issue, :issues
it_behaves_like 'issuables requiring filter', :issues
end
describe 'GET merge requests' do
it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
it_behaves_like 'issuables requiring filter', :merge_requests
end
end end
describe 'GET issues' do it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'issuables list meta-data', :issue, :issues it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
it_behaves_like 'issuables requiring filter', :issues
end
describe 'GET merge requests' do
it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
it_behaves_like 'issuables requiring filter', :merge_requests
end
end end

View file

@ -52,15 +52,58 @@ describe GraphqlController do
end end
end end
context 'token authentication' do
before do
stub_authentication_activity_metrics(debug: false)
end
let(:user) { create(:user, username: 'Simon') }
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
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
run_test_query!(private_token: personal_access_token.token)
expect(response).to have_gitlab_http_status(200)
expect(query_response).to eq('echo' => '"Simon" says: test success')
end
end
context 'when the personal access token has no api scope' do
it 'does not log the user in' do
personal_access_token.update(scopes: [:read_user])
run_test_query!(private_token: personal_access_token.token)
expect(response).to have_gitlab_http_status(200)
expect(query_response).to eq('echo' => 'nil says: test success')
end
end
context 'without token' do
it 'shows public data' do
run_test_query!
expect(query_response).to eq('echo' => 'nil says: test success')
end
end
end
# Chosen to exercise all the moving parts in GraphqlController#execute # Chosen to exercise all the moving parts in GraphqlController#execute
def run_test_query!(variables: { 'text' => 'test success' }) def run_test_query!(variables: { 'text' => 'test success' }, private_token: nil)
query = <<~QUERY query = <<~QUERY
query Echo($text: String) { query Echo($text: String) {
echo(text: $text) echo(text: $text)
} }
QUERY QUERY
post :execute, query: query, operationName: 'Echo', variables: variables post :execute, query: query, operationName: 'Echo', variables: variables, private_token: private_token
end end
def query_response def query_response

View file

@ -606,4 +606,24 @@ describe GroupsController do
end end
end end
end end
context 'token authentication' do
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
default_params.merge!(id: group)
end
end
it_behaves_like 'authenticates sessionless user', :issues, :atom, public: true do
before do
default_params.merge!(id: group, author_id: user.id)
end
end
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics, public: true do
before do
default_params.merge!(id: group)
end
end
end
end end

View file

@ -5,87 +5,115 @@ describe Projects::CommitsController do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
sign_in(user)
project.add_maintainer(user) project.add_maintainer(user)
end end
describe "GET commits_root" do context 'signed in' do
context "no ref is provided" do before do
it 'should redirect to the default branch of the project' do sign_in(user)
get(:commits_root, end
namespace_id: project.namespace,
project_id: project)
expect(response).to redirect_to project_commits_path(project) describe "GET commits_root" do
context "no ref is provided" do
it 'should redirect to the default branch of the project' do
get(:commits_root,
namespace_id: project.namespace,
project_id: project)
expect(response).to redirect_to project_commits_path(project)
end
end
end
describe "GET show" do
render_views
context 'with file path' do
before do
get(:show,
namespace_id: project.namespace,
project_id: project,
id: id)
end
context "valid branch, valid file" do
let(:id) { 'master/README.md' }
it { is_expected.to respond_with(:success) }
end
context "valid branch, invalid file" do
let(:id) { 'master/invalid-path.rb' }
it { is_expected.to respond_with(:not_found) }
end
context "invalid branch, valid file" do
let(:id) { 'invalid-branch/README.md' }
it { is_expected.to respond_with(:not_found) }
end
end
context "when the ref name ends in .atom" do
context "when the ref does not exist with the suffix" do
before do
get(:show,
namespace_id: project.namespace,
project_id: project,
id: "master.atom")
end
it "renders as atom" do
expect(response).to be_success
expect(response.content_type).to eq('application/atom+xml')
end
it 'renders summary with type=html' do
expect(response.body).to include('<summary type="html">')
end
end
context "when the ref exists with the suffix" do
before do
commit = project.repository.commit('master')
allow_any_instance_of(Repository).to receive(:commit).and_call_original
allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
get(:show,
namespace_id: project.namespace,
project_id: project,
id: "master.atom")
end
it "renders as HTML" do
expect(response).to be_success
expect(response.content_type).to eq('text/html')
end
end
end end
end end
end end
describe "GET show" do context 'token authentication' do
render_views context 'public project' do
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
public_project = create(:project, :repository, :public)
context 'with file path' do default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom")
before do end
get(:show,
namespace_id: project.namespace,
project_id: project,
id: id)
end
context "valid branch, valid file" do
let(:id) { 'master/README.md' }
it { is_expected.to respond_with(:success) }
end
context "valid branch, invalid file" do
let(:id) { 'master/invalid-path.rb' }
it { is_expected.to respond_with(:not_found) }
end
context "invalid branch, valid file" do
let(:id) { 'invalid-branch/README.md' }
it { is_expected.to respond_with(:not_found) }
end end
end end
context "when the ref name ends in .atom" do context 'private project' do
context "when the ref does not exist with the suffix" do it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do
before do before do
get(:show, private_project = create(:project, :repository, :private)
namespace_id: project.namespace, private_project.add_maintainer(user)
project_id: project,
id: "master.atom")
end
it "renders as atom" do default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom")
expect(response).to be_success
expect(response.content_type).to eq('application/atom+xml')
end
it 'renders summary with type=html' do
expect(response.body).to include('<summary type="html">')
end
end
context "when the ref exists with the suffix" do
before do
commit = project.repository.commit('master')
allow_any_instance_of(Repository).to receive(:commit).and_call_original
allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
get(:show,
namespace_id: project.namespace,
project_id: project,
id: "master.atom")
end
it "renders as HTML" do
expect(response).to be_success
expect(response.content_type).to eq('text/html')
end end
end end
end end

View file

@ -1068,4 +1068,40 @@ describe Projects::IssuesController do
end end
end end
end end
context 'private project with token authentication' do
let(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user', :index, :atom do
before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
it_behaves_like 'authenticates sessionless user', :calendar, :ics do
before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
end
context 'public project with token authentication' do
let(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
before do
default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
end
end
it_behaves_like 'authenticates sessionless user', :calendar, :ics, public: true do
before do
default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
end
end
end
end end

View file

@ -35,4 +35,26 @@ describe Projects::TagsController do
it { is_expected.to respond_with(:not_found) } it { is_expected.to respond_with(:not_found) }
end end
end end
context 'private project with token authentication' do
let(:private_project) { create(:project, :repository, :private) }
it_behaves_like 'authenticates sessionless user', :index, :atom do
before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
end
context 'public project with token authentication' do
let(:public_project) { create(:project, :repository, :public) }
it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
before do
default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
end
end
end
end end

View file

@ -882,6 +882,28 @@ describe ProjectsController do
end end
end end
context 'private project with token authentication' do
let(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user', :show, :atom do
before do
default_params.merge!(id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
end
context 'public project with token authentication' do
let(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
default_params.merge!(id: public_project, namespace_id: public_project.namespace)
end
end
end
def project_moved_message(redirect_route, project) def project_moved_message(redirect_route, project)
"Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
end end

View file

@ -395,6 +395,14 @@ describe UsersController do
end end
end end
context 'token authentication' do
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
default_params.merge!(username: user.username)
end
end
end
def user_moved_message(redirect_route, user) def user_moved_message(redirect_route, user)
"User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path." "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
end end

View file

@ -19,17 +19,17 @@ describe Gitlab::Auth::RequestAuthenticator do
allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user) allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user)
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
expect(subject.user).to eq sessionless_user expect(subject.user([:api])).to eq sessionless_user
end end
it 'returns session user if no sessionless user found' do it 'returns session user if no sessionless user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
expect(subject.user).to eq session_user expect(subject.user([:api])).to eq session_user
end end
it 'returns nil if no user found' do it 'returns nil if no user found' do
expect(subject.user).to be_blank expect(subject.user([:api])).to be_blank
end end
it 'bubbles up exceptions' do it 'bubbles up exceptions' do
@ -42,26 +42,26 @@ describe Gitlab::Auth::RequestAuthenticator do
let!(:feed_token_user) { build(:user) } let!(:feed_token_user) { build(:user) }
it 'returns access_token user first' do it 'returns access_token user first' do
allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user) allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user)
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
expect(subject.find_sessionless_user).to eq access_token_user expect(subject.find_sessionless_user([:api])).to eq access_token_user
end end
it 'returns feed_token user if no access_token user found' do it 'returns feed_token user if no access_token user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
expect(subject.find_sessionless_user).to eq feed_token_user expect(subject.find_sessionless_user([:api])).to eq feed_token_user
end end
it 'returns nil if no user found' do it 'returns nil if no user found' do
expect(subject.find_sessionless_user).to be_blank expect(subject.find_sessionless_user([:api])).to be_blank
end end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError) allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
expect(subject.find_sessionless_user).to be_blank expect(subject.find_sessionless_user([:api])).to be_blank
end end
end end
end end

View file

@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do
'rack.input' => '' 'rack.input' => ''
} }
end end
let(:request) { Rack::Request.new(env)} let(:request) { Rack::Request.new(env) }
def set_param(key, value) def set_param(key, value)
request.update_param(key, value) request.update_param(key, value)
@ -49,6 +49,7 @@ describe Gitlab::Auth::UserAuthFinders do
describe '#find_user_from_feed_token' do describe '#find_user_from_feed_token' do
context 'when the request format is atom' do context 'when the request format is atom' do
before do before do
env['SCRIPT_NAME'] = 'url.atom'
env['HTTP_ACCEPT'] = 'application/atom+xml' env['HTTP_ACCEPT'] = 'application/atom+xml'
end end
@ -56,17 +57,17 @@ describe Gitlab::Auth::UserAuthFinders do
it 'returns user if valid feed_token' do it 'returns user if valid feed_token' do
set_param(:feed_token, user.feed_token) set_param(:feed_token, user.feed_token)
expect(find_user_from_feed_token).to eq user expect(find_user_from_feed_token(:rss)).to eq user
end end
it 'returns nil if feed_token is blank' do it 'returns nil if feed_token is blank' do
expect(find_user_from_feed_token).to be_nil expect(find_user_from_feed_token(:rss)).to be_nil
end end
it 'returns exception if invalid feed_token' do it 'returns exception if invalid feed_token' do
set_param(:feed_token, 'invalid_token') set_param(:feed_token, 'invalid_token')
expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
end end
end end
@ -74,34 +75,38 @@ describe Gitlab::Auth::UserAuthFinders do
it 'returns user if valid rssd_token' do it 'returns user if valid rssd_token' do
set_param(:rss_token, user.feed_token) set_param(:rss_token, user.feed_token)
expect(find_user_from_feed_token).to eq user expect(find_user_from_feed_token(:rss)).to eq user
end end
it 'returns nil if rss_token is blank' do it 'returns nil if rss_token is blank' do
expect(find_user_from_feed_token).to be_nil expect(find_user_from_feed_token(:rss)).to be_nil
end end
it 'returns exception if invalid rss_token' do it 'returns exception if invalid rss_token' do
set_param(:rss_token, 'invalid_token') set_param(:rss_token, 'invalid_token')
expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
end end
end end
end end
context 'when the request format is not atom' do context 'when the request format is not atom' do
it 'returns nil' do it 'returns nil' do
env['SCRIPT_NAME'] = 'json'
set_param(:feed_token, user.feed_token) set_param(:feed_token, user.feed_token)
expect(find_user_from_feed_token).to be_nil expect(find_user_from_feed_token(:rss)).to be_nil
end end
end end
context 'when the request format is empty' do context 'when the request format is empty' do
it 'the method call does not modify the original value' do it 'the method call does not modify the original value' do
env['SCRIPT_NAME'] = 'url.atom'
env.delete('action_dispatch.request.formats') env.delete('action_dispatch.request.formats')
find_user_from_feed_token find_user_from_feed_token(:rss)
expect(env['action_dispatch.request.formats']).to be_nil expect(env['action_dispatch.request.formats']).to be_nil
end end
@ -111,8 +116,12 @@ describe Gitlab::Auth::UserAuthFinders do
describe '#find_user_from_access_token' do describe '#find_user_from_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
env['SCRIPT_NAME'] = 'url.atom'
end
it 'returns nil if no access_token present' do it 'returns nil if no access_token present' do
expect(find_personal_access_token).to be_nil expect(find_user_from_access_token).to be_nil
end end
context 'when validate_access_token! returns valid' do context 'when validate_access_token! returns valid' do
@ -131,9 +140,59 @@ describe Gitlab::Auth::UserAuthFinders do
end end
end end
describe '#find_user_from_web_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it 'returns exception if token has no user' do
allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
context 'no feed or API requests' do
it 'returns nil if the request is not RSS' do
expect(find_user_from_web_access_token(:rss)).to be_nil
end
it 'returns nil if the request is not ICS' do
expect(find_user_from_web_access_token(:ics)).to be_nil
end
it 'returns nil if the request is not API' do
expect(find_user_from_web_access_token(:api)).to be_nil
end
end
it 'returns the user for RSS requests' do
env['SCRIPT_NAME'] = 'url.atom'
expect(find_user_from_web_access_token(:rss)).to eq(user)
end
it 'returns the user for ICS requests' do
env['SCRIPT_NAME'] = 'url.ics'
expect(find_user_from_web_access_token(:ics)).to eq(user)
end
it 'returns the user for API requests' do
env['SCRIPT_NAME'] = '/api/endpoint'
expect(find_user_from_web_access_token(:api)).to eq(user)
end
end
describe '#find_personal_access_token' do describe '#find_personal_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
env['SCRIPT_NAME'] = 'url.atom'
end
context 'passed as header' do context 'passed as header' do
it 'returns token if valid personal_access_token' do it 'returns token if valid personal_access_token' do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token

View file

@ -0,0 +1,92 @@
shared_examples 'authenticates sessionless user' do |path, format, params|
params ||= {}
before do
stub_authentication_activity_metrics(debug: false)
end
let(:user) { create(:user) }
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:default_params) { { format: format }.merge(params.except(:public) || {}) }
context "when the 'personal_access_token' param is populated with the personal access token" do
it 'logs the user in' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
get path, default_params.merge(private_token: personal_access_token.token)
expect(response).to have_gitlab_http_status(200)
expect(controller.current_user).to eq(user)
end
it 'does not log the user in if page is public', if: params[:public] do
get path, default_params
expect(response).to have_gitlab_http_status(200)
expect(controller.current_user).to be_nil
end
end
context 'when the personal access token has no api scope', unless: params[:public] do
it 'does not log the user in' do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
personal_access_token.update(scopes: [:read_user])
get path, default_params.merge(private_token: personal_access_token.token)
expect(response).not_to have_gitlab_http_status(200)
end
end
context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
it 'logs the user in' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
@request.headers['PRIVATE-TOKEN'] = personal_access_token.token
get path, default_params
expect(response).to have_gitlab_http_status(200)
end
end
context "when the 'feed_token' param is populated with the feed token", if: format == :rss do
it "logs the user in" do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
get path, default_params.merge(feed_token: user.feed_token)
expect(response).to have_gitlab_http_status 200
end
end
context "when the 'feed_token' param is populated with an invalid feed token", if: format == :rss, unless: params[:public] do
it "logs the user" do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
get path, default_params.merge(feed_token: 'token')
expect(response.status).not_to eq 200
end
end
it "doesn't log the user in otherwise", unless: params[:public] do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
get path, default_params.merge(private_token: 'token')
expect(response.status).not_to eq(200)
end
end