keycloak-admin 1.1.3 → 1.1.5

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.
@@ -19,6 +19,6 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_dependency "http-cookie", "~> 1.0", ">= 1.0.3"
21
21
  spec.add_dependency "rest-client", "~> 2.0"
22
- spec.add_development_dependency "rspec", "3.12.0"
23
- spec.add_development_dependency "byebug", "11.1.3"
22
+ spec.add_development_dependency "rspec", "3.13.2"
23
+ spec.add_development_dependency "byebug", "12.0.0"
24
24
  end
@@ -108,6 +108,23 @@ module KeycloakAdmin
108
108
  role_representation
109
109
  end
110
110
 
111
+ # Remove a realm-level role from a group by the role name
112
+ def remove_realm_level_role_name!(group_id, role_name)
113
+ role_representation = RoleClient.new(@configuration, @realm_client).get(role_name)
114
+ url = "#{groups_url(group_id)}/role-mappings/realm"
115
+ execute_http do
116
+ RestClient::Request.execute(
117
+ @configuration.rest_client_options.merge(
118
+ url:,
119
+ method: :delete,
120
+ payload: create_payload([role_representation]),
121
+ headers: headers
122
+ )
123
+ )
124
+ end
125
+ true
126
+ end
127
+
111
128
  def groups_url(id=nil)
112
129
  if id
113
130
  "#{@realm_client.realm_admin_url}/groups/#{id}"
@@ -23,10 +23,12 @@ module KeycloakAdmin
23
23
  def remove_realm_level(role_representation_list)
24
24
  execute_http do
25
25
  RestClient::Request.execute(
26
- method: :delete,
27
- url: realm_level_url,
28
- payload: create_payload(role_representation_list),
29
- headers: headers
26
+ @configuration.rest_client_options.merge(
27
+ method: :delete,
28
+ url: realm_level_url,
29
+ payload: create_payload(role_representation_list),
30
+ headers: headers
31
+ )
30
32
  )
31
33
  end
32
34
  end
@@ -123,6 +123,13 @@ module KeycloakAdmin
123
123
  user_id
124
124
  end
125
125
 
126
+ def credentials(user_id)
127
+ response = execute_http do
128
+ RestClient::Resource.new(credentials_url(user_id), @configuration.rest_client_options).get(headers)
129
+ end
130
+ JSON.parse(response).map { |group_as_hash| CredentialRepresentation.from_hash(group_as_hash) }
131
+ end
132
+
126
133
  def forgot_password(user_id, lifespan=nil)
127
134
  execute_actions_email(user_id, ["UPDATE_PASSWORD"], lifespan)
128
135
  end
@@ -232,6 +239,11 @@ module KeycloakAdmin
232
239
  "#{users_url(user_id)}/groups"
233
240
  end
234
241
 
242
+ def credentials_url(user_id)
243
+ raise ArgumentError.new("user_id must be defined") if user_id.nil?
244
+ "#{users_url(user_id)}/credentials"
245
+ end
246
+
235
247
  def impersonation_url(user_id)
236
248
  raise ArgumentError.new("user_id must be defined") if user_id.nil?
237
249
  "#{users_url(user_id)}/impersonation"
@@ -1,6 +1,8 @@
1
1
  module KeycloakAdmin
2
2
  class CredentialRepresentation < Representation
3
- attr_accessor :type,
3
+ attr_accessor :id,
4
+ :type,
5
+ :userLabel,
4
6
  :device,
5
7
  :value,
6
8
  :hashedSaltedValue,
@@ -10,7 +12,9 @@ module KeycloakAdmin
10
12
  :algorithm,
11
13
  :digits,
12
14
  :period,
13
- :created_date,
15
+ :createdDate,
16
+ :credentialData,
17
+ :secretData,
14
18
  :config,
15
19
  :temporary
16
20
 
@@ -30,10 +34,39 @@ module KeycloakAdmin
30
34
  def self.from_hash(hash)
31
35
  credential = new
32
36
  hash.each do |key, value|
33
- property = "@#{key}".to_sym
34
- credential.instance_variable_set(property, value)
37
+ if credential.respond_to?("#{key}=")
38
+ credential.public_send("#{key}=", value)
39
+ end
35
40
  end
41
+
42
+ nested_attributes = safely_parse_nested_json(hash["credentialData"]).merge(safely_parse_nested_json(hash["secretData"]))
43
+
44
+ nested_attributes.each do |key, value|
45
+ if credential.respond_to?("#{key}=")
46
+ current_value = credential.public_send(key)
47
+ if current_value.nil?
48
+ credential.public_send("#{key}=", value)
49
+ end
50
+ end
51
+ end
52
+
36
53
  credential
