From 385ec77438ad74578306d45e4b2a8d2a140b6b58 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Wed, 25 Mar 2026 12:52:29 +0200 Subject: [PATCH 1/8] config: added rspec-its --- Gemfile.lock | 4 ++ keycloak-admin.gemspec | 5 +- lib/keycloak-admin.rb | 1 + ...ient_authz_scope_protocol_mapper_client.rb | 60 +++++++++++++++++++ lib/keycloak-admin/client/realm_client.rb | 4 ++ 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb diff --git a/Gemfile.lock b/Gemfile.lock index c5d6616..da707ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,6 +34,9 @@ GEM rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) + rspec-its (2.0.0) + rspec-core (>= 3.13.0) + rspec-expectations (>= 3.13.0) rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) @@ -46,6 +49,7 @@ DEPENDENCIES byebug (= 12.0.0) keycloak-admin! rspec (= 3.13.2) + rspec-its (= 2.0.0) BUNDLED WITH 2.1.4 diff --git a/keycloak-admin.gemspec b/keycloak-admin.gemspec index 3fc8a44..cbbb208 100644 --- a/keycloak-admin.gemspec +++ b/keycloak-admin.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |spec| spec.add_dependency "http-cookie", "~> 1.0", ">= 1.0.3" spec.add_dependency "rest-client", "~> 2.0" - spec.add_development_dependency "rspec", "3.13.2" - spec.add_development_dependency "byebug", "12.0.0" + spec.add_development_dependency "rspec", "3.13.2" + spec.add_development_dependency "rspec-its", "2.0.0" + spec.add_development_dependency "byebug", "12.0.0" end diff --git a/lib/keycloak-admin.rb b/lib/keycloak-admin.rb index c008c8f..f0f07c1 100644 --- a/lib/keycloak-admin.rb +++ b/lib/keycloak-admin.rb @@ -16,6 +16,7 @@ require_relative "keycloak-admin/client/configurable_token_client" require_relative "keycloak-admin/client/attack_detection_client" require_relative "keycloak-admin/client/client_authz_scope_client" +require_relative "keycloak-admin/client/client_authz_scope_protocol_mapper_client" require_relative "keycloak-admin/client/client_authz_resource_client" require_relative "keycloak-admin/client/client_authz_policy_client" require_relative "keycloak-admin/client/client_authz_permission_client" diff --git a/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb b/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb new file mode 100644 index 0000000..7d01dbb --- /dev/null +++ b/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb @@ -0,0 +1,60 @@ +module KeycloakAdmin + class ClientAuthzScopeProtocolMapperClient < Client + def initialize(configuration, realm_client, client_scope_id) + super(configuration) + + raise ArgumentError.new("realm must be defined") unless realm_client.name_defined? + + @realm_client = realm_client + @client_scope_id = client_scope_id + end + + def list + response = execute_http do + RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).get(headers) + end + + JSON.parse(response).map { |h| ProtocolMapperRepresentation.from_hash(h) } + end + + def get(mapper_id) + response = execute_http do + RestClient::Resource.new(protocol_mappers_url(mapper_id), @configuration.rest_client_options).get(headers) + end + + ProtocolMapperRepresentation.from_hash(JSON.parse(response)) + end + + def create!(mapper_representation) + response = execute_http do + RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).post( + create_payload(mapper_representation), headers + ) + end + + ProtocolMapperRepresentation.from_hash(JSON.parse(response)) + end + + def update(mapper_representation) + execute_http do + RestClient::Resource.new(protocol_mappers_url(mapper_representation.id), @configuration.rest_client_options).put( + create_payload(mapper_representation), headers + ) + end + end + + def delete(mapper_id) + execute_http do + RestClient::Resource.new(protocol_mappers_url(mapper_id), @configuration.rest_client_options).delete(headers) + end + + true + end + + def protocol_mappers_url(mapper_id = nil) + base = "#{@realm_client.realm_admin_url}/client-scopes/#{@client_scope_id}/protocol-mappers/models" + + mapper_id ? "#{base}/#{mapper_id}" : base + end + end +end diff --git a/lib/keycloak-admin/client/realm_client.rb b/lib/keycloak-admin/client/realm_client.rb index 174d787..fa84ded 100644 --- a/lib/keycloak-admin/client/realm_client.rb +++ b/lib/keycloak-admin/client/realm_client.rb @@ -103,6 +103,10 @@ def user(user_id) UserResource.new(@configuration, self, user_id) end + def client_authz_scope_protocol_mappers(client_scope_id) + ClientAuthzScopeProtocolMapperClient.new(@configuration, self, client_scope_id) + end + def authz_scopes(client_id, resource_id = nil) ClientAuthzScopeClient.new(@configuration, self, client_id, resource_id) end From 5f1a9557a87d97a0c6a6f476316a628721b0db72 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Wed, 25 Mar 2026 12:52:39 +0200 Subject: [PATCH 2/8] config: required rspec-its --- spec/spec_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 540d40b..304ab1f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ require_relative "../lib/keycloak-admin" require "byebug" +require "rspec/its" def configure KeycloakAdmin.configure do |config| From 1c60cf9356fd66207929c793ab6f67beac0c8c44 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Wed, 25 Mar 2026 12:53:05 +0200 Subject: [PATCH 3/8] test: client_authz_scope_protocol_mapper_client --- ...authz_scope_protocol_mapper_client_spec.rb | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 spec/client/client_authz_scope_protocol_mapper_client_spec.rb diff --git a/spec/client/client_authz_scope_protocol_mapper_client_spec.rb b/spec/client/client_authz_scope_protocol_mapper_client_spec.rb new file mode 100644 index 0000000..612c229 --- /dev/null +++ b/spec/client/client_authz_scope_protocol_mapper_client_spec.rb @@ -0,0 +1,197 @@ +RSpec.describe KeycloakAdmin::ClientAuthzScopeProtocolMapperClient do + let(:realm_name) { "valid-realm" } + let(:client_scope_id) { "valid-scope-id" } + let(:mapper_id) { "valid-mapper-id" } + + let(:mapper_json) do + <<~JSON + {"id":"valid-mapper-id","name":"my-claim","protocol":"openid-connect","protocolMapper":"oidc-hardcoded-claim-mapper","config":{"claim.name":"my_claim","claim.value":"bar","access.token.claim":"true"}} + JSON + end + + let(:audience_mapper_json) do + <<~JSON + {"protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","name":"audience-config-rvw-123","config":{"included.client.audience":"","included.custom.audience":"https://api.example.com","id.token.claim":"false","access.token.claim":"true","lightweight.claim":"false","introspection.token.claim":"true"}} + JSON + end + + describe "#initialize" do + context "when realm_name is defined" do + it "does not raise any error" do + expect { KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) }.to_not raise_error + end + end + + context "when realm_name is not defined" do + it "raises an argument error" do + expect { KeycloakAdmin.realm(nil).client_authz_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError) + end + end + end + + describe "#list" do + subject { @client.list } + + before(:each) do + @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + stub_token_client + allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response + end + + context "with a hardcoded claim mapper" do + let(:stub_response) { "[#{mapper_json}]" } + + its(:size) { is_expected.to eq 1 } + its(:first) { is_expected.to have_attributes(id: "valid-mapper-id", name: "my-claim", protocol: "openid-connect", protocolMapper: "oidc-hardcoded-claim-mapper") } + end + + context "with an audience mapper" do + let(:stub_response) { "[#{audience_mapper_json}]" } + + its(:size) { is_expected.to eq 1 } + its(:first) { is_expected.to have_attributes(name: "audience-config-rvw-123", protocol: "openid-connect", protocolMapper: "oidc-audience-mapper") } + end + + context "with multiple mappers" do + let(:stub_response) { "[#{mapper_json},#{audience_mapper_json}]" } + + its(:size) { is_expected.to eq 2 } + it { expect(subject.map(&:name)).to include("my-claim", "audience-config-rvw-123") } + end + end + + describe "#get" do + subject { @client.get(mapper_id) } + + before(:each) do + @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + stub_token_client + allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response + end + + context "with a hardcoded claim mapper" do + let(:stub_response) { mapper_json } + + its(:id) { is_expected.to eq "valid-mapper-id" } + its(:name) { is_expected.to eq "my-claim" } + its(:protocol) { is_expected.to eq "openid-connect" } + its(:protocolMapper) { is_expected.to eq "oidc-hardcoded-claim-mapper" } + end + + context "with an audience mapper" do + let(:stub_response) { audience_mapper_json } + + its(:name) { is_expected.to eq "audience-config-rvw-123" } + its(:protocol) { is_expected.to eq "openid-connect" } + its(:protocolMapper) { is_expected.to eq "oidc-audience-mapper" } + its(:config) { is_expected.to include("included.custom.audience" => "https://api.example.com", "access.token.claim" => "true", "introspection.token.claim" => "true", "id.token.claim" => "false") } + end + end + + describe "#create!" do + subject { @client.create!(mapper_representation) } + + before(:each) do + @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + stub_token_client + allow_any_instance_of(RestClient::Resource).to receive(:post).and_return stub_response + end + + context "with a hardcoded claim mapper" do + let(:stub_response) { mapper_json } + let(:mapper_representation) do + mapper = KeycloakAdmin::ProtocolMapperRepresentation.new + mapper.name = "my-claim" + mapper.protocol = "openid-connect" + mapper.protocolMapper = "oidc-hardcoded-claim-mapper" + mapper.config = { "claim.name" => "my_claim", "claim.value" => "bar", "access.token.claim" => "true" } + mapper + end + + its(:id) { is_expected.to eq "valid-mapper-id" } + its(:name) { is_expected.to eq "my-claim" } + its(:protocol) { is_expected.to eq "openid-connect" } + end + + context "with an audience mapper" do + let(:stub_response) { audience_mapper_json } + let(:mapper_representation) do + mapper = KeycloakAdmin::ProtocolMapperRepresentation.new + mapper.name = "audience-config-rvw-123" + mapper.protocol = "openid-connect" + mapper.protocolMapper = "oidc-audience-mapper" + mapper.config = { + "included.client.audience" => "", + "included.custom.audience" => "https://api.example.com", + "id.token.claim" => "false", + "access.token.claim" => "true", + "lightweight.claim" => "false", + "introspection.token.claim" => "true" + } + mapper + end + + its(:name) { is_expected.to eq "audience-config-rvw-123" } + its(:protocol) { is_expected.to eq "openid-connect" } + its(:protocolMapper) { is_expected.to eq "oidc-audience-mapper" } + its(:config) { is_expected.to include("included.custom.audience" => "https://api.example.com") } + end + end + + describe "#update" do + subject { @client.update(mapper_representation) } + + before(:each) do + @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + stub_token_client + allow_any_instance_of(RestClient::Resource).to receive(:put).and_return "" + end + + context "with a hardcoded claim mapper" do + let(:mapper_representation) { KeycloakAdmin::ProtocolMapperRepresentation.from_hash(JSON.parse(mapper_json)) } + + it "calls put on the mapper url" do + expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything) + subject + end + end + + context "with an audience mapper" do + let(:mapper_representation) { KeycloakAdmin::ProtocolMapperRepresentation.from_hash(JSON.parse(audience_mapper_json)) } + + it "calls put on the mapper url" do + expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything) + subject + end + end + end + + describe "#delete" do + subject { @client.delete(mapper_id) } + + before(:each) do + @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + stub_token_client + allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return "" + end + + it { is_expected.to eq true } + end + + describe "#protocol_mappers_url" do + let(:client) { KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) } + let(:base_url) { "http://auth.service.io/auth/admin/realms/valid-realm/client-scopes/valid-scope-id/protocol-mappers/models" } + + context "without a mapper_id" do + subject { client.protocol_mappers_url } + + it { is_expected.to eq base_url } + end + + context "with a mapper_id" do + subject { client.protocol_mappers_url(mapper_id) } + + it { is_expected.to eq "#{base_url}/valid-mapper-id" } + end + end +end From 3ff1e186525e968e61a519a026cdcbfa1d13f422 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Wed, 25 Mar 2026 16:09:50 +0200 Subject: [PATCH 4/8] feat: scope mapper crud client --- ...ient_authz_scope_protocol_mapper_client.rb | 4 +- lib/keycloak-admin/client/realm_client.rb | 2 +- ...authz_scope_protocol_mapper_client_spec.rb | 129 +++++++++++------- 3 files changed, 85 insertions(+), 50 deletions(-) diff --git a/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb b/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb index 7d01dbb..8b0c100 100644 --- a/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb +++ b/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb @@ -32,7 +32,7 @@ def create!(mapper_representation) ) end - ProtocolMapperRepresentation.from_hash(JSON.parse(response)) + true end def update(mapper_representation) @@ -41,6 +41,8 @@ def update(mapper_representation) create_payload(mapper_representation), headers ) end + + true end def delete(mapper_id) diff --git a/lib/keycloak-admin/client/realm_client.rb b/lib/keycloak-admin/client/realm_client.rb index fa84ded..4297c09 100644 --- a/lib/keycloak-admin/client/realm_client.rb +++ b/lib/keycloak-admin/client/realm_client.rb @@ -103,7 +103,7 @@ def user(user_id) UserResource.new(@configuration, self, user_id) end - def client_authz_scope_protocol_mappers(client_scope_id) + def authz_scope_protocol_mappers(client_scope_id) ClientAuthzScopeProtocolMapperClient.new(@configuration, self, client_scope_id) end diff --git a/spec/client/client_authz_scope_protocol_mapper_client_spec.rb b/spec/client/client_authz_scope_protocol_mapper_client_spec.rb index 612c229..574ab25 100644 --- a/spec/client/client_authz_scope_protocol_mapper_client_spec.rb +++ b/spec/client/client_authz_scope_protocol_mapper_client_spec.rb @@ -18,22 +18,20 @@ describe "#initialize" do context "when realm_name is defined" do it "does not raise any error" do - expect { KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) }.to_not raise_error + expect { KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) }.to_not raise_error end end context "when realm_name is not defined" do it "raises an argument error" do - expect { KeycloakAdmin.realm(nil).client_authz_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError) + expect { KeycloakAdmin.realm(nil).authz_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError) end end end describe "#list" do - subject { @client.list } - before(:each) do - @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response end @@ -41,30 +39,43 @@ context "with a hardcoded claim mapper" do let(:stub_response) { "[#{mapper_json}]" } - its(:size) { is_expected.to eq 1 } - its(:first) { is_expected.to have_attributes(id: "valid-mapper-id", name: "my-claim", protocol: "openid-connect", protocolMapper: "oidc-hardcoded-claim-mapper") } + it "returns one mapper" do + expect(@client.list.size).to eq 1 + end + + it "returns the correct mapper attributes" do + expect(@client.list.first).to have_attributes(id: "valid-mapper-id", name: "my-claim", protocol: "openid-connect", protocolMapper: "oidc-hardcoded-claim-mapper") + end end context "with an audience mapper" do let(:stub_response) { "[#{audience_mapper_json}]" } - its(:size) { is_expected.to eq 1 } - its(:first) { is_expected.to have_attributes(name: "audience-config-rvw-123", protocol: "openid-connect", protocolMapper: "oidc-audience-mapper") } + it "returns one mapper" do + expect(@client.list.size).to eq 1 + end + + it "returns the correct mapper attributes" do + expect(@client.list.first).to have_attributes(name: "audience-config-rvw-123", protocol: "openid-connect", protocolMapper: "oidc-audience-mapper") + end end context "with multiple mappers" do let(:stub_response) { "[#{mapper_json},#{audience_mapper_json}]" } - its(:size) { is_expected.to eq 2 } - it { expect(subject.map(&:name)).to include("my-claim", "audience-config-rvw-123") } + it "returns two mappers" do + expect(@client.list.size).to eq 2 + end + + it "includes both mapper names" do + expect(@client.list.map(&:name)).to include("my-claim", "audience-config-rvw-123") + end end end describe "#get" do - subject { @client.get(mapper_id) } - before(:each) do - @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response end @@ -72,27 +83,52 @@ context "with a hardcoded claim mapper" do let(:stub_response) { mapper_json } - its(:id) { is_expected.to eq "valid-mapper-id" } - its(:name) { is_expected.to eq "my-claim" } - its(:protocol) { is_expected.to eq "openid-connect" } - its(:protocolMapper) { is_expected.to eq "oidc-hardcoded-claim-mapper" } + it "returns the correct id" do + expect(@client.get(mapper_id).id).to eq "valid-mapper-id" + end + + it "returns the correct name" do + expect(@client.get(mapper_id).name).to eq "my-claim" + end + + it "returns the correct protocol" do + expect(@client.get(mapper_id).protocol).to eq "openid-connect" + end + + it "returns the correct protocolMapper" do + expect(@client.get(mapper_id).protocolMapper).to eq "oidc-hardcoded-claim-mapper" + end end context "with an audience mapper" do let(:stub_response) { audience_mapper_json } - its(:name) { is_expected.to eq "audience-config-rvw-123" } - its(:protocol) { is_expected.to eq "openid-connect" } - its(:protocolMapper) { is_expected.to eq "oidc-audience-mapper" } - its(:config) { is_expected.to include("included.custom.audience" => "https://api.example.com", "access.token.claim" => "true", "introspection.token.claim" => "true", "id.token.claim" => "false") } + it "returns the correct name" do + expect(@client.get(mapper_id).name).to eq "audience-config-rvw-123" + end + + it "returns the correct protocol" do + expect(@client.get(mapper_id).protocol).to eq "openid-connect" + end + + it "returns the correct protocolMapper" do + expect(@client.get(mapper_id).protocolMapper).to eq "oidc-audience-mapper" + end + + it "returns the correct config" do + expect(@client.get(mapper_id).config).to include( + "included.custom.audience" => "https://api.example.com", + "access.token.claim" => "true", + "introspection.token.claim" => "true", + "id.token.claim" => "false" + ) + end end end describe "#create!" do - subject { @client.create!(mapper_representation) } - before(:each) do - @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:post).and_return stub_response end @@ -108,9 +144,9 @@ mapper end - its(:id) { is_expected.to eq "valid-mapper-id" } - its(:name) { is_expected.to eq "my-claim" } - its(:protocol) { is_expected.to eq "openid-connect" } + it "creates successfully" do + expect(@client.create!(mapper_representation)).to be true + end end context "with an audience mapper" do @@ -131,18 +167,15 @@ mapper end - its(:name) { is_expected.to eq "audience-config-rvw-123" } - its(:protocol) { is_expected.to eq "openid-connect" } - its(:protocolMapper) { is_expected.to eq "oidc-audience-mapper" } - its(:config) { is_expected.to include("included.custom.audience" => "https://api.example.com") } + it "creates successfully" do + expect(@client.create!(mapper_representation)).to be true + end end end describe "#update" do - subject { @client.update(mapper_representation) } - before(:each) do - @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:put).and_return "" end @@ -152,7 +185,7 @@ it "calls put on the mapper url" do expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything) - subject + @client.update(mapper_representation) end end @@ -161,37 +194,37 @@ it "calls put on the mapper url" do expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything) - subject + @client.update(mapper_representation) end end end describe "#delete" do - subject { @client.delete(mapper_id) } - before(:each) do - @client = KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return "" end - it { is_expected.to eq true } + it "returns true" do + expect(@client.delete(mapper_id)).to eq true + end end describe "#protocol_mappers_url" do - let(:client) { KeycloakAdmin.realm(realm_name).client_authz_scope_protocol_mappers(client_scope_id) } + let(:client) { KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) } let(:base_url) { "http://auth.service.io/auth/admin/realms/valid-realm/client-scopes/valid-scope-id/protocol-mappers/models" } context "without a mapper_id" do - subject { client.protocol_mappers_url } - - it { is_expected.to eq base_url } + it "returns the base url" do + expect(client.protocol_mappers_url).to eq base_url + end end context "with a mapper_id" do - subject { client.protocol_mappers_url(mapper_id) } - - it { is_expected.to eq "#{base_url}/valid-mapper-id" } + it "returns the url with mapper_id appended" do + expect(client.protocol_mappers_url(mapper_id)).to eq "#{base_url}/valid-mapper-id" + end end end end From f918da1af5c1a0ba1b1b200ebb9d85cb3a2ed2c2 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Wed, 25 Mar 2026 16:09:58 +0200 Subject: [PATCH 5/8] revert: remove rspec-its --- Gemfile.lock | 4 ---- keycloak-admin.gemspec | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index da707ee..c5d6616 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,9 +34,6 @@ GEM rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-its (2.0.0) - rspec-core (>= 3.13.0) - rspec-expectations (>= 3.13.0) rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) @@ -49,7 +46,6 @@ DEPENDENCIES byebug (= 12.0.0) keycloak-admin! rspec (= 3.13.2) - rspec-its (= 2.0.0) BUNDLED WITH 2.1.4 diff --git a/keycloak-admin.gemspec b/keycloak-admin.gemspec index cbbb208..3fc8a44 100644 --- a/keycloak-admin.gemspec +++ b/keycloak-admin.gemspec @@ -19,7 +19,6 @@ Gem::Specification.new do |spec| spec.add_dependency "http-cookie", "~> 1.0", ">= 1.0.3" spec.add_dependency "rest-client", "~> 2.0" - spec.add_development_dependency "rspec", "3.13.2" - spec.add_development_dependency "rspec-its", "2.0.0" - spec.add_development_dependency "byebug", "12.0.0" + spec.add_development_dependency "rspec", "3.13.2" + spec.add_development_dependency "byebug", "12.0.0" end From 7e2e72bd0ae8a0c26b8e43947a8a77b06614f2e1 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Wed, 25 Mar 2026 16:18:49 +0200 Subject: [PATCH 6/8] chore: remove unnecessary rspec-its requirement --- spec/spec_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 304ab1f..540d40b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,6 @@ require_relative "../lib/keycloak-admin" require "byebug" -require "rspec/its" def configure KeycloakAdmin.configure do |config| From 7ecf94b933721cb9f0baaead516a15ceb6391e30 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Thu, 26 Mar 2026 08:25:27 +0200 Subject: [PATCH 7/8] refactor: resolve ambiguous naming of client scope classes / accessors --- lib/keycloak-admin.rb | 2 +- ... => client_scope_protocol_mapper_client.rb} | 4 ++-- lib/keycloak-admin/client/realm_client.rb | 4 ++-- ...lient_scope_protocol_mapper_client_spec.rb} | 18 +++++++++--------- 4 files changed, 14 insertions(+), 14 deletions(-) rename lib/keycloak-admin/client/{client_authz_scope_protocol_mapper_client.rb => client_scope_protocol_mapper_client.rb} (95%) rename spec/client/{client_authz_scope_protocol_mapper_client_spec.rb => client_scope_protocol_mapper_client_spec.rb} (89%) diff --git a/lib/keycloak-admin.rb b/lib/keycloak-admin.rb index f0f07c1..c28f57f 100644 --- a/lib/keycloak-admin.rb +++ b/lib/keycloak-admin.rb @@ -16,7 +16,7 @@ require_relative "keycloak-admin/client/configurable_token_client" require_relative "keycloak-admin/client/attack_detection_client" require_relative "keycloak-admin/client/client_authz_scope_client" -require_relative "keycloak-admin/client/client_authz_scope_protocol_mapper_client" +require_relative "keycloak-admin/client/client_scope_protocol_mapper_client" require_relative "keycloak-admin/client/client_authz_resource_client" require_relative "keycloak-admin/client/client_authz_policy_client" require_relative "keycloak-admin/client/client_authz_permission_client" diff --git a/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb b/lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb similarity index 95% rename from lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb rename to lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb index 8b0c100..fe51e7c 100644 --- a/lib/keycloak-admin/client/client_authz_scope_protocol_mapper_client.rb +++ b/lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb @@ -1,5 +1,5 @@ module KeycloakAdmin - class ClientAuthzScopeProtocolMapperClient < Client + class ClientScopeProtocolMapperClient < Client def initialize(configuration, realm_client, client_scope_id) super(configuration) @@ -26,7 +26,7 @@ def get(mapper_id) end def create!(mapper_representation) - response = execute_http do + execute_http do RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).post( create_payload(mapper_representation), headers ) diff --git a/lib/keycloak-admin/client/realm_client.rb b/lib/keycloak-admin/client/realm_client.rb index 4297c09..57f32e1 100644 --- a/lib/keycloak-admin/client/realm_client.rb +++ b/lib/keycloak-admin/client/realm_client.rb @@ -103,8 +103,8 @@ def user(user_id) UserResource.new(@configuration, self, user_id) end - def authz_scope_protocol_mappers(client_scope_id) - ClientAuthzScopeProtocolMapperClient.new(@configuration, self, client_scope_id) + def client_scope_protocol_mappers(client_scope_id) + ClientScopeProtocolMapperClient.new(@configuration, self, client_scope_id) end def authz_scopes(client_id, resource_id = nil) diff --git a/spec/client/client_authz_scope_protocol_mapper_client_spec.rb b/spec/client/client_scope_protocol_mapper_client_spec.rb similarity index 89% rename from spec/client/client_authz_scope_protocol_mapper_client_spec.rb rename to spec/client/client_scope_protocol_mapper_client_spec.rb index 574ab25..296188f 100644 --- a/spec/client/client_authz_scope_protocol_mapper_client_spec.rb +++ b/spec/client/client_scope_protocol_mapper_client_spec.rb @@ -1,4 +1,4 @@ -RSpec.describe KeycloakAdmin::ClientAuthzScopeProtocolMapperClient do +RSpec.describe KeycloakAdmin::ClientScopeProtocolMapperClient do let(:realm_name) { "valid-realm" } let(:client_scope_id) { "valid-scope-id" } let(:mapper_id) { "valid-mapper-id" } @@ -18,20 +18,20 @@ describe "#initialize" do context "when realm_name is defined" do it "does not raise any error" do - expect { KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) }.to_not raise_error + expect { KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) }.to_not raise_error end end context "when realm_name is not defined" do it "raises an argument error" do - expect { KeycloakAdmin.realm(nil).authz_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError) + expect { KeycloakAdmin.realm(nil).client_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError) end end end describe "#list" do before(:each) do - @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response end @@ -75,7 +75,7 @@ describe "#get" do before(:each) do - @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response end @@ -128,7 +128,7 @@ describe "#create!" do before(:each) do - @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:post).and_return stub_response end @@ -175,7 +175,7 @@ describe "#update" do before(:each) do - @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:put).and_return "" end @@ -201,7 +201,7 @@ describe "#delete" do before(:each) do - @client = KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) + @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) stub_token_client allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return "" end @@ -212,7 +212,7 @@ end describe "#protocol_mappers_url" do - let(:client) { KeycloakAdmin.realm(realm_name).authz_scope_protocol_mappers(client_scope_id) } + let(:client) { KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) } let(:base_url) { "http://auth.service.io/auth/admin/realms/valid-realm/client-scopes/valid-scope-id/protocol-mappers/models" } context "without a mapper_id" do From 24bc2304c947a5690d1149c5a6e3c058206d4918 Mon Sep 17 00:00:00 2001 From: Aaron Delate Date: Thu, 26 Mar 2026 08:25:38 +0200 Subject: [PATCH 8/8] docs: add documentation for new methods --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.md b/README.md index 074a70a..a81d8a7 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ All options have a default value. However, all of them can be changed in your in * Execute actions emails * Send forgot passsword mail * Client Authorization, create, update, get, delete Resource, Scope, Policy, Permission, Policy Enforcer +* Get list of protocol mappers for a client scope, create/update/get/delete a protocol mapper * Get list of organizations, create/update/get/delete an organization * Get list of members of an organization, add/remove members * Invite new or existing users to an organization @@ -757,6 +758,62 @@ KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'scope').delete(scop KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'resource').delete(resource_permission.id) ``` +### Manage Protocol Mappers for a Client Scope + +Protocol mappers allow you to transform tokens and assertions. The following operations are available on the protocol mappers of a given client scope. + +### List protocol mappers for a client scope + +Returns an array of `KeycloakAdmin::ProtocolMapperRepresentation`. + +```ruby +client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e" +KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).list +``` + +### Get a protocol mapper by its id + +Returns an instance of `KeycloakAdmin::ProtocolMapperRepresentation`. + +```ruby +client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e" +mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2" +KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id) +``` + +### Create a protocol mapper for a client scope + +Takes `mapper_representation` of type `KeycloakAdmin::ProtocolMapperRepresentation`. Returns `true` on success. + +```ruby +client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e" +mapper = KeycloakAdmin::ProtocolMapperRepresentation.new +mapper.name = "my-mapper" +mapper.protocol = "openid-connect" +mapper.protocolMapper = "oidc-usermodel-attribute-mapper" +mapper.config = { "user.attribute" => "locale", "claim.name" => "locale", "jsonType.label" => "String", "id.token.claim" => "true", "access.token.claim" => "true", "userinfo.token.claim" => "true" } +KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).create!(mapper) +``` + +### Update a protocol mapper for a client scope + +Takes `mapper_representation` of type `KeycloakAdmin::ProtocolMapperRepresentation` (must include its `id`). Returns `true` on success. + +```ruby +client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e" +mapper = KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id) +mapper.config["claim.name"] = "updated_claim" +KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).update(mapper) +``` + +### Delete a protocol mapper from a client scope + +```ruby +client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e" +mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2" +KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).delete(mapper_id) +``` + ## How to execute library tests From the `keycloak-admin-api` directory: