diff --git a/lib/puppet/provider/keycloak_protocol_mapper/kcadm.rb b/lib/puppet/provider/keycloak_protocol_mapper/kcadm.rb index 7224a78..b3a3d5c 100644 --- a/lib/puppet/provider/keycloak_protocol_mapper/kcadm.rb +++ b/lib/puppet/provider/keycloak_protocol_mapper/kcadm.rb @@ -1,238 +1,238 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'keycloak_api')) Puppet::Type.type(:keycloak_protocol_mapper).provide(:kcadm, parent: Puppet::Provider::KeycloakAPI) do desc '' mk_resource_methods def self.attribute_nameformat_map { uri: 'URI Reference', basic: 'Basic', unspecified: 'Unspecified', } end def self.get_attribute_nameformat(v) attribute_nameformat_map[v.to_sym] end def self.get_attribute_nameformat_reverse(v) attribute_nameformat_map.invert[v] end def self.instances protocol_mappers = [] realms.each do |realm| client_scopes_output = kcadm('get', 'client-scopes', realm) client_scope_data = JSON.parse(client_scopes_output) client_scope_data.each do |client_scope| client_scope_id = client_scope['id'] data = client_scope['protocolMappers'] || [] data.each do |d| protocol_mapper = {} protocol_mapper[:ensure] = :present protocol_mapper[:id] = d['id'] protocol_mapper[:realm] = realm protocol_mapper[:client_scope] = client_scope_id protocol_mapper[:resource_name] = d['name'] protocol_mapper[:protocol] = d['protocol'] protocol_mapper[:name] = "#{protocol_mapper[:resource_name]} for #{protocol_mapper[:client_scope]} on #{protocol_mapper[:realm]}" protocol_mapper[:type] = d['protocolMapper'] if ['oidc-usermodel-property-mapper', 'saml-user-property-mapper', 'oidc-usermodel-attribute-mapper'].include?(protocol_mapper[:type]) protocol_mapper[:user_attribute] = d['config']['user.attribute'] end if ['oidc-usermodel-property-mapper', 'oidc-group-membership-mapper', 'oidc-usermodel-attribute-mapper'].include?(protocol_mapper[:type]) protocol_mapper[:claim_name] = d['config']['claim.name'] protocol_mapper[:json_type_label] = d['config']['jsonType.label'] end - if protocol_mapper[:type] == 'oidc-group-membership-mapper' + if ['oidc-group-membership-mapper', 'saml-group-membership-mapper'].include?(protocol_mapper[:type]) protocol_mapper[:full_path] = d['config']['full.path'] end - if ['saml-user-property-mapper', 'saml-javascript-mapper'].include?(protocol_mapper[:type]) + if ['saml-group-membership-mapper', 'saml-user-property-mapper', 'saml-javascript-mapper'].include?(protocol_mapper[:type]) protocol_mapper[:friendly_name] = d['config']['friendly.name'] end if protocol_mapper[:type] == 'saml-javascript-mapper' protocol_mapper[:script] = d['config']['Script'] end if protocol_mapper[:protocol] == 'openid-connect' protocol_mapper[:id_token_claim] = d['config']['id.token.claim'] protocol_mapper[:access_token_claim] = d['config']['access.token.claim'] end unless ['oidc-audience-mapper'].include?(protocol_mapper[:type]) protocol_mapper[:userinfo_token_claim] = d['config']['userinfo.token.claim'] end if protocol_mapper[:type] == 'oidc-audience-mapper' protocol_mapper[:included_client_audience] = d['config']['included.client.audience'] end if protocol_mapper[:protocol] == 'saml' protocol_mapper[:attribute_name] = d['config']['attribute.name'] protocol_mapper[:attribute_nameformat] = get_attribute_nameformat_reverse(d['config']['attribute.nameformat']) end - if ['saml-role-list-mapper', 'saml-javascript-mapper'].include?(protocol_mapper[:type]) + if ['saml-group-membership-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper'].include?(protocol_mapper[:type]) protocol_mapper[:single] = d['config']['single'].to_s.to_sym end protocol_mappers << new(protocol_mapper) end end end protocol_mappers end def self.prefetch(resources) protocol_mappers = instances resources.keys.each do |name| provider = protocol_mappers.find do |c| c.resource_name == resources[name][:resource_name] && c.realm == resources[name][:realm] && c.client_scope == resources[name][:client_scope] end next unless provider resources[name].provider = provider end end def create raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil? raise(Puppet::Error, "Client scope is mandatory for #{resource.type} #{resource.name}") if resource[:client_scope].nil? data = {} data[:id] = resource[:id] || name_uuid(resource[:name]) data[:name] = resource[:resource_name] data[:protocol] = resource[:protocol] data[:protocolMapper] = resource[:type] data[:config] = {} if ['oidc-usermodel-property-mapper', 'saml-user-property-mapper', 'oidc-usermodel-attribute-mapper'].include?(resource[:type]) data[:config][:'user.attribute'] = resource[:user_attribute] if resource[:user_attribute] end if ['oidc-usermodel-property-mapper', 'oidc-group-membership-mapper', 'oidc-usermodel-attribute-mapper'].include?(resource[:type]) data[:config][:'claim.name'] = resource[:claim_name] if resource[:claim_name] data[:config][:'jsonType.label'] = resource[:json_type_label] if resource[:json_type_label] end - if resource[:type] == 'oidc-group-membership-mapper' + if ['oidc-group-membership-mapper', 'saml-group-membership-mapper'].include?(resource[:type]) data[:config][:'full.path'] = resource[:full_path] if resource[:full_path] end - if ['saml-user-property-mapper', 'saml-javascript-mapper'].include?(resource[:type]) + if ['saml-group-membership-mapper', 'saml-user-property-mapper', 'saml-javascript-mapper'].include?(resource[:type]) data[:config][:'friendly.name'] = resource[:friendly_name] if resource[:friendly_name] end if resource[:type] == 'saml-javascript-mapper' data[:config][:Script] = resource[:script] end if resource[:protocol] == 'openid-connect' data[:config][:'id.token.claim'] = resource[:id_token_claim] if resource[:id_token_claim] data[:config][:'access.token.claim'] = resource[:access_token_claim] if resource[:access_token_claim] end unless ['oidc-audience-mapper'].include?(resource[:type]) data[:config][:'userinfo.token.claim'] = resource[:userinfo_token_claim] if resource[:userinfo_token_claim] end if resource[:type] == 'oidc-audience-mapper' data[:config][:'included.client.audience'] = resource[:included_client_audience] if resource[:included_client_audience] end if resource[:protocol] == 'saml' data[:config][:'attribute.name'] = resource[:attribute_name] if resource[:attribute_name] data[:config][:'attribute.nameformat'] = self.class.get_attribute_nameformat(resource[:attribute_nameformat]) if resource[:attribute_nameformat] end - if ['saml-role-list-mapper', 'saml-javascript-mapper'].include?(resource[:type]) + if ['saml-group-membership-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper'].include?(resource[:type]) data[:config][:single] = resource[:single].to_s if resource[:single] end t = Tempfile.new('keycloak_protocol_mapper') t.write(JSON.pretty_generate(data)) t.close Puppet.debug(IO.read(t.path)) begin kcadm('create', "client-scopes/#{resource[:client_scope]}/protocol-mappers/models", resource[:realm], t.path) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm create protocol-mapper failed\nError message: #{e.message}" end @property_hash[:ensure] = :present end def destroy raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil? raise(Puppet::Error, "Client scope is mandatory for #{resource.type} #{resource.name}") if resource[:client_scope].nil? begin kcadm('delete', "client-scopes/#{resource[:client_scope]}/protocol-mappers/models/#{id}", resource[:realm]) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm delete realm 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 flush unless @property_flush.empty? raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil? raise(Puppet::Error, "Client scope is mandatory for #{resource.type} #{resource.name}") if resource[:client_scope].nil? data = {} data[:id] = @property_hash[:id] data[:name] = resource[:resource_name] data[:protocol] = resource[:protocol] data[:protocolMapper] = resource[:type] config = {} if ['oidc-usermodel-property-mapper', 'saml-user-property-mapper', 'oidc-usermodel-attribute-mapper'].include?(resource[:type]) config[:'user.attribute'] = resource[:user_attribute] if resource[:user_attribute] end if ['oidc-usermodel-property-mapper', 'oidc-group-membership-mapper', 'oidc-usermodel-attribute-mapper'].include?(resource[:type]) config[:'claim.name'] = resource[:claim_name] if resource[:claim_name] config[:'jsonType.label'] = resource[:json_type_label] if resource[:json_type_label] end - if resource[:type] == 'oidc-group-membership-mapper' + if ['oidc-group-membership-mapper', 'saml-group-membership-mapper'].include?(resource[:type]) config[:'full.path'] = resource[:full_path] if resource[:full_path] end - if ['saml-user-property-mapper', 'saml-javascript-mapper'].include?(resource[:type]) + if ['saml-group-membership-mapper', 'saml-user-property-mapper', 'saml-javascript-mapper'].include?(resource[:type]) config[:'friendly.name'] = resource[:friendly_name] if resource[:friendly_name] end if resource[:type] == 'saml-javascript-mapper' config[:Script] = resource[:script] end if resource[:protocol] == 'openid-connect' config[:'id.token.claim'] = resource[:id_token_claim] if resource[:id_token_claim] config[:'access.token.claim'] = resource[:access_token_claim] if resource[:access_token_claim] end unless ['oidc-audience-mapper'].include?(resource[:type]) config[:'userinfo.token.claim'] = resource[:userinfo_token_claim] if resource[:userinfo_token_claim] end if resource[:type] == 'oidc-audience-mapper' config[:'included.client.audience'] = resource[:included_client_audience] if resource[:included_client_audience] end if resource[:protocol] == 'saml' config[:'attribute.name'] = resource[:attribute_name] if resource[:attribute_name] config[:'attribute.nameformat'] = self.class.get_attribute_nameformat(resource[:attribute_nameformat]) if resource[:attribute_nameformat] end - if ['saml-role-list-mapper', 'saml-javascript-mapper'].include?(resource[:type]) + if ['saml-group-membership-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper'].include?(resource[:type]) config[:single] = resource[:single].to_s if resource[:single] end data[:config] = config unless config.empty? t = Tempfile.new('keycloak_protocol_mapper') t.write(JSON.pretty_generate(data)) t.close Puppet.debug(IO.read(t.path)) begin kcadm('update', "client-scopes/#{resource[:client_scope]}/protocol-mappers/models/#{id}", resource[:realm], t.path) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update component failed\nError message: #{e.message}" 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/type/keycloak_protocol_mapper.rb b/lib/puppet/type/keycloak_protocol_mapper.rb index f3bc8bb..3564f50 100644 --- a/lib/puppet/type/keycloak_protocol_mapper.rb +++ b/lib/puppet/type/keycloak_protocol_mapper.rb @@ -1,275 +1,276 @@ require_relative '../provider/keycloak_api' require_relative '../../puppet_x/keycloak/type' require_relative '../../puppet_x/keycloak/array_property' Puppet::Type.newtype(:keycloak_protocol_mapper) do desc <<-DESC Manage Keycloak client scope protocol mappers @example Add email protocol mapper to oidc-client client scope in realm test keycloak_protocol_mapper { "email for oidc-clients on test": claim_name => 'email', user_attribute => 'email', } DESC extend PuppetX::Keycloak::Type add_autorequires ensurable newparam(:name, namevar: true) do desc 'The protocol mapper name' end newparam(:id) do desc 'Id.' end newparam(:resource_name, namevar: true) do desc 'The protocol mapper name. Defaults to `name`.' defaultto do @resource[:name] end end newparam(:client_scope, namevar: true) do desc 'client scope' end newparam(:realm, namevar: true) do desc 'realm' end newproperty(:protocol) do desc 'protocol' defaultto('openid-connect') newvalues('openid-connect', 'saml') munge { |v| v } end newparam(:type) do desc <<-DESC protocolMapper. Default is `oidc-usermodel-property-mapper` for `protocol` `openid-connect` and `saml-user-property-mapper` for `protocol` `saml`. DESC newvalues( 'oidc-usermodel-property-mapper', 'oidc-usermodel-attribute-mapper', 'oidc-full-name-mapper', 'oidc-group-membership-mapper', 'oidc-audience-mapper', + 'saml-group-membership-mapper', 'saml-user-property-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper', ) defaultto do if @resource[:protocol] == 'openid-connect' 'oidc-usermodel-property-mapper' elsif @resource[:protocol] == 'saml' 'saml-user-property-mapper' else nil end end munge { |v| v } end newproperty(:user_attribute) do desc 'user.attribute. Default to `resource_name` for `type` `oidc-usermodel-property-mapper` or `saml-user-property-mapper`' defaultto do if ['oidc-usermodel-property-mapper', 'saml-user-property-mapper', 'oidc-usermodel-attribute-mapper'].include?(@resource[:type]) @resource[:resource_name] else nil end end end newproperty(:json_type_label) do desc 'json.type.label. Default to `String` for `type` `oidc-usermodel-property-mapper` and `oidc-group-membership-mapper`.' defaultto do if ['oidc-usermodel-property-mapper', 'oidc-group-membership-mapper', 'oidc-usermodel-attribute-mapper'].include?(@resource[:type]) 'String' else nil end end end newproperty(:full_path, boolean: true) do desc 'full.path. Default to `false` for `type` `oidc-group-membership-mapper`.' newvalues(:true, :false) defaultto do if @resource[:type] == 'oidc-group-membership-mapper' :false else nil end end end newproperty(:friendly_name) do desc 'friendly.name. Default to `resource_name` for `type` `saml-user-property-mapper`.' defaultto do if @resource[:type] == 'saml-user-property-mapper' @resource[:resource_name] else nil end end end newproperty(:attribute_name) do desc 'attribute.name Default to `resource_name` for `type` `saml-user-property-mapper`.' defaultto do if @resource[:type] == 'saml-user-property-mapper' @resource[:resource_name] else nil end end end newproperty(:claim_name) do desc 'claim.name' end newproperty(:id_token_claim, boolean: true) do desc 'id.token.claim. Default to `true` for `protocol` `openid-connect`.' newvalues(:true, :false) defaultto do if @resource['protocol'] == 'openid-connect' :true else nil end end end newproperty(:access_token_claim, boolean: true) do desc 'access.token.claim. Default to `true` for `protocol` `openid-connect`.' newvalues(:true, :false) defaultto do if @resource['protocol'] == 'openid-connect' :true else nil end end end newproperty(:userinfo_token_claim, boolean: true) do desc 'userinfo.token.claim. Default to `true` for `protocol` `openid-connect` except `type` of `oidc-audience-mapper`.' newvalues(:true, :false) defaultto do if @resource['protocol'] == 'openid-connect' && @resource['type'] != 'oidc-audience-mapper' :true else nil end end end newproperty(:attribute_nameformat) do desc 'attribute.nameformat' validate do |v| unless [:basic, :uri, :unspecified].include?(v.downcase.to_sym) raise ArgumentError, 'attribute_nameformat must be one of basic, uri, or unspecified' end end munge do |v| v.downcase.to_sym end end newproperty(:single, boolean: true) do desc 'single. Default to `false` for `type` `saml-role-list-mapper` or `saml-javascript-mapper`.' newvalues(:true, :false) defaultto do if ['saml-role-list-mapper', 'saml-javascript-mapper'].include?(@resource['type']) :false else nil end end end newproperty(:script) do desc <<-EOS Script, only valid for `type` of `saml-javascript-mapper`' Array values will be joined with newlines. Strings will be kept unchanged. EOS end newproperty(:included_client_audience) do desc 'included.client.audience Required for `type` of `oidc-audience-mapper`' end autorequire(:keycloak_client_scope) do requires = [] catalog.resources.each do |resource| next unless resource.class.to_s == 'Puppet::Type::Keycloak_client_scope' if resource[:resource_name] == self[:client_scope] requires << resource.name end end requires end def self.title_patterns [ [ %r{^((.+) for (\S+) on (\S+))$}, [ [:name], [:resource_name], [:client_scope], [:realm], ], ], [ %r{(.*)}, [ [:name], ], ], ] end validate do openid_connect_types = [ 'oidc-usermodel-property-mapper', 'oidc-full-name-mapper', 'oidc-group-membership-mapper', 'oidc-audience-mapper', 'oidc-usermodel-attribute-mapper', ] if self[:protocol] == 'openid-connect' && !openid_connect_types.include?(self[:type]) raise Puppet::Error, "type #{self[:type]} is not valid for protocol openid-connect" end - if self[:protocol] == 'saml' && !['saml-user-property-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper'].include?(self[:type]) + if self[:protocol] == 'saml' && !['saml-group-membership-mapper', 'saml-user-property-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper'].include?(self[:type]) raise Puppet::Error, "type #{self[:type]} is not valid for protocol saml" end - if self[:friendly_name] && !['saml-user-property-mapper', 'saml-javascript-mapper'].include?(self[:type]) + if self[:friendly_name] && !['saml-group-membership-mapper', 'saml-user-property-mapper', 'saml-javascript-mapper'].include?(self[:type]) raise Puppet::Error, "friendly_name is not valid for type #{self[:type]}" end if self[:attribute_name] && self[:protocol] != 'saml' raise Puppet::Error, "attribute_name is not valid for type #{self[:type]}" end if self[:attribute_nameformat] && self[:protocol] != 'saml' raise Puppet::Error, "attribute_nameformat is not valid for protocol #{self[:protocol]}" end - if self[:single] && !['saml-role-list-mapper', 'saml-javascript-mapper'].include?(self[:type]) + if self[:single] && !['saml-group-membership-mapper', 'saml-role-list-mapper', 'saml-javascript-mapper'].include?(self[:type]) raise Puppet::Error, "single is not valid for type #{self[:type]}" end if self[:type] == 'saml-javascript-mapper' && self[:script].nil? raise Puppet::Error, 'script is required for saml-javascript-mapper' end if self[:type] == 'oidc-audience-mapper' && self[:included_client_audience].nil? raise Puppet::Error, 'included_client_audience is required for oidc-audience-mapper' end end end