Commit a66355f5 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'if-6990-enforce_smartcard_session_for_git_and_api' into 'master'

Require session with smartcard login for Git access

See merge request gitlab-org/gitlab-ee!14368
parents a3019114 c4d76666
...@@ -664,6 +664,9 @@ production: &base ...@@ -664,6 +664,9 @@ production: &base
# Port where the client side certificate is requested by the webserver (NGINX/Apache) # Port where the client side certificate is requested by the webserver (NGINX/Apache)
# client_certificate_required_port: 3444 # client_certificate_required_port: 3444
# Browser session with smartcard sign-in is required for Git access
# required_for_git_access: false
## Kerberos settings ## Kerberos settings
kerberos: kerberos:
# Allow the HTTP Negotiate authentication method for Git clients # Allow the HTTP Negotiate authentication method for Git clients
......
...@@ -76,6 +76,7 @@ Gitlab.ee do ...@@ -76,6 +76,7 @@ Gitlab.ee do
Settings['smartcard'] ||= Settingslogic.new({}) Settings['smartcard'] ||= Settingslogic.new({})
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil?
Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil? Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil?
Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil?
end end
Settings['omniauth'] ||= Settingslogic.new({}) Settings['omniauth'] ||= Settingslogic.new({})
......
...@@ -28,6 +28,7 @@ class SmartcardController < ApplicationController ...@@ -28,6 +28,7 @@ class SmartcardController < ApplicationController
return return
end end
store_active_session
log_audit_event(user, with: certificate.auth_method) log_audit_event(user, with: certificate.auth_method)
sign_in_and_redirect(user) sign_in_and_redirect(user)
end end
...@@ -43,6 +44,10 @@ class SmartcardController < ApplicationController ...@@ -43,6 +44,10 @@ class SmartcardController < ApplicationController
end end
end end
def store_active_session
Gitlab::Auth::Smartcard::SessionEnforcer.new.update_session
end
def log_audit_event(user, options = {}) def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).for_authentication.security_event AuditEventService.new(user, user, options).for_authentication.security_event
end end
......
---
title: Require session with smartcard login for Git access
merge_request: 14368
author:
type: added
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
override :check override :check
def check(cmd, changes) def check(cmd, changes)
check_geo_license! check_geo_license!
check_smartcard_access!
super super
end end
...@@ -64,6 +65,18 @@ module EE ...@@ -64,6 +65,18 @@ module EE
end end
end end
def check_smartcard_access!
unless can_access_without_new_smartcard_login?
raise ::Gitlab::GitAccess::UnauthorizedError, 'Project requires smartcard login. Please login to GitLab using a smartcard.'
end
end
def can_access_without_new_smartcard_login?
return true unless user
!::Gitlab::Auth::Smartcard::SessionEnforcer.new.access_restricted?(user)
end
def geo? def geo?
actor == :geo actor == :geo
end end
......
...@@ -8,6 +8,10 @@ module Gitlab ...@@ -8,6 +8,10 @@ module Gitlab
def enabled? def enabled?
::License.feature_available?(:smartcard_auth) && ::Gitlab.config.smartcard.enabled ::License.feature_available?(:smartcard_auth) && ::Gitlab.config.smartcard.enabled
end end
def required_for_git_access?
self.enabled? && ::Gitlab.config.smartcard.required_for_git_access
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
module Auth
module Smartcard
class Session
SESSION_STORE_KEY = :smartcard_signins
def active?(user)
sessions = ActiveSession.list_sessions(user)
sessions.any? do |session|
Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY, session.with_indifferent_access )['last_signin_at']
end
end
def update_active(value)
current_session_data['last_signin_at'] = value
end
private
def current_session_data
Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Auth
module Smartcard
class SessionEnforcer
def update_session
session.update_active(DateTime.now)
end
def access_restricted?(user)
return false unless ::Gitlab::Auth::Smartcard.required_for_git_access?
!active_session?(user)
end
private
def session
@session ||= Smartcard::Session.new
end
def active_session?(user)
session.active?(user)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Auth::Smartcard::SessionEnforcer do
describe '#update_session' do
let(:session) { {} }
around do |example|
Gitlab::Session.with_session(session) do
example.run
end
end
it 'stores the time of last sign-in in session' do
expect { subject.update_session }.to change { session[:smartcard_signins] }
expect(session[:smartcard_signins]).to have_key('last_signin_at')
expect(session[:smartcard_signins]['last_signin_at']).not_to be_nil
end
end
describe '#access_restricted?' do
let(:user) { create(:user) }
subject { described_class.new.access_restricted?(user) }
before do
stub_licensed_features(smartcard_auth: true)
stub_smartcard_setting(enabled: true, required_for_git_access: true)
end
context 'with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
it { is_expected.to be_falsey }
end
context 'without any session' do
it { is_expected.to be_truthy }
end
context 'with the setting off' do
before do
stub_smartcard_setting(required_for_git_access: false)
end
it { is_expected.to be_falsey }
end
context 'with smartcard auth disabled' do
before do
stub_smartcard_setting(enabled: false)
end
it { is_expected.to be_falsey }
end
context 'without a license' do
before do
stub_licensed_features(smartcard_auth: false)
end
it { is_expected.to be_falsey }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Auth::Smartcard::Session do
describe '#active?' do
let(:user) { create(:user) }
subject { described_class.new.active?(user) }
context 'with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
it { is_expected.to be_truthy }
end
context 'without any session' do
it { is_expected.to be_falsey }
end
end
describe '#update_active' do
let(:now) { Time.now }
around do |example|
Gitlab::Session.with_session({}) do
example.run
end
end
it 'stores the time of last sign-in' do
subject.update_active(now)
expect(Gitlab::Session.current[:smartcard_signins]).to eq({ 'last_signin_at' => now })
end
end
end
...@@ -613,6 +613,61 @@ describe Gitlab::GitAccess do ...@@ -613,6 +613,61 @@ describe Gitlab::GitAccess do
end end
end end
describe '#check_smartcard_access!' do
before do
stub_licensed_features(smartcard_auth: true)
stub_smartcard_setting(enabled: true, required_for_git_access: true)
project.add_developer(user)
end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
it 'allows pull changes' do
expect { pull_changes }.not_to raise_error
end
it 'allows push changes' do
expect { push_changes }.not_to raise_error
end
end
context 'user without a smartcard session' do
it 'does not allow pull changes' do
expect { pull_changes }.to raise_error(Gitlab::GitAccess::UnauthorizedError)
end
it 'does not allow push changes' do
expect { push_changes }.to raise_error(Gitlab::GitAccess::UnauthorizedError)
end
end
context 'with the setting off' do
before do
stub_smartcard_setting(required_for_git_access: false)
end
it 'allows pull changes' do
expect { pull_changes }.not_to raise_error
end
it 'allows push changes' do
expect { push_changes }.not_to raise_error
end
end
end
private private
def access def access
......
...@@ -80,5 +80,68 @@ describe API::Internal do ...@@ -80,5 +80,68 @@ describe API::Internal do
end end
end end
end end
context 'smartcard session required' do
set(:project) { create(:project, :repository, :wiki_repo) }
subject do
post(
api("/internal/allowed"),
params: { key_id: key.id,
project: project.full_path,
gl_repository: "project-#{project.id}",
action: 'git-upload-pack',
secret_token: secret_token,
protocol: 'ssh' })
end
before do
stub_licensed_features(smartcard_auth: true)
stub_smartcard_setting(enabled: true, required_for_git_access: true)
project.add_developer(user)
end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
it "allows access" do
subject
expect(response).to have_gitlab_http_status(200)
end
end
context 'user without a smartcard session' do
it "does not allow access" do
subject
expect(response).to have_gitlab_http_status(401)
expect(json_response['message']).to eql('Project requires smartcard login. Please login to GitLab using a smartcard.')
end
end
context 'with the setting off' do
before do
stub_smartcard_setting(required_for_git_access: false)
end
it "allows access" do
subject
expect(response).to have_gitlab_http_status(200)
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::GitHttpController, type: :request do
include GitHttpHelpers
describe 'GET #info_refs' do
set(:user) { create(:user) }
set(:project) { create(:project, :repository, :private) }
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: user.username, password: user.password } }
context 'smartcard session required' do
subject { clone_get(path, env) }
before do
stub_licensed_features(smartcard_auth: true)
stub_smartcard_setting(enabled: true, required_for_git_access: true)
project.add_developer(user)
end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
it "allows access" do
subject
expect(response).to have_gitlab_http_status(200)
end
end
context 'user without a smartcard session' do
it "does not allow access" do
subject
expect(response).to have_gitlab_http_status(403)
expect(response.body).to eq('Project requires smartcard login. Please login to GitLab using a smartcard.')
end
end
context 'with the setting off' do
before do
stub_smartcard_setting(required_for_git_access: false)
end
it "allows access" do
subject
expect(response).to have_gitlab_http_status(200)
end
end
end
end
end
...@@ -8,6 +8,7 @@ describe SmartcardController, type: :request do ...@@ -8,6 +8,7 @@ describe SmartcardController, type: :request do
let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } } let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } }
let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) }
let(:audit_event_service) { instance_double(AuditEventService) } let(:audit_event_service) { instance_double(AuditEventService) }
let(:session_enforcer) { instance_double(Gitlab::Auth::Smartcard::SessionEnforcer) }
shared_examples 'a client certificate authentication' do |auth_method| shared_examples 'a client certificate authentication' do |auth_method|
context 'with smartcard_auth enabled' do context 'with smartcard_auth enabled' do
...@@ -33,6 +34,14 @@ describe SmartcardController, type: :request do ...@@ -33,6 +34,14 @@ describe SmartcardController, type: :request do
subject subject
end end
it 'stores active session' do
expect(::Gitlab::Auth::Smartcard::SessionEnforcer).to(
receive(:new).and_return(session_enforcer))
expect(session_enforcer).to receive(:update_session)
subject
end
context 'user does not exist' do context 'user does not exist' do
context 'signup allowed' do context 'signup allowed' do
it 'creates user' do it 'creates user' do
......
...@@ -29,5 +29,9 @@ module EE ...@@ -29,5 +29,9 @@ module EE
def stub_geo_setting(messages) def stub_geo_setting(messages)
allow(::Gitlab.config.geo).to receive_messages(to_settings(messages)) allow(::Gitlab.config.geo).to receive_messages(to_settings(messages))
end end
def stub_smartcard_setting(messages)
allow(::Gitlab.config.smartcard).to receive_messages(to_settings(messages))
end
end end
end end
...@@ -4,19 +4,24 @@ module Gitlab ...@@ -4,19 +4,24 @@ module Gitlab
class NamespacedSessionStore class NamespacedSessionStore
delegate :[], :[]=, to: :store delegate :[], :[]=, to: :store
def initialize(key) def initialize(key, session = Session.current)
@key = key @key = key
@session = session
end end
def initiated? def initiated?
!Session.current.nil? !session.nil?
end end
def store def store
return unless Session.current return unless session
Session.current[@key] ||= {} session[@key] ||= {}
Session.current[@key] session[@key]
end end
private
attr_reader :session
end end
end end
...@@ -4,19 +4,33 @@ require 'spec_helper' ...@@ -4,19 +4,33 @@ require 'spec_helper'
describe Gitlab::NamespacedSessionStore do describe Gitlab::NamespacedSessionStore do
let(:key) { :some_key } let(:key) { :some_key }
subject { described_class.new(key) }
it 'stores data under the specified key' do context 'current session' do
Gitlab::Session.with_session({}) do subject { described_class.new(key) }
subject[:new_data] = 123
expect(Thread.current[:session_storage][key]).to eq(new_data: 123) it 'stores data under the specified key' do
Gitlab::Session.with_session({}) do
subject[:new_data] = 123
expect(Thread.current[:session_storage][key]).to eq(new_data: 123)
end
end
it 'retrieves data from the given key' do
Thread.current[:session_storage] = { key => { existing_data: 123 } }
expect(subject[:existing_data]).to eq 123
end end
end end
it 'retrieves data from the given key' do context 'passed in session' do
Thread.current[:session_storage] = { key => { existing_data: 123 } } let(:data) { { 'data' => 42 } }
let(:session) { { 'some_key' => data } }
subject { described_class.new(key, session.with_indifferent_access) }
expect(subject[:existing_data]).to eq 123 it 'retrieves data from the given key' do
expect(subject['data']).to eq 42
end
end end
end end
module GitHttpHelpers module GitHttpHelpers
include WorkhorseHelpers
def clone_get(project, options = {}) def clone_get(project, options = {})
get "/#{project}/info/refs", params: { service: 'git-upload-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token)) get "/#{project}/info/refs", params: { service: 'git-upload-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end end
......
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