keycloak-admin 2.0.1 → 2.0.2

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +40 -4
  4. data/keycloak-admin.gemspec +14 -6
  5. data/lib/keycloak-admin/client/client.rb +22 -10
  6. data/lib/keycloak-admin/client/client_authz_permission_client.rb +1 -1
  7. data/lib/keycloak-admin/client/client_authz_policy_client.rb +1 -1
  8. data/lib/keycloak-admin/client/client_authz_resource_client.rb +1 -1
  9. data/lib/keycloak-admin/client/client_authz_scope_client.rb +1 -1
  10. data/lib/keycloak-admin/client/group_client.rb +2 -5
  11. data/lib/keycloak-admin/client/organization_client.rb +7 -7
  12. data/lib/keycloak-admin/client/user_client.rb +8 -4
  13. data/lib/keycloak-admin/error.rb +62 -0
  14. data/lib/keycloak-admin/version.rb +1 -1
  15. data/lib/keycloak-admin.rb +1 -0
  16. metadata +21 -56
  17. data/.github/workflows/Dockerfile +0 -24
  18. data/.github/workflows/ci.yml +0 -94
  19. data/.github/workflows/release.yml +0 -112
  20. data/.gitignore +0 -10
  21. data/.rspec +0 -2
  22. data/Dockerfile +0 -13
  23. data/Gemfile +0 -3
  24. data/Gemfile.lock +0 -54
  25. data/Rakefile +0 -6
  26. data/bin/console +0 -9
  27. data/spec/client/attack_detection_client_spec.rb +0 -102
  28. data/spec/client/client_authz_permission_client_spec.rb +0 -170
  29. data/spec/client/client_authz_policy_client_spec.rb +0 -170
  30. data/spec/client/client_authz_resource_client_spec.rb +0 -150
  31. data/spec/client/client_authz_scope_client_spec.rb +0 -134
  32. data/spec/client/client_client_spec.rb +0 -133
  33. data/spec/client/client_role_mappings_client_spec.rb +0 -83
  34. data/spec/client/client_scope_client_spec.rb +0 -220
  35. data/spec/client/client_scope_protocol_mapper_client_spec.rb +0 -230
  36. data/spec/client/client_spec.rb +0 -101
  37. data/spec/client/configurable_token_client_spec.rb +0 -34
  38. data/spec/client/group_client_spec.rb +0 -328
  39. data/spec/client/identity_provider_client_spec.rb +0 -92
  40. data/spec/client/organization_client_spec.rb +0 -595
  41. data/spec/client/realm_client_spec.rb +0 -155
  42. data/spec/client/resource_spec.rb +0 -223
  43. data/spec/client/response_spec.rb +0 -68
  44. data/spec/client/role_client_spec.rb +0 -79
  45. data/spec/client/role_mapper_client_spec.rb +0 -113
  46. data/spec/client/token_client_spec.rb +0 -68
  47. data/spec/client/user_client_spec.rb +0 -418
  48. data/spec/configuration_spec.rb +0 -150
  49. data/spec/gemspec_spec.rb +0 -25
  50. data/spec/integration/client_authorization_spec.rb +0 -93
  51. data/spec/representation/attack_detection_representation_spec.rb +0 -16
  52. data/spec/representation/client_authz_permission_representation_spec.rb +0 -52
  53. data/spec/representation/client_authz_policy_representation_spec.rb +0 -47
  54. data/spec/representation/client_authz_resource_representation_spec.rb +0 -33
  55. data/spec/representation/client_authz_scope_representation_spec.rb +0 -19
  56. data/spec/representation/client_representation_spec.rb +0 -119
  57. data/spec/representation/client_scope_representation_spec.rb +0 -125
  58. data/spec/representation/credential_representation_spec.rb +0 -68
  59. data/spec/representation/group_representation_spec.rb +0 -22
  60. data/spec/representation/identity_provider_mapper_representation_spec.rb +0 -24
  61. data/spec/representation/identity_provider_representation_spec.rb +0 -113
  62. data/spec/representation/impersonation_representation_spec.rb +0 -163
  63. data/spec/representation/organization_representation_spec.rb +0 -64
  64. data/spec/representation/protocol_mapper_representation_spec.rb +0 -57
  65. data/spec/representation/role_representation_spec.rb +0 -37
  66. data/spec/representation/session_representation_spec.rb +0 -16
  67. data/spec/representation/user_representation_spec.rb +0 -15
  68. data/spec/resource/group_resource_spec.rb +0 -14
  69. data/spec/resource/user_resource_spec.rb +0 -14
  70. data/spec/spec_helper.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a9c09a6765355416960bbc5c4564ad5dd5f526f65093b43d46ac10fc39d0ae7
