diff --git a/lib/puppet/provider/keycloak_api.rb b/lib/puppet/provider/keycloak_api.rb index 8eef918..50a3a6c 100644 --- a/lib/puppet/provider/keycloak_api.rb +++ b/lib/puppet/provider/keycloak_api.rb @@ -1,151 +1,158 @@ require 'puppet' require 'json' # Shared provider class class Puppet::Provider::KeycloakAPI < Puppet::Provider initvars # Unused but defined anyways commands kcadm_wrapper: '/opt/keycloak/bin/kcadm-wrapper.sh' @install_dir = nil @server = nil @realm = nil @user = nil @password = nil @use_wrapper = true class << self attr_accessor :install_dir attr_accessor :server attr_accessor :realm attr_accessor :user attr_accessor :password attr_accessor :use_wrapper end def self.type_properties resource_type.validproperties.reject { |p| p.to_sym == :ensure } end def type_properties self.class.type_properties end def self.camelize(value) str = value.to_s.split('_').map(&:capitalize).join str[0].downcase + str[1..-1] end def camelize(*args) self.class.camelize(*args) end def convert_property_value(value) case value when :true true when :false false else value end end - def self.kcadm(action, resource, realm = nil, file = nil, fields = nil) + def self.kcadm(action, resource, realm = nil, file = nil, fields = nil, print_id = false) kcadm_wrapper = '/opt/keycloak/bin/kcadm-wrapper.sh' arguments = [action, resource] - if ['create', 'update'].include?(action) + if ['create', 'update'].include?(action) && !print_id arguments << '-o' end if realm arguments << '-r' arguments << realm end if file arguments << '-f' arguments << file end if fields arguments << '--fields' arguments << fields.join(',') end + if action == 'create' && print_id + arguments << '--id' + end if use_wrapper == false || use_wrapper == :false auth_arguments = [ '--no-config', '--server', server, '--realm', self.realm, '--user', user, '--password', password ] cmd = [File.join(install_dir, 'bin/kcadm.sh')] + arguments + auth_arguments else cmd = [kcadm_wrapper] + arguments end execute(cmd, combine: false, failonfail: true) end def kcadm(*args) self.class.kcadm(*args) end def self.realms output = kcadm('get', 'realms', nil, nil, ['realm']) data = JSON.parse(output) realms = data.map { |r| r['realm'] } realms end + def realms + self.class.realms + end + def self.name_uuid(name) # Code lovingly taken from # https://github.com/puppetlabs/marionette-collective/blob/master/lib/mcollective/ssl.rb # This is the UUID version 5 type DNS name space which is as follows: # # 6ba7b810-9dad-11d1-80b4-00c04fd430c8 # uuid_name_space_dns = [0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8].map { |b| b.chr }.join sha1 = Digest::SHA1.new sha1.update(uuid_name_space_dns) sha1.update(name) # first 16 bytes.. bytes = sha1.digest[0, 16].bytes.to_a # version 5 adjustments bytes[6] &= 0x0f bytes[6] |= 0x50 # variant is DCE 1.1 bytes[8] &= 0x3f bytes[8] |= 0x80 bytes = [4, 2, 2, 2, 6].map do |i| bytes.slice!(0, i).pack('C*').unpack('H*') end bytes.join('-') end def name_uuid(*args) self.class.name_uuid(*args) end end diff --git a/lib/puppet/provider/keycloak_flow/kcadm.rb b/lib/puppet/provider/keycloak_flow/kcadm.rb index e479165..39a2ef9 100644 --- a/lib/puppet/provider/keycloak_flow/kcadm.rb +++ b/lib/puppet/provider/keycloak_flow/kcadm.rb @@ -1,121 +1,234 @@ 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) + executions_output = kcadm('get', "authentication/flows/#{d['alias']}/executions", realm) + 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[: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) } + execution[: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 { |c| c.alias == resources[name][:alias] && c.realm == resources[name][:realm] } + 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 - raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil? - data = {} - data[:id] = resource[:id] data[:alias] = resource[:alias] - data[:description] = resource[:description] - data[:providerId] = resource[:provider_id] - data[:topLevel] = true + 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[: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 - output = kcadm('create', 'authentication/flows', resource[:realm], t.path) - Puppet.debug("create flow output: #{output}") + 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 - raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil? + url = if resource[:top_level] == :true + "authentication/flows/#{id}" + else + "authentication/executions/#{id}" + end begin - kcadm('delete', "authentication/flows/#{id}", resource[:realm]) + 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? - raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil? - data = {} - data[:id] = resource[:id] - data[:alias] = resource[:alias] - data[:description] = resource[:description] - data[:providerId] = resource[:provider_id] - data[:topLevel] = true - t = Tempfile.new('keycloak_flow') - t.write(JSON.pretty_generate(data)) - t.close - Puppet.debug(IO.read(t.path)) - begin - kcadm('update', "authentication/flows/#{id}", resource[:realm], t.path) - rescue Puppet::ExecutionFailure => e - raise Puppet::Error, "kcadm update flow failed\nError message: #{e.message}" + 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[: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/lib/puppet/provider/keycloak_flow_execution/kcadm.rb b/lib/puppet/provider/keycloak_flow_execution/kcadm.rb new file mode 100644 index 0000000..982d8a8 --- /dev/null +++ b/lib/puppet/provider/keycloak_flow_execution/kcadm.rb @@ -0,0 +1,234 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'keycloak_api')) + +Puppet::Type.type(:keycloak_flow_execution).provide(:kcadm, parent: Puppet::Provider::KeycloakAPI) do + desc '' + + mk_resource_methods + + def self.instances + executions = [] + realms.each do |realm| + output = kcadm('get', 'authentication/flows', realm) + Puppet.debug("#{realm} flows: #{output}") + begin + flows = JSON.parse(output) + rescue JSON::ParserError + Puppet.debug('Unable to parse output from kcadm get flows') + flows = [] + end + + flows.each do |f| + if f['builtIn'] + Puppet.debug("Skipping builtIn flow #{f['alias']}") + next + end + Puppet.debug("Evaluate flow #{f['alias']}") + executions_output = kcadm('get', "authentication/flows/#{f['alias']}/executions", realm) + 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| + execution = {} + flow_alias = nil + execution[:ensure] = :present + execution[:id] = e['id'] + execution[:requirement] = e['requirement'] + execution[:configurable] = e['configurable'].to_s.to_sym if e.key?('configurable') + execution[:flow_alias] = f['alias'] + execution[:realm] = realm + execution[:index] = e['index'] + execution[:display_name] = e['displayName'] if e.key?('displayName') + if e['level'] != 0 + parent_level = levels.find { |k, _v| k == (e['level'] - 1) } + execution[:flow_alias] = parent_level[1][-1] if parent_level.size > 1 + end + execution[:provider_id] = e['providerId'] + execution[:alias] = e['alias'] + execution[:name] = "#{execution[:provider_id]} under #{execution[:flow_alias]} on #{realm}" + if e['authenticationFlow'] + flow_alias = e['displayName'] + end + levels[e['level']] = [] unless levels.key?(e['level']) + levels[e['level']] << flow_alias unless flow_alias.nil? + if e['authenticationFlow'] + Puppet.debug("Skipping authentication flow #{e['displayName']} for keycloak_flow_execution provider") + next + end + execution[:config_id] = e['authenticationConfig'] + if execution[:config_id] + config_output = kcadm('get', "authentication/config/#{execution[:config_id]}", realm) + Puppet.debug("#{realm} flow execution config: #{config_output}") + begin + config_data = JSON.parse(config_output) + rescue JSON::ParserError + Puppet.debug('Unable to parse output from kcadm get flow execution config') + config_data = [] + end + execution[:config] = config_data['config'] + end + Puppet.debug("EXECUTION: #{execution}") + executions << new(execution) + end + end + end + executions + end + + def self.prefetch(resources) + executions = instances + resources.keys.each do |name| + provider = executions.find do |c| + c.provider_id == resources[name][:provider_id] && c.flow_alias == resources[name][:flow_alias] && c.realm == resources[name][:realm] + end + if provider + resources[name].provider = provider + end + end + end + + def create + data = {} + data[:provider] = resource[:provider_id] + data[:displayName] = resource[:display_name] if resource[:display_name] + data[:configurable] = convert_property_value(resource[:configurable]) if resource[:configurable] + data[:alias] = resource[:alias] if resource[:alias] + t = Tempfile.new('keycloak_flow_execution') + t.write(JSON.pretty_generate(data)) + t.close + Puppet.debug(IO.read(t.path)) + begin + new_id = kcadm('create', "authentication/flows/#{resource[:flow_alias]}/executions/execution", resource[:realm], t.path, nil, true) + Puppet.debug("create flow execution output: #{new_id}") + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm create flow execution failed\nError message: #{e.message}" + end + if resource[:requirement] != 'DISABLED' + update_data = { + id: new_id.strip, + requirement: resource[:requirement], + } + tu = Tempfile.new('keycloak_flow_execution_update') + tu.write(JSON.pretty_generate(update_data)) + tu.close + Puppet.debug(IO.read(tu.path)) + begin + output = kcadm('update', "authentication/flows/#{resource[:flow_alias]}/executions", resource[:realm], tu.path) + 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 + if resource[:configurable] == :true && resource[:config] + config_data = {} + config_data[:alias] = resource[:alias] if resource[:alias] + config_data[:config] = resource[:config] + tc = Tempfile.new('keycloak_flow_execution_config') + tc.write(JSON.pretty_generate(config_data)) + tc.close + Puppet.debug(IO.read(tc.path)) + begin + output = kcadm('create', "authentication/executions/#{new_id.strip}/config", resource[:realm], tc.path) + Puppet.debug("create flow execution config output: #{output}") + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm create flow execution config failed\nError message: #{e.message}" + end + end + @property_hash[:ensure] = :present + end + + def destroy + begin + kcadm('delete', "authentication/executions/#{id}", 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? + if @property_flush[:requirement] + data = {} + data[:id] = id + data[:requirement] = resource[:requirement] if @property_flush[:requirement] + t = Tempfile.new('keycloak_flow_execution') + t.write(JSON.pretty_generate(data)) + t.close + Puppet.debug(IO.read(t.path)) + begin + kcadm('update', "authentication/flows/#{resource[:flow_alias]}/executions", resource[:realm], t.path, nil, true) + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm update flow execution failed\nError message: #{e.message}" + end + end + if @property_flush[:config] + config_data = {} + config_data[:id] = config_id + config_data[:alias] = resource[:alias] if resource[:alias] + config_data[:config] = resource[:config] + t = Tempfile.new('keycloak_flow_execution_config') + t.write(JSON.pretty_generate(config_data)) + t.close + Puppet.debug(IO.read(t.path)) + begin + kcadm('update', "authentication/config/#{config_id}", resource[:realm], t.path) + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm update flow execution config failed\nError message: #{e.message}" + end + end + if @property_flush[:index] + index_difference = current_priority - @property_flush[:index] + if index_difference.zero? + Puppet.notice("Index difference for Keycloak_flow_execution[#{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/lib/puppet/provider/keycloak_resource_validator/kcadm.rb b/lib/puppet/provider/keycloak_resource_validator/kcadm.rb new file mode 100644 index 0000000..3591d0c --- /dev/null +++ b/lib/puppet/provider/keycloak_resource_validator/kcadm.rb @@ -0,0 +1,79 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'keycloak_api')) + +Puppet::Type.type(:keycloak_resource_validator).provide(:kcadm, parent: Puppet::Provider::KeycloakAPI) do + desc "A provider for the resource type `keycloak_resource_validator`, + which validates a Keycloak resource exists." + + # Test to see if the resource exists, returns true if it does, false if it + # does not. + # + # Here we simply monopolize the resource API, to execute a test to see if the + # database is connectable. When we return a state of `false` it triggers the + # create method where we can return an error message. + # + # @return [bool] did the test succeed? + def exists? + start_time = Time.now + timeout = resource[:timeout] + + success = validator + + while success == false && ((Time.now - start_time) < timeout) + # It can take several seconds for the keycloak server to start up; + # especially on the first install. Therefore, our first connection attempt + # may fail. Here we have somewhat arbitrarily chosen to retry every 2 + # seconds until the configurable timeout has expired. + Puppet.notice("Failed to find resource #{resource[:test_key]}=#{resource[:test_value]} at #{resource[:test_url]}; sleeping 2 seconds before retry") + sleep 2 + success = validator + end + + unless success + Puppet.notice("Failed to find resource #{resource[:test_key]}=#{resource[:test_value]} at #{resource[:test_url]} within timeout window of #{timeout} seconds; giving up.") + end + + success + end + + # This method is called when the exists? method returns false. + # + # @return [void] + def create + # If `#create` is called, that means that `#exists?` returned false, which + # means that the connection could not be established... so we need to + # cause a failure here. + raise Puppet::Error, "Unable to find resource #{resource[:test_key]}=#{resource[:test_value]} at #{resource[:test_url]}" + end + + def test_realms + return @test_realms if @test_realms + @test_realms = if resource[:realm] + [resource[:realm]] + else + realms + end + end + + # Returns the existing validator, if one exists otherwise creates a new object + # from the class. + # + # @api private + def validator + test_realms.each do |realm| + output = kcadm('get', resource[:test_url], realm) + begin + data = JSON.parse(output) + rescue JSON::ParserError + Puppet.debug('Unable to parse output from kcadm get resource') + next + end + data.each do |d| + d.each_pair do |k, v| + next unless k == resource[:test_key].to_s + return true if v == resource[:test_value].to_s + end + end + end + false + end +end diff --git a/lib/puppet/type/keycloak_api.rb b/lib/puppet/type/keycloak_api.rb index af2140e..d957ba4 100644 --- a/lib/puppet/type/keycloak_api.rb +++ b/lib/puppet/type/keycloak_api.rb @@ -1,74 +1,71 @@ Dir[File.dirname(__FILE__) + '/keycloak*.rb'].each do |file| next if file == __FILE__ next if File.basename(file) == 'keycloak_conn_validator.rb' require file end Puppet::Type.newtype(:keycloak_api) do desc <<-DESC Type that configures API connection parameters for other keycloak types that use the Keycloak API. @example Define API access keycloak_api { 'keycloak' install_dir => '/opt/keycloak', server => 'http://localhost:8080/auth', realm => 'master', user => 'admin', password => 'changeme', } DESC newparam(:name, namevar: true) do desc 'Keycloak API config' end newparam(:install_dir) do desc 'Install location of Keycloak' end newparam(:server) do desc 'Auth URL for Keycloak server' defaultto('http://localhost:8080/auth') end newparam(:realm) do desc 'Realm for authentication' defaultto('master') end newparam(:user) do desc 'User for authentication' defaultto('admin') end newparam(:password) do desc 'Password for authentication' defaultto('changeme') end newparam(:use_wrapper, boolean: true) do desc 'Boolean that determines if kcadm_wrapper.sh should be used' newvalues(:true, :false) defaultto :false end def generate - [ - :keycloak_client_protocol_mapper, - :keycloak_client_scope, - :keycloak_client, - :keycloak_ldap_mapper, - :keycloak_ldap_user_provider, - :keycloak_protocol_mapper, - :keycloak_realm, - ].each do |res_type| + kcadm_types = [] + Dir[File.join(File.dirname(__FILE__), '../provider/keycloak_*/kcadm.rb')].each do |file| + type = File.basename(File.dirname(file)) + kcadm_types << type.to_sym + end + kcadm_types.each do |res_type| provider_class = Puppet::Type.type(res_type).provider(:kcadm) provider_class.install_dir = self[:install_dir] provider_class.server = self[:server] provider_class.realm = self[:realm] provider_class.user = self[:user] provider_class.password = self[:password] provider_class.use_wrapper = self[:use_wrapper] end [] end end diff --git a/lib/puppet/type/keycloak_flow.rb b/lib/puppet/type/keycloak_flow.rb index ea2bbd9..29b2463 100644 --- a/lib/puppet/type/keycloak_flow.rb +++ b/lib/puppet/type/keycloak_flow.rb @@ -1,76 +1,181 @@ require_relative '../../puppet_x/keycloak/type' require_relative '../../puppet_x/keycloak/array_property' +require_relative '../../puppet_x/keycloak/integer_property' Puppet::Type.newtype(:keycloak_flow) do desc <<-DESC Manage a Keycloak flow @example Add custom flow keycloak_flow { 'browser-with-duo': ensure => 'present', realm => 'test', } + +@example Add a flow execution to existing browser-with-duo flow + keycloak_flow { 'form-browser-with-duo under browser-with-duo on test': + ensure => 'present', + index => 2, + requirement => 'ALTERNATIVE', + top_level => false, + } + +**Autorequires** +* `keycloak_realm` defined for `realm` parameter +* `keycloak_flow` of `flow_alias` if `top_level=false` +* `keycloak_flow` of `flow_alias` if other `index` is lower and if `top_level=false` +* `keycloak_flow_execution` if `flow_alias` is the same and other `index` is lower and if `top_level=false` DESC extend PuppetX::Keycloak::Type add_autorequires ensurable newparam(:name, namevar: true) do desc 'The flow name' end newparam(:id) do - desc 'Id. Default to `$alias-$realm`' + desc 'Id. Default to `$alias-$realm` when top_level is true. Only applies to top_level=true' defaultto do - "#{@resource[:alias]}-#{@resource[:realm]}" + if @resource[:top_level] == :false + nil + else + "#{@resource[:alias]}-#{@resource[:realm]}" + end end end newparam(:alias, namevar: true) do desc 'Alias. Default to `name`.' defaultto do @resource[:name] end end - newparam(:realm, namevar: true) do - desc 'realm' + newparam(:flow_alias, namevar: true) do + desc 'flowAlias, required for top_level=false' end - newproperty(:description) do - desc 'description' + newparam(:realm, namevar: true) do + desc 'realm' end - newproperty(:provider_id) do + newparam(:provider_id) do desc 'providerId' newvalues('basic-flow', 'form-flow') defaultto('basic-flow') munge { |v| v.to_s } end + newparam(:type) do + desc 'sub-flow execution provider, default to `registration-page-form` for top_level=false and does not apply to top_level=true' + defaultto do + if @resource[:top_level] == :false + 'registration-page-form' + else + nil + end + end + munge { |v| v.to_s } + end + + newparam(:top_level, boolean: true) do + desc 'topLevel' + newvalues(:true, :false) + defaultto(:true) + end + + newproperty(:index, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'execution index, only applied to top_level=false, required for top_level=false' + end + + newproperty(:description) do + desc 'description' + end + + newproperty(:requirement) do + desc 'requirement, only applied to top_level=false and defaults to DISABLED' + newvalues('DISABLED', 'ALTERNATIVE', 'REQUIRED', 'CONDITIONAL', + 'disabled', 'alternative', 'required', 'conditional') + defaultto do + if @resource[:top_level] == :false + 'DISABLED' + else + nil + end + end + munge { |v| v.upcase.to_s } + end + def self.title_patterns [ + [ + %r{^((\S+) under (\S+) on (\S+))$}, + [ + [:name], + [:alias], + [:flow_alias], + [:realm], + ], + ], [ %r{^((\S+) on (\S+))$}, [ [:name], [:alias], [:realm], ], ], [ %r{(.*)}, [ [:name], ], ], ] end + autorequire(:keycloak_flow) do + requires = [] + catalog.resources.each do |resource| + next unless resource.class.to_s == 'Puppet::Type::Keycloak_flow' + next if self[:realm] != resource[:realm] + next if self[:top_level] == :true + if self[:flow_alias] == resource[:alias] + requires << resource.name + end + if !resource[:index].nil? && !self[:index].nil? && self[:index] > resource[:index] && self[:flow_alias] == resource[:flow_alias] + requires << resource.name + end + end + requires + end + + autorequire(:keycloak_flow_execution) do + requires = [] + catalog.resources.each do |resource| + next unless resource.class.to_s == 'Puppet::Type::Keycloak_flow_execution' + next if self[:realm] != resource[:realm] + next if self[:top_level] == :true + if self[:flow_alias] == resource[:flow_alias] && !self[:index].nil? && !resource[:index].nil? && self[:index] > resource[:index] + requires << resource.name + end + end + requires + end + validate do if self[:realm].nil? raise "Keycloak_flow[#{self[:name]}] must have a realm defined" end + if self[:ensure] == :present + if self[:top_level] == :false && self[:index].nil? + raise "Keycloak_flow[#{self[:name]}] index is required when top_level is false" + end + if self[:top_level] == :false && self[:flow_alias].nil? + raise "Keycloak_flow[#{self[:name]}] flow_alias is required when top_level is false" + end + end end end diff --git a/lib/puppet/type/keycloak_flow_execution.rb b/lib/puppet/type/keycloak_flow_execution.rb new file mode 100644 index 0000000..22b670d --- /dev/null +++ b/lib/puppet/type/keycloak_flow_execution.rb @@ -0,0 +1,191 @@ +require_relative '../../puppet_x/keycloak/type' +require_relative '../../puppet_x/keycloak/array_property' +require_relative '../../puppet_x/keycloak/integer_property' + +Puppet::Type.newtype(:keycloak_flow_execution) do + desc <<-DESC +Manage a Keycloak flow +@example Add an execution to a flow + keycloak_flow_execution { 'auth-cookie under browser-with-duo on test': + ensure => 'present', + configurable => false, + display_name => 'Cookie', + index => 0, + requirement => 'ALTERNATIVE', + } + +@example Add an execution to a execution flow that is one level deeper than top level + 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', + } + +@example Add an execution with a configuration + 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, + } + +**Autorequires** +* `keycloak_realm` defined for `realm` parameter +* `keycloak_flow` of value defined for `flow_alias` +* `keycloak_flow` if they share same `flow_alias` value and the other resource `index` is lower +* `keycloak_flow_execution` if `flow_alias` is the same and other `index` is lower + DESC + + extend PuppetX::Keycloak::Type + add_autorequires + + ensurable + + newparam(:name, namevar: true) do + desc 'The flow execution name' + end + + newparam(:id) do + desc 'read-only Id' + end + + newparam(:provider_id, namevar: true) do + desc 'provider' + munge { |v| v.to_s } + end + + newparam(:flow_alias, namevar: true) do + desc 'flowAlias' + end + + newparam(:realm, namevar: true) do + desc 'realm' + end + + newparam(:display_name) do + desc 'displayName' + end + + newproperty(:index, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'execution index' + munge { |v| v.to_i } + end + + newproperty(:configurable, boolean: true) do + desc 'configurable' + newvalues(:true, :false) + end + + newproperty(:requirement) do + desc 'requirement' + newvalues('DISABLED', 'ALTERNATIVE', 'REQUIRED', 'CONDITIONAL', + 'disabled', 'alternative', 'required', 'conditional') + defaultto('DISABLED') + munge { |v| v.upcase.to_s } + end + + newparam(:alias) do + desc 'alias' + end + + newproperty(:config) do + desc 'execution config' + validate do |value| + raise Puppet::Error, 'config must be a Hash' unless value.is_a?(Hash) + end + def change_to_s(currentvalue, _newvalue) + if currentvalue == :absent + 'created config' + else + 'changed config' + end + end + + def is_to_s(_currentvalue) # rubocop:disable Style/PredicateName + '[old config redacted]' + end + + def should_to_s(_newvalue) + '[new config redacted]' + end + end + + newparam(:config_id) do + desc 'read-only config ID' + end + + def self.title_patterns + [ + [ + %r{^((\S+) under (\S+) on (\S+))$}, + [ + [:name], + [:provider_id], + [:flow_alias], + [:realm], + ], + ], + [ + %r{(.*)}, + [ + [:name], + ], + ], + ] + end + + autorequire(:keycloak_flow) do + requires = [] + catalog.resources.each do |resource| + next unless resource.class.to_s == 'Puppet::Type::Keycloak_flow' + next if self[:realm] != resource[:realm] + if self[:flow_alias] == resource[:alias] + requires << resource.name + end + if !resource[:index].nil? && !self[:index].nil? && self[:index] > resource[:index] && self[:flow_alias] == resource[:flow_alias] + requires << resource.name + end + end + requires + end + + autorequire(:keycloak_flow_execution) do + requires = [] + catalog.resources.each do |resource| + next unless resource.class.to_s == 'Puppet::Type::Keycloak_flow_execution' + next if self[:realm] != resource[:realm] + if self[:flow_alias] == resource[:flow_alias] && !resource[:index].nil? && !self[:index].nil? && self[:index] > resource[:index] + requires << resource.name + end + end + requires + end + + validate do + if self[:realm].nil? + raise "Keycloak_flow_execution[#{self[:name]}] must have a realm defined" + end + if self[:ensure] == :present + if self[:index].nil? + raise "Keycloak_flow_execution[#{self[:name]}] index is required" + end + if self[:flow_alias].nil? + raise "Keycloak_flow_execution[#{self[:name]}] flow_alias is required" + end + if self[:provider_id].nil? + raise "Keycloak_flow_execution[#{self[:name]}] provider_id is required" + end + end + end +end diff --git a/lib/puppet/type/keycloak_resource_validator.rb b/lib/puppet/type/keycloak_resource_validator.rb new file mode 100644 index 0000000..e76a680 --- /dev/null +++ b/lib/puppet/type/keycloak_resource_validator.rb @@ -0,0 +1,55 @@ +require_relative '../../puppet_x/keycloak/integer_property' + +Puppet::Type.newtype(:keycloak_resource_validator) do + desc <<-DESC +Verify that a specific Keycloak resource is available + DESC + + ensurable + + newparam(:name, namevar: true) do + desc 'An arbitrary name used as the identity of the resource.' + end + + newparam(:test_url) do + desc 'URL to use for testing if the Keycloak database is up' + end + + newparam(:test_key) do + desc 'Key to lookup' + end + + newparam(:test_value) do + desc 'Value to lookup' + end + + newparam(:realm) do + desc 'Realm to query' + end + + newparam(:timeout) do + desc 'The max number of seconds that the validator should wait before giving up and deciding that keycloak is not running; defaults to 15 seconds.' + defaultto 30 + + validate do |value| + # This will raise an error if the string is not convertible to an integer + Integer(value) + end + + munge do |value| + Integer(value) + end + end + + validate do + if self[:test_url].nil? + raise "Keycloak_resource_validator[#{self[:name]}] test_url is required" + end + if self[:test_key].nil? + raise "Keycloak_resource_validator[#{self[:name]}] test_key is required" + end + if self[:test_value].nil? + raise "Keycloak_resource_validator[#{self[:name]}] test_value is required" + end + end +end diff --git a/lib/puppet_x/keycloak/integer_property.rb b/lib/puppet_x/keycloak/integer_property.rb new file mode 100644 index 0000000..77488cf --- /dev/null +++ b/lib/puppet_x/keycloak/integer_property.rb @@ -0,0 +1,13 @@ +require_relative './type' + +# Class to share among integer properties +class PuppetX::Keycloak::IntegerProperty < Puppet::Property + validate do |value| + unless value.to_s =~ %r{^[-]?\d+$} || value.to_s == 'absent' + raise ArgumentError, "#{name} should be an Integer" + end + end + munge do |value| + (value.to_s == 'absent') ? :absent : value.to_i + end +end diff --git a/manifests/spi_deployment.pp b/manifests/spi_deployment.pp index 740ce51..26c8875 100644 --- a/manifests/spi_deployment.pp +++ b/manifests/spi_deployment.pp @@ -1,73 +1,106 @@ # @summary Manage Keycloak SPI deployment # -# @example +# @example Add Duo SPI # keycloak::spi_deployment { 'duo-spi': # ensure => 'present', # deployed_name => 'keycloak-duo-spi-jar-with-dependencies.jar', # source => 'file:///path/to/source/keycloak-duo-spi-jar-with-dependencies.jar', # } # +# @example Add Duo SPI and check API for existance of resources before going onto dependenct resources +# keycloak::spi_deployment { 'duo-spi': +# deployed_name => 'keycloak-duo-spi-jar-with-dependencies.jar', +# source => 'file:///path/to/source/keycloak-duo-spi-jar-with-dependencies.jar', +# test_url => 'authentication/authenticator-providers', +# test_key => 'id', +# test_value => 'duo-mfa-authenticator', +# test_realm => 'test', +# before => Keycloak_flow_execution['duo-mfa-authenticator under form-browser-with-duo on test'], +# } +# # @param ensure # State of the deployment # @param deployed_name # Name of the file to be deployed. Defaults to `$name`. # @param source # Source of the deployment, supports 'file://', 'puppet://', 'https://' or 'http://' +# @param test_url +# URL to test for existance of resources created by this SPI +# @param test_key +# Key of resource when testing for resource created by this SPI +# @param test_value +# Value of the `test_key` when testing for resources created by this SPI +# @param test_realm +# Realm to query when looking for resources created by this SPI # define keycloak::spi_deployment ( Variant[Stdlib::Filesource, Stdlib::HTTPSUrl] $source, Enum['present', 'absent'] $ensure = 'present', String[1] $deployed_name = $name, + Optional[String] $test_url = undef, + Optional[String] $test_key = undef, + Optional[String] $test_value = undef, + Optional[String] $test_realm = undef, ) { include keycloak $dir = "${keycloak::install_base}/standalone/deployments" $basename = basename($source) $dest = "${dir}/${deployed_name}" $tmp = "${keycloak::install_base}/tmp/${basename}" $dodeploy = "${dest}.dodeploy" $deployed = "${dest}.deployed" if $ensure == 'present' { if $source =~ Stdlib::HTTPUrl or $source =~ Stdlib::HTTPSUrl { $_source = $tmp archive { $name: ensure => 'present', extract => false, path => $tmp, source => $source, creates => $tmp, cleanup => false, user => $keycloak::user, group => $keycloak::group, require => File["${keycloak::install_base}/tmp"], before => File[$dest], } } else { $_source = $source } file { $dest: ensure => 'file', source => $_source, owner => $keycloak::user, group => $keycloak::group, mode => '0644', require => Class['keycloak::install'], notify => Exec["${name}-dodeploy"], } exec { "${name}-dodeploy": path => '/usr/bin:/bin:/usr/sbin:/sbin', command => "touch ${dodeploy}", refreshonly => true, user => $keycloak::user, group => $keycloak::group, } + + if $test_url and $test_key and $test_value { + keycloak_resource_validator { $name: + test_url => $test_url, + test_key => $test_key, + test_value => $test_value, + realm => $test_realm, + require => Exec["${name}-dodeploy"], + } + } } if $ensure == 'absent' { file { $deployed: ensure => 'absent', } } } diff --git a/spec/acceptance/9_flow_spec.rb b/spec/acceptance/9_flow_spec.rb index 9f4168a..1d464a8 100644 --- a/spec/acceptance/9_flow_spec.rb +++ b/spec/acceptance/9_flow_spec.rb @@ -1,80 +1,220 @@ 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', + before => Keycloak_flow_execution['duo-mfa-authenticator under form-browser-with-duo on test'], + } keycloak_realm { 'test': ensure => 'present' } keycloak_flow { 'browser-with-duo on test': ensure => 'present', } + 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 { '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, + } + 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['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) + 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', + before => Keycloak_flow_execution['duo-mfa-authenticator under form-browser-with-duo 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 { '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_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 diff --git a/spec/fixtures/keycloak-duo-spi-jar-with-dependencies.jar b/spec/fixtures/keycloak-duo-spi-jar-with-dependencies.jar new file mode 100644 index 0000000..115fa04 Binary files /dev/null and b/spec/fixtures/keycloak-duo-spi-jar-with-dependencies.jar differ diff --git a/spec/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out b/spec/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out new file mode 100644 index 0000000..0fdebb8 --- /dev/null +++ b/spec/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out @@ -0,0 +1,49 @@ +[ { + "id" : "7df18dd4-ab97-4373-9f69-18e002e83935", + "requirement" : "ALTERNATIVE", + "displayName" : "Cookie", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED" ], + "configurable" : false, + "providerId" : "auth-cookie", + "level" : 0, + "index" : 0 +}, { + "id" : "d8c6a9db-bbf9-405f-bb0e-bc15c8f2b933", + "requirement" : "ALTERNATIVE", + "displayName" : "Identity Provider Redirector", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED" ], + "configurable" : true, + "providerId" : "identity-provider-redirector", + "level" : 0, + "index" : 1 +}, { + "id" : "32399b56-b350-4942-b6a9-46a783c3f692", + "requirement" : "ALTERNATIVE", + "displayName" : "form-browser-with-duo", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED", "CONDITIONAL" ], + "configurable" : false, + "authenticationFlow" : true, + "flowId" : "53751618-6a49-4682-b4e8-624f170b8507", + "level" : 0, + "index" : 2 +}, { + "id" : "b6332e87-09f5-48c4-ac90-6f280458adf9", + "requirement" : "REQUIRED", + "displayName" : "Username Password Form", + "requirementChoices" : [ "REQUIRED" ], + "configurable" : false, + "providerId" : "auth-username-password-form", + "level" : 1, + "index" : 0 +}, { + "id" : "e3afaefe-acba-406e-ad5d-3f270c8ab4ce", + "requirement" : "REQUIRED", + "displayName" : "Duo MFA", + "alias" : "Duo", + "requirementChoices" : [ "REQUIRED", "DISABLED" ], + "configurable" : true, + "providerId" : "duo-mfa-authenticator", + "authenticationConfig" : "be93a426-077f-4235-9686-677ff0706bf8", + "level" : 1, + "index" : 1 +} ] diff --git a/spec/fixtures/unit/puppet/provider/keycloak_flow_execution/kcadm/get-executions.out b/spec/fixtures/unit/puppet/provider/keycloak_flow_execution/kcadm/get-executions.out new file mode 100644 index 0000000..0fdebb8 --- /dev/null +++ b/spec/fixtures/unit/puppet/provider/keycloak_flow_execution/kcadm/get-executions.out @@ -0,0 +1,49 @@ +[ { + "id" : "7df18dd4-ab97-4373-9f69-18e002e83935", + "requirement" : "ALTERNATIVE", + "displayName" : "Cookie", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED" ], + "configurable" : false, + "providerId" : "auth-cookie", + "level" : 0, + "index" : 0 +}, { + "id" : "d8c6a9db-bbf9-405f-bb0e-bc15c8f2b933", + "requirement" : "ALTERNATIVE", + "displayName" : "Identity Provider Redirector", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED" ], + "configurable" : true, + "providerId" : "identity-provider-redirector", + "level" : 0, + "index" : 1 +}, { + "id" : "32399b56-b350-4942-b6a9-46a783c3f692", + "requirement" : "ALTERNATIVE", + "displayName" : "form-browser-with-duo", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED", "CONDITIONAL" ], + "configurable" : false, + "authenticationFlow" : true, + "flowId" : "53751618-6a49-4682-b4e8-624f170b8507", + "level" : 0, + "index" : 2 +}, { + "id" : "b6332e87-09f5-48c4-ac90-6f280458adf9", + "requirement" : "REQUIRED", + "displayName" : "Username Password Form", + "requirementChoices" : [ "REQUIRED" ], + "configurable" : false, + "providerId" : "auth-username-password-form", + "level" : 1, + "index" : 0 +}, { + "id" : "e3afaefe-acba-406e-ad5d-3f270c8ab4ce", + "requirement" : "REQUIRED", + "displayName" : "Duo MFA", + "alias" : "Duo", + "requirementChoices" : [ "REQUIRED", "DISABLED" ], + "configurable" : true, + "providerId" : "duo-mfa-authenticator", + "authenticationConfig" : "be93a426-077f-4235-9686-677ff0706bf8", + "level" : 1, + "index" : 1 +} ] diff --git a/spec/fixtures/unit/puppet/provider/keycloak_flow_execution/kcadm/get-test.out b/spec/fixtures/unit/puppet/provider/keycloak_flow_execution/kcadm/get-test.out new file mode 100644 index 0000000..916507f --- /dev/null +++ b/spec/fixtures/unit/puppet/provider/keycloak_flow_execution/kcadm/get-test.out @@ -0,0 +1,201 @@ +[ { + "id" : "011e993f-71b4-4bd5-a620-618dc5f6b9fd", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "REQUIRED", + "priority" : 20, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] +}, { + "id" : "1ce9d171-49d6-4374-a52b-62294e75ed47", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-spnego", + "requirement" : "DISABLED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "identity-provider-redirector", + "requirement" : "ALTERNATIVE", + "priority" : 25, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "forms", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] +}, { + "id" : "4f46ecb5-37d6-43b8-af67-5297ee3c2160", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-credential-email", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-password", + "requirement" : "REQUIRED", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "CONDITIONAL", + "priority" : 40, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] +}, { + "id" : "6c487494-c218-4036-83af-19c4f37a0ef0", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-jwt", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-secret-jwt", + "requirement" : "ALTERNATIVE", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-x509", + "requirement" : "ALTERNATIVE", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] +}, { + "id" : "7c4cfe8f-0ee4-483c-9dc9-8af0532e0ae2", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "requirement" : "REQUIRED", + "priority" : 10, + "flowAlias" : "registration form", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] +}, { + "id" : "bd31bebe-667f-49ac-844a-fcaf719756d8", + "alias" : "http challenge", + "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "no-cookie-redirect", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "REQUIRED", + "priority" : 20, + "flowAlias" : "Authentication Options", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] +}, { + "id" : "browser-with-duo-osc", + "alias" : "browser-with-duo", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : false, + "authenticationExecutions" : [ ] +}, { + "id" : "d5521ad4-f5af-4ac8-842f-ce401364adf6", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] +}, { + "id" : "e886bcf0-7915-46ff-b62f-ce61b25f7dfe", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-password", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "CONDITIONAL", + "priority" : 30, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] +} ] diff --git a/spec/spec_helper_acceptance_setup.rb b/spec/spec_helper_acceptance_setup.rb index e0f7bba..71ed1de 100644 --- a/spec/spec_helper_acceptance_setup.rb +++ b/spec/spec_helper_acceptance_setup.rb @@ -1,34 +1,37 @@ RSpec.configure do |c| c.add_setting :keycloak_version c.keycloak_version = (ENV['BEAKER_keycloak_version'] || '8.0.1') c.add_setting :keycloak_full c.keycloak_full = (ENV['BEAKER_keycloak_full'] == 'true' || ENV['BEAKER_keycloak_full'] == 'yes') end +proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) +scp_to(hosts, File.join(proj_root, 'spec/fixtures/keycloak-duo-spi-jar-with-dependencies.jar'), '/tmp/keycloak-duo-spi-jar-with-dependencies.jar') + hiera_yaml = <<-EOS --- version: 5 defaults: datadir: data data_hash: yaml_data hierarchy: - name: 'os family major release' path: "os/%{facts.os.family}/%{facts.os.release.major}.yaml" - name: "Common" path: "common.yaml" EOS # Hack until released: https://github.com/puppetlabs/puppetlabs-mysql/pull/1264 debian10_yaml = <<-EOS mysql::bindings::java_package_name: libmariadb-java EOS common_yaml = <<-EOS --- keycloak::version: '#{RSpec.configuration.keycloak_version}' postgresql::globals::service_status: 'service postgresql status' EOS create_remote_file(hosts, '/etc/puppetlabs/puppet/hiera.yaml', hiera_yaml) on hosts, 'mkdir -p /etc/puppetlabs/puppet/data' create_remote_file(hosts, '/etc/puppetlabs/puppet/data/common.yaml', common_yaml) on hosts, 'mkdir -p /etc/puppetlabs/puppet/data/os/Debian' create_remote_file(hosts, '/etc/puppetlabs/puppet/data/os/Debian/10.yaml', debian10_yaml) diff --git a/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb b/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb index 464eb7e..9c83151 100644 --- a/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb +++ b/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb @@ -1,80 +1,147 @@ require 'spec_helper' describe Puppet::Type.type(:keycloak_flow).provider(:kcadm) do let(:type) do Puppet::Type.type(:keycloak_flow) end let(:resource) do type.new(name: 'foo', realm: 'test') end describe 'self.instances' do it 'creates instances' do allow(described_class).to receive(:realms).and_return(['test']) allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test').and_return(my_fixture_read('get-test.out')) - expect(described_class.instances.length).to eq(9) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows/browser-with-duo/executions', 'test').and_return(my_fixture_read('get-executions.out')) + expect(described_class.instances.length).to eq(2) end it 'returns the resource for a flow' do allow(described_class).to receive(:realms).and_return(['test']) allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test').and_return(my_fixture_read('get-test.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows/browser-with-duo/executions', 'test').and_return(my_fixture_read('get-executions.out')) property_hash = described_class.instances[0].instance_variable_get('@property_hash') - expect(property_hash[:name]).to eq('first broker login on test') + expect(property_hash[:name]).to eq('browser-with-duo on test') end end # describe 'self.prefetch' do # let(:instances) do # all_realms.map { |f| described_class.new(f) } # end # let(:resources) do # all_realms.each_with_object({}) do |f, h| # h[f[:name]] = type.new(f.reject {|k,v| v.nil?}) # end # end # # before(:each) do # allow(described_class).to receive(:instances).and_return(instances) # end # # it 'should prefetch' do # resources.keys.each do |r| # expect(resources[r]).to receive(:provider=).with(described_class) # end # described_class.prefetch(resources) # end # end describe 'create' do - it 'creates a realm' do + it 'creates a flow' do temp = Tempfile.new('keycloak_flow') allow(Tempfile).to receive(:new).with('keycloak_flow').and_return(temp) - expect(resource.provider).to receive(:kcadm).with('create', 'authentication/flows', 'test', temp.path) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/flows', 'test', temp.path, nil, true) + resource.provider.create + property_hash = resource.provider.instance_variable_get('@property_hash') + expect(property_hash[:ensure]).to eq(:present) + end + it 'creates a flow and updates requirement' do + resource[:top_level] = false + resource[:requirement] = 'ALTERNATIVE' + resource[:flow_alias] = 'browser-with-duo' + temp = Tempfile.new('keycloak_flow') + tempu = Tempfile.new('keycloak_flow_execution') + allow(Tempfile).to receive(:new).with('keycloak_flow').and_return(temp) + allow(Tempfile).to receive(:new).with('keycloak_flow_execution').and_return(tempu) + expect(resource.provider).to receive(:kcadm).with('get', 'authentication/flows/browser-with-duo/executions', 'test').and_return(my_fixture_read('get-executions.out')) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/flows/browser-with-duo/executions/flow', 'test', temp.path, nil, true) \ + .and_return('53751618-6a49-4682-b4e8-624f170b8507') + expect(resource.provider).to receive(:kcadm).with('update', 'authentication/flows/browser-with-duo/executions', 'test', tempu.path, nil, true) resource.provider.create property_hash = resource.provider.instance_variable_get('@property_hash') expect(property_hash[:ensure]).to eq(:present) end end describe 'destroy' do - it 'deletes a realm' do + it 'deletes a flow' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) expect(resource.provider).to receive(:kcadm).with('delete', 'authentication/flows/foo-test', 'test') resource.provider.destroy property_hash = resource.provider.instance_variable_get('@property_hash') expect(property_hash).to eq({}) end + it 'deletes a flow that is not top level' do + allow(resource.provider).to receive(:id).and_return('uuid') + resource[:top_level] = false + expect(resource.provider).to receive(:kcadm).with('delete', 'authentication/executions/uuid', 'test') + resource.provider.destroy + property_hash = resource.provider.instance_variable_get('@property_hash') + expect(property_hash).to eq({}) + end end describe 'flush' do - it 'updates a realm' do + it 'updates a flow' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) temp = Tempfile.new('keycloak_flow') allow(Tempfile).to receive(:new).with('keycloak_flow').and_return(temp) - expect(resource.provider).to receive(:kcadm).with('update', 'authentication/flows/foo-test', 'test', temp.path) + expect(resource.provider).to receive(:kcadm).with('update', 'authentication/flows/foo-test', 'test', temp.path, nil, true) resource.provider.description = 'foobar' resource.provider.flush end + it 'updates a execution requirement' do + resource[:flow_alias] = 'browser-with-duo' + resource[:top_level] = false + temp = Tempfile.new('keycloak_flow') + allow(Tempfile).to receive(:new).with('keycloak_flow').and_return(temp) + expect(resource.provider).to receive(:kcadm).with('update', 'authentication/flows/browser-with-duo/executions', 'test', temp.path, nil, true) + resource.provider.requirement = 'ALTERNATIVE' + resource.provider.flush + end + it 'lowers priority twice' do + resource[:top_level] = false + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(0) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/lower-priority', 'test').twice + resource.provider.index = 2 + resource.provider.flush + end + it 'lowers priority once' do + resource[:top_level] = false + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(0) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/lower-priority', 'test').once + resource.provider.index = 1 + resource.provider.flush + end + it 'raise priority twice' do + resource[:top_level] = false + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(2) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/raise-priority', 'test').twice + resource.provider.index = 0 + resource.provider.flush + end + it 'raise priority once' do + resource[:top_level] = false + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(1) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/raise-priority', 'test').once + resource.provider.index = 0 + resource.provider.flush + end end end diff --git a/spec/unit/puppet/provider/keycloak_flow_execution/kcadm_spec.rb b/spec/unit/puppet/provider/keycloak_flow_execution/kcadm_spec.rb new file mode 100644 index 0000000..4962a10 --- /dev/null +++ b/spec/unit/puppet/provider/keycloak_flow_execution/kcadm_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe Puppet::Type.type(:keycloak_flow_execution).provider(:kcadm) do + let(:type) do + Puppet::Type.type(:keycloak_flow_execution) + end + let(:resource) do + type.new(name: 'foo', + realm: 'test', + flow_alias: 'browser-with-duo', + provider_id: 'auth-username-password-form', + index: 0) + end + + describe 'self.instances' do + it 'creates instances' do + allow(described_class).to receive(:realms).and_return(['test']) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test').and_return(my_fixture_read('get-test.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows/browser-with-duo/executions', 'test').and_return(my_fixture_read('get-executions.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/config/be93a426-077f-4235-9686-677ff0706bf8', 'test').and_return('{}') + expect(described_class.instances.length).to eq(4) + end + + it 'returns the resource for a flow' do + allow(described_class).to receive(:realms).and_return(['test']) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test').and_return(my_fixture_read('get-test.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows/browser-with-duo/executions', 'test').and_return(my_fixture_read('get-executions.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/config/be93a426-077f-4235-9686-677ff0706bf8', 'test').and_return('{}') + property_hash = described_class.instances[0].instance_variable_get('@property_hash') + expect(property_hash[:name]).to eq('auth-cookie under browser-with-duo on test') + end + end + # describe 'self.prefetch' do + # let(:instances) do + # all_realms.map { |f| described_class.new(f) } + # end + # let(:resources) do + # all_realms.each_with_object({}) do |f, h| + # h[f[:name]] = type.new(f.reject {|k,v| v.nil?}) + # end + # end + # + # before(:each) do + # allow(described_class).to receive(:instances).and_return(instances) + # end + # + # it 'should prefetch' do + # resources.keys.each do |r| + # expect(resources[r]).to receive(:provider=).with(described_class) + # end + # described_class.prefetch(resources) + # end + # end + describe 'create' do + it 'creates a flow execution' do + temp = Tempfile.new('keycloak_flow_execution') + allow(Tempfile).to receive(:new).with('keycloak_flow_execution').and_return(temp) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/flows/browser-with-duo/executions/execution', 'test', temp.path, nil, true).and_return('uuid') + resource.provider.create + property_hash = resource.provider.instance_variable_get('@property_hash') + expect(property_hash[:ensure]).to eq(:present) + end + it 'creates a flow execution and updates requirement' do + resource[:requirement] = 'ALTERNATIVE' + temp = Tempfile.new('keycloak_flow_execution') + tempu = Tempfile.new('keycloak_flow_execution_update') + allow(Tempfile).to receive(:new).with('keycloak_flow_execution').and_return(temp) + allow(Tempfile).to receive(:new).with('keycloak_flow_execution_update').and_return(tempu) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/flows/browser-with-duo/executions/execution', 'test', temp.path, nil, true).and_return('uuid') + expect(resource.provider).to receive(:kcadm).with('update', 'authentication/flows/browser-with-duo/executions', 'test', tempu.path) + resource.provider.create + property_hash = resource.provider.instance_variable_get('@property_hash') + expect(property_hash[:ensure]).to eq(:present) + end + it 'creates a flow execution and adds a config' do + resource[:configurable] = true + resource[:config] = { 'foo' => 'bar' } + temp = Tempfile.new('keycloak_flow_execution') + tempc = Tempfile.new('keycloak_flow_execution_config') + allow(Tempfile).to receive(:new).with('keycloak_flow_execution').and_return(temp) + allow(Tempfile).to receive(:new).with('keycloak_flow_execution_config').and_return(tempc) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/flows/browser-with-duo/executions/execution', 'test', temp.path, nil, true).and_return('uuid') + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/config', 'test', tempc.path) + resource.provider.create + property_hash = resource.provider.instance_variable_get('@property_hash') + expect(property_hash[:ensure]).to eq(:present) + end + end + + describe 'destroy' do + it 'deletes a realm' do + allow(resource.provider).to receive(:id).and_return('uuid') + expect(resource.provider).to receive(:kcadm).with('delete', 'authentication/executions/uuid', 'test') + resource.provider.destroy + property_hash = resource.provider.instance_variable_get('@property_hash') + expect(property_hash).to eq({}) + end + end + + describe 'flush' do + it 'updates a execution requirement' do + temp = Tempfile.new('keycloak_flow_execution') + allow(Tempfile).to receive(:new).with('keycloak_flow_execution').and_return(temp) + expect(resource.provider).to receive(:kcadm).with('update', 'authentication/flows/browser-with-duo/executions', 'test', temp.path, nil, true) + resource.provider.requirement = 'ALTERNATIVE' + resource.provider.flush + end + it 'updates a config' do + allow(resource.provider).to receive(:config_id).and_return('uuid') + temp = Tempfile.new('keycloak_flow_execution_config') + allow(Tempfile).to receive(:new).with('keycloak_flow_execution_config').and_return(temp) + expect(resource.provider).to receive(:kcadm).with('update', 'authentication/config/uuid', 'test', temp.path) + resource.provider.config = { 'foo' => 'bar' } + resource.provider.flush + end + it 'lowers priority twice' do + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(0) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/lower-priority', 'test').twice + resource.provider.index = 2 + resource.provider.flush + end + it 'lowers priority once' do + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(0) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/lower-priority', 'test').once + resource.provider.index = 1 + resource.provider.flush + end + it 'raise priority twice' do + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(2) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/raise-priority', 'test').twice + resource.provider.index = 0 + resource.provider.flush + end + it 'raise priority once' do + allow(resource.provider).to receive(:id).and_return('uuid') + allow(resource.provider).to receive(:current_priority).and_return(1) + expect(resource.provider).to receive(:kcadm).with('create', 'authentication/executions/uuid/raise-priority', 'test').once + resource.provider.index = 0 + resource.provider.flush + end + end +end diff --git a/spec/unit/puppet/type/keycloak_flow_execution_spec.rb b/spec/unit/puppet/type/keycloak_flow_execution_spec.rb new file mode 100644 index 0000000..11e1418 --- /dev/null +++ b/spec/unit/puppet/type/keycloak_flow_execution_spec.rb @@ -0,0 +1,241 @@ +require 'spec_helper' + +describe Puppet::Type.type(:keycloak_flow_execution) do + let(:default_config) do + { + name: 'foo', + realm: 'test', + index: 0, + flow_alias: 'bar', + provider_id: 'auth-username-password-form', + } + end + let(:config) do + default_config + end + let(:resource) do + described_class.new(config) + end + + it 'adds to catalog without raising an error' do + catalog = Puppet::Resource::Catalog.new + expect { + catalog.add_resource resource + }.not_to raise_error + end + + it 'has a name' do + expect(resource[:name]).to eq('foo') + end + + it 'has realm' do + expect(resource[:realm]).to eq('test') + end + + it 'handles componsite name' do + component = described_class.new(name: 'foo under bar on test') + expect(component[:name]).to eq('foo under bar on test') + expect(component[:provider_id]).to eq('foo') + expect(component[:flow_alias]).to eq('bar') + expect(component[:realm]).to eq('test') + end + + defaults = {} + + describe 'basic properties' do + # Test basic properties + [ + :display_name, + :alias, + ].each do |p| + it "should accept a #{p}" do + config[p] = 'foo' + expect(resource[p]).to eq('foo') + end + next unless defaults[p] + it "should have default for #{p}" do + expect(resource[p]).to eq(defaults[p]) + end + end + end + + describe 'boolean properties' do + # Test boolean properties + [ + :configurable, + ].each do |p| + it "should accept true for #{p}" do + config[p] = true + expect(resource[p]).to eq(:true) + end + it "should accept true for #{p} string" do + config[p] = 'true' + expect(resource[p]).to eq(:true) + end + it "should accept false for #{p}" do + config[p] = false + expect(resource[p]).to eq(:false) + end + it "should accept false for #{p} string" do + config[p] = 'false' + expect(resource[p]).to eq(:false) + end + it "should not accept strings for #{p}" do + config[p] = 'foo' + expect { + resource + }.to raise_error(%r{foo}) + end + next unless defaults[p] + it "should have default for #{p}" do + expect(resource[p]).to eq(defaults[p]) + end + end + end + + describe 'integer properties' do + # Integer properties + [ + :index, + ].each do |p| + it "should accept integer for #{p}" do + config[p] = 1 + expect(resource[p]).to eq(1) + end + it "should accept integer string for #{p}" do + config[p] = '1' + expect(resource[p]).to eq(1) + end + it "should not accept non-integer for #{p}" do + config[p] = 'foo' + expect { resource }.to raise_error(%r{Integer}) + end + next unless defaults[p] + it "should have default for #{p}" do + expect(resource[p]).to eq(defaults[p]) + end + end + end + + describe 'requirement' do + it 'defaults to DISABLED for top_level=false' do + expect(resource[:requirement]).to eq('DISABLED') + end + [ + 'DISABLED', 'ALTERNATIVE', 'REQUIRED', 'CONDITIONAL' + ].each do |v| + it "accepts value #{v}" do + config[:requirement] = v + expect(resource[:requirement]).to eq(v) + end + it "accepts lowercase value #{v}" do + config[:requirement] = v.downcase + expect(resource[:requirement]).to eq(v) + end + end + it 'does not accept invalid value' do + config[:requirement] = 'foo' + expect { resource }.to raise_error(%r{foo}) + end + end + + describe 'config' do + it 'accepts hash' do + config[:config] = { 'foo' => 'bar' } + expect(resource[:config]).to eq('foo' => 'bar') + end + it 'requires hash' do + config[:config] = 'foo' + expect { resource }.to raise_error(%r{must be a Hash}) + end + end + + it 'autorequires keycloak_conn_validator' do + keycloak_conn_validator = Puppet::Type.type(:keycloak_conn_validator).new(name: 'keycloak') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_conn_validator + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_conn_validator.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires kcadm-wrapper.sh' do + file = Puppet::Type.type(:file).new(name: 'kcadm-wrapper.sh', path: '/opt/keycloak/bin/kcadm-wrapper.sh') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource file + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(file.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires keycloak_realm' do + keycloak_realm = Puppet::Type.type(:keycloak_realm).new(name: 'test') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_realm + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_realm.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires keycloak_flow of parent flow' do + keycloak_flow = Puppet::Type.type(:keycloak_flow).new(name: 'bar on test') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_flow + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_flow.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires keycloak_flow of lower index' do + config[:index] = 1 + keycloak_flow = Puppet::Type.type(:keycloak_flow).new(name: 'baz under bar on test', index: 0) + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_flow + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_flow.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires keycloak_flow_execution of lower index' do + config[:index] = 1 + keycloak_flow_execution = Puppet::Type.type(:keycloak_flow_execution).new(name: 'baz under bar on test', index: 0) + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_flow_execution + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_flow_execution.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + describe 'validations' do + it 'requires realm' do + config.delete(:realm) + expect { resource }.to raise_error(%r{must have a realm defined}) + end + it 'requires index when present' do + config.delete(:index) + config[:ensure] = 'present' + expect { resource }.to raise_error(%r{index is required}) + end + it 'does not require index for absent' do + config.delete(:index) + config[:ensure] = 'absent' + expect { resource }.not_to raise_error + end + it 'requires flow_alias' do + config.delete(:flow_alias) + config[:ensure] = 'present' + expect { resource }.to raise_error(%r{flow_alias is required}) + end + it 'requires provider_id' do + config.delete(:provider_id) + config[:ensure] = 'present' + expect { resource }.to raise_error(%r{provider_id is required}) + end + end +end diff --git a/spec/unit/puppet/type/keycloak_flow_spec.rb b/spec/unit/puppet/type/keycloak_flow_spec.rb index 012ab37..eccf7fb 100644 --- a/spec/unit/puppet/type/keycloak_flow_spec.rb +++ b/spec/unit/puppet/type/keycloak_flow_spec.rb @@ -1,158 +1,278 @@ require 'spec_helper' describe Puppet::Type.type(:keycloak_flow) do let(:default_config) do { name: 'foo', realm: 'test', + index: 0, + flow_alias: 'bar', } end let(:config) do default_config end let(:resource) do described_class.new(config) end it 'adds to catalog without raising an error' do catalog = Puppet::Resource::Catalog.new expect { catalog.add_resource resource }.not_to raise_error end it 'has a name' do expect(resource[:name]).to eq('foo') end - it 'has client_id default to name' do + it 'has alias default to name' do expect(resource[:alias]).to eq('foo') end - it 'has id default to name' do + it 'has id default to name-realm' do expect(resource[:id]).to eq('foo-test') end it 'has realm' do expect(resource[:realm]).to eq('test') end it 'handles componsite name' do component = described_class.new(name: 'foo on test') expect(component[:name]).to eq('foo on test') expect(component[:alias]).to eq('foo') expect(component[:realm]).to eq('test') end - it 'defaults to provider_id=basic-flow' do - expect(resource[:provider_id]).to eq('basic-flow') - end - - it 'does not allow invalid provider_id' do - config[:provider_id] = 'foo' - expect { - resource - }.to raise_error(%r{foo}) - end - - defaults = {} + defaults = { + top_level: :true, + } describe 'basic properties' do # Test basic properties [ :description, ].each do |p| it "should accept a #{p}" do config[p] = 'foo' expect(resource[p]).to eq('foo') end next unless defaults[p] it "should have default for #{p}" do expect(resource[p]).to eq(defaults[p]) end end end describe 'boolean properties' do # Test boolean properties [ + :top_level, ].each do |p| it "should accept true for #{p}" do config[p] = true expect(resource[p]).to eq(:true) end it "should accept true for #{p} string" do config[p] = 'true' expect(resource[p]).to eq(:true) end it "should accept false for #{p}" do config[p] = false expect(resource[p]).to eq(:false) end it "should accept false for #{p} string" do config[p] = 'false' expect(resource[p]).to eq(:false) end it "should not accept strings for #{p}" do config[p] = 'foo' expect { resource }.to raise_error(%r{foo}) end next unless defaults[p] it "should have default for #{p}" do expect(resource[p]).to eq(defaults[p]) end end end - describe 'array properties' do - # Array properties + describe 'integer properties' do + # Integer properties [ + :index, ].each do |p| - it "should accept array for #{p}" do - config[p] = ['foo', 'bar'] - expect(resource[p]).to eq(['foo', 'bar']) + it "should accept integer for #{p}" do + config[p] = 1 + expect(resource[p]).to eq(1) + end + it "should accept integer string for #{p}" do + config[p] = '1' + expect(resource[p]).to eq(1) + end + it "should not accept non-integer for #{p}" do + config[p] = 'foo' + expect { resource }.to raise_error(%r{Integer}) end next unless defaults[p] it "should have default for #{p}" do expect(resource[p]).to eq(defaults[p]) end end end + describe 'provider_id' do + it 'defaults to provider_id=basic-flow' do + expect(resource[:provider_id]).to eq('basic-flow') + end + + it 'does not allow invalid provider_id' do + config[:provider_id] = 'foo' + expect { + resource + }.to raise_error(%r{foo}) + end + end + + describe 'type' do + it 'defaults to type=nil' do + expect(resource[:type]).to be_nil + end + + it 'sets default when not top level' do + config[:top_level] = false + expect(resource[:type]).to eq('registration-page-form') + end + end + + describe 'requirement' do + it 'defaults to DISABLED for top_level=false' do + config[:top_level] = false + expect(resource[:requirement]).to eq('DISABLED') + end + it 'has no default for top_level=true' do + config[:top_level] = true + expect(resource[:requirement]).to be_nil + end + [ + 'DISABLED', 'ALTERNATIVE', 'REQUIRED', 'CONDITIONAL' + ].each do |v| + it "accepts value #{v}" do + config[:requirement] = v + expect(resource[:requirement]).to eq(v) + end + it "accepts lowercase value #{v}" do + config[:requirement] = v.downcase + expect(resource[:requirement]).to eq(v) + end + end + it 'does not accept invalid value' do + config[:requirement] = 'foo' + expect { resource }.to raise_error(%r{foo}) + end + end + it 'autorequires keycloak_conn_validator' do keycloak_conn_validator = Puppet::Type.type(:keycloak_conn_validator).new(name: 'keycloak') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource keycloak_conn_validator rel = resource.autorequire[0] expect(rel.source.ref).to eq(keycloak_conn_validator.ref) expect(rel.target.ref).to eq(resource.ref) end it 'autorequires kcadm-wrapper.sh' do file = Puppet::Type.type(:file).new(name: 'kcadm-wrapper.sh', path: '/opt/keycloak/bin/kcadm-wrapper.sh') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource file rel = resource.autorequire[0] expect(rel.source.ref).to eq(file.ref) expect(rel.target.ref).to eq(resource.ref) end it 'autorequires keycloak_realm' do keycloak_realm = Puppet::Type.type(:keycloak_realm).new(name: 'test') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource keycloak_realm rel = resource.autorequire[0] expect(rel.source.ref).to eq(keycloak_realm.ref) expect(rel.target.ref).to eq(resource.ref) end - it 'requires realm' do - config.delete(:realm) - expect { resource }.to raise_error(%r{must have a realm defined}) + it 'autorequires keycloak_flow of parent flow' do + config[:top_level] = false + keycloak_flow = Puppet::Type.type(:keycloak_flow).new(name: 'bar on test') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_flow + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_flow.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires keycloak_flow of lower index' do + config[:top_level] = false + config[:index] = 1 + keycloak_flow = Puppet::Type.type(:keycloak_flow).new(name: 'baz under bar on test', index: 0) + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_flow + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_flow.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + it 'autorequires keycloak_flow_execution of lower index' do + config[:top_level] = false + config[:index] = 1 + keycloak_flow_execution = Puppet::Type.type(:keycloak_flow_execution).new(name: 'baz under bar on test', index: 0) + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource keycloak_flow_execution + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(keycloak_flow_execution.ref) + expect(rel.target.ref).to eq(resource.ref) + end + + describe 'validations' do + it 'requires realm' do + config.delete(:realm) + expect { resource }.to raise_error(%r{must have a realm defined}) + end + it 'requires index when present' do + config.delete(:index) + config[:top_level] = false + config[:ensure] = 'present' + expect { resource }.to raise_error(%r{index is required}) + end + it 'does not require index for absent' do + config.delete(:index) + config[:ensure] = 'absent' + expect { resource }.not_to raise_error + end + it 'does not require index for top level' do + config.delete(:index) + config[:ensure] = 'present' + config[:top_level] = true + expect { resource }.not_to raise_error + end + it 'requires flow_alias when top_level is false' do + config.delete(:flow_alias) + config[:top_level] = false + config[:ensure] = 'present' + expect { resource }.to raise_error(%r{flow_alias is required}) + end + it 'does not require flow_alias when top_level' do + config.delete(:flow_alias) + config[:top_level] = true + config[:ensure] = 'present' + expect { resource }.not_to raise_error + end end end