Commit 1c812850 authored by James Edwards-Jones's avatar James Edwards-Jones

Group SAML Omniauth Provider

parent 26ec59a1
......@@ -12,9 +12,9 @@ module EE
group_saml_enabled? && !group.subgroup? && can?(current_user, :admin_group_saml, group)
end
def saml_link(text, group_id, redirect: nil, html_class: 'btn')
redirect ||= group_path(group_id)
url = omniauth_authorize_path(:user, :group_saml, group_id: group_id, redirect_to: redirect)
def saml_link(text, group_path, redirect: nil, html_class: 'btn')
redirect ||= group_path(group_path)
url = omniauth_authorize_path(:user, :group_saml, group_path: group_path, redirect_to: redirect)
link_to(text, url, method: :post, class: html_class)
end
......
module Gitlab
module Auth
module GroupSaml
class DynamicSettings
include Enumerable
delegate :each, :keys, :[], to: :settings
def initialize(saml_provider)
@saml_provider = saml_provider
end
def settings
@settings ||= configured_settings.merge(default_settings)
end
private
def configured_settings
@saml_provider&.settings || {}
end
def default_settings
{
idp_sso_target_url_runtime_params: { redirect_to: :RelayState }
}
end
end
end
end
end
module Gitlab
module Auth
module GroupSaml
class GroupLookup
def initialize(env)
@env = env
end
def path
path_from_callback_path || path_from_params
end
def group
Group.find_by_full_path(path)
end
def saml_provider
group&.saml_provider
end
def group_saml_enabled?
saml_provider && group.feature_available?(:group_saml)
end
private
attr_reader :env
def path_from_callback_path
path = env['PATH_INFO']
path_regex = Gitlab::PathRegex.saml_callback_regex
path.match(path_regex).try(:[], :group)
end
def path_from_params
params = Rack::Request.new(env).params
params['group_path']
end
end
end
end
end
......@@ -2,6 +2,28 @@ module OmniAuth
module Strategies
class GroupSaml < SAML
option :name, 'group_saml'
option :callback_path, ->(env) { callback?(env) }
def setup_phase
# Set devise scope for custom callback URL
env["devise.mapping"] = Devise.mappings[:user]
group_lookup = Gitlab::Auth::GroupSaml::GroupLookup.new(env)
unless group_lookup.group_saml_enabled?
raise ActionController::RoutingError, group_lookup.path
end
saml_provider = group_lookup.saml_provider
dynamic_settings = Gitlab::Auth::GroupSaml::DynamicSettings.new(saml_provider)
env['omniauth.strategy'].options.merge!(dynamic_settings.settings)
super
end
def self.callback?(env)
env['PATH_INFO'] =~ Gitlab::PathRegex.saml_callback_regex
end
end
end
end
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfx31eeaa1f-4f9a-7dbc-200c-4d556bac4fc9" Version="2.0" IssueInstant="2012-11-08T20:39:54Z" Destination="http://localhost/groups/my-group/-/saml/callback" InResponseTo="_5ad34590-0c12-0130-2b62-109add67ce12">
<saml:Issuer>http://localhost:9000/saml2/idp/metadata.php</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ds:Reference URI="#pfx31eeaa1f-4f9a-7dbc-200c-4d556bac4fc9">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ds:DigestValue>WSulGKooo1K7yYnKfXy88BRqgXM=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>N8G4Meh60EnU5U113JH3fHEr3nA+87kemKZDkqfEZnGHrfwfO2KhSbKEsU6M1ELq8ZCNDxYCFhbfwJOWij5+qkMD1gMYqvH2Hz169l5smEAfkmtovJwq+2lVO7AtVLez065rx2g+n2DmZx82H3ynrMV0vTDEQ2AohJPZjsRoNgY=</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIDHDCCAoWgAwIBAgIJAJq7aJgm4De9MA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxKjAoBgNVBAMTIU9tbmlBdXRoIFNBTUwgVGVzdGluZyBDZXJ0aWZpY2F0ZTAeFw0xMjExMDgyMDI5NTFaFw0xMjEyMDgyMDI5NTFaMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxKjAoBgNVBAMTIU9tbmlBdXRoIFNBTUwgVGVzdGluZyBDZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzwg6Rakhf6boh5E2zve9Lp6dSjMTdrJhFQ1WRZwMOfRO7GPD9c7RetnxuPbg6PyBfSdFoGCJvMswDJMa7DZAlbgsf1WyOw9gaHzgf4j79XlFpis3XKJX8i1vUxxEVW7pYrUTJU0xbZ75l3AXtfnHXnxURF0eiD+s51nKBtnSkRcCAwEAAaOBzTCByjAdBgNVHQ4EFgQULih6jYTJ3XNDs53KSVwt4F9G2agwgZoGA1UdIwSBkjCBj4AULih6jYTJ3XNDs53KSVwt4F9G2aihbKRqMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxKjAoBgNVBAMTIU9tbmlBdXRoIFNBTUwgVGVzdGluZyBDZXJ0aWZpY2F0ZYIJAJq7aJgm4De9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAg+rrwZooo8DE47jmaBX+vUJBIVgSKdB45bDkL7FgTca2h1tsmgUL9nFyvp9FDEQ7IYw5599ywhrQf9GfsIZ374G7ie9C8JqURbdiP4/MMvOjV1RzyypXByfaY20tDwgz6JlLs7snh7O3s93FKpWhCjHE434CJwa1q5nHqNTgkZw=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
<saml:Assertion xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="pfxe689248c-47f0-1e59-d2bb-546563043b6c" Version="2.0" IssueInstant="2012-11-08T20:39:54Z">
<saml:Issuer>http://localhost:9000/saml2/idp/metadata.php</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ds:Reference URI="#pfxe689248c-47f0-1e59-d2bb-546563043b6c">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ds:DigestValue>20g3ohE5p7icP5ZQ3CSRkSpGaME=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>m9+Hq+RDNJyKWGsqCpqmkXt/6dz/NQUkdzeF5YHSezVuLFJajB+QC2aSeyic5H5Z0LBkQscjZ1sgme7Hyeo+ZvBgDrBejP6bZfMyaNrET6JTKXxXnrSI0txEL7oXGgnWLJX+oTUWLJgO+PHAUGeS9AgbKcBTQjaW7aW8uh4WtJg=</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIDHDCCAoWgAwIBAgIJAJq7aJgm4De9MA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxKjAoBgNVBAMTIU9tbmlBdXRoIFNBTUwgVGVzdGluZyBDZXJ0aWZpY2F0ZTAeFw0xMjExMDgyMDI5NTFaFw0xMjEyMDgyMDI5NTFaMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxKjAoBgNVBAMTIU9tbmlBdXRoIFNBTUwgVGVzdGluZyBDZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzwg6Rakhf6boh5E2zve9Lp6dSjMTdrJhFQ1WRZwMOfRO7GPD9c7RetnxuPbg6PyBfSdFoGCJvMswDJMa7DZAlbgsf1WyOw9gaHzgf4j79XlFpis3XKJX8i1vUxxEVW7pYrUTJU0xbZ75l3AXtfnHXnxURF0eiD+s51nKBtnSkRcCAwEAAaOBzTCByjAdBgNVHQ4EFgQULih6jYTJ3XNDs53KSVwt4F9G2agwgZoGA1UdIwSBkjCBj4AULih6jYTJ3XNDs53KSVwt4F9G2aihbKRqMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxKjAoBgNVBAMTIU9tbmlBdXRoIFNBTUwgVGVzdGluZyBDZXJ0aWZpY2F0ZYIJAJq7aJgm4De9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAg+rrwZooo8DE47jmaBX+vUJBIVgSKdB45bDkL7FgTca2h1tsmgUL9nFyvp9FDEQ7IYw5599ywhrQf9GfsIZ374G7ie9C8JqURbdiP4/MMvOjV1RzyypXByfaY20tDwgz6JlLs7snh7O3s93FKpWhCjHE434CJwa1q5nHqNTgkZw=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml:Subject>
<saml:NameID SPNameQualifier="http://localhost/groups/my-group" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2012-11-08T20:44:54Z" Recipient="http://localhost/groups/my-group/-/saml/callback" InResponseTo="_5ad34590-0c12-0130-2b62-109add67ce12" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2012-11-08T20:39:24Z" NotOnOrAfter="2012-11-08T20:44:54Z">
<saml:AudienceRestriction>
<saml:Audience>http://localhost/groups/my-group</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2012-11-08T20:39:54Z" SessionNotOnOrAfter="2012-11-09T04:39:54Z" SessionIndex="_17c45b5f1bb209798b06536ab9594723aa80634c58">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="first_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">Rajiv</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="last_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">Manglani</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">user@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="company_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">Example Company</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
require 'spec_helper'
describe Gitlab::Auth::GroupSaml::DynamicSettings do
let(:saml_provider) { create(:saml_provider) }
subject { described_class.new(saml_provider) }
it 'behaves like an enumerator for settings' do
expect(subject.to_h).to be_a(Hash)
end
it 'configures requests to transfrom redirect_to to RelayState' do
expect(subject[:idp_sso_target_url_runtime_params]).to eq( redirect_to: :RelayState )
end
describe 'sets settings from saml_provider' do
specify 'assertion_consumer_service_url' do
expect(subject.keys).to include(:assertion_consumer_service_url)
end
specify 'issuer' do
expect(subject.keys).to include(:issuer)
end
specify 'idp_cert_fingerprint' do
expect(subject.keys).to include(:idp_cert_fingerprint)
end
specify 'idp_sso_target_url' do
expect(subject.keys).to include(:idp_sso_target_url)
end
specify 'name_identifier_format' do
expect(subject.keys).to include(:name_identifier_format)
end
end
end
require 'spec_helper'
describe Gitlab::Auth::GroupSaml::GroupLookup do
let(:query_string) { 'group_path=the-group' }
let(:path_info) { double }
def subject(params = {})
@subject ||= begin
env = {
"rack.input" => double,
'PATH_INFO' => path_info
}.merge(params)
described_class.new(env)
end
end
context 'on request path' do
let(:path_info) { '/users/auth/group_saml' }
it 'can detect group_path from rack.input body params' do
subject( 'REQUEST_METHOD' => 'POST', 'rack.input' => StringIO.new(query_string) )
expect(subject.path).to eq 'the-group'
end
it 'can detect group_path from query params' do
subject( "QUERY_STRING" => query_string )
expect(subject.path).to eq 'the-group'
end
end
context 'on callback path' do
let(:path_info) { '/groups/callback-group/-/saml/callback' }
it 'can extract group_path from PATH_INFO' do
expect(subject.path).to eq 'callback-group'
end
it 'does not allow params to take precedence' do
subject( "QUERY_STRING" => query_string )
expect(subject.path).to eq 'callback-group'
end
end
it 'looks up group by path' do
group = create(:group)
allow(subject).to receive(:path) { group.path }
expect(subject.group).to be_a(Group)
end
it 'exposes saml_provider' do
saml_provider = create(:saml_provider)
allow(subject).to receive(:group) { saml_provider.group }
expect(subject.saml_provider).to be_a(SamlProvider)
end
end
require 'spec_helper'
describe OmniAuth::Strategies::GroupSaml, type: :strategy do
let(:strategy) { [OmniAuth::Strategies::GroupSaml, {}] }
let!(:group) { create(:group, name: 'my-group') }
let(:idp_sso_url) { 'https://saml.example.com/adfs/ls' }
let(:fingerprint) { 'C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB' }
let!(:saml_provider) { create(:saml_provider, group: group, sso_url: idp_sso_url, certificate_fingerprint: fingerprint) }
let!(:unconfigured_group) { create(:group, name: 'unconfigured-group') }
let(:saml_response) do
fixture = File.read('ee/spec/fixtures/saml/response.xml')
Base64.encode64(fixture)
end
before do
stub_licensed_features(group_saml: true)
end
describe 'callback_path option' do
let(:callback_path) { OmniAuth::Strategies::GroupSaml.default_options[:callback_path] }
def check(path)
callback_path.call( "PATH_INFO" => path )
end
it 'dynamically detects /groups/:group_path/-/saml/callback' do
expect(check("/groups/some-group/-/saml/callback")).to be_truthy
end
it 'rejects default callback paths' do
expect(check('/saml/callback')).to be_falsey
expect(check('/auth/saml/callback')).to be_falsey
expect(check('/auth/group_saml/callback')).to be_falsey
expect(check('/users/auth/saml/callback')).to be_falsey
expect(check('/users/auth/group_saml/callback')).to be_falsey
end
end
describe 'POST /groups/:group_path/-/saml/callback' do
context 'with valid SAMLResponse' do
before do
allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:validate_signature) { true }
allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:validate_session_expiration) { true }
allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:validate_subject_confirmation) { true }
allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:validate_conditions) { true }
end
it 'sets the auth hash based on the response' do
post "/groups/my-group/-/saml/callback", SAMLResponse: saml_response
expect(auth_hash[:info]['email']).to eq("user@example.com")
end
end
context 'with invalid SAMLResponse' do
it 'redirects somewhere so failure messages can be displayed' do
post "/groups/my-group/-/saml/callback", SAMLResponse: saml_response
expect(last_response.location).to include('failure')
end
end
it 'returns 404 when if group is not found' do
expect do
post "/groups/not-a-group/-/saml/callback", SAMLResponse: saml_response
end.to raise_error(ActionController::RoutingError)
end
context 'Group SAML not licensed for group' do
before do
stub_licensed_features(group_saml: false)
end
it 'returns 404' do
expect do
post "/groups/my-group/-/saml/callback", SAMLResponse: saml_response
end.to raise_error(ActionController::RoutingError)
end
end
end
describe 'POST /users/auth/group_saml' do
it 'redirects to the provider login page' do
post '/users/auth/group_saml', group_path: 'my-group'
expect(last_response).to redirect_to(/#{Regexp.quote(idp_sso_url)}/)
end
it 'returns 404 for groups without SAML configured' do
expect do
post '/users/auth/group_saml', group_path: 'unconfigured-group'
end.to raise_error(ActionController::RoutingError)
end
it 'returns 404 when if group is not found' do
expect do
post '/users/auth/group_saml', group_path: 'not-a-group'
end.to raise_error(ActionController::RoutingError)
end
it 'returns 404 when missing group_path param' do
expect do
post '/users/auth/group_saml'
end.to raise_error(ActionController::RoutingError)
end
end
end
module StrategyHelpers
include Rack::Test::Methods
include ActionDispatch::Assertions::ResponseAssertions
include Shoulda::Matchers::ActionController
include OmniAuth::Test::StrategyTestCase
def post(*args)
super.tap do
@response = ActionDispatch::TestResponse.from_response(last_response) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
def auth_hash
last_request.env['omniauth.auth']
end
end
RSpec.configure do |config|
config.include StrategyHelpers, type: :strategy
config.around(:all, type: :strategy) do |example|
begin
original_mode = OmniAuth.config.test_mode
original_on_failure = OmniAuth.config.on_failure
OmniAuth.config.test_mode = false
OmniAuth.config.on_failure = OmniAuth::FailureEndpoint
example.run
ensure
OmniAuth.config.test_mode = original_mode
OmniAuth.config.on_failure = original_on_failure
end
end
end
......@@ -229,6 +229,10 @@ module Gitlab
}x
end
def saml_callback_regex
@saml_callback_regex ||= %r(\A\/groups\/(?<group>#{full_namespace_route_regex})\/\-\/saml\/callback\z).freeze
end
private
def single_line_regexp(regex)
......
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