4
- data.tar.gz: b2dd1762448fad5a096b94adc3d18b30640235fe02efa1e77269797bfa3f9464
3
+ metadata.gz: 9bc43409191a911c85de270b70021cbe48d0372844c5a2a4d87409f250d6b646
4
+ data.tar.gz: eee1d04d6026a04da64769c02cbe49ec9f84d186a1fafefc45476925979333c6
5
5
  SHA512:
6
- metadata.gz: 3274b863ef8b7d295b82d5dd87a6d3340ac96ac08c1f38c822ad531b204f4fd5755704a905ccc308821e199d17cc8c73959c3dc1d39a5a13079ce133f1ed4854
7
- data.tar.gz: 7c8d7b5f54eb523f27eaaa83893d8b5190d4647ed6d7424a6ac1b55168ffa02fb426171b424330db86d976e00c84e1644d3bab3cf4fd398d1eb9cf4deff2e679
6
+ metadata.gz: 96fccdf9c111acf6cfc67d6d05f7fd3b7e87140292d63df5dd9fdfebb228c30595416ae23ee8e465e06be1b54e3be857d8a64f599a447d642082086054805eb4
7
+ data.tar.gz: c49b8b8e7b2a13ae05eb69c47c3a0ee4c935a4e8a689fbf8863d01f843f95ee2506460e2ccf852223586266ae3591ee9cefff83819d087410b2fe53ccfadf0c1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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
+ ## [2.0.2] - 2026-08-01
9
+
10
+ * [Breaking] HTTP failures now raise a `KeycloakAdmin::ApiError` subclass instead of a bare `RuntimeError`.
11
+ * Messages are unchanged, so code matching on the text keeps working.
12
+ * Code rescuing `RuntimeError` explicitly must be updated.
13
+ * A create call answering something other than `201 Created` now raises `KeycloakAdmin::UnexpectedResponseError`.
14
+ * [Feature] A `401` answer now drops the cached access token and replays the request once with a freshly fetched one, so a token revoked before its advertised expiry no longer fails every call until it lapses.
15
+ * [Fix] `GroupClient#members` called ActiveSupport's `Object#try` and raised `NoMethodError` outside a Rails application.
16
+ * [Fix] `UserClient#execute_actions_email` (and `#forgot_password`) called ActiveSupport's `Numeric#seconds` and raised `NoMethodError` outside a Rails application whenever a `lifespan` was passed.
17
+ * [Fix] `ClientAuthzPolicyClient#find_by` opened a second `?` in its URL, which folded `name`, `type`, `first` and `max` into the value of `permission`; Keycloak silently returned an unfiltered list.
18
+ * [Fix] Query parameters are now percent-encoded everywhere they are built. Search terms, and the `redirect_uri` of `UserClient#execute_actions_email`, containing a space, `&` or `=` used to break the URL or forge extra parameters.
19
+ * [Fix] `OrganizationClient#build` raised `NameError` instead of the intended `ArgumentError` when `domains` was not an `Array`.
20
+
8
21
  ## [2.0.1] - 2026-07-31
9
22
 
10
23
  * [Feature] Requests are now logged through `config.logger` (method, URL, and response status) via Faraday's `:logger` middleware.
data/README.md CHANGED
@@ -16,7 +16,7 @@ This gem *does not* require Rails.
16
16
  For example, using `bundle`, add this line to your Gemfile.
17
17
 
18
18
  ```ruby
19
- gem "keycloak-admin", "2.0.1"
19
+ gem "keycloak-admin", "2.0.2"
20
20
  ```
21
21
 
22
22
  ## Login
@@ -109,6 +109,42 @@ All options have a default value. However, all of them can be changed in your in
109
109
  | `faraday_options` | `{}`| Hash | Optional | Options to pass to `Faraday.new` (e.g. `request:`, `ssl:`, `proxy:`) | `{ request: { timeout: 5 } }` | 
110
110
 
111
111
 
