From 0eae726e0aab97aca2155dbba5e71d3276a0cfbb Mon Sep 17 00:00:00 2001 From: James Smith Date: Tue, 14 Aug 2018 11:15:25 -0400 Subject: [PATCH] [LG-487] piv/cac available based on email domain **Why**: We want to roll out piv/cac across the government, but in a measured way since we don't want to advertise it to folk we know that we can't support their piv/cac. **How**: Allow a configurable list of domains that we know we should be able to support. Allow suffix matching if starting with a `.` so we can support an agency that uses multiple subdomains, but also allow full matching for those agencies for which we know we can only support a subset. We assume that a subset of an agency for our purposes here will share an email domain. We don't require a user to have a proper email *and* a relationship with a particular SP. We require at least one of a relationship with a particular SP and a supported email domain. --- app/models/user.rb | 4 +- app/policies/piv_cac_login_option_policy.rb | 25 ++++ config/application.yml.example | 3 + .../piv_cac_login_option_policy_spec.rb | 115 ++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 spec/policies/piv_cac_login_option_policy_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index c986946eab4..2156ec60694 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,11 +56,11 @@ def confirm_piv_cac?(proposed_uuid) end def piv_cac_enabled? - FeatureManagement.piv_cac_enabled? && x509_dn_uuid.present? + PivCacLoginOptionPolicy.new(self).enabled? end def piv_cac_available? - piv_cac_enabled? || identities.any?(&:piv_cac_available?) + PivCacLoginOptionPolicy.new(self).available? end def need_two_factor_authentication?(_request) diff --git a/app/policies/piv_cac_login_option_policy.rb b/app/policies/piv_cac_login_option_policy.rb index aca26fe7c5d..96e2eb5ce94 100644 --- a/app/policies/piv_cac_login_option_policy.rb +++ b/app/policies/piv_cac_login_option_policy.rb @@ -7,7 +7,32 @@ def configured? FeatureManagement.piv_cac_enabled? && user.x509_dn_uuid.present? end + def enabled? + configured? + end + + def available? + enabled? || available_for_email? || user.identities.any?(&:piv_cac_available?) + end + private attr_reader :user + + def available_for_email? + piv_cac_email_domains = Figaro.env.piv_cac_email_domains + return if piv_cac_email_domains.blank? + + domain_list = JSON.parse(piv_cac_email_domains) + (_, email_domain) = user.email.split(/@/, 2) + domain_list.any? { |supported_domain| domain_match?(email_domain, supported_domain) } + end + + def domain_match?(given, matcher) + if matcher[0] == '.' + given.end_with?(matcher) + else + given == matcher + end + end end diff --git a/config/application.yml.example b/config/application.yml.example index 8f508c03e07..95594bce15f 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -150,6 +150,7 @@ development: password_pepper: 'f22d4b2cafac9066fe2f4416f5b7a32c' password_strength_enabled: 'true' piv_cac_agencies: '["Test Government Agency"]' + piv_cac_email_domains: '[".mil"]' piv_cac_enabled: 'true' piv_cac_verify_token_secret: 'ee7f20f44cdc2ba0c6830f70470d1d1d059e1279cdb58134db92b35947b1528ef5525ece5910cf4f2321ab989a618feea12ef95711dbc62b9601e8520a34ee12' piv_cac_service_url: 'https://localhost:8443/' @@ -276,6 +277,7 @@ production: password_pepper: # generate via `rake secret` password_strength_enabled: 'true' piv_cac_agencies: '["DOD","NGA","EOP"]' + piv_cac_email_domains: '[".mil"]' piv_cac_enabled: 'false' pkcs11_lib: '/opt/cloudhsm/lib/libcloudhsm_pkcs11.so' programmable_sms_countries: 'US,CA,MX' @@ -397,6 +399,7 @@ test: password_pepper: 'f22d4b2cafac9066fe2f4416f5b7a32c' password_strength_enabled: 'false' piv_cac_agencies: '["Test Government Agency"]' + piv_cac_email_domains: '[".mil"]' piv_cac_enabled: 'true' piv_cac_service_url: 'https://localhost:8443/' piv_cac_verify_token_secret: '3ac13bfa23e22adae321194c083e783faf89469f6f85dcc0802b27475c94b5c3891b5657bd87d0c1ad65de459166440512f2311018db90d57b15d8ab6660748f' diff --git a/spec/policies/piv_cac_login_option_policy_spec.rb b/spec/policies/piv_cac_login_option_policy_spec.rb new file mode 100644 index 00000000000..2c5289bec26 --- /dev/null +++ b/spec/policies/piv_cac_login_option_policy_spec.rb @@ -0,0 +1,115 @@ +require 'rails_helper' + +describe PivCacLoginOptionPolicy do + let(:subject) { described_class.new(user) } + + describe '#configured?' do + context 'without a piv configured' do + let(:user) { build(:user) } + + it { expect(subject.configured?).to be_falsey } + end + + context 'with a piv configured' do + let(:user) { build(:user, :with_piv_or_cac) } + + it { expect(subject.configured?).to be_truthy } + end + end + + describe '#enabled?' do + context 'without a piv configured' do + let(:user) { build(:user) } + + it { expect(subject.configured?).to be_falsey } + end + + context 'with a piv configured' do + let(:user) { build(:user, :with_piv_or_cac) } + + it { expect(subject.configured?).to be_truthy } + end + end + + describe '#available?' do + let(:user) { build(:user) } + + context 'when enabled' do + before(:each) do + allow(subject).to receive(:enabled?).and_return(true) + end + + it { expect(subject.available?).to be_truthy } + end + + context 'when available for the email' do + before(:each) do + allow(subject).to receive(:available_for_email?).and_return(true) + end + + it { expect(subject.available?).to be_truthy } + end + + context 'when associated with a supported identity' do + before(:each) do + identity = double + allow(identity).to receive(:piv_cac_available?).and_return(true) + allow(user).to receive(:identities).and_return([identity]) + end + + it { expect(subject.available?).to be_truthy } + end + + context 'when not enabled and not available for the email and not a supported identity' do + before(:each) do + identity = double + allow(identity).to receive(:piv_cac_available?).and_return(false) + allow(user).to receive(:identities).and_return([identity]) + allow(subject).to receive(:enabled?).and_return(false) + allow(subject).to receive(:available_for_email?).and_return(false) + end + + it { expect(subject.available?).to be_falsey } + end + end + + describe '#available_for_email?' do + let(:result) { subject.send(:available_for_email?) } + + context 'with a configured parent domain' do + before(:each) do + allow(Figaro.env).to receive(:piv_cac_email_domains).and_return('[".example.com"]') + end + + context 'and a supported email subdomain' do + let(:user) { build(:user, email: 'someone@foo.example.com') } + + it { expect(result).to be_truthy } + end + + context 'and a an email at that domain' do + let(:user) { build(:user, email: 'someone@example.com') } + + it { expect(result).to be_falsey } + end + end + + context 'with a configured full domain' do + before(:each) do + allow(Figaro.env).to receive(:piv_cac_email_domains).and_return('["example.com"]') + end + + context 'and an email subdomain' do + let(:user) { build(:user, email: 'someone@foo.example.com') } + + it { expect(result).to be_falsey } + end + + context 'and a an email at that domain' do + let(:user) { build(:user, email: 'someone@example.com') } + + it { expect(result).to be_truthy } + end + end + end +end