37
54
  end
55
+
56
+ class << self
57
+ private
58
+
59
+ def safely_parse_nested_json(json_string)
60
+ if json_string.nil? || json_string.strip.empty?
61
+ {}
62
+ else
63
+ begin
64
+ JSON.parse(json_string)
65
+ rescue JSON::ParserError
66
+ {}
67
+ end
68
+ end
69
+ end
70
+ end
38
71
  end
39
72
  end
@@ -1,3 +1,3 @@
1
1
  module KeycloakAdmin
2
- VERSION = "1.1.3"
2
+ VERSION = "1.1.5"
3
3
  end
@@ -31,7 +31,7 @@ RSpec.describe KeycloakAdmin::ClientAuthzPermissionClient do
31
31
  it "does not raise any error" do
32
32
  expect {
33
33
  @realm.authz_permissions("", type)
34
- }.to raise_error
34
+ }.to raise_error(ArgumentError)
35
35
  end
36
36
  end
37
37
  end
@@ -31,7 +31,7 @@ RSpec.describe KeycloakAdmin::ClientAuthzPolicyClient do
31
31
  it "does not raise any error" do
32
32
  expect {
33
33
  @realm.authz_policies("", type)
34
- }.to raise_error
34
+ }.to raise_error(ArgumentError)
35
35
  end
36
36
  end
37
37
  end
@@ -255,4 +255,74 @@ RSpec.describe KeycloakAdmin::GroupClient do
255
255
  expect { @group_client.delete("test_group_id") }.to raise_error("error")
256
256
  end
257
257
  end
258
+
259
+ describe '#get_realm_level_roles' do
260
+ let(:realm_name) { 'valid-realm' }
261
+ before(:each) do
262
+ @group_client = KeycloakAdmin.realm(realm_name).groups
263
+ stub_token_client
264
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return '[{"id":"role-id","name":"role-name"}]'
265
+ end
266
+
267
+ it 'gets all realm-level roles for a group' do
268
+ roles = @group_client.get_realm_level_roles('test-group-id')
269
+ expect(roles.length).to eq 1
270
+ expect(roles[0].id).to eq 'role-id'
271
+ expect(roles[0].name).to eq 'role-name'
272
+ end
273
+ end
274
+
275
+ describe '#add_realm_level_role_name!' do
276
+ let(:realm_name) { 'valid-realm' }
277
+
278
+ before(:each) do
279
+ @group_client = KeycloakAdmin.realm(realm_name).groups
280
+
281
+ stub_token_client
282
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ''
283
+ end
284
+
285
+ it 'adds a realm-level role to a group' do
286
+ role_representation = double
287
+ allow(role_representation).to receive(:name).and_return 'test-role-name'
288
+
289
+ role_client = double
290
+ allow(role_client).to receive(:get).with('test-role-name').and_return role_representation
291
+ allow(KeycloakAdmin::RoleClient).to receive(:new).and_return role_client
292
+
293
+ result = @group_client.add_realm_level_role_name!('test-group-id', 'test-role-name')
294
+ expect(result).to eq role_representation
295
+ end
296
+ end
297
+
298
+ describe '#remove_realm_level_role_name!' do
299
+ let(:realm_name) { 'valid-realm' }
300
+
301
+ before(:each) do
302
+ @group_client = KeycloakAdmin.realm(realm_name).groups
303
+
304
+ stub_token_client
305
+ allow(RestClient::Request).to receive(:execute).and_return ''
306
+ end
307
+
308
+ it 'deletes a realm-level role from a group' do
309
+ role_representation = double
310
+ allow(role_representation).to receive(:name).and_return 'test-role-name'
311
+
312
+ role_client = double
313
+ allow(role_client).to receive(:get).with('test-role-name').and_return role_representation
314
+ allow(KeycloakAdmin::RoleClient).to receive(:new).and_return role_client
315
+
316
+ result = @group_client.remove_realm_level_role_name!('test-group-id', 'test-role-name')
317
+ expect(result).to be(true)
318
+ expect(RestClient::Request).to have_received(:execute).with(
319
+ hash_including(
320
+ url: "http://auth.service.io/auth/admin/realms/valid-realm/groups/test-group-id/role-mappings/realm",
321
+ method: :delete,
322
+ payload: @group_client.send(:create_payload, [role_representation]),
323
+ headers: @group_client.send(:headers)
324
+ )
325
+ )
326
+ end
327
+ end
258
328
  end