112
+ ## Error handling
113
+
114
+ Every HTTP failure raises a `KeycloakAdmin::ApiError` carrying the `status`, `body` and `headers`
115
+ of the response, so you never have to parse the message. Rescue as narrowly or as broadly as you need:
116
+
117
+ ```ruby
118
+ begin
119
+ KeycloakAdmin.realm("a-realm").users.get(user_id)
120
+ rescue KeycloakAdmin::NotFoundError => e # exactly 404
121
+ nil
122
+ rescue KeycloakAdmin::ClientError => e # any 4xx
123
+ logger.warn("Rejected with #{e.status}: #{e.body}")
124
+ raise
125
+ rescue KeycloakAdmin::ServerError => e # any 5xx
126
+ raise
127
+ end
128
+ ```
129
+
130
+ | Class | Raised on |
131
+ | ---- | ---- |
132
+ | `KeycloakAdmin::Error` | Base class of every error this gem raises |
133
+ | `KeycloakAdmin::ApiError` | Any non-2xx answer; parent of the two families below |
134
+ | `KeycloakAdmin::ClientError` | Any 4xx |
135
+ | `KeycloakAdmin::BadRequestError` | 400 |
136
+ | `KeycloakAdmin::UnauthorizedError` | 401 |
137
+ | `KeycloakAdmin::ForbiddenError` | 403 |
138
+ | `KeycloakAdmin::NotFoundError` | 404 |
139
+ | `KeycloakAdmin::ConflictError` | 409 |
140
+ | `KeycloakAdmin::ServerError` | Any 5xx |
141
+ | `KeycloakAdmin::UnexpectedResponseError` | A create call answered something other than `201 Created` |
142
+
143
+ `Faraday::TimeoutError` and the other connection-level Faraday errors are left untouched and propagate as-is, since no response ever came back to describe.
144
+
145
+ A `401` is handled before it reaches you: the cached access token is dropped and the request is replayed once with a freshly fetched one, so a token revoked before its advertised expiry does not fail every call until it lapses `KeycloakAdmin::UnauthorizedError` is raised only if the replay is
146
+ refused too.
147
+
112
148
  ## Use Cases
113
149
 
114
150
  ### Supported features
@@ -348,7 +384,7 @@ KeycloakAdmin.realm("a_realm").groups.list
348
384
 
349
385
  Returns an array of `KeycloakAdmin::GroupRepresentation`.
350
386
 
