Commit abca6ee4 authored by Stan Hu's avatar Stan Hu

Merge branch '9310-reduce-the-scope-of-geo-jwt-step-2' into 'master'

Resolve "Reduce the scope of Geo JWT. Step 2"

Closes #9310

See merge request gitlab-org/gitlab-ee!9502
parents eb967bd2 be9c2246
...@@ -271,7 +271,6 @@ questions from [owasp.org](https://www.owasp.org). ...@@ -271,7 +271,6 @@ questions from [owasp.org](https://www.owasp.org).
- LFS and File ID. - LFS and File ID.
- Upload and File ID. - Upload and File ID.
- Job Artifact and File ID. - Job Artifact and File ID.
- Geo JWTs scopes are not enforced, but will be in a future version (currently scheduled for GitLab 11.9).
### What access requirements have been defined for URI and Service calls? ### What access requirements have been defined for URI and Service calls?
......
...@@ -4,6 +4,7 @@ module EE ...@@ -4,6 +4,7 @@ module EE
module Projects module Projects
module GitHttpController module GitHttpController
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :render_ok override :render_ok
def render_ok def render_ok
...@@ -46,20 +47,31 @@ module EE ...@@ -46,20 +47,31 @@ module EE
override :authenticate_user override :authenticate_user
def authenticate_user def authenticate_user
return super unless geo_request? return super unless geo_request?
return render_bad_geo_auth('Bad token') unless decoded_authorization
return render_bad_geo_auth('Unauthorized scope') unless jwt_scope_valid?
payload = ::Gitlab::Geo::JwtRequestDecoder.new(request.headers['Authorization']).decode # grant access
if payload
@authentication_result = ::Gitlab::Auth::Result.new(nil, project, :geo, [:download_code, :push_code]) # rubocop:disable Gitlab/ModuleWithInstanceVariables @authentication_result = ::Gitlab::Auth::Result.new(nil, project, :geo, [:download_code, :push_code]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
return # grant access
end
render_bad_geo_auth('Bad token')
rescue ::Gitlab::Geo::InvalidDecryptionKeyError rescue ::Gitlab::Geo::InvalidDecryptionKeyError
render_bad_geo_auth("Invalid decryption key") render_bad_geo_auth("Invalid decryption key")
rescue ::Gitlab::Geo::InvalidSignatureTimeError rescue ::Gitlab::Geo::InvalidSignatureTimeError
render_bad_geo_auth("Invalid signature time ") render_bad_geo_auth("Invalid signature time ")
end end
def jwt_scope_valid?
decoded_authorization[:scope] == ::Gitlab::Geo::JwtRequestDecoder.build_repository_scope(repository_type, project.id)
end
def repository_type
wiki? ? 'wiki' : 'repository'
end
def decoded_authorization
strong_memoize(:decoded_authorization) do
::Gitlab::Geo::JwtRequestDecoder.new(request.headers['Authorization']).decode
end
end
def render_bad_geo_auth(message) def render_bad_geo_auth(message)
render plain: "Geo JWT authentication failed: #{message}", status: :unauthorized render plain: "Geo JWT authentication failed: #{message}", status: :unauthorized
end end
......
...@@ -103,12 +103,11 @@ module Geo ...@@ -103,12 +103,11 @@ module Geo
# Build a JWT header for authentication # Build a JWT header for authentication
def jwt_authentication_header def jwt_authentication_header
authorization = ::Gitlab::Geo::RepoSyncRequest.new(scope: gl_repository).authorization authorization = ::Gitlab::Geo::RepoSyncRequest.new(
{ "http.#{remote_url}.extraHeader" => "Authorization: #{authorization}" } scope: ::Gitlab::Geo::JwtRequestDecoder.build_repository_scope(type, project.id)
end ).authorization
def gl_repository { "http.#{remote_url}.extraHeader" => "Authorization: #{authorization}" }
"#{type}-#{project.id}"
end end
def remote_url def remote_url
......
...@@ -6,22 +6,32 @@ module Geo ...@@ -6,22 +6,32 @@ module Geo
# * Returning the necessary response data to send the file back # * Returning the necessary response data to send the file back
class FileUploadService < FileService class FileUploadService < FileService
attr_reader :auth_header attr_reader :auth_header
include ::Gitlab::Utils::StrongMemoize
def initialize(params, auth_header) def initialize(params, auth_header)
super(params[:type], params[:id]) super(params[:type], params[:id])
@auth_header = auth_header @auth_header = auth_header
end end
def execute
# Returns { code: :ok, file: CarrierWave File object } upon success # Returns { code: :ok, file: CarrierWave File object } upon success
data = ::Gitlab::Geo::JwtRequestDecoder.new(auth_header).decode def execute
return unless data.present? return unless decoded_authorization.present? && jwt_scope_valid?
uploader_klass.new(object_db_id, data).execute uploader_klass.new(object_db_id, decoded_authorization).execute
end end
private private
def jwt_scope_valid?
(decoded_authorization[:file_type] == object_type.to_s) && (decoded_authorization[:file_id] == object_db_id)
end
def decoded_authorization
strong_memoize(:decoded_authorization) do
::Gitlab::Geo::JwtRequestDecoder.new(auth_header).decode
end
end
def uploader_klass def uploader_klass
"Gitlab::Geo::#{service_klass_name}Uploader".constantize "Gitlab::Geo::#{service_klass_name}Uploader".constantize
rescue NameError => e rescue NameError => e
......
---
title: Enforce Geo JWT tokens scope
merge_request: 9502
author:
type: changed
...@@ -4,6 +4,7 @@ module EE ...@@ -4,6 +4,7 @@ module EE
module API module API
module Helpers module Helpers
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
def require_node_to_be_enabled! def require_node_to_be_enabled!
forbidden! 'Geo node is disabled.' unless ::Gitlab::Geo.current_node&.enabled? forbidden! 'Geo node is disabled.' unless ::Gitlab::Geo.current_node&.enabled?
...@@ -14,16 +15,10 @@ module EE ...@@ -14,16 +15,10 @@ module EE
end end
def authenticate_by_gitlab_geo_node_token! def authenticate_by_gitlab_geo_node_token!
auth_header = headers['Authorization'] unauthorized! unless authorization_header_valid?
begin
unless auth_header && ::Gitlab::Geo::JwtRequestDecoder.new(auth_header).decode
unauthorized!
end
rescue ::Gitlab::Geo::InvalidDecryptionKeyError, ::Gitlab::Geo::InvalidSignatureTimeError => e rescue ::Gitlab::Geo::InvalidDecryptionKeyError, ::Gitlab::Geo::InvalidSignatureTimeError => e
render_api_error!(e.to_s, 401) render_api_error!(e.to_s, 401)
end end
end
override :current_user override :current_user
def current_user def current_user
...@@ -39,6 +34,14 @@ module EE ...@@ -39,6 +34,14 @@ module EE
end end
end end
def authorization_header_valid?
auth_header = headers['Authorization']
return unless auth_header
scope = ::Gitlab::Geo::JwtRequestDecoder.new(auth_header).decode.try { |x| x[:scope] }
scope == ::Gitlab::Geo::API_SCOPE
end
def check_project_feature_available!(feature) def check_project_feature_available!(feature)
not_found! unless user_project.feature_available?(feature) not_found! unless user_project.feature_available?(feature)
end end
......
...@@ -12,6 +12,10 @@ module Gitlab ...@@ -12,6 +12,10 @@ module Gitlab
token_type == ::Gitlab::Geo::BaseRequest::GITLAB_GEO_AUTH_TOKEN_TYPE token_type == ::Gitlab::Geo::BaseRequest::GITLAB_GEO_AUTH_TOKEN_TYPE
end end
def self.build_repository_scope(repository_type, project_id)
[repository_type, project_id].join('-')
end
attr_reader :auth_header attr_reader :auth_header
def initialize(auth_header) def initialize(auth_header)
......
...@@ -80,5 +80,14 @@ describe EE::API::Helpers do ...@@ -80,5 +80,14 @@ describe EE::API::Helpers do
expect(JSON.parse(last_response.body)).to eq({ 'message' => 'Gitlab::Geo::InvalidSignatureTimeError' }) expect(JSON.parse(last_response.body)).to eq({ 'message' => 'Gitlab::Geo::InvalidSignatureTimeError' })
end end
it 'returns unauthorized response when scope is not valid' do
allow_any_instance_of(::Gitlab::Geo::JwtRequestDecoder).to receive(:decode).and_return(scope: 'invalid_scope')
header 'Authorization', 'test'
get 'protected', params: { current_user: 'test' }
expect(JSON.parse(last_response.body)).to eq({ 'message' => '401 Unauthorized' })
end
end end
end end
...@@ -7,6 +7,12 @@ describe Gitlab::Geo::JwtRequestDecoder do ...@@ -7,6 +7,12 @@ describe Gitlab::Geo::JwtRequestDecoder do
subject { described_class.new(request.headers['Authorization']) } subject { described_class.new(request.headers['Authorization']) }
describe ".build_repository_scope" do
it 'returns a scope that consolidates repository type and project id' do
expect(described_class.build_repository_scope('wiki', 5)).to eq('wiki-5')
end
end
describe '#decode' do describe '#decode' do
it 'decodes correct data' do it 'decodes correct data' do
expect(subject.decode).to eq(data) expect(subject.decode).to eq(data)
......
...@@ -21,6 +21,7 @@ describe "Git HTTP requests (Geo)" do ...@@ -21,6 +21,7 @@ describe "Git HTTP requests (Geo)" do
let!(:key_for_user_without_push_access) { create(:key, user: user_without_push_access) } let!(:key_for_user_without_push_access) { create(:key, user: user_without_push_access) }
let(:env) { valid_geo_env } let(:env) { valid_geo_env }
let(:auth_token_with_invalid_scope) { Gitlab::Geo::BaseRequest.new(scope: "invalid-#{project.id}").authorization }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -346,6 +347,50 @@ describe "Git HTTP requests (Geo)" do ...@@ -346,6 +347,50 @@ describe "Git HTTP requests (Geo)" do
end end
end end
end end
context 'invalid scope' do
let(:repository_path) { project.full_path }
subject do
make_request
response
end
def make_request
get "/#{repository_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end
shared_examples_for 'unauthorized because of invalid scope' do
it { is_expected.to have_gitlab_http_status(:unauthorized) }
it 'returns correct error' do
expect(subject.parsed_body).to eq('Geo JWT authentication failed: Unauthorized scope')
end
end
context 'invalid scope of Geo JWT token' do
let(:env) { geo_env(auth_token_with_invalid_scope) }
include_examples 'unauthorized because of invalid scope'
end
context 'Geo JWT token scopes for wiki and repository are not interchangeable' do
context 'wiki scope' do
let(:auth_token_with_valid_wiki_scope) { Gitlab::Geo::BaseRequest.new(scope: "wiki-#{project.id}").authorization }
let(:env) { geo_env(auth_token_with_valid_wiki_scope) }
include_examples 'unauthorized because of invalid scope'
end
context 'respository scope' do
let(:repository_path) { project.wiki.full_path }
let(:auth_token_with_valid_repository_scope) { Gitlab::Geo::BaseRequest.new(scope: "repository-#{project.id}").authorization }
let(:env) { geo_env(auth_token_with_valid_repository_scope) }
include_examples 'unauthorized because of invalid scope'
end
end
end
end end
def valid_geo_env def valid_geo_env
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment