keycloak-admin 1.1.6 → 1.2.0

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: cdb86e769e4a945994d979061cd6b44cb49fd40dd72c2c2b4ff40470e4386dc7
4
+ data.tar.gz: caaf2546d1802ec1b837d4f7bc43878c32a107978e61ea6e51e33e99a3fc0a3f
5
5
  SHA512:
6
- metadata.gz: 33416a0ad66f8f054a2192f560fbd72d6d68d1a29101521d8bfa2be5bd705888f46740d325ee265ef0e983bbd611901e7902db2a14f5693bd066c250692bb417
7
- data.tar.gz: 6e17f0a84457d4bf8ce7332b756b0892fbe51a606a8895e5434fca458d9b936594006057820fb3e753d1713b5fdd895fe546f82f90a60fea948f3065e4905bed
6
+ metadata.gz: 96e7940acc1d832bf9ff990642cec3a470450ea6bf19259cc301d080b48b8cea95cd668c2ca9b63e818769e1b67904d3d1b6e0c1ef2fe932ecd0919a0e10ec47
7
+ data.tar.gz: 01eecef72d13cb4f47d7387f21ee4dd70d7eb54275ae4738828bd57b2d21d193a1ef1ca03b330d06840a5d6192771b7204ed1131ba5e26b5087d8dc01f988a1f
@@ -19,25 +19,37 @@ permissions:
19
19
  jobs:
20
20
  test:
21
21
  runs-on: ubuntu-latest
22
- services:
23
- keycloak:
24
- image: tillawy/keycloak-github-actions:25.0.1
25
- ports:
26
- - 8080:8080
27
- options: '--health-cmd "exec 3<>/dev/tcp/localhost/8080" --health-interval 5s --health-timeout 5s --health-retries 10 --health-start-period 100s'
28
- env:
29
- KEYCLOAK_ADMIN: "admin"
30
- KEYCLOAK_ADMIN_PASSWORD: "admin"
31
- KC_HOSTNAME: "http://localhost:8080"
32
- KC_HOSTNAME_ADMIN: "http://localhost:8080"
33
- KC_HTTP_ENABLED: "true"
34
- KC_DB: "dev-file"
35
22
 
36
23
  strategy:
37
24
  matrix:
38
25
  ruby-version: ['3.2']
39
26
 
40
27
  steps:
28
+ - name: Start Keycloak
29
+ run: |
30
+ docker run -d --name keycloak -p 8080:8080 \
31
+ -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
32
+ -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
33
+ -e KC_HOSTNAME=http://localhost:8080 \
34
+ -e KC_HOSTNAME_ADMIN=http://localhost:8080 \
35
+ -e KC_HTTP_ENABLED=true \
36
+ -e KC_DB=dev-file \
37
+ quay.io/keycloak/keycloak:26.7.0 \
38
+ start-dev
39
+
40
+ - name: Wait for Keycloak to be ready
41
+ run: |
42
+ for i in $(seq 1 30); do
43
+ if curl --silent --fail http://localhost:8080/realms/master > /dev/null; then
44
+ echo "Keycloak is ready"
45
+ exit 0
46
+ fi
47
+ sleep 5
48
+ done
49
+ echo "Keycloak did not become ready in time"
50
+ docker logs keycloak
51
+ exit 1
52
+
41
53
  - name: create realm
42
54
  run: |
43
55
  TOKEN=$(curl --silent --location --request POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
@@ -68,7 +80,7 @@ jobs:
68
80
  "redirectUris":["http://localhost:8180/demo"]
69
81
  }'
70
82
 
71
- - uses: actions/checkout@v4
83
+ - uses: actions/checkout@v6
72
84
  - name: Set up Ruby
73
85
  uses: ruby/setup-ruby@v1
74
86
  with:
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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.2.0] - 2026-07-30
9
+
10
+ * [Chore] Bump `http-cookie` dependency from 1.1.0 to 1.1.6.
11
+ * [Chore] Upgrade Docker base image from `ruby:3.2.2-slim-bullseye` to `ruby:3.3.12-slim-trixie`.
12
+ * [Chore] CI now boots Keycloak 26.7.0 (`quay.io/keycloak/keycloak`) instead of the abandoned `tillawy/keycloak-github-actions:25.0.1`
13
+
14
+ ## [1.1.7] - 2026-03-27
15
+
16
+ * [Feature] Client scopes - supported operations: `create!`, `get`, `delete`, `list`, and `search`.
17
+ * [Feature] Client scopes protocol mappers - supported operations: `create!`, `get`, `delete`, `list`, and `search`.
18
+
8
19
  ## [1.1.6] - 2026-01-05
9
20
 
