diff --git a/lib/puppet/provider/keycloak_flow/kcadm.rb b/lib/puppet/provider/keycloak_flow/kcadm.rb index 39a2ef9..eec8c15 100644 --- a/lib/puppet/provider/keycloak_flow/kcadm.rb +++ b/lib/puppet/provider/keycloak_flow/kcadm.rb @@ -1,234 +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 + 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[: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[: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/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out b/spec/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out index 0fdebb8..eb647d0 100644 --- a/spec/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out +++ b/spec/fixtures/unit/puppet/provider/keycloak_flow/kcadm/get-executions.out @@ -1,49 +1,68 @@ [ { "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 +}, { + "id" : "5d11e430-a522-469b-8548-285fb0593480", + "requirement" : "CONDITIONAL", + "displayName" : "check-duo", + "requirementChoices" : [ "REQUIRED", "ALTERNATIVE", "DISABLED", "CONDITIONAL" ], + "configurable" : false, + "authenticationFlow" : true, + "flowId" : "a1408b3c-5242-421b-b60f-2dbdceb11cbb", + "level" : 1, + "index" : 2 +}, { + "id" : "93ade6f4-ee01-4dfd-840b-8ee9ef96fdd1", + "requirement" : "DISABLED", + "displayName" : "Condition - user role", + "requirementChoices" : [ "REQUIRED", "DISABLED" ], + "configurable" : true, + "providerId" : "conditional-user-role", + "level" : 2, + "index" : 0 } ] diff --git a/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb b/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb index 9c83151..2d05f7c 100644 --- a/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb +++ b/spec/unit/puppet/provider/keycloak_flow/kcadm_spec.rb @@ -1,147 +1,151 @@ 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')) 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) + expect(described_class.instances.length).to eq(3) 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('browser-with-duo on test') + property_hash = described_class.instances[1].instance_variable_get('@property_hash') + expect(property_hash[:name]).to eq('form-browser-with-duo under browser-with-duo on test') + property_hash = described_class.instances[2].instance_variable_get('@property_hash') + expect(property_hash[:name]).to eq('check-duo under form-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' 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, 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 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 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, 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