diff --git a/lib/puppet/type/keycloak_realm.rb b/lib/puppet/type/keycloak_realm.rb index edbe87c7..5a151779 100644 --- a/lib/puppet/type/keycloak_realm.rb +++ b/lib/puppet/type/keycloak_realm.rb @@ -338,12 +338,88 @@ def should_to_s(_newvalue) newvalues(:true, :false) end + newproperty(:permanent_lockout, boolean: true) do + desc 'permanentLockout' + newvalues(:true, :false) + defaultto :false + end + + newproperty(:max_failure_wait_seconds, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'maxFailureWaitSeconds' + defaultto 900 + end + + newproperty(:minimum_quick_login_wait_seconds, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'minimumQuickLoginWaitSeconds' + defaultto 60 + end + + newproperty(:wait_increment_seconds, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'waitIncrementSeconds' + defaultto 60 + end + + newproperty(:quick_login_check_milli_seconds, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'quickLoginCheckMilliSeconds' + defaultto 1_000 + end + + newproperty(:max_delta_time_seconds, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'maxDeltaTimeSeconds' + defaultto 43_200 + end + + newproperty(:failure_factor, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'failureFactor' + defaultto 30 + end + newparam(:manage_roles, boolean: true) do desc 'Manage realm roles' newvalues(:true, :false) defaultto(:true) end + newproperty(:otp_policy_type) do + desc 'otpPolicyType' + newvalues('totp', 'hotp') + defaultto 'totp' + end + + newproperty(:otp_policy_algorithm) do + desc 'otpPolicyAlgorithm' + newvalues('HmacSHA1', 'HmacSHA256', 'HmacSHA512') + defaultto 'HmacSHA1' + end + + newproperty(:otp_policy_initial_counter, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'otpPolicyInitialCounter' + defaultto 0 + end + + newproperty(:otp_policy_digits) do + desc 'otpPolicyDigits' + newvalues(6, 8) + defaultto 6 + munge { |v| v.to_i } + end + + newproperty(:otp_policy_look_ahead_window, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'otpPolicyLookAheadWindow' + defaultto 1 + end + + newproperty(:otp_policy_period, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'otpPolicyPeriod' + defaultto 30 + end + + newproperty(:otp_policy_code_reusable, boolean: true) do + desc 'otpPolicyCodeReusable' + newvalues(:true, :false) + defaultto :false + end + newproperty(:roles, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do desc 'roles' defaultto ['offline_access', 'uma_authorization'] @@ -357,6 +433,116 @@ def insync?(is) end end + newproperty(:web_authn_policy_rp_entity_name) do + desc 'webAuthnPolicyRpEntityName' + defaultto 'keycloak' + end + + newproperty(:web_authn_policy_signature_algorithms, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do + desc 'webAuthnPolicySignatureAlgorithms' + defaultto ['ES256'] + end + + newproperty(:web_authn_policy_rp_id) do + desc 'webAuthnPolicyRpId' + defaultto '' + end + + newproperty(:web_authn_policy_attestation_conveyance_preference) do + desc 'webAuthnPolicyAttestationConveyancePreference' + newvalues('none', 'direct', 'indirect', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_authenticator_attachment) do + desc 'webAuthnPolicyAuthenticatorAttachment' + newvalues('platform', 'cross-platform', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_require_resident_key) do + desc 'webAuthnPolicyRequireResidentKey' + newvalues('No', 'Yes', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_user_verification_requirement) do + desc 'webAuthnPolicyUserVerificationRequirement' + newvalues('required', 'preferred', 'discouraged', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_create_timeout, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'webAuthnPolicyCreateTimeout' + defaultto 0 + end + + newproperty(:web_authn_policy_avoid_same_authenticator_register, boolean: true) do + desc 'webAuthnPolicyAvoidSameAuthenticatorRegister' + newvalues(:true, :false) + defaultto :false + end + + newproperty(:web_authn_policy_acceptable_aaguids, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do + desc 'webAuthnPolicyAcceptableAaguids' + defaultto [] + end + + newproperty(:web_authn_policy_passwordless_rp_entity_name) do + desc 'webAuthnPolicyPasswordlessRpEntityName' + defaultto 'keycloak' + end + + newproperty(:web_authn_policy_passwordless_signature_algorithms, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do + desc 'webAuthnPolicyPasswordlessSignatureAlgorithms' + defaultto ['ES256'] + end + + newproperty(:web_authn_policy_passwordless_rp_id) do + desc 'webAuthnPolicyPasswordlessRpId' + defaultto '' + end + + newproperty(:web_authn_policy_passwordless_attestation_conveyance_preference) do + desc 'webAuthnPolicyPasswordlessAttestationConveyancePreference' + newvalues('none', 'direct', 'indirect', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_passwordless_authenticator_attachment) do + desc 'webAuthnPolicyPasswordlessAuthenticatorAttachment' + newvalues('platform', 'cross-platform', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_passwordless_require_resident_key) do + desc 'webAuthnPolicyPasswordlessRequireResidentKey' + newvalues('No', 'Yes', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_passwordless_user_verification_requirement) do + desc 'webAuthnPolicyPasswordlessUserVerificationRequirement' + newvalues('required', 'preferred', 'discouraged', 'not specified') + defaultto 'not specified' + end + + newproperty(:web_authn_policy_passwordless_create_timeout, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'webAuthnPolicyPasswordlessCreateTimeout' + defaultto 0 + end + + newproperty(:web_authn_policy_passwordless_avoid_same_authenticator_register, boolean: true) do + desc 'webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister' + newvalues(:true, :false) + defaultto :false + end + + newproperty(:web_authn_policy_passwordless_acceptable_aaguids, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do + desc 'webAuthnPolicyPasswordlessAcceptableAaguids' + defaultto [] + end + newproperty(:custom_properties) do desc 'custom properties to pass as realm configurations' defaultto {} diff --git a/spec/acceptance/2_realm_spec.rb b/spec/acceptance/2_realm_spec.rb index 0c0466e3..d06b4185 100644 --- a/spec/acceptance/2_realm_spec.rb +++ b/spec/acceptance/2_realm_spec.rb @@ -214,9 +214,41 @@ class { 'keycloak': } default_locale => 'en', supported_locales => ['en','de'], custom_properties => { - 'failureFactor' => 60, 'revokeRefreshToken' => true, }, + failure_factor => 60, + permanent_lockout => true, + max_failure_wait_seconds => 999, + minimum_quick_login_wait_seconds => 40, + wait_increment_seconds => 10, + quick_login_check_milli_seconds => 10, + max_delta_time_seconds => 3600, + otp_policy_type => 'totp', + otp_policy_algorithm => 'HmacSHA512', + otp_policy_initial_counter => 1, + otp_policy_digits => 8, + otp_policy_period => 30, + otp_policy_code_reusable => true, + web_authn_policy_rp_entity_name => 'Keycloak', + web_authn_policy_signature_algorithms => ['ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512'], + web_authn_policy_rp_id => 'https://example.com', + web_authn_policy_attestation_conveyance_preference => 'direct', + web_authn_policy_authenticator_attachment => 'cross-platform', + web_authn_policy_require_resident_key => 'No', + web_authn_policy_user_verification_requirement => 'required', + web_authn_policy_create_timeout => 600, + web_authn_policy_avoid_same_authenticator_register => true, + web_authn_policy_acceptable_aaguids => ['d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1'], + web_authn_policy_passwordless_rp_entity_name => 'Keycloak', + web_authn_policy_passwordless_signature_algorithms => ['ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512'], + web_authn_policy_passwordless_rp_id => 'https://example.com', + web_authn_policy_passwordless_attestation_conveyance_preference => 'direct', + web_authn_policy_passwordless_authenticator_attachment => 'cross-platform', + web_authn_policy_passwordless_require_resident_key => 'No', + web_authn_policy_passwordless_user_verification_requirement => 'required', + web_authn_policy_passwordless_create_timeout => 600, + web_authn_policy_passwordless_avoid_same_authenticator_register => true, + web_authn_policy_passwordless_acceptable_aaguids => ['d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1'], } PUPPET_PP @@ -263,10 +295,42 @@ class { 'keycloak': } expect(data['adminTheme']).to eq('keycloak.v2') expect(data['emailTheme']).to eq('keycloak.v2') expect(data['failureFactor']).to eq(60) + expect(data['permanentLockout']).to eq(true) + expect(data['maxFailureWaitSeconds']).to eq(999) + expect(data['minimumQuickLoginWaitSeconds']).to eq(40) + expect(data['waitIncrementSeconds']).to eq(10) + expect(data['quickLoginCheckMilliSeconds']).to eq(10) + expect(data['maxDeltaTimeSeconds']).to eq(3600) expect(data['revokeRefreshToken']).to eq(true) expect(data['internationalizationEnabled']).to eq(true) expect(data['defaultLocale']).to eq('en') expect(data['supportedLocales']).to eq(['de', 'en']) + expect(data['otpPolicyType']).to eq('totp') + expect(data['otpPolicyAlgorithm']).to eq('HmacSHA512') + expect(data['otpPolicyInitialCounter']).to eq(1) + expect(data['otpPolicyDigits']).to eq(8) + expect(data['otpPolicyPeriod']).to eq(30) + expect(data['otpPolicyCodeReusable']).to eq(true) + expect(data['webAuthnPolicyRpEntityName']).to eq('Keycloak') + expect(data['webAuthnPolicySignatureAlgorithms']).to eq(['ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512']) + expect(data['webAuthnPolicyRpId']).to eq('https://example.com') + expect(data['webAuthnPolicyAttestationConveyancePreference']).to eq('direct') + expect(data['webAuthnPolicyAuthenticatorAttachment']).to eq('cross-platform') + expect(data['webAuthnPolicyRequireResidentKey']).to eq('No') + expect(data['webAuthnPolicyUserVerificationRequirement']).to eq('required') + expect(data['webAuthnPolicyCreateTimeout']).to eq(600) + expect(data['webAuthnPolicyAvoidSameAuthenticatorRegister']).to eq(true) + expect(data['webAuthnPolicyAcceptableAaguids']).to eq(['d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1']) + expect(data['webAuthnPolicyPasswordlessRpEntityName']).to eq('Keycloak') + expect(data['webAuthnPolicyPasswordlessSignatureAlgorithms']).to eq(['ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512']) + expect(data['webAuthnPolicyPasswordlessRpId']).to eq('https://example.com') + expect(data['webAuthnPolicyPasswordlessAttestationConveyancePreference']).to eq('direct') + expect(data['webAuthnPolicyPasswordlessAuthenticatorAttachment']).to eq('cross-platform') + expect(data['webAuthnPolicyPasswordlessRequireResidentKey']).to eq('No') + expect(data['webAuthnPolicyPasswordlessUserVerificationRequirement']).to eq('required') + expect(data['webAuthnPolicyPasswordlessCreateTimeout']).to eq(600) + expect(data['webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister']).to eq(true) + expect(data['webAuthnPolicyPasswordlessAcceptableAaguids']).to eq(['d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1']) end end diff --git a/spec/unit/puppet/type/keycloak_realm_spec.rb b/spec/unit/puppet/type/keycloak_realm_spec.rb index a7024920..42a48eba 100644 --- a/spec/unit/puppet/type/keycloak_realm_spec.rb +++ b/spec/unit/puppet/type/keycloak_realm_spec.rb @@ -57,9 +57,142 @@ admin_events_enabled: :false, admin_events_details_enabled: :false, offline_session_max_lifespan_enabled: :false, - internationalization_enabled: :false + internationalization_enabled: :false, + permanent_lockout: :false, + max_failure_wait_seconds: 900, + minimum_quick_login_wait_seconds: 60, + wait_increment_seconds: 60, + quick_login_check_milli_seconds: 1_000, + max_delta_time_seconds: 43_200, + failure_factor: 30, + otp_policy_type: 'totp', + otp_policy_algorithm: 'HmacSHA1', + otp_policy_initial_counter: 0, + otp_policy_digits: 6, + otp_policy_look_ahead_window: 1, + otp_policy_period: 30, + otp_policy_code_reusable: :false, + web_authn_policy_rp_entity_name: 'keycloak', + web_authn_policy_signature_algorithms: ['ES256'], + web_authn_policy_rp_id: '', + web_authn_policy_attestation_conveyance_preference: 'not specified', + web_authn_policy_authenticator_attachment: 'not specified', + web_authn_policy_require_resident_key: 'not specified', + web_authn_policy_user_verification_requirement: 'not specified', + web_authn_policy_create_timeout: 0, + web_authn_policy_avoid_same_authenticator_register: :false, + web_authn_policy_acceptable_aaguids: [], + web_authn_policy_passwordless_rp_entity_name: 'keycloak', + web_authn_policy_passwordless_signature_algorithms: ['ES256'], + web_authn_policy_passwordless_rp_id: '', + web_authn_policy_passwordless_attestation_conveyance_preference: 'not specified', + web_authn_policy_passwordless_authenticator_attachment: 'not specified', + web_authn_policy_passwordless_require_resident_key: 'not specified', + web_authn_policy_passwordless_user_verification_requirement: 'not specified', + web_authn_policy_passwordless_create_timeout: 0, + web_authn_policy_passwordless_avoid_same_authenticator_register: :false, + web_authn_policy_passwordless_acceptable_aaguids: [] } + describe 'otp_policy_digits' do + it 'accepts 6 for otp_policy_digits' do + config[:otp_policy_digits] = 6 + expect(resource[:otp_policy_digits]).to eq(6) + end + + it 'accepts 8 for otp_policy_digits' do + config[:otp_policy_digits] = 8 + expect(resource[:otp_policy_digits]).to eq(8) + end + + it 'does not accept 7 for otp_policy_digits' do + config[:otp_policy_digits] = 7 + expect { + resource + }.to raise_error(%r{7}) + end + + it 'does not accept 5 for otp_policy_digits' do + config[:otp_policy_digits] = 5 + expect { + resource + }.to raise_error(%r{5}) + end + + it 'has default for otp_policy_digits' do + expect(resource[:otp_policy_digits]).to eq(defaults[:otp_policy_digits]) + end + + it 'does not accept nil for otp_policy_digits' do + config[:otp_policy_digits] = nil + expect { + resource + }.to raise_error(%r{nil}) + end + + it 'does not accept empty for otp_policy_digits' do + config[:otp_policy_digits] = '' + expect { + resource + }.to raise_error(%r{Invalid value ""}) + end + + it 'does not accept foo for otp_policy_digits' do + config[:otp_policy_digits] = 'foo' + expect { + resource + }.to raise_error(%r{Invalid value "foo"}) + end + end + + # Test enumerable properties + describe 'enumerable properties' do + { + otp_policy_type: [:totp, :hotp], + otp_policy_algorithm: [:HmacSHA1, :HmacSHA256, :HmacSHA512], + web_authn_policy_attestation_conveyance_preference: [:none, :indirect, :direct], + web_authn_policy_authenticator_attachment: [:platform, :'cross-platform'], + web_authn_policy_require_resident_key: [:Yes, :No], + web_authn_policy_user_verification_requirement: [:required, :preferred, :discouraged], + web_authn_policy_passwordless_attestation_conveyance_preference: [:none, :indirect, :direct], + web_authn_policy_passwordless_authenticator_attachment: [:platform, :'cross-platform'], + web_authn_policy_passwordless_require_resident_key: [:Yes, :No], + web_authn_policy_passwordless_user_verification_requirement: [:required, :preferred, :discouraged] + }.each do |p, values| + values.each do |v| + it "accepts #{v} for #{p}" do + config[p] = v + expect(resource[p]).to eq(v) + end + end + + it "does not accept foo for #{p}" do + config[p] = 'foo' + expect { + resource + }.to raise_error(%r{foo}) + end + + it "does not accept empty for #{p}" do + config[p] = '' + expect { + resource + }.to raise_error(%r{Invalid value ""}) + end + + it "does not accept nil for #{p}" do + config[p] = nil + expect { + resource + }.to raise_error(%r{nil}) + end + + it "has default for #{p}" do + expect(resource[p]).to eq(defaults[p].to_sym) + end + end + end + describe 'basic properties' do # Test basic properties [ @@ -85,7 +218,11 @@ :smtp_server_from_display_name, :smtp_server_reply_to, :smtp_server_reply_to_display_name, - :default_locale + :default_locale, + :web_authn_policy_rp_entity_name, + :web_authn_policy_rp_id, + :web_authn_policy_passwordless_rp_entity_name, + :web_authn_policy_passwordless_rp_id ].each do |p| it "accepts a #{p}" do config[p] = 'foo' @@ -116,7 +253,18 @@ :action_token_generated_by_user_lifespan, :offline_session_idle_timeout, :offline_session_max_lifespan, - :smtp_server_port + :smtp_server_port, + :max_failure_wait_seconds, + :minimum_quick_login_wait_seconds, + :wait_increment_seconds, + :quick_login_check_milli_seconds, + :max_delta_time_seconds, + :failure_factor, + :otp_policy_initial_counter, + :otp_policy_look_ahead_window, + :otp_policy_period, + :web_authn_policy_create_timeout, + :web_authn_policy_passwordless_create_timeout ].each do |p| it "accepts a #{p}" do config[p] = 100 @@ -151,7 +299,9 @@ :smtp_server_starttls, :smtp_server_ssl, :brute_force_protected, - :offline_session_max_lifespan_enabled + :offline_session_max_lifespan_enabled, + :permanent_lockout, + :otp_policy_code_reusable ].each do |p| it "accepts true for #{p}" do config[p] = true @@ -195,7 +345,11 @@ :optional_client_scopes, :events_listeners, :supported_locales, - :roles + :roles, + :web_authn_policy_signature_algorithms, + :web_authn_policy_acceptable_aaguids, + :web_authn_policy_passwordless_signature_algorithms, + :web_authn_policy_passwordless_acceptable_aaguids ].each do |p| it "accepts array for #{p}" do config[p] = ['foo', 'bar']