351
- According to [the documentation](https://www.keycloak.org/docs-api/22.0.1/rest-api/index.html#_groups):
387
+ According to [the documentation](https://www.keycloak.org/docs-api/22.0.2/rest-api/index.html#_groups):
352
388
  * When providing a `String` parameter, this produces an arbitrary search string
353
389
  * When providing a `Hash`, you can specify other fields (_e.g_ q, max, first)
354
390
 
@@ -898,6 +934,6 @@ scoped RubyGems credential.
898
934
  3. Tag the commit and push the tag:
899
935
 
900
936
  ```
901
- $ git tag -a v2.0.1 -m "Version 2.0.1"
902
- $ git push origin v2.0.1
937
+ $ git tag -a v2.0.2 -m "Version 2.0.2"
938
+ $ git push origin v2.0.2
903
939
  ```
@@ -12,15 +12,23 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Keycloak Admin REST API client written in Ruby"
13
13
  spec.license = "MIT"
14
14
 
15
- spec.files = `git ls-files -z`.split("\x0")
15
+ spec.files = Dir.glob(["lib/**/*.rb", "CHANGELOG.md", "MIT-LICENSE", "README.md", "keycloak-admin.gemspec"]).sort
16
16
  spec.require_paths = ["lib"]
17
17
 
18
- spec.required_ruby_version = '>= 3.1'
18
+ spec.required_ruby_version = ">= 3.1"
19
+
20
+ spec.metadata = {
21
+ "source_code_uri" => spec.homepage,
22
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
23
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
24
+ "rubygems_mfa_required" => "true"
25
+ }
19
26
 
20
27
  spec.add_dependency "http-cookie", "~> 1.0", ">= 1.0.3"
21
28
  spec.add_dependency "faraday", "~> 2.0"
22
29
  spec.add_dependency "base64"
23
- spec.add_development_dependency "rspec", "3.13.2"
24
- spec.add_development_dependency "byebug", "12.0.0"
25
- spec.add_development_dependency "rake", ">= 13.0"
26
- end
30
+ spec.add_development_dependency "rspec", "3.13.2"
31
+ spec.add_development_dependency "byebug", "12.0.0"
32
+ spec.add_development_dependency "rake", ">= 13.0"
33
+ spec.add_development_dependency "webmock", "~> 3.0"
34
+ end
@@ -1,3 +1,5 @@
1
+ require "uri"
2
+
1
3
  module KeycloakAdmin
2
4
  class Client
3
5
 
@@ -25,16 +27,24 @@ module KeycloakAdmin
25
27
  end
26
28
 
27
29
  def execute_http
28
- yield
29
- rescue Faraday::TimeoutError
30
- raise
31
- rescue Faraday::ClientError, Faraday::ServerError => e
32
- http_error(e.response)
30
+ replayed = false
31
+ begin
32
+ yield
33
+ rescue Faraday::TimeoutError
34
+ raise
35
+ rescue Faraday::ClientError, Faraday::ServerError => e
36
+ if !replayed && e.response && e.response[:status] == 401 && !@configuration.cached_token.nil?
37
+ @configuration.clear_cached_token!
38
+ replayed = true
39
+ retry
40
+ end
41
+ raise ApiError.from_response(e.response)
42
+ end
33
43
  end
34
44
 
35
45
  def created_id(response)
36
46
  unless response.status == 201
37
- raise "Create method returned status #{response.reason_phrase} (Code: #{response.status}); expected status: Created (201)"
47
+ raise UnexpectedResponseError.new(response.status, response.reason_phrase)
38
48
  end
39
49
  (_head, _separator, id) = response.headers[:location].rpartition("/")
40
50
  id
@@ -52,6 +62,12 @@ module KeycloakAdmin
52
62
 
53
63
  private
54
64
 
65
+ def build_query(parameters)
66
+ parameters.map do |name, value|
67
+ "#{URI.encode_uri_component(name.to_s)}=#{URI.encode_uri_component(value.to_s)}"
68
+ end.join("&")
69
+ end
70
+
55
71
  def fetch_token
56
72
  KeycloakAdmin.create_client(@configuration, @configuration.client_realm_name).token.get
57
73
  end
@@ -59,9 +75,5 @@ module KeycloakAdmin
59
75
  def resource(url)
60
76
  Resource.new(url, @configuration.faraday_options, @configuration.logger)
61
77
  end
62
-
63
- def http_error(response)
64
- raise "Keycloak: The request failed with response code #{response[:status]} and message: #{response[:body]}"
65
- end
66
78
  end
67
79
  end
@@ -20,7 +20,7 @@ module KeycloakAdmin
20
20
 
21
21
  def find_by(name, resource_param, scope = nil)
22
22
  response = execute_http do
23
- url = "#{authz_permission_url(@client_id)}?name=#{name}&resource=#{resource_param}&type=#{@type}&scope=#{scope}&deep=true&first=0&max=100"
23
+ url = "#{authz_permission_url(@client_id)}?#{build_query(name: name, resource: resource_param, type: @type, scope: scope, deep: true, first: 0, max: 100)}"
24
24
  resource(url).get(headers)
25
25
  end
26
26
  JSON.parse(response).map { |role_as_hash| ClientAuthzPermissionRepresentation.from_hash(role_as_hash) }
@@ -33,7 +33,7 @@ module KeycloakAdmin
33
33
 
34
34
  def find_by(name, type)
35
35
  response = execute_http do
36
- url = "#{authz_policy_url(@client_id, @type)}?permission=false&name=#{name}&type=#{type}&first=0&max=11"
36
+ url = "#{authz_policy_url(@client_id, @type)}&#{build_query(name: name, type: type, first: 0, max: 11)}"
37
37
  resource(url).get(headers)
38
38
  end
39
39
  JSON.parse(response).map { |role_as_hash| ClientAuthzPolicyRepresentation.from_hash(role_as_hash) }
@@ -47,7 +47,7 @@ module KeycloakAdmin
47
47
 
48
48
  def find_by(name, type, uris, owner, scope)
49
49
  response = execute_http do
50
- url = "#{authz_resources_url(@client_id)}?name=#{name}&type=#{type}&uris=#{uris}&owner=#{owner}&scope=#{scope}&deep=true&first=0&max=100"
50
+ url = "#{authz_resources_url(@client_id)}?#{build_query(name: name, type: type, uris: uris, owner: owner, scope: scope, deep: true, first: 0, max: 100)}"
51
51
  resource(url).get(headers)
52
52
  end
53
53
  JSON.parse(response).map { |role_as_hash| ClientAuthzResourceRepresentation.from_hash(role_as_hash) }
@@ -35,7 +35,7 @@ module KeycloakAdmin
35
35
  end
36
36
 
37
37
  def search(name)
38
- url = "#{authz_scopes_url(@client_id)}?first=0&max=11&deep=false&name=#{name}"
38
+ url = "#{authz_scopes_url(@client_id)}?#{build_query(first: 0, max: 11, deep: false, name: name)}"
39
39
  response = execute_http do
40
40
  resource(url).get(headers)
41
41
  end
@@ -75,11 +75,8 @@ module KeycloakAdmin
75
75
 
76
76
  def members(group_id, first=0, max=100)
77
77
  url = "#{groups_url(group_id)}/members"
78
- query = {first: first.try(:to_i), max: max.try(:to_i)}.compact
79
- unless query.empty?
80
- query_string = query.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
81
- url = "#{url}?#{query_string}"
82
- end
78
+ query = {first: first&.to_i, max: max&.to_i}.compact
79
+ url = "#{url}?#{build_query(query)}" unless query.empty?
83
80
  response = execute_http do
84
81
  resource(url).get(headers)
85
82
  end
@@ -170,24 +170,24 @@ module KeycloakAdmin
170
170
  end
171
171
 
172
172
  def count_url(exact, query, search)
173
- query_parameters = {exact: exact, q: query, search: search}.compact.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
173
+ query_parameters = build_query({exact: exact, q: query, search: search}.compact)
174
174
  "#{organizations_url}/count?#{query_parameters}"
175
175
  end
176
176
 
177
177
  def organizations_url_with_parameters(brief_representation, exact, first, max, query, search)
178
- query_parameters = {
178
+ query_parameters = build_query({
179
179
  briefRepresentation: brief_representation,
180
180
  exact: exact,
181
181
  first: first,
182
182
  max: max,
183
183
  q: query,
184
184
  search: search
185
- }.compact.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
185
+ }.compact)
186
186
  "#{organizations_url}?#{query_parameters}"
187
187
  end
188
188
 
189
189
  def associated_with_member_url(member_id, brief_representation=true)
190
- "#{organizations_url}/members/#{member_id}/organizations?briefRepresentation=#{brief_representation}"
190
+ "#{organizations_url}/members/#{member_id}/organizations?#{build_query(briefRepresentation: brief_representation)}"
191
191
  end
192
192
 
193
193
  def members_count_url(organization_id)
@@ -212,19 +212,19 @@ module KeycloakAdmin
212
212
  end
213
213
 
214
214
  def members_url_with_query_parameters(organization_id, exact, first, max, membership_type, search)
215
- query_parameters = {
215
+ query_parameters = build_query({
216
216
  exact: exact,
217
217
  first: first,
218
218
  max: max,
219
219
  membershipType: membership_type,
220
220
  search: search
221
- }.compact.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
221
+ }.compact)
222
222
  "#{organization_url(organization_id)}/members?#{query_parameters}"
223
223
  end
224
224
 
225
225
  def build(name, alias_name, enabled, description, redirect_url=nil, domains=[], attributes={})
226
226
  unless domains.is_a?(Array)
227
- raise ArgumentError.new("domains must be an Array, got #{new_domains.class}")
227
+ raise ArgumentError.new("domains must be an Array, got #{domains.class}")
228
228
  end
229
229
 
230
230
  unless domains.all? { |domain| domain.is_a?(KeycloakAdmin::OrganizationDomainRepresentation) }
@@ -141,10 +141,14 @@ module KeycloakAdmin
141
141
  def execute_actions_email(user_id, actions=[], lifespan=nil, redirect_uri=nil, client_id=nil)
142
142
  raise ArgumentError.new("client_id must be defined") if client_id.nil? && !redirect_uri.nil?
143
143
  execute_http do
144
- lifespan_param = lifespan.nil? ? "" : "&lifespan=#{lifespan.seconds}"
145
- redirect_uri_param = redirect_uri.nil? ? "" : "&redirect_uri=#{redirect_uri}"
146
- client_id_param = client_id.nil? ? "" : "client_id=#{client_id}"
147
- Resource.put("#{execute_actions_email_url(user_id)}?#{client_id_param}#{redirect_uri_param}#{lifespan_param}", create_payload(actions), headers, @configuration.logger)
144
+ query = {}
145
+ query[:client_id] = client_id unless client_id.nil?
146
+ query[:redirect_uri] = redirect_uri unless redirect_uri.nil?
147
+ query[:lifespan] = lifespan.to_i unless lifespan.nil?
148
+
149
+ url = execute_actions_email_url(user_id)
150
+ url = "#{url}?#{build_query(query)}" unless query.empty?
151
+ Resource.put(url, create_payload(actions), headers, @configuration.logger)
148
152
  end
149
153
  user_id
150
154
  end
@@ -0,0 +1,62 @@
1
+ module KeycloakAdmin
2
+ class Error < StandardError; end
3
+
4
+ # Raised when Keycloak answers with a non-2xx status.
5
+ #
6
+ # Callers can rescue the precise failure (KeycloakAdmin::NotFoundError), a whole family
7
+ # (KeycloakAdmin::ClientError for any 4xx) or every HTTP failure (KeycloakAdmin::ApiError),
8
+ # and read #status/#body off the exception rather than parsing the message.
9
+ class ApiError < Error
10
+ attr_reader :status, :body, :headers
11
+
12
+ def initialize(status, body, headers = {})
13
+ @status = status
14
+ @body = body
15
+ @headers = headers || {}
16
+ super("Keycloak: The request failed with response code #{status} and message: #{body}")
17
+ end
18
+
19
+ def self.from_response(response)
20
+ response ||= {}
21
+ status = response[:status]
22
+ error_class_for(status).new(status, response[:body], response[:headers])
23
+ end
24
+
25
+ def self.error_class_for(status)
26
+ case status
27
+ when 400 then BadRequestError
28
+ when 401 then UnauthorizedError
29
+ when 403 then ForbiddenError
30
+ when 404 then NotFoundError
31
+ when 409 then ConflictError
32
+ when 400..499 then ClientError
33
+ when 500..599 then ServerError
34
+ else ApiError
35
+ end
36
+ end
37
+ end
38
+
39
+ # Any 4xx: the request itself was rejected.
40
+ class ClientError < ApiError; end
41
+
42
+ class BadRequestError < ClientError; end
43
+ class UnauthorizedError < ClientError; end
44
+ class ForbiddenError < ClientError; end
45
+ class NotFoundError < ClientError; end
46
+ class ConflictError < ClientError; end
47
+
48
+ # Any 5xx: Keycloak accepted the request but failed to serve it.
49
+ class ServerError < ApiError; end
50
+
51
+ # Raised when a create call succeeds at the HTTP level but answers something other than
52
+ # 201 Created, leaving no Location header to read the new resource's id from.
53
+ class UnexpectedResponseError < Error
54
+ attr_reader :status, :reason_phrase
55
+
56
+ def initialize(status, reason_phrase)
57
+ @status = status
58
+ @reason_phrase = reason_phrase
59
+ super("Create method returned status #{reason_phrase} (Code: #{status}); expected status: Created (201)")
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,3 @@
1
1
  module KeycloakAdmin
2
- VERSION = "2.0.1"
2
+ VERSION = "2.0.2"
3
3
  end
@@ -1,5 +1,6 @@
1
1
  require "logger"
2
2
 
3
+ require_relative "keycloak-admin/error"
3
4
  require_relative "keycloak-admin/configuration"
4
5
  require_relative "keycloak-admin/client/response"
5
6
  require_relative "keycloak-admin/client/resource"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
@@ -99,6 +99,20 @@ dependencies:
99
99
  - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '13.0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: webmock
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '3.0'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.0'
102
116
  description: Keycloak Admin REST API client written in Ruby
103
117
  email:
104
118
  - lorent.lempereur.dev@gmail.com
@@ -106,19 +120,9 @@ executables: []
106
120
  extensions: []
107
121
  extra_rdoc_files: []
108
122
  files:
109
- - ".github/workflows/Dockerfile"
110
- - ".github/workflows/ci.yml"
111
- - ".github/workflows/release.yml"
112
- - ".gitignore"
113
- - ".rspec"
114
123
  - CHANGELOG.md
115
- - Dockerfile
116
- - Gemfile
117
- - Gemfile.lock
118
124
  - MIT-LICENSE
119
125
  - README.md
120
- - Rakefile
121
- - bin/console
122
126
  - keycloak-admin.gemspec
123
127
  - lib/keycloak-admin.rb
124
128
  - lib/keycloak-admin/client/attack_detection_client.rb
@@ -144,6 +148,7 @@ files:
144
148
  - lib/keycloak-admin/client/token_client.rb
145
149
  - lib/keycloak-admin/client/user_client.rb
146
150
  - lib/keycloak-admin/configuration.rb
151
+ - lib/keycloak-admin/error.rb
147
152
  - lib/keycloak-admin/representation/attack_detection_representation.rb
148
153
  - lib/keycloak-admin/representation/camel_json.rb
149
154
  - lib/keycloak-admin/representation/client_authz_permission_representation.rb
@@ -174,54 +179,14 @@ files:
174
179
  - lib/keycloak-admin/resource/group_resource.rb
175
180
  - lib/keycloak-admin/resource/user_resource.rb
176
181
  - lib/keycloak-admin/version.rb
177
- - spec/client/attack_detection_client_spec.rb
178
- - spec/client/client_authz_permission_client_spec.rb
179
- - spec/client/client_authz_policy_client_spec.rb
180
- - spec/client/client_authz_resource_client_spec.rb
181
- - spec/client/client_authz_scope_client_spec.rb
182
- - spec/client/client_client_spec.rb
183
- - spec/client/client_role_mappings_client_spec.rb
184
- - spec/client/client_scope_client_spec.rb
185
- - spec/client/client_scope_protocol_mapper_client_spec.rb
186
- - spec/client/client_spec.rb
187
- - spec/client/configurable_token_client_spec.rb
188
- - spec/client/group_client_spec.rb
189
- - spec/client/identity_provider_client_spec.rb
190
- - spec/client/organization_client_spec.rb
191
- - spec/client/realm_client_spec.rb
192
- - spec/client/resource_spec.rb
193
- - spec/client/response_spec.rb
194
- - spec/client/role_client_spec.rb
195
- - spec/client/role_mapper_client_spec.rb
196
- - spec/client/token_client_spec.rb
197
- - spec/client/user_client_spec.rb
198
- - spec/configuration_spec.rb
199
- - spec/gemspec_spec.rb
200
- - spec/integration/client_authorization_spec.rb
201
- - spec/representation/attack_detection_representation_spec.rb
202
- - spec/representation/client_authz_permission_representation_spec.rb
203
- - spec/representation/client_authz_policy_representation_spec.rb
204
- - spec/representation/client_authz_resource_representation_spec.rb
205
- - spec/representation/client_authz_scope_representation_spec.rb
206
- - spec/representation/client_representation_spec.rb
207
- - spec/representation/client_scope_representation_spec.rb
208
- - spec/representation/credential_representation_spec.rb
209
- - spec/representation/group_representation_spec.rb
210
- - spec/representation/identity_provider_mapper_representation_spec.rb
211
- - spec/representation/identity_provider_representation_spec.rb
212
- - spec/representation/impersonation_representation_spec.rb
213
- - spec/representation/organization_representation_spec.rb
214
- - spec/representation/protocol_mapper_representation_spec.rb
215
- - spec/representation/role_representation_spec.rb
216
- - spec/representation/session_representation_spec.rb
217
- - spec/representation/user_representation_spec.rb
218
- - spec/resource/group_resource_spec.rb
219
- - spec/resource/user_resource_spec.rb
220
- - spec/spec_helper.rb
221
182
  homepage: https://github.com/looorent/keycloak-admin-ruby
222
183
  licenses:
223
184
  - MIT
224
- metadata: {}
185
+ metadata:
186
+ source_code_uri: https://github.com/looorent/keycloak-admin-ruby
187
+ changelog_uri: https://github.com/looorent/keycloak-admin-ruby/blob/main/CHANGELOG.md
188
+ bug_tracker_uri: https://github.com/looorent/keycloak-admin-ruby/issues
189
+ rubygems_mfa_required: 'true'
225
190
  rdoc_options: []
226
191
  require_paths:
227
192
  - lib
@@ -1,24 +0,0 @@
1
- ### Dockerfile for: tillawy/keycloak-github-actions
2
- ##
3
- ## To build & push
4
- # docker buildx build . --platform linux/amd64 -t tillawy/keycloak-github-actions:25.0.1
5
- # docker push tillawy/keycloak-github-actions:25.0.1
6
- #
7
- ## To Run Locally
8
- # docker run \
9
- # --rm \
10
- # -p 8080:8080 \
11
- # -e KEYCLOAK_ADMIN="admin" \
12
- # -e KEYCLOAK_ADMIN_PASSWORD="admin" \
13
- # -e KC_HOSTNAME="http://localhost:8080" \
14
- # -e KC_HOSTNAME_ADMIN="http://localhost:8080" \
15
- # -e KC_HTTP_ENABLED="true" \
16
- # -e KC_DB="dev-file" \
17
- # tillawy/keycloak-github-actions:25.0.1
18
-
19
- FROM quay.io/keycloak/keycloak:25.0.1
20
-
21
- ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
22
-
23
- CMD ["start-dev", "--hostname=http://localhost:8080" , "--hostname-admin=http://localhost:8080" , "--http-enabled=true", "--verbose"]
24
-
@@ -1,94 +0,0 @@
1
- # This workflow uses actions that are not certified by GitHub.
2
- # They are provided by a third-party and are governed by
3
- # separate terms of service, privacy policy, and support
4
- # documentation.
5
- # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
- # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
-
8
- name: Ruby
9
-
10
- on:
11
- push:
12
- branches: [ "main" ]
13
- pull_request:
14
- branches: [ "main" ]
15
-
16
- permissions:
17
- contents: read
18
-
19
- jobs:
20
- test:
21
- name: Ruby ${{ matrix.ruby-version }}
22
- runs-on: ubuntu-24.04
23
-
24
- strategy:
25
- fail-fast: false
26
- matrix:
27
- ruby-version: ['3.1', '3.2', '3.3', '3.4']
28
-
29
- steps:
30
- - name: Start Keycloak
31
- run: |
32
- docker run -d --name keycloak -p 8080:8080 \
33
- -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
34
- -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
35
- -e KC_HOSTNAME=http://localhost:8080 \
36
- -e KC_HOSTNAME_ADMIN=http://localhost:8080 \
37
- -e KC_HTTP_ENABLED=true \
38
- -e KC_DB=dev-file \
39
- quay.io/keycloak/keycloak:26.7.0 \
40
- start-dev
41
-
42
- - name: Wait for Keycloak to be ready
43
- run: |
44
- for i in $(seq 1 30); do
45
- if curl --silent --fail http://localhost:8080/realms/master > /dev/null; then
46
- echo "Keycloak is ready"
47
- exit 0
48
- fi
49
- sleep 5
50
- done
51
- echo "Keycloak did not become ready in time"
52
- docker logs keycloak
53
- exit 1
54
-
55
- - name: create realm
56
- run: |
57
- TOKEN=$(curl --silent --location --request POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
58
- --header 'Content-Type: application/x-www-form-urlencoded' \
59
- --data-urlencode 'grant_type=password' \
60
- --data-urlencode 'username=admin' \
61
- --data-urlencode 'password=admin' \
62
- --data-urlencode 'client_id=admin-cli' | jq -r '.access_token')
63
-
64
- curl --silent --show-error -L -X POST "http://localhost:8080/admin/realms" \
65
- --header "Content-Type: application/json" \
66
- --header "Authorization: Bearer ${TOKEN}" \
67
- --data '{"realm":"dummy","enabled":true}'
68
-
69
- curl --silent --show-error --request POST 'http://localhost:8080/admin/realms/dummy/clients' \
70
- --header 'Authorization: Bearer '$TOKEN \
71
- --header 'Content-Type: application/json' \
72
- --data-raw '{
73
- "clientId":"dummy-client",
74
- "enabled":true,
75
- "consentRequired": false,
76
- "attributes":{},
77
- "serviceAccountsEnabled": true,
78
- "protocol":"openid-connect",
79
- "publicClient":false,
80
- "authorizationServicesEnabled": true,
81
- "clientAuthenticatorType":"client-secret",
82
- "redirectUris":["http://localhost:8180/demo"]
83
- }'
84
-
85
- - uses: actions/checkout@v6
86
- - name: Set up Ruby
87
- uses: ruby/setup-ruby@v1
88
- with:
89
- ruby-version: ${{ matrix.ruby-version }}
90
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
91
- - name: Run tests
92
- run: bundle exec rspec
93
- env:
94
- GITHUB_ACTIONS: true