diff --git a/lib/puppet/provider/keycloak_flow/kcadm.rb b/lib/puppet/provider/keycloak_flow/kcadm.rb index 97937d2..1647758 100644 --- a/lib/puppet/provider/keycloak_flow/kcadm.rb +++ b/lib/puppet/provider/keycloak_flow/kcadm.rb @@ -1,241 +1,242 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'keycloak_api')) Puppet::Type.type(:keycloak_flow).provide(:kcadm, parent: Puppet::Provider::KeycloakAPI) do desc '' mk_resource_methods def self.instances flows = [] realms.each do |realm| output = kcadm('get', 'authentication/flows', realm) Puppet.debug("#{realm} flows: #{output}") begin data = JSON.parse(output) rescue JSON::ParserError Puppet.debug('Unable to parse output from kcadm get flows') data = [] end data.each do |d| if d['builtIn'] Puppet.debug("Skipping builtIn flow #{d['alias']}") next end flow = {} flow[:ensure] = :present flow[:top_level] = :true flow[:id] = d['id'] flow[:alias] = d['alias'] flow[:realm] = realm flow[:description] = d['description'] flow[:provider_id] = d['providerId'] flow[:name] = "#{flow[:alias]} on #{flow[:realm]}" flows << new(flow) begin executions_output = kcadm('get', "authentication/flows/#{d['alias']}/executions", realm) rescue Puppet.notice("Unable to query flow #{d['alias']} executions") executions_output = '[]' end Puppet.debug("#{realm} flow executions: #{executions_output}") begin executions_data = JSON.parse(executions_output) rescue JSON::ParserError Puppet.debug('Unable to parse output from kcadm get flow executions') executions_data = [] end levels = {} executions_data.each do |e| unless e['authenticationFlow'] Puppet.debug("Skipping non-authentication flow #{e['displayName']} for keycloak_flow") next end flow = {} flow[:ensure] = :present flow[:top_level] = :false flow[:id] = e['id'] flow[:requirement] = e['requirement'] flow[:configurable] = e['configurable'] if e.key?('configurable') flow[:flow_alias] = d['alias'] flow[:realm] = realm + flow[:description] = e['description'] flow[:index] = e['index'] flow[:display_name] = e['displayName'] flow[:alias] = e['displayName'] if e['level'] != 0 parent_level = levels.find { |k, _v| k == (e['level'] - 1) } flow[:flow_alias] = parent_level[1][-1] if parent_level.size > 1 end flow[:name] = "#{flow[:alias]} under #{flow[:flow_alias]} on #{realm}" levels[e['level']] = [] unless levels.key?(e['level']) levels[e['level']] << flow[:alias] flows << new(flow) end end end flows end def self.prefetch(resources) flows = instances resources.keys.each do |name| provider = flows.find do |c| (c.alias == resources[name][:alias] && c.flow_alias == resources[name][:flow_alias] && c.realm == resources[name][:realm]) || (c.alias == resources[name][:alias] && c.realm == resources[name][:realm]) end if provider resources[name].provider = provider end end end def create data = {} data[:alias] = resource[:alias] if resource[:top_level] == :true data[:id] = resource[:id] data[:description] = resource[:description] data[:providerId] = resource[:provider_id] data[:topLevel] = true url = 'authentication/flows' else data[:provider] = resource[:type] data[:description] = resource[:description] data[:type] = resource[:provider_id] url = "authentication/flows/#{resource[:flow_alias]}/executions/flow" end t = Tempfile.new('keycloak_flow') t.write(JSON.pretty_generate(data)) t.close Puppet.debug(IO.read(t.path)) begin new_id = kcadm('create', url, resource[:realm], t.path, nil, true) Puppet.debug("create flow output: #{new_id}") rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm create flow failed\nError message: #{e.message}" end if resource[:top_level] == :false && resource[:requirement] execution_output = kcadm('get', "authentication/flows/#{resource[:flow_alias]}/executions", resource[:realm]) begin execution_data = JSON.parse(execution_output) rescue JSON::ParserError Puppet.debug('Unable to parse output from kcadm get flow executions') execution_data = [] end execution_id = nil execution_data.each do |ed| next unless ed['flowId'] == new_id.strip execution_id = ed['id'] end unless execution_id.nil? update_data = { id: execution_id, requirement: resource[:requirement], } t = Tempfile.new('keycloak_flow_execution') t.write(JSON.pretty_generate(update_data)) t.close Puppet.debug(IO.read(t.path)) begin output = kcadm('update', "authentication/flows/#{resource[:flow_alias]}/executions", resource[:realm], t.path, nil, true) Puppet.debug("update flow execution output: #{output}") rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update flow execution failed\nError message: #{e.message}" end end end @property_hash[:ensure] = :present end def destroy url = if resource[:top_level] == :true "authentication/flows/#{id}" else "authentication/executions/#{id}" end begin kcadm('delete', url, resource[:realm]) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm delete flow failed\nError message: #{e.message}" end @property_hash.clear end def exists? @property_hash[:ensure] == :present end def initialize(value = {}) super(value) @property_flush = {} end type_properties.each do |prop| define_method "#{prop}=".to_sym do |value| @property_flush[prop] = value end end def current_priority data = {} begin output = kcadm('get', "authentication/executions/#{id}", resource[:realm]) data = JSON.parse(output) rescue Puppet::ExecutionFailure => e Puppet.debug("kcadm get execution failed\nError message: #{e.message}") rescue JSON::ParserError Puppet.debug('Unable to parse output from kcadm get execution') end data['priority'] || resource[:index] end def flush unless @property_flush.empty? data = {} if resource[:top_level] == :true data[:id] = resource[:id] data[:alias] = resource[:alias] data[:description] = resource[:description] data[:providerId] = resource[:provider_id] data[:topLevel] = true url = "authentication/flows/#{id}" elsif @property_flush[:requirement] data[:id] = id data[:description] = resource[:description] data[:requirement] = resource[:requirement] url = "authentication/flows/#{resource[:flow_alias]}/executions" end unless data.empty? t = Tempfile.new('keycloak_flow') t.write(JSON.pretty_generate(data)) t.close Puppet.debug(IO.read(t.path)) begin kcadm('update', url, resource[:realm], t.path, nil, true) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update flow failed\nError message: #{e.message}" end end if resource[:top_level] == :false && @property_flush[:index] index_difference = current_priority - @property_flush[:index] if index_difference.zero? Puppet.notice("Index difference for Keycloak_flow[#{resource[:name]}] is unchanged, skipping.") elsif index_difference < 0 incrementer = 1 action = 'lower-priority' else incrementer = -1 action = 'raise-priority' end while index_difference != 0 kcadm('create', "authentication/executions/#{id}/#{action}", resource[:realm]) index_difference += incrementer end end end # Collect the resources again once they've been changed (that way `puppet # resource` will show the correct values after changes have been made). @property_hash = resource.to_hash end end diff --git a/spec/acceptance/9_flow_spec.rb b/spec/acceptance/9_flow_spec.rb index a799d23..0ff204d 100644 --- a/spec/acceptance/9_flow_spec.rb +++ b/spec/acceptance/9_flow_spec.rb @@ -1,265 +1,269 @@ require 'spec_helper_acceptance' describe 'flow types:', if: RSpec.configuration.keycloak_full do context 'creates flow' do it 'runs successfully' do pp = <<-EOS include mysql::server class { 'keycloak': datasource_driver => 'mysql', } keycloak::spi_deployment { 'duo-spi': deployed_name => 'keycloak-duo-spi-jar-with-dependencies.jar', source => 'file:///tmp/keycloak-duo-spi-jar-with-dependencies.jar', test_url => 'authentication/authenticator-providers', test_key => 'id', test_value => 'duo-mfa-authenticator', test_realm => 'test', test_before => [ 'Keycloak_flow[form-browser-with-duo]', 'Keycloak_flow[form-browser-with-duo2]', 'Keycloak_flow_execution[duo-mfa-authenticator under form-browser-with-duo on test]', 'Keycloak_flow_execution[duo-mfa-authenticator under form-browser-with-duo2 on test]', ], } keycloak_realm { 'test': ensure => 'present' } keycloak_flow { 'browser-with-duo on test': - ensure => 'present', + ensure => 'present', + description => 'Browser with DUO', } keycloak_flow_execution { 'duo-mfa-authenticator under form-browser-with-duo on test': ensure => 'present', configurable => true, display_name => 'Duo MFA', alias => 'Duo', config => { "duomfa.akey" => "foo-akey", "duomfa.apihost" => "api-foo.duosecurity.com", "duomfa.skey" => "secret", "duomfa.ikey" => "foo-ikey", "duomfa.groups" => "duo" }, requirement => 'REQUIRED', index => 1, } keycloak_flow_execution { 'duo-mfa-authenticator under form-browser-with-duo2 on test': ensure => 'present', configurable => true, display_name => 'Duo MFA', alias => 'Duo2', requirement => 'REQUIRED', index => 0, } keycloak_flow_execution { 'auth-username-password-form under form-browser-with-duo on test': ensure => 'present', configurable => false, display_name => 'Username Password Form', index => 0, requirement => 'REQUIRED', } keycloak_flow { 'form-browser-with-duo under browser-with-duo on test': ensure => 'present', index => 2, requirement => 'ALTERNATIVE', top_level => false, + description => 'Form Browser with DUO', } keycloak_flow { 'form-browser-with-duo2 under browser-with-duo on test': ensure => 'present', index => 3, requirement => 'REQUIRED', top_level => false, } keycloak_flow_execution { 'auth-cookie under browser-with-duo on test': ensure => 'present', configurable => false, display_name => 'Cookie', index => 0, requirement => 'ALTERNATIVE', } keycloak_flow_execution { 'identity-provider-redirector under browser-with-duo on test': ensure => 'present', configurable => true, display_name => 'Identity Provider Redirector', index => 1, requirement => 'ALTERNATIVE', } EOS apply_manifest(pp, catch_failures: true) apply_manifest(pp, catch_changes: true) end it 'has created a flow' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get authentication/flows/browser-with-duo-test -r test' do data = JSON.parse(stdout) expect(data['alias']).to eq('browser-with-duo') + expect(data['description']).to eq('Browser with DUO') expect(data['topLevel']).to eq(true) end end it 'has executions' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get authentication/flows/browser-with-duo/executions -r test' do data = JSON.parse(stdout) cookie = data.find { |d| d['providerId'] == 'auth-cookie' } expect(cookie['index']).to eq(0) idp = data.find { |d| d['providerId'] == 'identity-provider-redirector' } expect(idp['index']).to eq(1) form = data.find { |d| d['displayName'] == 'form-browser-with-duo' } expect(form['index']).to eq(2) + expect(form['description']).to eq('Form Browser with DUO') auth_form = data.find { |d| d['providerId'] == 'auth-username-password-form' } expect(auth_form['index']).to eq(0) duo = data.find { |d| d['providerId'] == 'duo-mfa-authenticator' } expect(duo['index']).to eq(1) end end end context 'updates flow' do it 'runs successfully' do pp = <<-EOS include mysql::server class { 'keycloak': datasource_driver => 'mysql', } keycloak::spi_deployment { 'duo-spi': deployed_name => 'keycloak-duo-spi-jar-with-dependencies.jar', source => 'file:///tmp/keycloak-duo-spi-jar-with-dependencies.jar', test_url => 'authentication/authenticator-providers', test_key => 'id', test_value => 'duo-mfa-authenticator', test_realm => 'test', test_before => [ 'Keycloak_flow[form-browser-with-duo]', 'Keycloak_flow[form-browser-with-duo2]', 'Keycloak_flow_execution[duo-mfa-authenticator under form-browser-with-duo on test]', 'Keycloak_flow_execution[duo-mfa-authenticator under form-browser-with-duo2 on test]', ], } keycloak_realm { 'test': ensure => 'present' } keycloak_flow { 'browser-with-duo on test': ensure => 'present', description => 'browser with Duo', } keycloak_flow_execution { 'duo-mfa-authenticator under form-browser-with-duo on test': ensure => 'present', configurable => true, display_name => 'Duo MFA', alias => 'Duo', config => { "duomfa.akey" => "foo-akey2", "duomfa.apihost" => "api-foo.duosecurity.com", "duomfa.skey" => "secret2", "duomfa.ikey" => "foo-ikey2", "duomfa.groups" => "duo,duo2" }, requirement => 'REQUIRED', index => 0, } keycloak_flow_execution { 'duo-mfa-authenticator under form-browser-with-duo2 on test': ensure => 'present', configurable => true, display_name => 'Duo MFA', alias => 'Duo2', config => { "duomfa.akey" => "foo-akey2", "duomfa.apihost" => "api-foo.duosecurity.com", "duomfa.skey" => "secret2", "duomfa.ikey" => "foo-ikey2", "duomfa.groups" => "duo,duo2" }, requirement => 'REQUIRED', index => 0, } keycloak_flow_execution { 'auth-username-password-form under form-browser-with-duo on test': ensure => 'present', configurable => false, display_name => 'Username Password Form', index => 1, requirement => 'REQUIRED', } keycloak_flow { 'form-browser-with-duo under browser-with-duo on test': ensure => 'present', index => 2, requirement => 'REQUIRED', top_level => false, } keycloak_flow { 'form-browser-with-duo2 under browser-with-duo on test': ensure => 'present', index => 3, requirement => 'REQUIRED', top_level => false, } keycloak_flow_execution { 'auth-cookie under browser-with-duo on test': ensure => 'present', configurable => false, display_name => 'Cookie', index => 1, requirement => 'ALTERNATIVE', } keycloak_flow_execution { 'identity-provider-redirector under browser-with-duo on test': ensure => 'present', configurable => true, display_name => 'Identity Provider Redirector', index => 0, requirement => 'ALTERNATIVE', } EOS apply_manifest(pp, catch_failures: true) apply_manifest(pp, catch_changes: true) end it 'has updated a flow' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get authentication/flows/browser-with-duo-test -r test' do data = JSON.parse(stdout) expect(data['description']).to eq('browser with Duo') end end it 'has executions' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get authentication/flows/browser-with-duo/executions -r test' do data = JSON.parse(stdout) cookie = data.find { |d| d['providerId'] == 'auth-cookie' } expect(cookie['index']).to eq(1) idp = data.find { |d| d['providerId'] == 'identity-provider-redirector' } expect(idp['index']).to eq(0) form = data.find { |d| d['displayName'] == 'form-browser-with-duo' } expect(form['index']).to eq(2) auth_form = data.find { |d| d['providerId'] == 'auth-username-password-form' } expect(auth_form['index']).to eq(1) duo = data.find { |d| d['providerId'] == 'duo-mfa-authenticator' } expect(duo['index']).to eq(0) end end end context 'ensure => absent' do it 'runs successfully' do pp = <<-EOS include mysql::server class { 'keycloak': datasource_driver => 'mysql', } keycloak_flow { 'browser-with-duo on test': ensure => 'absent', } keycloak_flow_execution { 'auth-cookie under browser-with-duo on test': ensure => 'absent', } keycloak_flow_execution { 'identity-provider-redirector under browser-with-duo on test': ensure => 'absent', } EOS apply_manifest(pp, catch_failures: true) apply_manifest(pp, catch_changes: true) end it 'has deleted a flow' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get authentication/flows -r test' do data = JSON.parse(stdout) d = data.select { |o| o['alias'] == 'browser-with-duo' }[0] expect(d).to be_nil end end end end