keycloak-admin 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db97e606c0a299c2daff7be37a2df4a1c2a7261d2430b62dd196b8d29dc9117d
4
- data.tar.gz: a1581e41e26b30d576273036ccf87abf463db0e4a80cd1f3a0961b87f874c373
3
+ metadata.gz: 0e12885f5810b15fdc63cc227cb48c1e5a730b6328d3021e4de319d1859fb996
4
+ data.tar.gz: 5b691cf6498c9754c8778ed043501c94620ef0ca3a7649ae1e2d8ebf310047c8
5
5
  SHA512:
6
- metadata.gz: 33416a0ad66f8f054a2192f560fbd72d6d68d1a29101521d8bfa2be5bd705888f46740d325ee265ef0e983bbd611901e7902db2a14f5693bd066c250692bb417
7
- data.tar.gz: 6e17f0a84457d4bf8ce7332b756b0892fbe51a606a8895e5434fca458d9b936594006057820fb3e753d1713b5fdd895fe546f82f90a60fea948f3065e4905bed
6
+ metadata.gz: 552d1b3896305cbe8799ca2627631f049fe8a526b3450fec6c4a360dd9ac2139185b91e642973acf2ee9c1af2bdf49073e5cf66c2eafbc3d5a72c8749331cb53
7
+ data.tar.gz: 2fbd6f1e6034c0654ac56b856d67fc975fab74a6642071e0bb6f5e8fcdd0b2989042ee2a6aef735e767ca6a9ba53d471870e58b97d1cd71df68b62036677eea4
@@ -68,7 +68,7 @@ jobs:
68
68
  "redirectUris":["http://localhost:8180/demo"]
69
69
  }'
70
70
 
71
- - uses: actions/checkout@v4
71
+ - uses: actions/checkout@v6
72
72
  - name: Set up Ruby
73
73
  uses: ruby/setup-ruby@v1
74
74
  with:
data/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.7] - 2026-03-27
9
+
10
+ * [Feature] Client scopes - supported operations: `create!`, `get`, `delete`, `list`, and `search`.
11
+ * [Feature] Client scopes protocol mappers - supported operations: `create!`, `get`, `delete`, `list`, and `search`.
12
+
8
13
  ## [1.1.6] - 2026-01-05
9
14
 
10
15
  * [Feature] Support for Organizations (Multi-tenancy):
data/Gemfile.lock CHANGED
@@ -1,25 +1,29 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- keycloak-admin (1.1.6)
4
+ keycloak-admin (1.1.7)
5
5
  http-cookie (~> 1.0, >= 1.0.3)
6
6
  rest-client (~> 2.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- byebug (12.0.0)
11
+ byebug (13.0.0)
12
+ reline (>= 0.6.0)
12
13
  diff-lcs (1.6.2)
13
14
  domain_name (0.6.20240107)
14
15
  http-accept (1.7.0)
15
16
  http-cookie (1.1.0)
16
17
  domain_name (~> 0.5)
18
+ io-console (0.8.2)
17
19
  logger (1.7.0)
18
20
  mime-types (3.7.0)
19
21
  logger
20
22
  mime-types-data (~> 3.2025, >= 3.2025.0507)
21
- mime-types-data (3.2025.0924)
23
+ mime-types-data (3.2026.0317)
22
24
  netrc (0.11.0)
25
+ reline (0.6.3)
26
+ io-console (~> 0.5)
23
27
  rest-client (2.1.0)
24
28
  http-accept (>= 1.7.0, < 2.0)
25
29
  http-cookie (>= 1.0.2, < 2.0)
@@ -34,16 +38,16 @@ GEM
34
38
  rspec-expectations (3.13.5)
35
39
  diff-lcs (>= 1.2.0, < 2.0)
36
40
  rspec-support (~> 3.13.0)
37
- rspec-mocks (3.13.7)
41
+ rspec-mocks (3.13.8)
38
42
  diff-lcs (>= 1.2.0, < 2.0)
39
43
  rspec-support (~> 3.13.0)
40
- rspec-support (3.13.6)
44
+ rspec-support (3.13.7)
41
45
 
42
46
  PLATFORMS
43
47
  ruby
44
48
 
45
49
  DEPENDENCIES
46
- byebug (= 12.0.0)
50
+ byebug (= 13.0.0)
47
51
  keycloak-admin!
48
52
  rspec (= 3.13.2)
49
53
 
data/README.md CHANGED
@@ -135,6 +135,8 @@ All options have a default value. However, all of them can be changed in your in
135
135
  * Execute actions emails
136
136
  * Send forgot passsword mail
137
137
  * Client Authorization, create, update, get, delete Resource, Scope, Policy, Permission, Policy Enforcer
138
+ * Get list of client scopes, create/save/get/delete/search a client scope
139
+ * Get list of protocol mappers for a client scope, create/save/get/delete a protocol mapper
138
140
  * Get list of organizations, create/update/get/delete an organization
139
141
  * Get list of members of an organization, add/remove members
140
142
  * Invite new or existing users to an organization
@@ -757,6 +759,120 @@ KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'scope').delete(scop
757
759
  KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'resource').delete(resource_permission.id)