@@ -65,4 +65,49 @@ RSpec.describe KeycloakAdmin::RoleMapperClient do
65
65
  @role_mapper_client.save_realm_level(role_list)
66
66
  end
67
67
  end
68
+
69
+ describe "#remove_realm_level" do
70
+ let(:realm_name) { "valid-realm" }
71
+ let(:user_id) { "test_user" }
72
+ let(:role_list) { [
73
+ KeycloakAdmin::RoleRepresentation.from_hash(
74
+ "id" => "d9e3376b-f602-4086-8eee-89fea73c73ea"
75
+ )
76
+ ] }
77
+ let(:expected_url) { "http://auth.service.io/auth/admin/realms/valid-realm/users/test_user/role-mappings/realm" }
78
+
79
+ before(:each) do
80
+ @role_mapper_client = KeycloakAdmin.realm(realm_name).user(user_id).role_mapper
81
+
82
+ stub_token_client
83
+ end
84
+
85
+ it "removes realm-level role mappings" do
86
+ expect(RestClient::Request).to receive(:execute).with(
87
+ hash_including(
88
+ method: :delete,
89
+ url: expected_url,
90
+ payload: role_list.to_json
91
+ )
92
+ )
93
+
94
+ @role_mapper_client.remove_realm_level(role_list)
95
+ end
96
+
97
+ it "passes rest client options" do
98
+ rest_client_options = {timeout: 10}
99
+ allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:rest_client_options).and_return rest_client_options
100
+
101
+ expect(RestClient::Request).to receive(:execute).with(
102
+ hash_including(
103
+ method: :delete,
104
+ url: expected_url,
105
+ payload: role_list.to_json,
106
+ timeout: 10
107
+ )
108
+ )
109
+
110
+ @role_mapper_client.remove_realm_level(role_list)
111
+ end
112
+ end
68
113
  end
@@ -370,4 +370,49 @@ RSpec.describe KeycloakAdmin::TokenClient do
370
370
  end
371
371
  end
372
372
  end
373
+
374
+ describe '#credentials' do
375
+ let(:realm_name) { "valid-realm" }
376
+
377
+ before(:each) do
378
+ @user_client = KeycloakAdmin.realm(realm_name).users
379
+ stub_token_client
380
+ json_payload = <<-'payload'
381
+ [
382
+ {
383
+ "id": "2ff4b4d0-fd72-4c6e-9684-02ab337687c2",
384
+ "type": "password",
385
+ "userLabel": "My password",
386
+ "createdDate": 1767604673211,
387
+ "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
388
+ },
389
+ {
390
+ "id": "34389672-9356-4154-9ed6-6c212b869010",
391
+ "type": "otp",
392
+ "userLabel": "Smartphone",
393
+ "createdDate": 1767605202060,
394
+ "credentialData": "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}"
395
+ }
396
+ ]
397
+ payload
398
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
399
+ end
400
+
401
+ context 'when user_id is defined' do
402
+ let(:user_id) { '95985b21-d884-4bbd-b852-cb8cd365afc2' }
403
+ it 'returns list of credentials' do
404
+ response = @user_client.credentials(user_id)
405
+ expect(response.size).to eq 2
406
+ expect(response[0].id).to eq "2ff4b4d0-fd72-4c6e-9684-02ab337687c2"
407
+ expect(response[1].id).to eq "34389672-9356-4154-9ed6-6c212b869010"
408
+ end
409
+ end
410
+
411
+ context 'when user_id is not defined' do
412
+ let(:user_id) { nil }
413
+ it 'raise argument error' do
414
+ expect { @user_client.credentials(user_id) }.to raise_error(ArgumentError)
415
+ end
416
+ end
417
+ end
373
418
  end
@@ -1,7 +1,7 @@
1
1
  RSpec.describe 'ClientAuthorization' do
2
2
 
3
- before do
4
- skip unless ENV["GITHUB_ACTIONS"]
3
+ before(:each) do
4
+ skip("This test requires to be run in a Github action.") unless ENV["GITHUB_ACTIONS"]
5
5
 
6
6
  KeycloakAdmin.configure do |config|
7
7
  config.use_service_account = false
@@ -14,14 +14,12 @@ RSpec.describe 'ClientAuthorization' do
14
14
  end
15
15
  end
16
16
 
17
- after do
17
+ after(:each) do
18
18
  configure
19
19
  end
20
20
 
21
21
  describe "ClientAuthorization Suite" do
22
22
  it do
23
- skip unless ENV["GITHUB_ACTIONS"]
24
-
25
23
  realm_name = "dummy"
