maestrano-ruby-test 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +45 -0
- data/LICENSE +21 -0
- data/README.md +794 -0
- data/Rakefile +40 -0
- data/bin/maestrano-console +9 -0
- data/lib/maestrano.rb +271 -0
- data/lib/maestrano/account/bill.rb +14 -0
- data/lib/maestrano/account/recurring_bill.rb +14 -0
- data/lib/maestrano/api/error/authentication_error.rb +8 -0
- data/lib/maestrano/api/error/base_error.rb +24 -0
- data/lib/maestrano/api/error/connection_error.rb +8 -0
- data/lib/maestrano/api/error/invalid_request_error.rb +14 -0
- data/lib/maestrano/api/list_object.rb +37 -0
- data/lib/maestrano/api/object.rb +187 -0
- data/lib/maestrano/api/operation/base.rb +215 -0
- data/lib/maestrano/api/operation/create.rb +18 -0
- data/lib/maestrano/api/operation/delete.rb +13 -0
- data/lib/maestrano/api/operation/list.rb +18 -0
- data/lib/maestrano/api/operation/update.rb +59 -0
- data/lib/maestrano/api/resource.rb +47 -0
- data/lib/maestrano/api/util.rb +122 -0
- data/lib/maestrano/open_struct.rb +11 -0
- data/lib/maestrano/saml/attribute_value.rb +15 -0
- data/lib/maestrano/saml/metadata.rb +64 -0
- data/lib/maestrano/saml/request.rb +93 -0
- data/lib/maestrano/saml/response.rb +201 -0
- data/lib/maestrano/saml/schemas/saml20assertion_schema.xsd +283 -0
- data/lib/maestrano/saml/schemas/saml20protocol_schema.xsd +302 -0
- data/lib/maestrano/saml/schemas/xenc_schema.xsd +146 -0
- data/lib/maestrano/saml/schemas/xmldsig_schema.xsd +318 -0
- data/lib/maestrano/saml/settings.rb +37 -0
- data/lib/maestrano/saml/validation_error.rb +7 -0
- data/lib/maestrano/sso.rb +86 -0
- data/lib/maestrano/sso/base_group.rb +31 -0
- data/lib/maestrano/sso/base_membership.rb +25 -0
- data/lib/maestrano/sso/base_user.rb +75 -0
- data/lib/maestrano/sso/group.rb +24 -0
- data/lib/maestrano/sso/session.rb +107 -0
- data/lib/maestrano/sso/user.rb +34 -0
- data/lib/maestrano/version.rb +3 -0
- data/lib/maestrano/xml_security/signed_document.rb +170 -0
- data/maestrano.gemspec +32 -0
- data/maestrano.png +0 -0
- data/test/helpers/api_helpers.rb +115 -0
- data/test/helpers/saml_helpers.rb +62 -0
- data/test/maestrano/account/bill_test.rb +48 -0
- data/test/maestrano/account/recurring_bill_test.rb +49 -0
- data/test/maestrano/api/list_object_test.rb +20 -0
- data/test/maestrano/api/object_test.rb +28 -0
- data/test/maestrano/api/resource_test.rb +343 -0
- data/test/maestrano/api/util_test.rb +31 -0
- data/test/maestrano/maestrano_test.rb +260 -0
- data/test/maestrano/open_struct_test.rb +10 -0
- data/test/maestrano/saml/request_test.rb +168 -0
- data/test/maestrano/saml/response_test.rb +290 -0
- data/test/maestrano/saml/settings_test.rb +51 -0
- data/test/maestrano/sso/base_group_test.rb +54 -0
- data/test/maestrano/sso/base_membership_test.rb +45 -0
- data/test/maestrano/sso/base_user_test.rb +114 -0
- data/test/maestrano/sso/group_test.rb +47 -0
- data/test/maestrano/sso/session_test.rb +161 -0
- data/test/maestrano/sso/user_test.rb +65 -0
- data/test/maestrano/sso_test.rb +105 -0
- data/test/maestrano/xml_security/signed_document.rb +163 -0
- data/test/support/saml/certificates/certificate1 +12 -0
- data/test/support/saml/certificates/r1_certificate2_base64 +1 -0
- data/test/support/saml/responses/adfs_response_sha1.xml +46 -0
- data/test/support/saml/responses/adfs_response_sha256.xml +46 -0
- data/test/support/saml/responses/adfs_response_sha384.xml +46 -0
- data/test/support/saml/responses/adfs_response_sha512.xml +46 -0
- data/test/support/saml/responses/no_signature_ns.xml +48 -0
- data/test/support/saml/responses/open_saml_response.xml +56 -0
- data/test/support/saml/responses/r1_response6.xml.base64 +1 -0
- data/test/support/saml/responses/response1.xml.base64 +1 -0
- data/test/support/saml/responses/response2.xml.base64 +79 -0
- data/test/support/saml/responses/response3.xml.base64 +66 -0
- data/test/support/saml/responses/response4.xml.base64 +93 -0
- data/test/support/saml/responses/response5.xml.base64 +102 -0
- data/test/support/saml/responses/response_with_ampersands.xml +139 -0
- data/test/support/saml/responses/response_with_ampersands.xml.base64 +93 -0
- data/test/support/saml/responses/response_with_multiple_attribute_values.xml +57 -0
- data/test/support/saml/responses/simple_saml_php.xml +71 -0
- data/test/support/saml/responses/starfield_response.xml.base64 +1 -0
- data/test/support/saml/responses/wrapped_response_2.xml.base64 +150 -0
- data/test/test_helper.rb +47 -0
- metadata +315 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module Saml
|
|
3
|
+
class Settings
|
|
4
|
+
NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
|
5
|
+
NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'
|
|
6
|
+
NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName'
|
|
7
|
+
NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos'
|
|
8
|
+
NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
|
|
9
|
+
NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
|
|
10
|
+
NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
|
|
11
|
+
PROTOCOL_BINDING_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
|
12
|
+
|
|
13
|
+
def initialize(overrides = {})
|
|
14
|
+
config = DEFAULTS.merge(overrides)
|
|
15
|
+
config.each do |k,v|
|
|
16
|
+
acc = "#{k.to_s}=".to_sym
|
|
17
|
+
self.send(acc, v) if self.respond_to? acc
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier
|
|
21
|
+
attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format
|
|
22
|
+
attr_accessor :authn_context
|
|
23
|
+
attr_accessor :idp_slo_target_url
|
|
24
|
+
attr_accessor :name_identifier_value
|
|
25
|
+
attr_accessor :sessionindex
|
|
26
|
+
attr_accessor :assertion_consumer_logout_service_url
|
|
27
|
+
attr_accessor :compress_request
|
|
28
|
+
attr_accessor :double_quote_xml_attribute_values
|
|
29
|
+
attr_accessor :passive
|
|
30
|
+
attr_accessor :protocol_binding
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
DEFAULTS = {:compress_request => true, :double_quote_xml_attribute_values => false}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
|
|
4
|
+
# Return the saml_settings based on
|
|
5
|
+
# Maestrano configuration
|
|
6
|
+
def self.saml_settings
|
|
7
|
+
settings = Maestrano::Saml::Settings.new
|
|
8
|
+
settings.assertion_consumer_service_url = self.consume_url
|
|
9
|
+
settings.issuer = Maestrano.param('api.id')
|
|
10
|
+
settings.idp_sso_target_url = self.idp_url
|
|
11
|
+
settings.idp_cert_fingerprint = Maestrano.param('sso_x509_fingerprint')
|
|
12
|
+
settings.name_identifier_format = Maestrano.param('sso_name_id_format')
|
|
13
|
+
settings
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Build a new SAML Request
|
|
17
|
+
def self.build_request(get_params = {})
|
|
18
|
+
Maestrano::Saml::Request.new(get_params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Build a new SAML response
|
|
22
|
+
def self.build_response(saml_post_param)
|
|
23
|
+
Maestrano::Saml::Response.new(saml_post_param)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.enabled?
|
|
27
|
+
!!Maestrano.param('sso.enabled')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.init_url
|
|
31
|
+
host = Maestrano.param('sso.idm')
|
|
32
|
+
path = Maestrano.param('sso.init_path')
|
|
33
|
+
return "#{host}#{path}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.consume_url
|
|
37
|
+
host = Maestrano.param('sso.idm')
|
|
38
|
+
path = Maestrano.param('sso.consume_path')
|
|
39
|
+
return "#{host}#{path}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.logout_url
|
|
43
|
+
host = Maestrano.param('api_host')
|
|
44
|
+
path = '/app_logout'
|
|
45
|
+
return "#{host}#{path}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.unauthorized_url
|
|
49
|
+
host = Maestrano.param('api_host')
|
|
50
|
+
path = '/app_access_unauthorized'
|
|
51
|
+
return "#{host}#{path}";
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.idp_url
|
|
55
|
+
host = Maestrano.param('api_host')
|
|
56
|
+
api_base = Maestrano.param('api_base')
|
|
57
|
+
endpoint = 'auth/saml'
|
|
58
|
+
return "#{host}#{api_base}#{endpoint}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.session_check_url(user_uid,sso_session)
|
|
62
|
+
host = Maestrano.param('api_host')
|
|
63
|
+
api_base = Maestrano.param('api_base')
|
|
64
|
+
endpoint = 'auth/saml'
|
|
65
|
+
return URI.escape("#{host}#{api_base}#{endpoint}/#{user_uid}?session=#{sso_session}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Set maestrano attributes in session
|
|
69
|
+
# Takes the BaseUser hash representation and current session
|
|
70
|
+
# in arguments
|
|
71
|
+
def self.set_session(session, auth)
|
|
72
|
+
Maestrano::SSO::Session.from_user_auth_hash(session,auth).save
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Destroy the maestrano session in http session
|
|
76
|
+
def self.clear_session(session)
|
|
77
|
+
session.delete(:maestrano)
|
|
78
|
+
session.delete('maestrano')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Metaclass definitions
|
|
82
|
+
class << self
|
|
83
|
+
alias_method :unset_session, :clear_session
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
class BaseGroup
|
|
4
|
+
attr_accessor :local_id
|
|
5
|
+
attr_reader :uid,:country, :company_name, :free_trial_end_at
|
|
6
|
+
|
|
7
|
+
# Initializer
|
|
8
|
+
# @param Maestrano::SAML::Response
|
|
9
|
+
def initialize(saml_response)
|
|
10
|
+
att = saml_response.attributes
|
|
11
|
+
@uid = att['group_uid']
|
|
12
|
+
@country = att['country']
|
|
13
|
+
@free_trial_end_at = Time.iso8601(att['group_end_free_trial'])
|
|
14
|
+
@company_name = att['company_name']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_hash
|
|
18
|
+
{
|
|
19
|
+
provider: 'maestrano',
|
|
20
|
+
uid: self.uid,
|
|
21
|
+
info: {
|
|
22
|
+
free_trial_end_at: self.free_trial_end_at,
|
|
23
|
+
company_name: self.company_name,
|
|
24
|
+
country: self.country,
|
|
25
|
+
},
|
|
26
|
+
extra: {}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
class BaseMembership
|
|
4
|
+
attr_reader :user_uid,:group_uid,:role
|
|
5
|
+
|
|
6
|
+
# Initializer
|
|
7
|
+
# @param Maestrano::SAML::Response
|
|
8
|
+
def initialize(saml_response)
|
|
9
|
+
att = saml_response.attributes
|
|
10
|
+
@user_uid = att['uid']
|
|
11
|
+
@group_uid = att['group_uid']
|
|
12
|
+
@role = att['group_role']
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_hash
|
|
16
|
+
{
|
|
17
|
+
provider: 'maestrano',
|
|
18
|
+
group_uid: self.group_uid,
|
|
19
|
+
user_uid: self.user_uid,
|
|
20
|
+
role: self.role
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
class BaseUser
|
|
4
|
+
attr_accessor :local_id
|
|
5
|
+
attr_reader :sso_session,:sso_session_recheck,
|
|
6
|
+
:group_uid,:group_role,:uid,:virtual_uid,:email,
|
|
7
|
+
:virtual_email,:first_name, :last_name,:country, :company_name
|
|
8
|
+
|
|
9
|
+
# Initializer
|
|
10
|
+
# @param Maestrano::SAML::Response
|
|
11
|
+
def initialize(saml_response)
|
|
12
|
+
att = saml_response.attributes
|
|
13
|
+
@sso_session = att['mno_session']
|
|
14
|
+
@sso_session_recheck = Time.iso8601(att['mno_session_recheck'])
|
|
15
|
+
@group_uid = att['group_uid']
|
|
16
|
+
@group_role = att['group_role']
|
|
17
|
+
@uid = att['uid']
|
|
18
|
+
@virtual_uid = att['virtual_uid']
|
|
19
|
+
@email = att['email']
|
|
20
|
+
@virtual_email = att['virtual_email']
|
|
21
|
+
@first_name = att['name']
|
|
22
|
+
@last_name = att['surname']
|
|
23
|
+
@country = att['country']
|
|
24
|
+
@company_name = att['company_name']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_uid
|
|
28
|
+
if Maestrano.param('sso.creation_mode') == 'real'
|
|
29
|
+
return self.uid
|
|
30
|
+
else
|
|
31
|
+
return self.virtual_uid
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_email
|
|
36
|
+
if Maestrano.param('sso.creation_mode') == 'real'
|
|
37
|
+
return self.email
|
|
38
|
+
else
|
|
39
|
+
return self.virtual_email
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Hash representation of the resource
|
|
44
|
+
def to_hash
|
|
45
|
+
{
|
|
46
|
+
provider: 'maestrano',
|
|
47
|
+
uid: self.to_uid,
|
|
48
|
+
info: {
|
|
49
|
+
email: self.to_email,
|
|
50
|
+
first_name: self.first_name,
|
|
51
|
+
last_name: self.last_name,
|
|
52
|
+
country: self.country,
|
|
53
|
+
company_name: self.company_name,
|
|
54
|
+
},
|
|
55
|
+
extra: {
|
|
56
|
+
uid: self.uid,
|
|
57
|
+
virtual_uid: self.virtual_uid,
|
|
58
|
+
real_email: self.email,
|
|
59
|
+
virtual_email: self.virtual_email,
|
|
60
|
+
group: {
|
|
61
|
+
uid: self.group_uid,
|
|
62
|
+
role: self.group_role,
|
|
63
|
+
},
|
|
64
|
+
session: {
|
|
65
|
+
uid: self.uid,
|
|
66
|
+
token: self.sso_session,
|
|
67
|
+
recheck: self.sso_session_recheck,
|
|
68
|
+
group_uid: self.group_uid
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
module Group
|
|
4
|
+
def find_for_maestrano_auth(auth)
|
|
5
|
+
# E.g with Rails
|
|
6
|
+
# where(auth.slice(:provider, :uid)).first_or_create do |group|
|
|
7
|
+
# group.provider = auth[:provider]
|
|
8
|
+
# group.uid = auth[:uid]
|
|
9
|
+
# group.name = (auth[:info][:company_name] || 'Your Group')
|
|
10
|
+
# group.country = auth[:info][:country]
|
|
11
|
+
# end
|
|
12
|
+
raise NoMethodError, "You need to override find_for_maestrano_auth in your #{self.class.name} model"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def maestrano?
|
|
16
|
+
if self.respond_to?(:provider)
|
|
17
|
+
return self.provider.to_s == 'maestrano'
|
|
18
|
+
else
|
|
19
|
+
raise NoMethodError, "You need to override maestrano? in your #{self.class.name} model"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
class Session
|
|
4
|
+
attr_accessor :session, :uid, :session_token, :recheck, :group_uid
|
|
5
|
+
|
|
6
|
+
# Load a Maestrano::SSO::Session object from a
|
|
7
|
+
# hash generated by Maestrano::SSO::BaseUser#to_hash
|
|
8
|
+
def self.from_user_auth_hash(session, auth)
|
|
9
|
+
instance = self.new({})
|
|
10
|
+
instance.session = session
|
|
11
|
+
|
|
12
|
+
if (extra = (auth[:extra] || auth['extra'])) && (sso_session = (extra[:session] || extra['session']))
|
|
13
|
+
instance.uid = (sso_session[:uid] || sso_session['uid'])
|
|
14
|
+
instance.session_token = (sso_session[:token] || sso_session['token'])
|
|
15
|
+
instance.group_uid = (sso_session[:group_uid] || sso_session['group_uid'])
|
|
16
|
+
if recheck = (sso_session[:recheck] || sso_session['recheck'])
|
|
17
|
+
instance.recheck = recheck
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
return instance
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(session)
|
|
24
|
+
self.session = session
|
|
25
|
+
if (self.session = session)
|
|
26
|
+
begin
|
|
27
|
+
if mno_session = (self.session[:maestrano] || self.session['maestrano'])
|
|
28
|
+
decrypted_session = JSON.parse(Base64.decode64(mno_session))
|
|
29
|
+
self.uid = decrypted_session['uid']
|
|
30
|
+
self.session_token = decrypted_session['session']
|
|
31
|
+
self.recheck = Time.iso8601(decrypted_session['session_recheck'])
|
|
32
|
+
self.group_uid = decrypted_session['group_uid']
|
|
33
|
+
end
|
|
34
|
+
rescue
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remote_check_required?
|
|
40
|
+
if self.uid && self.session_token && self.recheck
|
|
41
|
+
return (self.recheck <= Time.now)
|
|
42
|
+
end
|
|
43
|
+
return true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check remote maestrano session and update the
|
|
47
|
+
# recheck attribute if the session is still valid
|
|
48
|
+
# Return true if the session is still valid and
|
|
49
|
+
# false otherwise
|
|
50
|
+
def perform_remote_check
|
|
51
|
+
# Get remote session info
|
|
52
|
+
url = Maestrano::SSO.session_check_url(self.uid, self.session_token)
|
|
53
|
+
begin
|
|
54
|
+
response = RestClient.get(url)
|
|
55
|
+
response = JSON.parse(response)
|
|
56
|
+
rescue Exception => e
|
|
57
|
+
response = {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Process response
|
|
61
|
+
if response['valid'] && response['recheck']
|
|
62
|
+
self.recheck = Time.iso8601(response['recheck'])
|
|
63
|
+
return true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
return false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check whether this mno session is valid or not
|
|
70
|
+
# Return true if SLO is disabled (via sso.slo_enabled config
|
|
71
|
+
# param)
|
|
72
|
+
# Return false if no session defined
|
|
73
|
+
# ---
|
|
74
|
+
# opts:
|
|
75
|
+
# if_session: if true then the session will be
|
|
76
|
+
# considered valid if the http session is nil or does
|
|
77
|
+
# not have a maestrano key. Useful when the validity of
|
|
78
|
+
# a session should be restricted to maestrano users only
|
|
79
|
+
# within an application
|
|
80
|
+
def valid?(opts = {})
|
|
81
|
+
return true unless Maestrano.param('sso.slo_enabled')
|
|
82
|
+
return true if opts[:if_session] && (!self.session || (!self.session[:maestrano] && !self.session['maestrano']))
|
|
83
|
+
return false unless self.session
|
|
84
|
+
|
|
85
|
+
if self.remote_check_required?
|
|
86
|
+
if perform_remote_check
|
|
87
|
+
self.save
|
|
88
|
+
return true
|
|
89
|
+
else
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
return true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def save
|
|
97
|
+
self.session[:maestrano] = Base64.encode64({
|
|
98
|
+
uid: self.uid,
|
|
99
|
+
session: self.session_token,
|
|
100
|
+
session_recheck: self.recheck.utc.iso8601,
|
|
101
|
+
group_uid: self.group_uid
|
|
102
|
+
}.to_json)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Maestrano
|
|
2
|
+
module SSO
|
|
3
|
+
module User
|
|
4
|
+
def find_for_maestrano_auth(auth)
|
|
5
|
+
# E.g with Rails
|
|
6
|
+
# where(auth.slice(:provider, :uid)).first_or_create do |user|
|
|
7
|
+
# user.provider = auth[:provider]
|
|
8
|
+
# user.uid = auth[:uid]
|
|
9
|
+
# user.email = auth[:info][:email]
|
|
10
|
+
# user.name = auth[:info][:first_name]
|
|
11
|
+
# user.surname = auth[:info][:last_name]
|
|
12
|
+
# user.country = auth[:info][:country]
|
|
13
|
+
# user.company = auth[:info][:company_name]
|
|
14
|
+
# end
|
|
15
|
+
raise NoMethodError, "You need to override find_for_maestrano_auth in your #{self.class.name} model"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Check whether the user is a maestrano one
|
|
19
|
+
def maestrano?
|
|
20
|
+
if self.respond_to?(:provider)
|
|
21
|
+
return self.provider.to_s == 'maestrano'
|
|
22
|
+
else
|
|
23
|
+
raise NoMethodError, "You need to override maestrano? in your #{self.class.name} model"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check whether the SSO session is still valid
|
|
28
|
+
# or not
|
|
29
|
+
def maestrano_session_valid?(session)
|
|
30
|
+
Maestrano::SSO::Session.new(session).valid?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# The contents of this file are subject to the terms
|
|
2
|
+
# of the Common Development and Distribution License
|
|
3
|
+
# (the License). You may not use this file except in
|
|
4
|
+
# compliance with the License.
|
|
5
|
+
#
|
|
6
|
+
# You can obtain a copy of the License at
|
|
7
|
+
# https://opensso.dev.java.net/public/CDDLv1.0.html or
|
|
8
|
+
# opensso/legal/CDDLv1.0.txt
|
|
9
|
+
# See the License for the specific language governing
|
|
10
|
+
# permission and limitations under the License.
|
|
11
|
+
#
|
|
12
|
+
# When distributing Covered Code, include this CDDL
|
|
13
|
+
# Header Notice in each file and include the License file
|
|
14
|
+
# at opensso/legal/CDDLv1.0.txt.
|
|
15
|
+
# If applicable, add the following below the CDDL Header,
|
|
16
|
+
# with the fields enclosed by brackets [] replaced by
|
|
17
|
+
# your own identifying information:
|
|
18
|
+
# "Portions Copyrighted [year] [name of copyright owner]"
|
|
19
|
+
#
|
|
20
|
+
# $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
|
|
21
|
+
#
|
|
22
|
+
# Copyright 2007 Sun Microsystems Inc. All Rights Reserved
|
|
23
|
+
# Portions Copyrighted 2007 Todd W Saxton.
|
|
24
|
+
|
|
25
|
+
require 'rubygems'
|
|
26
|
+
require "rexml/document"
|
|
27
|
+
require "rexml/xpath"
|
|
28
|
+
require "openssl"
|
|
29
|
+
require 'nokogiri'
|
|
30
|
+
require "digest/sha1"
|
|
31
|
+
require "digest/sha2"
|
|
32
|
+
require "maestrano/saml/validation_error"
|
|
33
|
+
|
|
34
|
+
module Maestrano
|
|
35
|
+
module XMLSecurity
|
|
36
|
+
class SignedDocument < REXML::Document
|
|
37
|
+
C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
|
|
38
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
|
39
|
+
|
|
40
|
+
attr_accessor :signed_element_id
|
|
41
|
+
|
|
42
|
+
def initialize(response)
|
|
43
|
+
super(response)
|
|
44
|
+
extract_signed_element_id
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_document(idp_cert_fingerprint, soft = true)
|
|
48
|
+
# get cert from response
|
|
49
|
+
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
|
|
50
|
+
raise Maestrano::Saml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
|
|
51
|
+
base64_cert = cert_element.text
|
|
52
|
+
cert_text = Base64.decode64(base64_cert)
|
|
53
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
|
54
|
+
|
|
55
|
+
# check cert matches registered idp cert
|
|
56
|
+
fingerprint = Digest::SHA1.hexdigest(cert.to_der)
|
|
57
|
+
|
|
58
|
+
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
|
|
59
|
+
return soft ? false : (raise Maestrano::Saml::ValidationError.new("Fingerprint mismatch"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
validate_signature(base64_cert, soft)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_signature(base64_cert, soft = true)
|
|
66
|
+
# validate references
|
|
67
|
+
|
|
68
|
+
# check for inclusive namespaces
|
|
69
|
+
inclusive_namespaces = extract_inclusive_namespaces
|
|
70
|
+
|
|
71
|
+
document = Nokogiri.parse(self.to_s)
|
|
72
|
+
|
|
73
|
+
# create a working copy so we don't modify the original
|
|
74
|
+
@working_copy ||= REXML::Document.new(self.to_s).root
|
|
75
|
+
|
|
76
|
+
# store and remove signature node
|
|
77
|
+
@sig_element ||= begin
|
|
78
|
+
element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
|
|
79
|
+
element.remove
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# verify signature
|
|
84
|
+
signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
|
|
85
|
+
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
|
86
|
+
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
|
87
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
|
88
|
+
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
|
89
|
+
noko_sig_element.remove
|
|
90
|
+
|
|
91
|
+
# check digests
|
|
92
|
+
REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
|
|
93
|
+
uri = ref.attributes.get_attribute("URI").value
|
|
94
|
+
|
|
95
|
+
hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
|
|
96
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
|
97
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
|
98
|
+
|
|
99
|
+
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
|
|
100
|
+
|
|
101
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
|
102
|
+
digest_value = Base64.decode64(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG}).text)
|
|
103
|
+
|
|
104
|
+
unless digests_match?(hash, digest_value)
|
|
105
|
+
return soft ? false : (raise Maestrano::Saml::ValidationError.new("Digest mismatch"))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
base64_signature = REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}).text
|
|
110
|
+
signature = Base64.decode64(base64_signature)
|
|
111
|
+
|
|
112
|
+
# get certificate object
|
|
113
|
+
cert_text = Base64.decode64(base64_cert)
|
|
114
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
|
115
|
+
|
|
116
|
+
# signature method
|
|
117
|
+
signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
|
|
118
|
+
|
|
119
|
+
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
|
|
120
|
+
return soft ? false : (raise Maestrano::Saml::ValidationError.new("Key validation error"))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def digests_match?(hash, digest_value)
|
|
129
|
+
hash == digest_value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def extract_signed_element_id
|
|
133
|
+
reference_element = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
|
|
134
|
+
self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def canon_algorithm(element)
|
|
138
|
+
algorithm = element.attribute('Algorithm').value if element
|
|
139
|
+
case algorithm
|
|
140
|
+
when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
|
141
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
|
|
142
|
+
when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
|
|
143
|
+
else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def algorithm(element)
|
|
148
|
+
algorithm = element.attribute("Algorithm").value if element
|
|
149
|
+
algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
|
|
150
|
+
case algorithm
|
|
151
|
+
when 256 then OpenSSL::Digest::SHA256
|
|
152
|
+
when 384 then OpenSSL::Digest::SHA384
|
|
153
|
+
when 512 then OpenSSL::Digest::SHA512
|
|
154
|
+
else
|
|
155
|
+
OpenSSL::Digest::SHA1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def extract_inclusive_namespaces
|
|
160
|
+
if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
|
|
161
|
+
prefix_list = element.attributes.get_attribute("PrefixList").value
|
|
162
|
+
prefix_list.split(" ")
|
|
163
|
+
else
|
|
164
|
+
[]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|