diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d13a2..48fee93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### 3.0.9 +* Added `Scoping` element to an `AuthnRequest` + +### 3.0.8 +* Backward compatibility fix. (#147) + +### 3.0.7 +* Added signature config and response location + ### 3.0.6 * Fix the encryption of an EncryptedID element with multiple recipients. diff --git a/lib/saml.rb b/lib/saml.rb index b117228..f944c64 100644 --- a/lib/saml.rb +++ b/lib/saml.rb @@ -197,6 +197,9 @@ module Elements require 'saml/elements/entities_descriptor' require 'saml/elements/attribute_query' require 'saml/elements/evidence' + require 'saml/elements/idp_entry' + require 'saml/elements/idp_list' + require 'saml/elements/scoping' end module Rails diff --git a/lib/saml/authn_request.rb b/lib/saml/authn_request.rb index f29c53d..694e2fc 100644 --- a/lib/saml/authn_request.rb +++ b/lib/saml/authn_request.rb @@ -14,6 +14,7 @@ class AuthnRequest attribute :provider_name, String, :tag => "ProviderName" has_one :requested_authn_context, Saml::Elements::RequestedAuthnContext + has_one :scoping, Saml::Elements::Scoping validates :force_authn, :inclusion => [true, false, nil] validates :assertion_consumer_service_index, :numericality => true, :if => lambda { |val| diff --git a/lib/saml/elements/idp_entry.rb b/lib/saml/elements/idp_entry.rb new file mode 100644 index 0000000..766f2ad --- /dev/null +++ b/lib/saml/elements/idp_entry.rb @@ -0,0 +1,16 @@ +module Saml + module Elements + class IdpEntry + include Saml::Base + + tag 'IDPEntry' + namespace 'samlp' + + attribute :provider_id, String, :tag => 'ProviderID' + attribute :name, String, :tag => 'Name' + attribute :loc, String, :tag => 'Loc' + + validates :provider_id, :presence => true + end + end +end diff --git a/lib/saml/elements/idp_list.rb b/lib/saml/elements/idp_list.rb new file mode 100644 index 0000000..615eee8 --- /dev/null +++ b/lib/saml/elements/idp_list.rb @@ -0,0 +1,19 @@ +module Saml + module Elements + class IdpList + include Saml::Base + + tag 'IDPList' + namespace 'samlp' + + has_many :idp_entries, Saml::Elements::IdpEntry + + validates :idp_entries, :presence => true + + def initialize(*args) + super(*args) + self.idp_entries ||= [] + end + end + end +end diff --git a/lib/saml/elements/scoping.rb b/lib/saml/elements/scoping.rb new file mode 100644 index 0000000..4d96a23 --- /dev/null +++ b/lib/saml/elements/scoping.rb @@ -0,0 +1,12 @@ +module Saml + module Elements + class Scoping + include Saml::Base + + tag 'Scoping' + namespace 'samlp' + + has_one :idp_list, Saml::Elements::IdpList + end + end +end diff --git a/lib/saml/version.rb b/lib/saml/version.rb index 1e67ce6..3aa5064 100644 --- a/lib/saml/version.rb +++ b/lib/saml/version.rb @@ -1,3 +1,3 @@ module Saml - VERSION = '3.0.8' + VERSION = '3.0.9' end diff --git a/spec/factories/all.rb b/spec/factories/all.rb index 98be907..faba50d 100644 --- a/spec/factories/all.rb +++ b/spec/factories/all.rb @@ -245,4 +245,15 @@ class StatementDummy factory :session_index, class: Saml::Elements::SessionIndex do value 'SessionIndex' end + + factory :scoping, :class => Saml::Elements::Scoping do + end + + factory :idp_list, :class => Saml::Elements::IdpList do + end + + factory :idp_entry, :class => Saml::Elements::IdpEntry do + provider_id 'ProviderID' + end + end diff --git a/spec/fixtures/authn_request.xml b/spec/fixtures/authn_request.xml index 8966597..396a2ba 100644 --- a/spec/fixtures/authn_request.xml +++ b/spec/fixtures/authn_request.xml @@ -8,4 +8,10 @@ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + + diff --git a/spec/lib/saml/authn_request_spec.rb b/spec/lib/saml/authn_request_spec.rb index 232fae1..18d1d79 100644 --- a/spec/lib/saml/authn_request_spec.rb +++ b/spec/lib/saml/authn_request_spec.rb @@ -12,7 +12,7 @@ Saml::AuthnRequest.tag_name.should == "AuthnRequest" end - [:force_authn, :assertion_consumer_service_index, :assertion_consumer_service_url, :protocol_binding, :provider_name, :requested_authn_context, :is_passive].each do |attribute| + [:force_authn, :assertion_consumer_service_index, :assertion_consumer_service_url, :protocol_binding, :provider_name, :requested_authn_context, :is_passive, :scoping].each do |attribute| it "should accept the #{attribute} attribute" do authn_request.should respond_to(attribute) end @@ -78,6 +78,10 @@ authn_request.requested_authn_context.should be_a(Saml::Elements::RequestedAuthnContext) end + it 'should create a Saml::Elements::Scoping' do + expect(authn_request.scoping).to be_a(Saml::Elements::Scoping) + end + context "with two requested_authn_context_class_refs" do let(:authn_request_xml) { File.read(File.join('spec', 'fixtures', 'authn_request_with_two_authn_contexts.xml'))} diff --git a/spec/lib/saml/elements/idp_entry_spec.rb b/spec/lib/saml/elements/idp_entry_spec.rb new file mode 100644 index 0000000..1bca621 --- /dev/null +++ b/spec/lib/saml/elements/idp_entry_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Saml::Elements::IdpEntry do + let(:idp_entry) { FactoryGirl.build :idp_entry } + + describe 'optional fields' do + [:name, :loc].each do |field| + it "should respond to the '#{field}' field" do + expect(subject).to respond_to(field) + end + end + end + + describe 'required fields' do + [:provider_id].each do |field| + it "should have the #{field} field" do + subject.should respond_to(field) + end + + it "should check the presence of #{field}" do + subject.send("#{field}=", nil) + subject.should_not be_valid + end + end + end + +end diff --git a/spec/lib/saml/elements/idp_list_spec.rb b/spec/lib/saml/elements/idp_list_spec.rb new file mode 100644 index 0000000..79f05fb --- /dev/null +++ b/spec/lib/saml/elements/idp_list_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Saml::Elements::IdpList do + let(:idp_list) { FactoryGirl.build :idp_list } + + describe 'required fields' do + [:idp_entries].each do |field| + it "should have the #{field} field" do + subject.should respond_to(field) + end + + it "should check the presence of #{field}" do + subject.send("#{field}=", nil) + subject.should_not be_valid + end + end + end + + describe '#idp_entries' do + it 'returns an empty array if no IDP entries have been registered' do + expect(subject.idp_entries).to eq [] + end + end + + describe '#parse_xml' do + let(:authn_request_xml) { File.read(File.join('spec', 'fixtures', 'authn_request.xml')) } + let(:idp_list) { Saml::Elements::IdpList.parse(authn_request_xml, single: true) } + + it 'should create a new Saml::Elements::IdpList' do + expect(idp_list).to be_a(Saml::Elements::IdpList) + end + + it 'should parse the IDP entries' do + aggregate_failures do + expect(idp_list.idp_entries.count).to eq 2 + + expect(idp_list.idp_entries.first).to be_a(Saml::Elements::IdpEntry) + expect(idp_list.idp_entries.first.provider_id).to eq 'provider-id-1' + expect(idp_list.idp_entries.first.name).to eq 'Provider name 1' + expect(idp_list.idp_entries.first.loc).to eq 'https://idp1.example.com' + + expect(idp_list.idp_entries.second).to be_a(Saml::Elements::IdpEntry) + expect(idp_list.idp_entries.second.provider_id).to eq 'provider-id-2' + expect(idp_list.idp_entries.second.name).to eq 'Provider name 2' + expect(idp_list.idp_entries.second.loc).to eq 'https://idp2.example.com' + end + end + end + +end diff --git a/spec/lib/saml/elements/scoping_spec.rb b/spec/lib/saml/elements/scoping_spec.rb new file mode 100644 index 0000000..b287ca8 --- /dev/null +++ b/spec/lib/saml/elements/scoping_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Saml::Elements::Scoping do + let(:idp_entry) { FactoryGirl.build :scoping } + + describe 'optional fields' do + [:idp_list].each do |field| + it "should respond to the '#{field}' field" do + expect(subject).to respond_to(field) + end + end + end + + describe '#parse_xml' do + let(:authn_request_xml) { File.read(File.join('spec', 'fixtures', 'authn_request.xml')) } + let(:scoping) { Saml::Elements::Scoping.parse(authn_request_xml, single: true) } + + it 'should create a new Saml::Elements::Scoping' do + expect(scoping).to be_a(Saml::Elements::Scoping) + end + + it 'should parse the IDP list' do + expect(scoping.idp_list).to be_a(Saml::Elements::IdpList) + end + end + +end