26
24
 
27
25
  client = KeycloakAdmin.realm(realm_name).clients.find_by_client_id("dummy-client")
@@ -0,0 +1,68 @@
1
+
2
+ RSpec.describe KeycloakAdmin::CredentialRepresentation do
3
+ describe ".from_json" do
4
+ it "parses a password" do
5
+ json_payload = <<-'payload'
6
+ {
7
+ "id": "2ff4b4d0-fd72-4c6e-9684-02ab337687c2",
8
+ "type": "password",
9
+ "userLabel": "My password",
10
+ "createdDate": 1767604673211,
11
+ "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
12
+ }
13
+ payload
14
+
15
+ credential = described_class.from_json(json_payload)
16
+ expect(credential).to be
17
+ expect(credential).to be_a described_class
18
+ expect(credential.id).to eq "2ff4b4d0-fd72-4c6e-9684-02ab337687c2"
19
+ expect(credential.type).to eq "password"
20
+ expect(credential.createdDate).to eq 1767604673211
21
+ expect(credential.credentialData).to eq "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
22
+ expect(credential.userLabel).to eq "My password"
23
+ expect(credential.device).to be_nil
24
+ expect(credential.value).to be_nil
25
+ expect(credential.hashedSaltedValue).to be_nil
26
+ expect(credential.salt).to be_nil
27
+ expect(credential.hashIterations).to eq 5
28
+ expect(credential.counter).to be_nil
29
+ expect(credential.algorithm).to eq "argon2"
30
+ expect(credential.digits).to be_nil
31
+ expect(credential.period).to be_nil
32
+ expect(credential.config).to be_nil
33
+ expect(credential.temporary).to be_nil
34
+ end
35
+
36
+ it "parses an otp" do
37
+ json_payload = <<-'payload'
38
+ {
39
+ "id": "34389672-9356-4154-9ed6-6c212b869010",
40
+ "type": "otp",
41
+ "userLabel": "Smartphone",
42
+ "createdDate": 1767605202060,
43
+ "credentialData": "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}"
44
+ }
45
+ payload
46
+
47
+ credential = described_class.from_json(json_payload)
48
+ expect(credential).to be
49
+ expect(credential).to be_a described_class
50
+ expect(credential.id).to eq "34389672-9356-4154-9ed6-6c212b869010"
51
+ expect(credential.type).to eq "otp"
52
+ expect(credential.createdDate).to eq 1767605202060
53
+ expect(credential.credentialData).to eq "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}"
54
+ expect(credential.userLabel).to eq "Smartphone"
55
+ expect(credential.device).to be_nil
56
+ expect(credential.value).to be_nil
57
+ expect(credential.hashedSaltedValue).to be_nil
58
+ expect(credential.salt).to be_nil
59
+ expect(credential.hashIterations).to be_nil
60
+ expect(credential.counter).to eq 0
61
+ expect(credential.algorithm).to eq "HmacSHA1"
62
+ expect(credential.digits).to eq 6
63
+ expect(credential.period).to eq 30
64
+ expect(credential.config).to be_nil
65
+ expect(credential.temporary).to be_nil
66
+ end
67
+ end
68
+ 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.3
4
+ version: 1.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-12 00:00:00.000000000 Z
11
+ date: 2026-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-cookie
@@ -50,28 +50,28 @@ dependencies:
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 3.12.0
53
+ version: 3.13.2
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 3.12.0
60
+ version: 3.13.2
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: byebug
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 11.1.3
67
+ version: 12.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: 11.1.3
74
+ version: 12.0.0
75
75
  description: Keycloak Admin REST API client written in Ruby
76
76
  email:
77
77
  - lorent.lempereur.dev@gmail.com
@@ -160,6 +160,7 @@ files:
160
160
  - spec/representation/client_authz_resource_representation_spec.rb
161
161
  - spec/representation/client_authz_scope_representation_spec.rb
162
162
  - spec/representation/client_representation_spec.rb
163
+ - spec/representation/credential_representation_spec.rb
163
164
  - spec/representation/group_representation_spec.rb
164
165
  - spec/representation/identity_provider_mapper_representation_spec.rb
165
166
  - spec/representation/identity_provider_representation_spec.rb
@@ -190,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
191
  - !ruby/object:Gem::Version
191
192
  version: '0'
192
193
  requirements: []
193
- rubygems_version: 3.5.11
194
+ rubygems_version: 3.3.7
194
195
  signing_key:
195
196
  specification_version: 4
196
197
  summary: Keycloak Admin REST API client written in Ruby