758
760
  ```
759
761
 
762
+ ### Manage Client Scopes
763
+
764
+ ### List all client scopes in a realm
765
+
766
+ Returns an array of `KeycloakAdmin::ClientScopeRepresentation`.
767
+
768
+ ```ruby
769
+ KeycloakAdmin.realm("a_realm").client_scopes.list
770
+ ```
771
+
772
+ ### Get a client scope by its id
773
+
774
+ Returns an instance of `KeycloakAdmin::ClientScopeRepresentation`.
775
+
776
+ ```ruby
777
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
778
+ KeycloakAdmin.realm("a_realm").client_scopes.get(client_scope_id)
779
+ ```
780
+
781
+ ### Search for client scopes by name
782
+
783
+ Returns an array of `KeycloakAdmin::ClientScopeRepresentation` whose names contain the given substring.
784
+
785
+ ```ruby
786
+ KeycloakAdmin.realm("a_realm").client_scopes.search("my-scope")
787
+ ```
788
+
789
+ ### Create a client scope
790
+
791
+ Takes `scope_representation` of type `KeycloakAdmin::ClientScopeRepresentation`. Returns `true` on success.
792
+
793
+ ```ruby
794
+ scope = KeycloakAdmin::ClientScopeRepresentation.new
795
+ scope.name = "my-scope"
796
+ scope.description = "My custom scope"
797
+ scope.protocol = "openid-connect"
798
+ scope.attributes = { "display.on.consent.screen" => "true", "include.in.token.scope" => "true" }
799
+ KeycloakAdmin.realm("a_realm").client_scopes.create!(scope)
800
+ ```
801
+
802
+ ### Save (update) a client scope
803
+
804
+ Takes `scope_representation` of type `KeycloakAdmin::ClientScopeRepresentation` (must include its `id`). Returns `true` on success.
805
+
806
+ ```ruby
807
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
808
+ scope = KeycloakAdmin.realm("a_realm").client_scopes.get(client_scope_id)
809
+ scope.description = "Updated description"
810
+ KeycloakAdmin.realm("a_realm").client_scopes.save(scope)
811
+ ```
812
+
813
+ ### Delete a client scope
814
+
815
+ ```ruby
816
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
817
+ KeycloakAdmin.realm("a_realm").client_scopes.delete(client_scope_id)
818
+ ```
819
+
820
+ ### Manage Protocol Mappers for a Client Scope
821
+
822
+ Protocol mappers allow you to transform tokens and assertions. The following operations are available on the protocol mappers of a given client scope.
823
+
824
+ ### List protocol mappers for a client scope
825
+
826
+ Returns an array of `KeycloakAdmin::ProtocolMapperRepresentation`.
827
+
828
+ ```ruby
829
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
830
+ KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).list
831
+ ```
832
+
833
+ ### Get a protocol mapper by its id
834
+
835
+ Returns an instance of `KeycloakAdmin::ProtocolMapperRepresentation`.
836
+
837
+ ```ruby
838
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
839
+ mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
840
+ KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id)
841
+ ```
842
+
843
+ ### Create a protocol mapper for a client scope
844
+
845
+ Takes `mapper_representation` of type `KeycloakAdmin::ProtocolMapperRepresentation`. Returns `true` on success.
846
+
847
+ ```ruby
848
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
849
+ mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
850
+ mapper.name = "my-mapper"
851
+ mapper.protocol = "openid-connect"
852
+ mapper.protocolMapper = "oidc-usermodel-attribute-mapper"
853
+ mapper.config = { "user.attribute" => "locale", "claim.name" => "locale", "jsonType.label" => "String", "id.token.claim" => "true", "access.token.claim" => "true", "userinfo.token.claim" => "true" }
854
+ KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).create!(mapper)
855
+ ```
856
+
857
+ ### Save (update) a protocol mapper for a client scope
858
+
859
+ Takes `mapper_representation` of type `KeycloakAdmin::ProtocolMapperRepresentation` (must include its `id`). Returns `true` on success.
860
+
861
+ ```ruby
862
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
863
+ mapper = KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id)
864
+ mapper.config["claim.name"] = "updated_claim"
865
+ KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).save(mapper)
866
+ ```
867
+
868
+ ### Delete a protocol mapper from a client scope
869
+
870
+ ```ruby
871
+ client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
872
+ mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
873
+ KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).delete(mapper_id)
874
+ ```
875
+
760
876
  ## How to execute library tests
761
877
 
762
878
  From the `keycloak-admin-api` directory:
@@ -20,5 +20,5 @@ Gem::Specification.new do |spec|
20
20
  spec.add_dependency "http-cookie", "~> 1.0", ">= 1.0.3"
21
21
  spec.add_dependency "rest-client", "~> 2.0"
22
22
  spec.add_development_dependency "rspec", "3.13.2"
23
- spec.add_development_dependency "byebug", "12.0.0"
23
+ spec.add_development_dependency "byebug", "13.0.0"
24
24
  end
@@ -0,0 +1,65 @@
1
+ module KeycloakAdmin
2
+ class ClientScopeClient < Client
3
+ def initialize(configuration, realm_client)
4
+ super(configuration)
5
+
6
+ raise ArgumentError.new("realm must be defined") unless realm_client.name_defined?
7
+
8
+ @realm_client = realm_client
9
+ end
10
+
11
+ def list
12
+ response = execute_http do
13
+ RestClient::Resource.new(client_scopes_url, @configuration.rest_client_options).get(headers)
14
+ end
15
+
16
+ JSON.parse(response).map { |h| ClientScopeRepresentation.from_hash(h) }
17
+ end
18
+
19
+ def get(client_scope_id)
20
+ response = execute_http do
21
+ RestClient::Resource.new(client_scopes_url(client_scope_id), @configuration.rest_client_options).get(headers)
22
+ end
23
+
24
+ ClientScopeRepresentation.from_hash(JSON.parse(response))
25
+ end
26
+
27
+ def create!(client_scope_representation)
28
+ execute_http do
29
+ RestClient::Resource.new(client_scopes_url, @configuration.rest_client_options).post(
30
+ create_payload(client_scope_representation), headers
31
+ )
32
+ end
33
+
34
+ true
35
+ end
36
+
37
+ def save(client_scope_representation)
38
+ execute_http do
39
+ RestClient::Resource.new(client_scopes_url(client_scope_representation.id), @configuration.rest_client_options).put(
40
+ create_payload(client_scope_representation), headers
41
+ )
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ def search(name)
48
+ list.select { |scope| scope&.name&.include?(name) }
49
+ end
50
+
51
+ def delete(client_scope_id)
52
+ execute_http do
53
+ RestClient::Resource.new(client_scopes_url(client_scope_id), @configuration.rest_client_options).delete(headers)
54
+ end
55
+
56
+ true
57
+ end
58
+
59
+ def client_scopes_url(client_scope_id = nil)
60
+ base = "#{@realm_client.realm_admin_url}/client-scopes"
61
+
62
+ client_scope_id ? "#{base}/#{client_scope_id}" : base
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,62 @@
1
+ module KeycloakAdmin
2
+ class ClientScopeProtocolMapperClient < Client
3
+ def initialize(configuration, realm_client, client_scope_id)
4
+ super(configuration)
5
+
6
+ raise ArgumentError.new("realm must be defined") unless realm_client.name_defined?
7
+
8
+ @realm_client = realm_client
9
+ @client_scope_id = client_scope_id
10
+ end
11
+
12
+ def list
13
+ response = execute_http do
14
+ RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).get(headers)
15
+ end
16
+
17
+ JSON.parse(response).map { |h| ProtocolMapperRepresentation.from_hash(h) }
18
+ end
19
+
20
+ def get(mapper_id)
21
+ response = execute_http do
22
+ RestClient::Resource.new(protocol_mappers_url(mapper_id), @configuration.rest_client_options).get(headers)
23
+ end
24
+
25
+ ProtocolMapperRepresentation.from_hash(JSON.parse(response))
26
+ end
27
+
28
+ def create!(mapper_representation)
29
+ execute_http do
30
+ RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).post(
31
+ create_payload(mapper_representation), headers
32
+ )
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ def save(mapper_representation)
39
+ execute_http do
40
+ RestClient::Resource.new(protocol_mappers_url(mapper_representation.id), @configuration.rest_client_options).put(
41
+ create_payload(mapper_representation), headers
42
+ )
43
+ end
44
+
45
+ true
46
+ end
47
+
48
+ def delete(mapper_id)
49
+ execute_http do
50
+ RestClient::Resource.new(protocol_mappers_url(mapper_id), @configuration.rest_client_options).delete(headers)
51
+ end
52
+
53
+ true
54
+ end
55
+
56
+ def protocol_mappers_url(mapper_id = nil)
57
+ base = "#{@realm_client.realm_admin_url}/client-scopes/#{@client_scope_id}/protocol-mappers/models"
58
+
59
+ mapper_id ? "#{base}/#{mapper_id}" : base
60
+ end
61
+ end
62
+ end
@@ -103,6 +103,14 @@ module KeycloakAdmin
103
103
  UserResource.new(@configuration, self, user_id)
104
104
  end
105
105
 
106
+ def client_scopes
107
+ ClientScopeClient.new(@configuration, self)
108
+ end
109
+
110
+ def client_scope_protocol_mappers(client_scope_id)
111
+ ClientScopeProtocolMapperClient.new(@configuration, self, client_scope_id)
112
+ end
113
+
106
114
  def authz_scopes(client_id, resource_id = nil)
107
115
  ClientAuthzScopeClient.new(@configuration, self, client_id, resource_id)
108
116
  end
@@ -0,0 +1,21 @@
1
+ module KeycloakAdmin
2
+ class ClientScopeRepresentation < Representation
3
+ attr_accessor :id,
4
+ :name,
5
+ :description,
6
+ :protocol,
7
+ :attributes,
8
+ :protocol_mappers
9
+
10
+ def self.from_hash(hash)
11
+ rep = new
12
+ rep.id = hash["id"]
13
+ rep.name = hash["name"]
14
+ rep.description = hash["description"]
15
+ rep.protocol = hash["protocol"]
16
+ rep.attributes = hash["attributes"]
17
+ rep.protocol_mappers = (hash["protocolMappers"] || []).map { |m| ProtocolMapperRepresentation.from_hash(m) }
18
+ rep
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module KeycloakAdmin
2
- VERSION = "1.1.6"
2
+ VERSION = "1.1.7"
3
3
  end
@@ -16,6 +16,8 @@ require_relative "keycloak-admin/client/identity_provider_client"
16
16
  require_relative "keycloak-admin/client/configurable_token_client"
17
17
  require_relative "keycloak-admin/client/attack_detection_client"
18
18
  require_relative "keycloak-admin/client/client_authz_scope_client"
19
+ require_relative "keycloak-admin/client/client_scope_client"
20
+ require_relative "keycloak-admin/client/client_scope_protocol_mapper_client"
19
21
  require_relative "keycloak-admin/client/client_authz_resource_client"
20
22
  require_relative "keycloak-admin/client/client_authz_policy_client"
21
23
  require_relative "keycloak-admin/client/client_authz_permission_client"
@@ -39,6 +41,7 @@ require_relative "keycloak-admin/representation/identity_provider_mapper_represe
39
41
  require_relative "keycloak-admin/representation/identity_provider_representation"
40
42
  require_relative "keycloak-admin/representation/attack_detection_representation"
41
43
  require_relative "keycloak-admin/representation/session_representation"
44
+ require_relative "keycloak-admin/representation/client_scope_representation"
42
45
  require_relative "keycloak-admin/representation/client_authz_scope_representation"
43
46
  require_relative "keycloak-admin/representation/client_authz_resource_representation"
44
47
  require_relative "keycloak-admin/representation/client_authz_policy_representation"
@@ -0,0 +1,220 @@
1
+ RSpec.describe KeycloakAdmin::ClientScopeClient do
2
+ let(:realm_name) { "valid-realm" }
3
+ let(:client_scope_id) { "valid-scope-id" }
4
+
5
+ let(:scope_json) do
6
+ <<~JSON
7
+ {"id":"valid-scope-id","name":"my-scope","description":"A test scope","protocol":"openid-connect","attributes":{"display.on.consent.screen":"true","include.in.token.scope":"true"}}
8
+ JSON
9
+ end
10
+
11
+ let(:scope_with_mappers_json) do
12
+ <<~JSON
13
+ {"id":"valid-scope-id","name":"my-scope","description":"A test scope","protocol":"openid-connect","attributes":{},"protocolMappers":[{"id":"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"}}]}
14
+ JSON
15
+ end
16
+
17
+ describe "#initialize" do
18
+ context "when realm_name is defined" do
19
+ it "does not raise any error" do
20
+ expect { KeycloakAdmin.realm(realm_name).client_scopes }.to_not raise_error
21
+ end
22
+ end
23
+
24
+ context "when realm_name is not defined" do
25
+ it "raises an argument error" do
26
+ expect { KeycloakAdmin.realm(nil).client_scopes }.to raise_error(ArgumentError)
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#list" do
32
+ before(:each) do
33
+ @client = KeycloakAdmin.realm(realm_name).client_scopes
34
+ stub_token_client
35
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response
36
+ end
37
+
38
+ context "with one scope" do
39
+ let(:stub_response) { "[#{scope_json}]" }
40
+
41
+ it "returns one scope" do
42
+ expect(@client.list.size).to eq 1
43
+ end
44
+
45
+ it "returns the correct scope attributes" do
46
+ expect(@client.list.first).to have_attributes(
47
+ id: "valid-scope-id",
48
+ name: "my-scope",
49
+ description: "A test scope",
50
+ protocol: "openid-connect"
51
+ )
52
+ end
53
+
54
+ it "returns attributes map" do
55
+ expect(@client.list.first.attributes).to include(
56
+ "display.on.consent.screen" => "true",
57
+ "include.in.token.scope" => "true"
58
+ )
59
+ end
60
+ end
61
+
62
+ context "with multiple scopes" do
63
+ let(:second_scope_json) { '{"id":"other-scope-id","name":"other-scope","protocol":"openid-connect"}' }
64
+ let(:stub_response) { "[#{scope_json},#{second_scope_json}]" }
65
+
66
+ it "returns two scopes" do
67
+ expect(@client.list.size).to eq 2
68
+ end
69
+
70
+ it "includes both scope names" do
71
+ expect(@client.list.map(&:name)).to include("my-scope", "other-scope")
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "#get" do
77
+ before(:each) do
78
+ @client = KeycloakAdmin.realm(realm_name).client_scopes
79
+ stub_token_client
80
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response
81
+ end
82
+
83
+ context "without protocol mappers" do
84
+ let(:stub_response) { scope_json }
85
+
86
+ it "returns the correct id" do
87
+ expect(@client.get(client_scope_id).id).to eq "valid-scope-id"
88
+ end
89
+
90
+ it "returns the correct name" do
91
+ expect(@client.get(client_scope_id).name).to eq "my-scope"
92
+ end
93
+
94
+ it "returns the correct description" do
95
+ expect(@client.get(client_scope_id).description).to eq "A test scope"
96
+ end
97
+
98
+ it "returns the correct protocol" do
99
+ expect(@client.get(client_scope_id).protocol).to eq "openid-connect"
100
+ end
101
+
102
+ it "returns an empty protocolMappers list" do
103
+ expect(@client.get(client_scope_id).protocol_mappers).to eq []
104
+ end
105
+ end
106
+
107
+ context "with protocol mappers" do
108
+ let(:stub_response) { scope_with_mappers_json }
109
+
110
+ it "returns protocol mappers" do
111
+ expect(@client.get(client_scope_id).protocol_mappers.size).to eq 1
112
+ end
113
+
114
+ it "returns the correct mapper name" do
115
+ expect(@client.get(client_scope_id).protocol_mappers.first.name).to eq "my-claim"
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "#create!" do
121
+ before(:each) do
122
+ @client = KeycloakAdmin.realm(realm_name).client_scopes
123
+ stub_token_client
124
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
125
+ end
126
+
127
+ let(:scope_representation) do
128
+ scope = KeycloakAdmin::ClientScopeRepresentation.new
129
+ scope.name = "my-scope"
130
+ scope.description = "A test scope"
131
+ scope.protocol = "openid-connect"
132
+ scope.attributes = { "display.on.consent.screen" => "true", "include.in.token.scope" => "true" }
133
+ scope
134
+ end
135
+
136
+ it "creates successfully" do
137
+ expect(@client.create!(scope_representation)).to be true
138
+ end
139
+ end
140
+
141
+ describe "#save" do
142
+ before(:each) do
143
+ @client = KeycloakAdmin.realm(realm_name).client_scopes
144
+ stub_token_client
145
+ allow_any_instance_of(RestClient::Resource).to receive(:put).and_return ""
146
+ end
147
+
148
+ let(:scope_representation) { KeycloakAdmin::ClientScopeRepresentation.from_hash(JSON.parse(scope_json)) }
149
+
150
+ it "calls put on the scope url" do
151
+ expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything)
152
+ @client.save(scope_representation)
153
+ end
154
+
155
+ it "returns true" do
156
+ expect(@client.save(scope_representation)).to be true
157
+ end
158
+ end
159
+
160
+ describe "#search" do
161
+ let(:second_scope_json) { '{"id":"other-scope-id","name":"other-scope","protocol":"openid-connect"}' }
162
+
163
+ before(:each) do
164
+ @client = KeycloakAdmin.realm(realm_name).client_scopes
165
+ stub_token_client
166
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return "[#{scope_json},#{second_scope_json}]"
167
+ end
168
+
169
+ context "when the name matches one scope" do
170
+ it "returns only the matching scope" do
171
+ expect(@client.search("my-scope").size).to eq 1
172
+ end
173
+
174
+ it "returns the correct scope" do
175
+ expect(@client.search("my-scope").first).to have_attributes(id: "valid-scope-id", name: "my-scope")
176
+ end
177
+ end
178
+
179
+ context "when the name is a partial match" do
180
+ it "returns all scopes containing the substring" do
181
+ expect(@client.search("scope").size).to eq 2
182
+ end
183
+ end
184
+
185
+ context "when no scope matches" do
186
+ it "returns an empty array" do
187
+ expect(@client.search("unknown")).to eq []
188
+ end
189
+ end
190
+ end
191
+
192
+ describe "#delete" do
193
+ before(:each) do
194
+ @client = KeycloakAdmin.realm(realm_name).client_scopes
195
+ stub_token_client
196
+ allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return ""
197
+ end
198
+
199
+ it "returns true" do
200
+ expect(@client.delete(client_scope_id)).to eq true
201
+ end
202
+ end
203
+
204
+ describe "#client_scopes_url" do
205
+ let(:client) { KeycloakAdmin.realm(realm_name).client_scopes }
206
+ let(:base_url) { "http://auth.service.io/auth/admin/realms/valid-realm/client-scopes" }
207
+
208
+ context "without a client_scope_id" do
209
+ it "returns the base url" do
210
+ expect(client.client_scopes_url).to eq base_url
211
+ end
212
+ end
213
+
214
+ context "with a client_scope_id" do
215
+ it "returns the url with client_scope_id appended" do
216
+ expect(client.client_scopes_url(client_scope_id)).to eq "#{base_url}/valid-scope-id"
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,230 @@
1
+ RSpec.describe KeycloakAdmin::ClientScopeProtocolMapperClient do
2
+ let(:realm_name) { "valid-realm" }
3
+ let(:client_scope_id) { "valid-scope-id" }
4
+ let(:mapper_id) { "valid-mapper-id" }
5
+
6
+ let(:mapper_json) do
7
+ <<~JSON
8
+ {"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"}}
9
+ JSON
10
+ end
11
+
12
+ let(:audience_mapper_json) do
13
+ <<~JSON
14
+ {"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"}}
15
+ JSON
16
+ end
17
+
18
+ describe "#initialize" do
19
+ context "when realm_name is defined" do
20
+ it "does not raise any error" do
21
+ expect { KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) }.to_not raise_error
22
+ end
23
+ end
24
+
25
+ context "when realm_name is not defined" do
26
+ it "raises an argument error" do
27
+ expect { KeycloakAdmin.realm(nil).client_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "#list" do
33
+ before(:each) do
34
+ @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
35
+ stub_token_client
36
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response
37
+ end
38
+
39
+ context "with a hardcoded claim mapper" do
40
+ let(:stub_response) { "[#{mapper_json}]" }
41
+
42
+ it "returns one mapper" do
43
+ expect(@client.list.size).to eq 1
44
+ end
45
+
46
+ it "returns the correct mapper attributes" do
47
+ expect(@client.list.first).to have_attributes(id: "valid-mapper-id", name: "my-claim", protocol: "openid-connect", protocolMapper: "oidc-hardcoded-claim-mapper")
48
+ end
49
+ end
50
+
51
+ context "with an audience mapper" do
52
+ let(:stub_response) { "[#{audience_mapper_json}]" }
53
+
54
+ it "returns one mapper" do
55
+ expect(@client.list.size).to eq 1
56
+ end
57
+
58
+ it "returns the correct mapper attributes" do
59
+ expect(@client.list.first).to have_attributes(name: "audience-config-rvw-123", protocol: "openid-connect", protocolMapper: "oidc-audience-mapper")
60
+ end
61
+ end
62
+
63
+ context "with multiple mappers" do
64
+ let(:stub_response) { "[#{mapper_json},#{audience_mapper_json}]" }
65
+
66
+ it "returns two mappers" do
67
+ expect(@client.list.size).to eq 2
68
+ end
69
+
70
+ it "includes both mapper names" do
71
+ expect(@client.list.map(&:name)).to include("my-claim", "audience-config-rvw-123")
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "#get" do
77
+ before(:each) do
78
+ @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
79
+ stub_token_client
80
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response
81
+ end
82
+
83
+ context "with a hardcoded claim mapper" do
84
+ let(:stub_response) { mapper_json }
85
+
86
+ it "returns the correct id" do
87
+ expect(@client.get(mapper_id).id).to eq "valid-mapper-id"
88
+ end
89
+
90
+ it "returns the correct name" do
91
+ expect(@client.get(mapper_id).name).to eq "my-claim"
92
+ end
93
+
94
+ it "returns the correct protocol" do
95
+ expect(@client.get(mapper_id).protocol).to eq "openid-connect"
96
+ end
97
+
98
+ it "returns the correct protocolMapper" do
99
+ expect(@client.get(mapper_id).protocolMapper).to eq "oidc-hardcoded-claim-mapper"
100
+ end
101
+ end
102
+
103
+ context "with an audience mapper" do
104
+ let(:stub_response) { audience_mapper_json }
105
+
106
+ it "returns the correct name" do
107
+ expect(@client.get(mapper_id).name).to eq "audience-config-rvw-123"
108
+ end
109
+
110
+ it "returns the correct protocol" do
111
+ expect(@client.get(mapper_id).protocol).to eq "openid-connect"
112
+ end
113
+
114
+ it "returns the correct protocolMapper" do
115
+ expect(@client.get(mapper_id).protocolMapper).to eq "oidc-audience-mapper"
116
+ end
117
+
118
+ it "returns the correct config" do
119
+ expect(@client.get(mapper_id).config).to include(
120
+ "included.custom.audience" => "https://api.example.com",
121
+ "access.token.claim" => "true",
122
+ "introspection.token.claim" => "true",
123
+ "id.token.claim" => "false"
124
+ )
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "#create!" do
130
+ before(:each) do
131
+ @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
132
+ stub_token_client
133
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return stub_response
134
+ end
135
+
136
+ context "with a hardcoded claim mapper" do
137
+ let(:stub_response) { mapper_json }
138
+ let(:mapper_representation) do
139
+ mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
140
+ mapper.name = "my-claim"
141
+ mapper.protocol = "openid-connect"
142
+ mapper.protocolMapper = "oidc-hardcoded-claim-mapper"
143
+ mapper.config = { "claim.name" => "my_claim", "claim.value" => "bar", "access.token.claim" => "true" }
144
+ mapper
145
+ end
146
+
147
+ it "creates successfully" do
148
+ expect(@client.create!(mapper_representation)).to be true
149
+ end
150
+ end
151
+
152
+ context "with an audience mapper" do
153
+ let(:stub_response) { audience_mapper_json }
154
+ let(:mapper_representation) do
155
+ mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
156
+ mapper.name = "audience-config-rvw-123"
157
+ mapper.protocol = "openid-connect"
158
+ mapper.protocolMapper = "oidc-audience-mapper"
159
+ mapper.config = {
160
+ "included.client.audience" => "",
161
+ "included.custom.audience" => "https://api.example.com",
162
+ "id.token.claim" => "false",
163
+ "access.token.claim" => "true",
164
+ "lightweight.claim" => "false",
165
+ "introspection.token.claim" => "true"
166
+ }
167
+ mapper
168
+ end
169
+
170
+ it "creates successfully" do
171
+ expect(@client.create!(mapper_representation)).to be true
172
+ end
173
+ end
174
+ end
175
+
176
+ describe "#save" do
177
+ before(:each) do
178
+ @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
179
+ stub_token_client
180
+ allow_any_instance_of(RestClient::Resource).to receive(:put).and_return ""
181
+ end
182
+
183
+ context "with a hardcoded claim mapper" do
184
+ let(:mapper_representation) { KeycloakAdmin::ProtocolMapperRepresentation.from_hash(JSON.parse(mapper_json)) }
185
+
186
+ it "calls put on the mapper url" do
187
+ expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything)
188
+ @client.save(mapper_representation)
189
+ end
190
+ end
191
+
192
+ context "with an audience mapper" do
193
+ let(:mapper_representation) { KeycloakAdmin::ProtocolMapperRepresentation.from_hash(JSON.parse(audience_mapper_json)) }
194
+
195
+ it "calls put on the mapper url" do
196
+ expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything)
197
+ @client.save(mapper_representation)
198
+ end
199
+ end
200
+ end
201
+
202
+ describe "#delete" do
203
+ before(:each) do
204
+ @client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
205
+ stub_token_client
206
+ allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return ""
207
+ end
208
+
209
+ it "returns true" do
210
+ expect(@client.delete(mapper_id)).to eq true
211
+ end
212
+ end
213
+
214
+ describe "#protocol_mappers_url" do
215
+ let(:client) { KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) }
216
+ let(:base_url) { "http://auth.service.io/auth/admin/realms/valid-realm/client-scopes/valid-scope-id/protocol-mappers/models" }
217
+
218
+ context "without a mapper_id" do
219
+ it "returns the base url" do
220
+ expect(client.protocol_mappers_url).to eq base_url
221
+ end
222
+ end
223
+
224
+ context "with a mapper_id" do
225
+ it "returns the url with mapper_id appended" do
226
+ expect(client.protocol_mappers_url(mapper_id)).to eq "#{base_url}/valid-mapper-id"
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe KeycloakAdmin::ClientScopeRepresentation do
4
+ describe ".from_hash" do
5
+ context "with all fields" do
6
+ let(:hash) do
7
+ {
8
+ "id" => "valid-scope-id",
9
+ "name" => "my-scope",
10
+ "description" => "A test scope",
11
+ "protocol" => "openid-connect",
12
+ "attributes" => {
13
+ "display.on.consent.screen" => "true",
14
+ "include.in.token.scope" => "true"
15
+ },
16
+ "protocolMappers" => [
17
+ {
18
+ "id" => "mapper-id",
19
+ "name" => "my-claim",
20
+ "protocol" => "openid-connect",
21
+ "protocolMapper" => "oidc-hardcoded-claim-mapper",
22
+ "config" => { "claim.name" => "my_claim", "claim.value" => "bar" }
23
+ }
24
+ ]
25
+ }
26
+ end
27
+
28
+ subject { described_class.from_hash(hash) }
29
+
30
+ it "returns an instance of the class" do
31
+ expect(subject).to be_a described_class
32
+ end
33
+
34
+ it "sets id" do
35
+ expect(subject.id).to eq "valid-scope-id"
36
+ end
37
+
38
+ it "sets name" do
39
+ expect(subject.name).to eq "my-scope"
40
+ end
41
+
42
+ it "sets description" do
43
+ expect(subject.description).to eq "A test scope"
44
+ end
45
+
46
+ it "sets protocol" do
47
+ expect(subject.protocol).to eq "openid-connect"
48
+ end
49
+
50
+ it "sets attributes" do
51
+ expect(subject.attributes).to eq(
52
+ "display.on.consent.screen" => "true",
53
+ "include.in.token.scope" => "true"
54
+ )
55
+ end
56
+
57
+ it "deserializes protocolMappers as ProtocolMapperRepresentation objects" do
58
+ expect(subject.protocol_mappers.size).to eq 1
59
+ expect(subject.protocol_mappers.first).to be_a KeycloakAdmin::ProtocolMapperRepresentation
60
+ end
61
+
62
+ it "sets the correct mapper attributes" do
63
+ expect(subject.protocol_mappers.first).to have_attributes(
64
+ id: "mapper-id",
65
+ name: "my-claim",
66
+ protocol: "openid-connect",
67
+ protocolMapper: "oidc-hardcoded-claim-mapper"
68
+ )
69
+ end
70
+ end
71
+
72
+ context "without protocolMappers" do
73
+ subject { described_class.from_hash({ "id" => "valid-scope-id", "name" => "my-scope" }) }
74
+
75
+ it "defaults protocolMappers to an empty array" do
76
+ expect(subject.protocol_mappers).to eq []
77
+ end
78
+ end
79
+
80
+ context "with minimal fields" do
81
+ subject { described_class.from_hash({ "name" => "my-scope", "protocol" => "saml" }) }
82
+
83
+ it "sets name" do
84
+ expect(subject.name).to eq "my-scope"
85
+ end
86
+
87
+ it "sets protocol" do
88
+ expect(subject.protocol).to eq "saml"
89
+ end
90
+
91
+ it "leaves id nil" do
92
+ expect(subject.id).to be_nil
93
+ end
94
+
95
+ it "leaves description nil" do
96
+ expect(subject.description).to be_nil
97
+ end
98
+
99
+ it "leaves attributes nil" do
100
+ expect(subject.attributes).to be_nil
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "#to_json" do
106
+ subject do
107
+ described_class.from_hash(
108
+ "id" => "valid-scope-id",
109
+ "name" => "my-scope",
110
+ "description" => "A test scope",
111
+ "protocol" => "openid-connect",
112
+ "attributes" => { "include.in.token.scope" => "true" }
113
+ )
114
+ end
115
+
116
+ it "serializes to JSON" do
117
+ parsed = JSON.parse(subject.to_json)
118
+ expect(parsed["id"]).to eq "valid-scope-id"
119
+ expect(parsed["name"]).to eq "my-scope"
120
+ expect(parsed["description"]).to eq "A test scope"
121
+ expect(parsed["protocol"]).to eq "openid-connect"
122
+ expect(parsed["attributes"]).to eq("include.in.token.scope" => "true")
123
+ end
124
+ end
125
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-05 00:00:00.000000000 Z
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-cookie
@@ -64,14 +64,14 @@ dependencies:
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 12.0.0
67
+ version: 13.0.0
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - '='
73
73
  - !ruby/object:Gem::Version
74
- version: 12.0.0
74
+ version: 13.0.0
75
75
  description: Keycloak Admin REST API client written in Ruby
76
76
  email:
77
77
  - lorent.lempereur.dev@gmail.com
@@ -101,6 +101,8 @@ files:
101
101
  - lib/keycloak-admin/client/client_client.rb
102
102
  - lib/keycloak-admin/client/client_role_client.rb
103
103
  - lib/keycloak-admin/client/client_role_mappings_client.rb
104
+ - lib/keycloak-admin/client/client_scope_client.rb
105
+ - lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb
104
106
  - lib/keycloak-admin/client/configurable_token_client.rb
105
107
  - lib/keycloak-admin/client/group_client.rb
106
108
  - lib/keycloak-admin/client/identity_provider_client.rb
@@ -119,6 +121,7 @@ files:
119
121
  - lib/keycloak-admin/representation/client_authz_resource_representation.rb
120
122
  - lib/keycloak-admin/representation/client_authz_scope_representation.rb
121
123
  - lib/keycloak-admin/representation/client_representation.rb
124
+ - lib/keycloak-admin/representation/client_scope_representation.rb
122
125
  - lib/keycloak-admin/representation/credential_representation.rb
123
126
  - lib/keycloak-admin/representation/federated_identity_representation.rb
124
127
  - lib/keycloak-admin/representation/group_representation.rb
@@ -147,6 +150,8 @@ files:
147
150
  - spec/client/client_authz_scope_client_spec.rb
148
151
  - spec/client/client_client_spec.rb
149
152
  - spec/client/client_role_mappings_client_spec.rb
153
+ - spec/client/client_scope_client_spec.rb
154
+ - spec/client/client_scope_protocol_mapper_client_spec.rb
150
155
  - spec/client/client_spec.rb
151
156
  - spec/client/configurable_token_client_spec.rb
152
157
  - spec/client/group_client_spec.rb
@@ -165,6 +170,7 @@ files:
165
170
  - spec/representation/client_authz_resource_representation_spec.rb
166
171
  - spec/representation/client_authz_scope_representation_spec.rb
167
172
  - spec/representation/client_representation_spec.rb
173
+ - spec/representation/client_scope_representation_spec.rb
168
174
  - spec/representation/credential_representation_spec.rb
169
175
  - spec/representation/group_representation_spec.rb
170
176
  - spec/representation/identity_provider_mapper_representation_spec.rb
@@ -182,7 +188,7 @@ homepage: https://github.com/looorent/keycloak-admin-ruby
182
188
  licenses:
183
189
  - MIT
184
190
  metadata: {}
185
- post_install_message:
191
+ post_install_message:
186
192
  rdoc_options: []
187
193
  require_paths:
188
194
  - lib
@@ -197,8 +203,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
203
  - !ruby/object:Gem::Version
198
204
  version: '0'
199
205
  requirements: []
200
- rubygems_version: 3.3.7
201
- signing_key:
206
+ rubygems_version: 3.0.3.1
207
+ signing_key:
202
208
  specification_version: 4
203
209
  summary: Keycloak Admin REST API client written in Ruby
204
210
  test_files: []