10
21
  * [Feature] Support for Organizations (Multi-tenancy):
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.2.2-slim-bullseye
1
+ FROM ruby:3.3.12-slim-trixie
2
2
 
3
3
  RUN apt-get update -qq && apt-get install -y build-essential git ruby-dev && apt-get clean && \
4
4
  mkdir -p /usr/src/app/lib/keycloak-admin
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.2.0)
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
- http-cookie (1.1.0)
16
+ http-cookie (1.1.6)
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.0701)
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,18 +38,18 @@ 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
 
50
54
  BUNDLED WITH
51
- 2.1.4
55
+ 2.7.2
data/README.md CHANGED
@@ -12,7 +12,7 @@ This gem *does not* require Rails.
12
12
  For example, using `bundle`, add this line to your Gemfile.
13
13
 
14
14
  ```ruby
15
- gem "keycloak-admin", "1.1.6"
15
+ gem "keycloak-admin", "1.2.0"
16
16
  ```
17
17
 
18
18
  ## Login
@@ -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.2.0"
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,13 @@
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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-01-05 00:00:00.000000000 Z
10
+ date: 2026-07-30 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: http-cookie
@@ -64,14 +63,14 @@ dependencies:
64
63
  requirements:
65
64
  - - '='
66
65
  - !ruby/object:Gem::Version
67
- version: 12.0.0
66
+ version: 13.0.0
68
67
  type: :development
69
68
  prerelease: false
70
69
  version_requirements: !ruby/object:Gem::Requirement
71
70
  requirements:
72
71
  - - '='
73
72
  - !ruby/object:Gem::Version
74
- version: 12.0.0
73
+ version: 13.0.0
75
74
  description: Keycloak Admin REST API client written in Ruby
76
75
  email:
77
76
  - lorent.lempereur.dev@gmail.com
@@ -101,6 +100,8 @@ files:
101
100
  - lib/keycloak-admin/client/client_client.rb
102
101
  - lib/keycloak-admin/client/client_role_client.rb
103
102
  - lib/keycloak-admin/client/client_role_mappings_client.rb
103
+ - lib/keycloak-admin/client/client_scope_client.rb
104
+ - lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb
104
105
  - lib/keycloak-admin/client/configurable_token_client.rb
105
106
  - lib/keycloak-admin/client/group_client.rb
106
107
  - lib/keycloak-admin/client/identity_provider_client.rb
@@ -119,6 +120,7 @@ files:
119
120
  - lib/keycloak-admin/representation/client_authz_resource_representation.rb
120
121
  - lib/keycloak-admin/representation/client_authz_scope_representation.rb
121
122
  - lib/keycloak-admin/representation/client_representation.rb
123
+ - lib/keycloak-admin/representation/client_scope_representation.rb
122
124
  - lib/keycloak-admin/representation/credential_representation.rb
123
125
  - lib/keycloak-admin/representation/federated_identity_representation.rb
124
126
  - lib/keycloak-admin/representation/group_representation.rb
@@ -147,6 +149,8 @@ files:
147
149
  - spec/client/client_authz_scope_client_spec.rb
148
150
  - spec/client/client_client_spec.rb
149
151
  - spec/client/client_role_mappings_client_spec.rb
152
+ - spec/client/client_scope_client_spec.rb
153
+ - spec/client/client_scope_protocol_mapper_client_spec.rb
150
154
  - spec/client/client_spec.rb
151
155
  - spec/client/configurable_token_client_spec.rb
152
156
  - spec/client/group_client_spec.rb
@@ -165,6 +169,7 @@ files:
165
169
  - spec/representation/client_authz_resource_representation_spec.rb
166
170
  - spec/representation/client_authz_scope_representation_spec.rb
167
171
  - spec/representation/client_representation_spec.rb
172
+ - spec/representation/client_scope_representation_spec.rb
168
173
  - spec/representation/credential_representation_spec.rb
169
174
  - spec/representation/group_representation_spec.rb
170
175
  - spec/representation/identity_provider_mapper_representation_spec.rb
@@ -182,7 +187,6 @@ homepage: https://github.com/looorent/keycloak-admin-ruby
182
187
  licenses:
183
188
  - MIT
184
189
  metadata: {}
185
- post_install_message:
186
190
  rdoc_options: []
187
191
  require_paths:
188
192
  - lib
@@ -197,8 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
201
  - !ruby/object:Gem::Version
198
202
  version: '0'
199
203
  requirements: []
200
- rubygems_version: 3.3.7
201
- signing_key:
204
+ rubygems_version: 3.6.2
202
205
  specification_version: 4
203
206
  summary: Keycloak Admin REST API client written in Ruby
204
207
  test_files: []