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:
parent
e122e14ac6
commit
fe5f75930e
28 changed files with 551 additions and 294 deletions
|
@ -12,11 +12,11 @@ class ApplicationController < ActionController::Base
|
|||
include WorkhorseHelper
|
||||
include EnforcesTwoFactorAuthentication
|
||||
include WithPerformanceBar
|
||||
include SessionlessAuthentication
|
||||
# this can be removed after switching to rails 5
|
||||
# https://gitlab.com/gitlab-org/gitlab-ce/issues/51908
|
||||
include InvalidUTF8ErrorHandler unless Gitlab.rails5?
|
||||
|
||||
before_action :authenticate_sessionless_user!
|
||||
before_action :authenticate_user!
|
||||
before_action :enforce_terms!, if: :should_enforce_terms?
|
||||
before_action :validate_user_service_ticket!
|
||||
|
@ -153,13 +153,6 @@ class ApplicationController < ActionController::Base
|
|||
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)
|
||||
Raven.capture_exception(exception) if sentry_enabled?
|
||||
|
||||
|
@ -426,25 +419,11 @@ class ApplicationController < ActionController::Base
|
|||
Gitlab::I18n.with_user_locale(current_user, &block)
|
||||
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
|
||||
# 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'))
|
||||
end
|
||||
|
||||
def sessionless_user?
|
||||
current_user && !session.keys.include?('warden.user.user.key')
|
||||
end
|
||||
|
||||
def peek_request?
|
||||
request.path.start_with?('/-/peek')
|
||||
end
|
||||
|
|
28
app/controllers/concerns/sessionless_authentication.rb
Normal file
28
app/controllers/concerns/sessionless_authentication.rb
Normal 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
|
|
@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
|||
include ParamsBackwardCompatibility
|
||||
include RendersMemberAccess
|
||||
|
||||
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
||||
before_action :set_non_archived_param
|
||||
before_action :default_sorting
|
||||
skip_cross_project_access_check :index, :starred
|
||||
|
|
|
@ -4,6 +4,9 @@ class DashboardController < Dashboard::ApplicationController
|
|||
include IssuesAction
|
||||
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 :projects, only: [:issues, :merge_requests]
|
||||
before_action :set_show_full_reference, only: [:issues, :merge_requests]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class GraphqlController < ApplicationController
|
||||
# Unauthenticated users have access to the API for public data
|
||||
skip_before_action :authenticate_user!
|
||||
prepend_before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
|
||||
|
||||
before_action :check_graphql_feature_flag!
|
||||
|
||||
|
|
|
@ -9,6 +9,9 @@ class GroupsController < Groups::ApplicationController
|
|||
|
||||
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 :group, except: [:index, :new, :create]
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
include ExtractsPath
|
||||
include RendersCommits
|
||||
|
||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
before_action :whitelist_query_limiting, except: :commits_root
|
||||
before_action :require_non_empty_project
|
||||
before_action :assign_ref_vars, except: :commits_root
|
||||
|
|
|
@ -9,10 +9,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
include IssuesCalendar
|
||||
include SpammableActions
|
||||
|
||||
def self.authenticate_user_only_actions
|
||||
%i[new]
|
||||
end
|
||||
|
||||
def self.issue_except_actions
|
||||
%i[index calendar new create bulk_update]
|
||||
end
|
||||
|
@ -21,7 +17,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
%i[index calendar]
|
||||
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 :check_issues_available!
|
||||
|
@ -232,16 +231,18 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
] + [{ label_ids: [], assignee_ids: [] }]
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
def authenticate_new_issue!
|
||||
return if current_user
|
||||
|
||||
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?
|
||||
store_location_for :user, request.fullpath
|
||||
end
|
||||
|
||||
redirect_to new_user_session_path, notice: notice
|
||||
end
|
||||
|
||||
def serializer
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class Projects::TagsController < Projects::ApplicationController
|
||||
include SortingHelper
|
||||
|
||||
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
||||
|
||||
# Authorize
|
||||
before_action :require_non_empty_project
|
||||
before_action :authorize_download_code!
|
||||
|
|
|
@ -7,6 +7,8 @@ class ProjectsController < Projects::ApplicationController
|
|||
include PreviewMarkdown
|
||||
include SendFileUpload
|
||||
|
||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
|
||||
before_action :whitelist_query_limiting, only: [:create]
|
||||
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
|
||||
before_action :redirect_git_extension, only: [:show]
|
||||
|
|
|
@ -14,6 +14,7 @@ class UsersController < ApplicationController
|
|||
calendar_activities: true
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
before_action :user, except: [:exists]
|
||||
before_action :authorize_read_user_profile!,
|
||||
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
|
||||
|
|
5
changelogs/unreleased/security-fix-pat-web-access.yml
Normal file
5
changelogs/unreleased/security-fix-pat-web-access.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Restrict Personal Access Tokens to API scope on web requests
|
||||
merge_request:
|
||||
author:
|
||||
type: security
|
|
@ -33,22 +33,22 @@ class Rack::Attack
|
|||
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
|
||||
Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
|
||||
req.api_request? &&
|
||||
req.authenticated_user_id
|
||||
req.authenticated_user_id([:api])
|
||||
end
|
||||
|
||||
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
|
||||
Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
|
||||
req.web_request? &&
|
||||
req.authenticated_user_id
|
||||
req.authenticated_user_id([:api, :rss, :ics])
|
||||
end
|
||||
|
||||
class Request
|
||||
def unauthenticated?
|
||||
!authenticated_user_id
|
||||
!authenticated_user_id([:api, :rss, :ics])
|
||||
end
|
||||
|
||||
def authenticated_user_id
|
||||
Gitlab::Auth::RequestAuthenticator.new(self).user&.id
|
||||
def authenticated_user_id(request_formats)
|
||||
Gitlab::Auth::RequestAuthenticator.new(self).user(request_formats)&.id
|
||||
end
|
||||
|
||||
def api_request?
|
||||
|
|
|
@ -13,12 +13,18 @@ module Gitlab
|
|||
@request = request
|
||||
end
|
||||
|
||||
def user
|
||||
find_sessionless_user || find_user_from_warden
|
||||
def user(request_formats)
|
||||
request_formats.each do |format|
|
||||
user = find_sessionless_user(format)
|
||||
|
||||
return user if user
|
||||
end
|
||||
|
||||
find_user_from_warden
|
||||
end
|
||||
|
||||
def find_sessionless_user
|
||||
find_user_from_access_token || find_user_from_feed_token
|
||||
def find_sessionless_user(request_format)
|
||||
find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format)
|
||||
rescue Gitlab::Auth::AuthenticationError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -27,8 +27,8 @@ module Gitlab
|
|||
current_request.env['warden']&.authenticate if verified_request?
|
||||
end
|
||||
|
||||
def find_user_from_feed_token
|
||||
return unless rss_request? || ics_request?
|
||||
def find_user_from_feed_token(request_format)
|
||||
return unless valid_rss_format?(request_format)
|
||||
|
||||
# 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
|
||||
|
@ -38,6 +38,17 @@ module Gitlab
|
|||
User.find_by_feed_token(token) || raise(UnauthorizedError)
|
||||
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
|
||||
return unless access_token
|
||||
|
||||
|
@ -109,6 +120,26 @@ module Gitlab
|
|||
@current_request ||= ensure_action_dispatch_request(request)
|
||||
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?
|
||||
current_request.path.ends_with?('.atom') || current_request.format.atom?
|
||||
end
|
||||
|
@ -116,6 +147,10 @@ module Gitlab
|
|||
def ics_request?
|
||||
current_request.path.ends_with?('.ics') || current_request.format.ics?
|
||||
end
|
||||
|
||||
def api_request?
|
||||
current_request.path.starts_with?("/api/")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -107,59 +107,6 @@ describe ApplicationController do
|
|||
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
|
||||
controller(described_class) do
|
||||
# The anonymous controller will report 401 and fail to run any actions.
|
||||
|
@ -224,74 +171,6 @@ describe ApplicationController do
|
|||
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
|
||||
it 'renders 404 if authenticated' do
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
|
@ -557,36 +436,6 @@ describe ApplicationController do
|
|||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
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
|
||||
|
||||
|
|
5
spec/controllers/dashboard/projects_controller_spec.rb
Normal file
5
spec/controllers/dashboard/projects_controller_spec.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Dashboard::ProjectsController do
|
||||
it_behaves_like 'authenticates sessionless user', :index, :atom
|
||||
end
|
|
@ -1,21 +1,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe DashboardController do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
context 'signed in' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
before do
|
||||
project.add_maintainer(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
|
||||
|
||||
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
|
||||
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
|
||||
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
|
||||
end
|
||||
|
|
|
@ -52,15 +52,58 @@ describe GraphqlController do
|
|||
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
|
||||
def run_test_query!(variables: { 'text' => 'test success' })
|
||||
def run_test_query!(variables: { 'text' => 'test success' }, private_token: nil)
|
||||
query = <<~QUERY
|
||||
query Echo($text: String) {
|
||||
echo(text: $text)
|
||||
}
|
||||
QUERY
|
||||
|
||||
post :execute, query: query, operationName: 'Echo', variables: variables
|
||||
post :execute, query: query, operationName: 'Echo', variables: variables, private_token: private_token
|
||||
end
|
||||
|
||||
def query_response
|
||||
|
|
|
@ -606,4 +606,24 @@ describe GroupsController do
|
|||
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
|
||||
|
|
|
@ -5,87 +5,115 @@ describe Projects::CommitsController do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
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)
|
||||
context 'signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
describe "GET show" do
|
||||
render_views
|
||||
context 'token authentication' do
|
||||
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
|
||||
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) }
|
||||
default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the ref name ends in .atom" do
|
||||
context "when the ref does not exist with the suffix" do
|
||||
context 'private project' do
|
||||
it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do
|
||||
before do
|
||||
get(:show,
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: "master.atom")
|
||||
end
|
||||
private_project = create(:project, :repository, :private)
|
||||
private_project.add_maintainer(user)
|
||||
|
||||
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')
|
||||
default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1068,4 +1068,40 @@ describe Projects::IssuesController do
|
|||
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
|
||||
|
|
|
@ -35,4 +35,26 @@ describe Projects::TagsController do
|
|||
it { is_expected.to respond_with(:not_found) }
|
||||
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
|
||||
|
|
|
@ -882,6 +882,28 @@ describe ProjectsController do
|
|||
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)
|
||||
"Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
|
||||
end
|
||||
|
|
|
@ -395,6 +395,14 @@ describe UsersController do
|
|||
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)
|
||||
"User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
|
||||
end
|
||||
|
|
|
@ -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_user_from_warden).and_return(session_user)
|
||||
|
||||
expect(subject.user).to eq sessionless_user
|
||||
expect(subject.user([:api])).to eq sessionless_user
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(subject.user).to eq session_user
|
||||
expect(subject.user([:api])).to eq session_user
|
||||
end
|
||||
|
||||
it 'returns nil if no user found' do
|
||||
expect(subject.user).to be_blank
|
||||
expect(subject.user([:api])).to be_blank
|
||||
end
|
||||
|
||||
it 'bubbles up exceptions' do
|
||||
|
@ -42,26 +42,26 @@ describe Gitlab::Auth::RequestAuthenticator do
|
|||
let!(:feed_token_user) { build(:user) }
|
||||
|
||||
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)
|
||||
|
||||
expect(subject.find_sessionless_user).to eq access_token_user
|
||||
expect(subject.find_sessionless_user([:api])).to eq access_token_user
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(subject.find_sessionless_user).to eq feed_token_user
|
||||
expect(subject.find_sessionless_user([:api])).to eq feed_token_user
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do
|
|||
'rack.input' => ''
|
||||
}
|
||||
end
|
||||
let(:request) { Rack::Request.new(env)}
|
||||
let(:request) { Rack::Request.new(env) }
|
||||
|
||||
def set_param(key, value)
|
||||
request.update_param(key, value)
|
||||
|
@ -49,6 +49,7 @@ describe Gitlab::Auth::UserAuthFinders do
|
|||
describe '#find_user_from_feed_token' do
|
||||
context 'when the request format is atom' do
|
||||
before do
|
||||
env['SCRIPT_NAME'] = 'url.atom'
|
||||
env['HTTP_ACCEPT'] = 'application/atom+xml'
|
||||
end
|
||||
|
||||
|
@ -56,17 +57,17 @@ describe Gitlab::Auth::UserAuthFinders do
|
|||
it 'returns user if valid feed_token' do
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
it 'returns exception if invalid feed_token' do
|
||||
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
|
||||
|
||||
|
@ -74,34 +75,38 @@ describe Gitlab::Auth::UserAuthFinders do
|
|||
it 'returns user if valid rssd_token' do
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
it 'returns exception if invalid rss_token' do
|
||||
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
|
||||
|
||||
context 'when the request format is not atom' do
|
||||
it 'returns nil' do
|
||||
env['SCRIPT_NAME'] = 'json'
|
||||
|
||||
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
|
||||
|
||||
context 'when the request format is empty' do
|
||||
it 'the method call does not modify the original value' do
|
||||
env['SCRIPT_NAME'] = 'url.atom'
|
||||
|
||||
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
|
||||
end
|
||||
|
@ -111,8 +116,12 @@ describe Gitlab::Auth::UserAuthFinders do
|
|||
describe '#find_user_from_access_token' do
|
||||
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
|
||||
expect(find_personal_access_token).to be_nil
|
||||
expect(find_user_from_access_token).to be_nil
|
||||
end
|
||||
|
||||
context 'when validate_access_token! returns valid' do
|
||||
|
@ -131,9 +140,59 @@ describe Gitlab::Auth::UserAuthFinders do
|
|||
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
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
before do
|
||||
env['SCRIPT_NAME'] = 'url.atom'
|
||||
end
|
||||
|
||||
context 'passed as header' do
|
||||
it 'returns token if valid personal_access_token' do
|
||||
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue