keycloak-admin 2.0.1 → 2.0.3
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 +4 -4
- data/CHANGELOG.md +40 -0
- data/MIT-LICENSE +18 -17
- data/README.md +102 -5
- data/keycloak-admin.gemspec +15 -6
- data/lib/keycloak-admin/client/attack_detection_client.rb +2 -1
- data/lib/keycloak-admin/client/client.rb +36 -11
- data/lib/keycloak-admin/client/client_authz_permission_client.rb +5 -4
- data/lib/keycloak-admin/client/client_authz_policy_client.rb +4 -3
- data/lib/keycloak-admin/client/client_authz_resource_client.rb +15 -11
- data/lib/keycloak-admin/client/client_authz_scope_client.rb +5 -4
- data/lib/keycloak-admin/client/client_client.rb +6 -2
- data/lib/keycloak-admin/client/client_role_client.rb +2 -1
- data/lib/keycloak-admin/client/client_role_mappings_client.rb +3 -2
- data/lib/keycloak-admin/client/client_scope_client.rb +2 -1
- data/lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb +3 -2
- data/lib/keycloak-admin/client/configurable_token_client.rb +2 -1
- data/lib/keycloak-admin/client/group_client.rb +15 -9
- data/lib/keycloak-admin/client/identity_provider_client.rb +2 -1
- data/lib/keycloak-admin/client/organization_client.rb +12 -11
- data/lib/keycloak-admin/client/realm_client.rb +3 -2
- data/lib/keycloak-admin/client/resource.rb +8 -6
- data/lib/keycloak-admin/client/response.rb +1 -0
- data/lib/keycloak-admin/client/role_client.rb +3 -6
- data/lib/keycloak-admin/client/role_mapper_client.rb +2 -1
- data/lib/keycloak-admin/client/token_client.rb +1 -0
- data/lib/keycloak-admin/client/user_client.rb +70 -47
- data/lib/keycloak-admin/configuration.rb +37 -2
- data/lib/keycloak-admin/error.rb +63 -0
- data/lib/keycloak-admin/path_encoding.rb +24 -0
- data/lib/keycloak-admin/representation/attack_detection_representation.rb +1 -0
- data/lib/keycloak-admin/representation/camel_json.rb +7 -3
- data/lib/keycloak-admin/representation/client_authz_policy_config_representation.rb +1 -0
- data/lib/keycloak-admin/representation/client_authz_policy_representation.rb +1 -0
- data/lib/keycloak-admin/representation/client_authz_resource_representation.rb +1 -0
- data/lib/keycloak-admin/representation/client_authz_scope_representation.rb +1 -0
- data/lib/keycloak-admin/representation/client_representation.rb +4 -5
- data/lib/keycloak-admin/representation/client_scope_representation.rb +1 -0
- data/lib/keycloak-admin/representation/credential_representation.rb +1 -0
- data/lib/keycloak-admin/representation/federated_identity_representation.rb +1 -0
- data/lib/keycloak-admin/representation/group_representation.rb +1 -0
- data/lib/keycloak-admin/representation/identity_provider_mapper_representation.rb +1 -0
- data/lib/keycloak-admin/representation/identity_provider_representation.rb +1 -0
- data/lib/keycloak-admin/representation/impersonation_redirection_representation.rb +1 -0
- data/lib/keycloak-admin/representation/impersonation_representation.rb +1 -0
- data/lib/keycloak-admin/representation/member_representation.rb +1 -0
- data/lib/keycloak-admin/representation/organization_domain_representation.rb +1 -0
- data/lib/keycloak-admin/representation/organization_representation.rb +1 -1
- data/lib/keycloak-admin/representation/protocol_mapper_representation.rb +1 -0
- data/lib/keycloak-admin/representation/realm_representation.rb +1 -0
- data/lib/keycloak-admin/representation/representation.rb +7 -5
- data/lib/keycloak-admin/representation/role_representation.rb +2 -1
- data/lib/keycloak-admin/representation/session_representation.rb +2 -1
- data/lib/keycloak-admin/representation/token_representation.rb +1 -0
- data/lib/keycloak-admin/representation/user_representation.rb +1 -0
- data/lib/keycloak-admin/resource/base_role_containing_resource.rb +4 -1
- data/lib/keycloak-admin/resource/group_resource.rb +1 -0
- data/lib/keycloak-admin/resource/user_resource.rb +1 -0
- data/lib/keycloak-admin/version.rb +2 -1
- data/lib/keycloak-admin.rb +4 -0
- metadata +36 -56
- data/.github/workflows/Dockerfile +0 -24
- data/.github/workflows/ci.yml +0 -94
- data/.github/workflows/release.yml +0 -112
- data/.gitignore +0 -10
- data/.rspec +0 -2
- data/Dockerfile +0 -13
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -54
- data/Rakefile +0 -6
- data/bin/console +0 -9
- data/spec/client/attack_detection_client_spec.rb +0 -102
- data/spec/client/client_authz_permission_client_spec.rb +0 -170
- data/spec/client/client_authz_policy_client_spec.rb +0 -170
- data/spec/client/client_authz_resource_client_spec.rb +0 -150
- data/spec/client/client_authz_scope_client_spec.rb +0 -134
- data/spec/client/client_client_spec.rb +0 -133
- data/spec/client/client_role_mappings_client_spec.rb +0 -83
- data/spec/client/client_scope_client_spec.rb +0 -220
- data/spec/client/client_scope_protocol_mapper_client_spec.rb +0 -230
- data/spec/client/client_spec.rb +0 -101
- data/spec/client/configurable_token_client_spec.rb +0 -34
- data/spec/client/group_client_spec.rb +0 -328
- data/spec/client/identity_provider_client_spec.rb +0 -92
- data/spec/client/organization_client_spec.rb +0 -595
- data/spec/client/realm_client_spec.rb +0 -155
- data/spec/client/resource_spec.rb +0 -223
- data/spec/client/response_spec.rb +0 -68
- data/spec/client/role_client_spec.rb +0 -79
- data/spec/client/role_mapper_client_spec.rb +0 -113
- data/spec/client/token_client_spec.rb +0 -68
- data/spec/client/user_client_spec.rb +0 -418
- data/spec/configuration_spec.rb +0 -150
- data/spec/gemspec_spec.rb +0 -25
- data/spec/integration/client_authorization_spec.rb +0 -93
- data/spec/representation/attack_detection_representation_spec.rb +0 -16
- data/spec/representation/client_authz_permission_representation_spec.rb +0 -52
- data/spec/representation/client_authz_policy_representation_spec.rb +0 -47
- data/spec/representation/client_authz_resource_representation_spec.rb +0 -33
- data/spec/representation/client_authz_scope_representation_spec.rb +0 -19
- data/spec/representation/client_representation_spec.rb +0 -119
- data/spec/representation/client_scope_representation_spec.rb +0 -125
- data/spec/representation/credential_representation_spec.rb +0 -68
- data/spec/representation/group_representation_spec.rb +0 -22
- data/spec/representation/identity_provider_mapper_representation_spec.rb +0 -24
- data/spec/representation/identity_provider_representation_spec.rb +0 -113
- data/spec/representation/impersonation_representation_spec.rb +0 -163
- data/spec/representation/organization_representation_spec.rb +0 -64
- data/spec/representation/protocol_mapper_representation_spec.rb +0 -57
- data/spec/representation/role_representation_spec.rb +0 -37
- data/spec/representation/session_representation_spec.rb +0 -16
- data/spec/representation/user_representation_spec.rb +0 -15
- data/spec/resource/group_resource_spec.rb +0 -14
- data/spec/resource/user_resource_spec.rb +0 -14
- data/spec/spec_helper.rb +0 -36
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class GroupClient < Client
|
|
3
4
|
def initialize(configuration, realm_client)
|
|
@@ -21,8 +22,16 @@ module KeycloakAdmin
|
|
|
21
22
|
JSON.parse(response).map { |group_as_hash| GroupRepresentation.from_hash(group_as_hash) }
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# Unlike the users endpoint, this one applies no default cap: a bare list already returns
|
|
26
|
+
# every group. Passing neither bound sends no query parameter at all, exactly as before.
|
|
27
|
+
#
|
|
28
|
+
# Keycloak 19 only paginates here when both bounds are present - a lone `max` is ignored and
|
|
29
|
+
# the whole list comes back. Sending `first=0` alongside it makes the call behave the same on
|
|
30
|
+
# every supported version, and is a no-op from Keycloak 23 on.
|
|
31
|
+
def list(first: nil, max: nil)
|
|
32
|
+
first = 0 if first.nil? && !max.nil?
|
|
33
|
+
pagination = {first: first, max: max}.compact
|
|
34
|
+
search(pagination.empty? ? nil : pagination)
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
def search(query)
|
|
@@ -75,11 +84,8 @@ module KeycloakAdmin
|
|
|
75
84
|
|
|
76
85
|
def members(group_id, first=0, max=100)
|
|
77
86
|
url = "#{groups_url(group_id)}/members"
|
|
78
|
-
query = {first: first
|
|
79
|
-
unless query.empty?
|
|
80
|
-
query_string = query.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
|
|
81
|
-
url = "#{url}?#{query_string}"
|
|
82
|
-
end
|
|
87
|
+
query = {first: first&.to_i, max: max&.to_i}.compact
|
|
88
|
+
url = "#{url}?#{build_query(query)}" unless query.empty?
|
|
83
89
|
response = execute_http do
|
|
84
90
|
resource(url).get(headers)
|
|
85
91
|
end
|
|
@@ -114,7 +120,7 @@ module KeycloakAdmin
|
|
|
114
120
|
url = "#{groups_url(group_id)}/role-mappings/realm"
|
|
115
121
|
execute_http do
|
|
116
122
|
Resource.execute(
|
|
117
|
-
|
|
123
|
+
connection_options.merge(
|
|
118
124
|
url: url,
|
|
119
125
|
method: :delete,
|
|
120
126
|
payload: create_payload([role_representation]),
|
|
@@ -128,7 +134,7 @@ module KeycloakAdmin
|
|
|
128
134
|
|
|
129
135
|
def groups_url(id=nil)
|
|
130
136
|
if id
|
|
131
|
-
"#{@realm_client.realm_admin_url}/groups/#{id}"
|
|
137
|
+
"#{@realm_client.realm_admin_url}/groups/#{encode_segment(id)}"
|
|
132
138
|
else
|
|
133
139
|
"#{@realm_client.realm_admin_url}/groups"
|
|
134
140
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class IdentityProviderClient < Client
|
|
3
4
|
def initialize(configuration, realm_client)
|
|
@@ -38,7 +39,7 @@ module KeycloakAdmin
|
|
|
38
39
|
|
|
39
40
|
def identity_providers_url(internal_id_or_alias=nil)
|
|
40
41
|
if internal_id_or_alias
|
|
41
|
-
"#{@realm_client.realm_admin_url}/identity-provider/instances/#{internal_id_or_alias}"
|
|
42
|
+
"#{@realm_client.realm_admin_url}/identity-provider/instances/#{encode_segment(internal_id_or_alias)}"
|
|
42
43
|
else
|
|
43
44
|
"#{@realm_client.realm_admin_url}/identity-provider/instances"
|
|
44
45
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class OrganizationClient < Client
|
|
3
4
|
def initialize(configuration, realm_client)
|
|
@@ -69,7 +70,7 @@ module KeycloakAdmin
|
|
|
69
70
|
def get_identity_provider(organization_id, identity_provider_alias)
|
|
70
71
|
raise ArgumentError.new("identity_provider_alias must be defined") if identity_provider_alias.nil?
|
|
71
72
|
response = execute_http do
|
|
72
|
-
resource(
|
|
73
|
+
resource(identity_provider_url(organization_id, identity_provider_alias)).get(headers)
|
|
73
74
|
end
|
|
74
75
|
IdentityProviderRepresentation.from_hash(JSON.parse(response))
|
|
75
76
|
end
|
|
@@ -157,7 +158,7 @@ module KeycloakAdmin
|
|
|
157
158
|
|
|
158
159
|
def organization_url(organization_id)
|
|
159
160
|
raise ArgumentError.new("organization_id must be defined") if organization_id.nil?
|
|
160
|
-
"#{organizations_url}/#{organization_id}"
|
|
161
|
+
"#{organizations_url}/#{encode_segment(organization_id)}"
|
|
161
162
|
end
|
|
162
163
|
|
|
163
164
|
def identity_providers_url(organization_id)
|
|
@@ -166,28 +167,28 @@ module KeycloakAdmin
|
|
|
166
167
|
|
|
167
168
|
def identity_provider_url(organization_id, identity_provider_alias)
|
|
168
169
|
raise ArgumentError.new("identity_provider_alias must be defined") if identity_provider_alias.nil?
|
|
169
|
-
"#{identity_providers_url(organization_id)}/#{identity_provider_alias}"
|
|
170
|
+
"#{identity_providers_url(organization_id)}/#{encode_segment(identity_provider_alias)}"
|
|
170
171
|
end
|
|
171
172
|
|
|
172
173
|
def count_url(exact, query, search)
|
|
173
|
-
query_parameters = {exact: exact, q: query, search: search}.compact
|
|
174
|
+
query_parameters = build_query({exact: exact, q: query, search: search}.compact)
|
|
174
175
|
"#{organizations_url}/count?#{query_parameters}"
|
|
175
176
|
end
|
|
176
177
|
|
|
177
178
|
def organizations_url_with_parameters(brief_representation, exact, first, max, query, search)
|
|
178
|
-
query_parameters = {
|
|
179
|
+
query_parameters = build_query({
|
|
179
180
|
briefRepresentation: brief_representation,
|
|
180
181
|
exact: exact,
|
|
181
182
|
first: first,
|
|
182
183
|
max: max,
|
|
183
184
|
q: query,
|
|
184
185
|
search: search
|
|
185
|
-
}.compact
|
|
186
|
+
}.compact)
|
|
186
187
|
"#{organizations_url}?#{query_parameters}"
|
|
187
188
|
end
|
|
188
189
|
|
|
189
190
|
def associated_with_member_url(member_id, brief_representation=true)
|
|
190
|
-
"#{organizations_url}/members/#{member_id}/organizations
|
|
191
|
+
"#{organizations_url}/members/#{encode_segment(member_id)}/organizations?#{build_query(briefRepresentation: brief_representation)}"
|
|
191
192
|
end
|
|
192
193
|
|
|
193
194
|
def members_count_url(organization_id)
|
|
@@ -196,7 +197,7 @@ module KeycloakAdmin
|
|
|
196
197
|
|
|
197
198
|
def member_url(organization_id, member_id)
|
|
198
199
|
raise ArgumentError.new("member_id must be defined") if member_id.nil?
|
|
199
|
-
"#{organization_url(organization_id)}/members/#{member_id}"
|
|
200
|
+
"#{organization_url(organization_id)}/members/#{encode_segment(member_id)}"
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
def invite_existing_user_url(organization_id)
|
|
@@ -212,19 +213,19 @@ module KeycloakAdmin
|
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
def members_url_with_query_parameters(organization_id, exact, first, max, membership_type, search)
|
|
215
|
-
query_parameters = {
|
|
216
|
+
query_parameters = build_query({
|
|
216
217
|
exact: exact,
|
|
217
218
|
first: first,
|
|
218
219
|
max: max,
|
|
219
220
|
membershipType: membership_type,
|
|
220
221
|
search: search
|
|
221
|
-
}.compact
|
|
222
|
+
}.compact)
|
|
222
223
|
"#{organization_url(organization_id)}/members?#{query_parameters}"
|
|
223
224
|
end
|
|
224
225
|
|
|
225
226
|
def build(name, alias_name, enabled, description, redirect_url=nil, domains=[], attributes={})
|
|
226
227
|
unless domains.is_a?(Array)
|
|
227
|
-
raise ArgumentError.new("domains must be an Array, got #{
|
|
228
|
+
raise ArgumentError.new("domains must be an Array, got #{domains.class}")
|
|
228
229
|
end
|
|
229
230
|
|
|
230
231
|
unless domains.all? { |domain| domain.is_a?(KeycloakAdmin::OrganizationDomainRepresentation) }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class RealmClient < Client
|
|
3
4
|
def initialize(configuration, realm_name=nil)
|
|
@@ -37,7 +38,7 @@ module KeycloakAdmin
|
|
|
37
38
|
|
|
38
39
|
def realm_url
|
|
39
40
|
if @realm_name
|
|
40
|
-
"#{server_url}/realms/#{@realm_name}"
|
|
41
|
+
"#{server_url}/realms/#{encode_segment(@realm_name)}"
|
|
41
42
|
else
|
|
42
43
|
"#{server_url}/realms"
|
|
43
44
|
end
|
|
@@ -45,7 +46,7 @@ module KeycloakAdmin
|
|
|
45
46
|
|
|
46
47
|
def realm_admin_url
|
|
47
48
|
if @realm_name
|
|
48
|
-
"#{server_url}/admin/realms/#{@realm_name}"
|
|
49
|
+
"#{server_url}/admin/realms/#{encode_segment(@realm_name)}"
|
|
49
50
|
else
|
|
50
51
|
"#{server_url}/admin/realms"
|
|
51
52
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require "faraday"
|
|
2
3
|
require "uri"
|
|
3
4
|
|
|
@@ -18,13 +19,11 @@ module KeycloakAdmin
|
|
|
18
19
|
new(url, options, logger).send(:request, method, payload, headers)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def self.put(url, payload, headers = {}, logger = nil)
|
|
22
|
-
new(url, {}, logger).put(payload, headers)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
22
|
def initialize(url, options = {}, logger = nil)
|
|
23
|
+
options = (options || {}).dup
|
|
24
|
+
@adapter = options.delete(:adapter)
|
|
26
25
|
@url = url
|
|
27
|
-
@options = options
|
|
26
|
+
@options = options
|
|
28
27
|
@logger = logger
|
|
29
28
|
end
|
|
30
29
|
|
|
@@ -75,7 +74,10 @@ module KeycloakAdmin
|
|
|
75
74
|
# headers by default, which would print the bearer token on every call; headers: false
|
|
76
75
|
# keeps this to method/url/status only. Bodies are already off by Faraday's own default.
|
|
77
76
|
f.response :logger, @logger, headers: false if @logger
|
|
78
|
-
|
|
77
|
+
# Faraday's default net_http adapter wraps every request in http.start, which reopens
|
|
78
|
+
# the connection each time; config.faraday_adapter lets a caller swap in a pooling one.
|
|
79
|
+
# Splatted so an adapter needing arguments can be given as [:name, options].
|
|
80
|
+
f.adapter(*Array(@adapter || Faraday.default_adapter))
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
83
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class RoleClient < Client
|
|
3
4
|
def initialize(configuration, realm_client)
|
|
@@ -15,8 +16,6 @@ module KeycloakAdmin
|
|
|
15
16
|
|
|
16
17
|
# Returns the role representation for the specified role name
|
|
17
18
|
def get(name)
|
|
18
|
-
# allows special characters in the name like space
|
|
19
|
-
name = URI.encode_uri_component(name)
|
|
20
19
|
response = execute_http do
|
|
21
20
|
resource(role_name_url(name)).get(headers)
|
|
22
21
|
end
|
|
@@ -25,8 +24,6 @@ module KeycloakAdmin
|
|
|
25
24
|
|
|
26
25
|
# Lists all groups that have the specified role name assigned
|
|
27
26
|
def list_groups(name)
|
|
28
|
-
# allows special characters in the name like space
|
|
29
|
-
name = URI.encode_uri_component(name)
|
|
30
27
|
response = execute_http do
|
|
31
28
|
resource("#{role_name_url(name)}/groups").get(headers)
|
|
32
29
|
end
|
|
@@ -49,11 +46,11 @@ module KeycloakAdmin
|
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
def role_id_url(id)
|
|
52
|
-
"#{@realm_client.realm_admin_url}/roles-by-id/#{id}"
|
|
49
|
+
"#{@realm_client.realm_admin_url}/roles-by-id/#{encode_segment(id)}"
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
def role_name_url(name)
|
|
56
|
-
"#{@realm_client.realm_admin_url}/roles/#{name}"
|
|
53
|
+
"#{@realm_client.realm_admin_url}/roles/#{encode_segment(name)}"
|
|
57
54
|
end
|
|
58
55
|
end
|
|
59
56
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class RoleMapperClient < Client
|
|
3
4
|
def initialize(configuration, user_resource)
|
|
@@ -23,7 +24,7 @@ module KeycloakAdmin
|
|
|
23
24
|
def remove_realm_level(role_representation_list)
|
|
24
25
|
execute_http do
|
|
25
26
|
Resource.execute(
|
|
26
|
-
|
|
27
|
+
connection_options.merge(
|
|
27
28
|
method: :delete,
|
|
28
29
|
url: realm_level_url,
|
|
29
30
|
payload: create_payload(role_representation_list),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module KeycloakAdmin
|
|
2
3
|
class UserClient < Client
|
|
3
4
|
def initialize(configuration, realm_client)
|
|
@@ -6,55 +7,61 @@ module KeycloakAdmin
|
|
|
6
7
|
@realm_client = realm_client
|
|
7
8
|
end
|
|
8
9
|
|
|
10
|
+
# Reads the id of the new user off the Location header rather than searching for it
|
|
11
|
+
# afterwards: Keycloak's `search` parameter matches a substring of the username, email,
|
|
12
|
+
# first name or last name, so an existing user merely containing this email was returned
|
|
13
|
+
# instead of the one just created.
|
|
9
14
|
def create!(username, email, password, email_verified, locale, attributes={})
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
response = post_user(build(username, email, password, email_verified, locale, attributes))
|
|
16
|
+
get(created_id(response))
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def save(user_representation)
|
|
15
|
-
|
|
16
|
-
resource(users_url).post(
|
|
17
|
-
create_payload(user_representation), headers
|
|
18
|
-
)
|
|
19
|
-
end
|
|
20
|
+
post_user(user_representation)
|
|
20
21
|
user_representation
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# pay attention that, since Keycloak 24.0.4, partial updates of attributes are not authorized anymore
|
|
24
25
|
def update(user_id, user_representation_body)
|
|
25
26
|
raise ArgumentError.new("user_id must be defined") if user_id.nil?
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
execute_http do
|
|
28
|
+
Resource.execute(
|
|
29
|
+
connection_options.merge(
|
|
30
|
+
method: :put,
|
|
31
|
+
url: users_url(user_id),
|
|
32
|
+
payload: create_payload(user_representation_body),
|
|
33
|
+
headers: headers,
|
|
34
|
+
logger: @configuration.logger
|
|
35
|
+
)
|
|
33
36
|
)
|
|
34
|
-
|
|
37
|
+
end
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def add_group(user_id, group_id)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
execute_http do
|
|
42
|
+
Resource.execute(
|
|
43
|
+
connection_options.merge(
|
|
44
|
+
method: :put,
|
|
45
|
+
url: "#{users_url(user_id)}/groups/#{encode_segment(group_id)}",
|
|
46
|
+
payload: create_payload({}),
|
|
47
|
+
headers: headers,
|
|
48
|
+
logger: @configuration.logger
|
|
49
|
+
)
|
|
45
50
|
)
|
|
46
|
-
|
|
51
|
+
end
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def remove_group(user_id, group_id)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
execute_http do
|
|
56
|
+
Resource.execute(
|
|
57
|
+
connection_options.merge(
|
|
58
|
+
method: :delete,
|
|
59
|
+
url: "#{users_url(user_id)}/groups/#{encode_segment(group_id)}",
|
|
60
|
+
headers: headers,
|
|
61
|
+
logger: @configuration.logger
|
|
62
|
+
)
|
|
56
63
|
)
|
|
57
|
-
|
|
64
|
+
end
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
def add_client_roles_on_user(user_id, client_id, role_representations)
|
|
@@ -94,8 +101,12 @@ module KeycloakAdmin
|
|
|
94
101
|
JSON.parse(response).map { |user_as_hash| UserRepresentation.from_hash(user_as_hash) }
|
|
95
102
|
end
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
# Keycloak answers this endpoint with at most 100 users unless `max` says otherwise, so a
|
|
105
|
+
# bare `list` silently truncates a larger realm. Passing neither bound sends no query
|
|
106
|
+
# parameter at all, exactly as before.
|
|
107
|
+
def list(first: nil, max: nil)
|
|
108
|
+
pagination = {first: first, max: max}.compact
|
|
109
|
+
search(pagination.empty? ? nil : pagination)
|
|
99
110
|
end
|
|
100
111
|
|
|
101
112
|
def delete(user_id)
|
|
@@ -112,13 +123,13 @@ module KeycloakAdmin
|
|
|
112
123
|
JSON.parse(response).map { |group_as_hash| GroupRepresentation.from_hash(group_as_hash) }
|
|
113
124
|
end
|
|
114
125
|
|
|
115
|
-
def update_password(user_id, new_password)
|
|
126
|
+
def update_password(user_id, new_password, temporary: false)
|
|
116
127
|
execute_http do
|
|
117
128
|
Resource.execute(
|
|
118
|
-
|
|
129
|
+
connection_options.merge(
|
|
119
130
|
method: :put,
|
|
120
131
|
url: reset_password_url(user_id),
|
|
121
|
-
payload: { type: "password", value: new_password, temporary:
|
|
132
|
+
payload: { type: "password", value: new_password, temporary: temporary }.to_json,
|
|
122
133
|
headers: headers,
|
|
123
134
|
logger: @configuration.logger
|
|
124
135
|
)
|
|
@@ -141,19 +152,23 @@ module KeycloakAdmin
|
|
|
141
152
|
def execute_actions_email(user_id, actions=[], lifespan=nil, redirect_uri=nil, client_id=nil)
|
|
142
153
|
raise ArgumentError.new("client_id must be defined") if client_id.nil? && !redirect_uri.nil?
|
|
143
154
|
execute_http do
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
query = {}
|
|
156
|
+
query[:client_id] = client_id unless client_id.nil?
|
|
157
|
+
query[:redirect_uri] = redirect_uri unless redirect_uri.nil?
|
|
158
|
+
query[:lifespan] = lifespan.to_i unless lifespan.nil?
|
|
159
|
+
|
|
160
|
+
url = execute_actions_email_url(user_id)
|
|
161
|
+
url = "#{url}?#{build_query(query)}" unless query.empty?
|
|
162
|
+
resource(url).put(create_payload(actions), headers)
|
|
148
163
|
end
|
|
149
164
|
user_id
|
|
150
165
|
end
|
|
151
166
|
|
|
152
167
|
def impersonate(user_id)
|
|
153
|
-
impersonation = get_redirect_impersonation(user_id)
|
|
154
168
|
response = execute_http do
|
|
169
|
+
impersonation = get_redirect_impersonation(user_id)
|
|
155
170
|
Resource.execute(
|
|
156
|
-
|
|
171
|
+
connection_options.merge(
|
|
157
172
|
method: :post,
|
|
158
173
|
url: impersonation.impersonation_url,
|
|
159
174
|
payload: impersonation.body.to_json,
|
|
@@ -179,7 +194,7 @@ module KeycloakAdmin
|
|
|
179
194
|
|
|
180
195
|
execute_http do
|
|
181
196
|
Resource.execute(
|
|
182
|
-
|
|
197
|
+
connection_options.merge(
|
|
183
198
|
method: :post,
|
|
184
199
|
url: logout_url(user_id),
|
|
185
200
|
headers: headers,
|
|
@@ -202,7 +217,7 @@ module KeycloakAdmin
|
|
|
202
217
|
|
|
203
218
|
execute_http do
|
|
204
219
|
Resource.execute(
|
|
205
|
-
|
|
220
|
+
connection_options.merge(
|
|
206
221
|
method: :post,
|
|
207
222
|
url: federated_identity_url(user_id, idp_id),
|
|
208
223
|
payload: fed_id_rep.to_json,
|
|
@@ -221,14 +236,14 @@ module KeycloakAdmin
|
|
|
221
236
|
|
|
222
237
|
def users_url(id=nil)
|
|
223
238
|
if id
|
|
224
|
-
"#{@realm_client.realm_admin_url}/users/#{id}"
|
|
239
|
+
"#{@realm_client.realm_admin_url}/users/#{encode_segment(id)}"
|
|
225
240
|
else
|
|
226
241
|
"#{@realm_client.realm_admin_url}/users"
|
|
227
242
|
end
|
|
228
243
|
end
|
|
229
244
|
|
|
230
245
|
def user_client_role_mappings_url(user_id, client_id)
|
|
231
|
-
"#{users_url(user_id)}/role-mappings/clients/#{client_id}"
|
|
246
|
+
"#{users_url(user_id)}/role-mappings/clients/#{encode_segment(client_id)}"
|
|
232
247
|
end
|
|
233
248
|
|
|
234
249
|
def reset_password_url(user_id)
|
|
@@ -259,7 +274,7 @@ module KeycloakAdmin
|
|
|
259
274
|
def federated_identity_url(user_id, identity_provider)
|
|
260
275
|
raise ArgumentError.new("user_id must be defined") if user_id.nil?
|
|
261
276
|
raise ArgumentError.new("identity_provider must be defined") if identity_provider.nil?
|
|
262
|
-
"#{users_url(user_id)}/federated-identity/#{identity_provider}"
|
|
277
|
+
"#{users_url(user_id)}/federated-identity/#{encode_segment(identity_provider)}"
|
|
263
278
|
end
|
|
264
279
|
|
|
265
280
|
def logout_url(user_id)
|
|
@@ -270,13 +285,21 @@ module KeycloakAdmin
|
|
|
270
285
|
|
|
271
286
|
private
|
|
272
287
|
|
|
288
|
+
def post_user(user_representation)
|
|
289
|
+
execute_http do
|
|
290
|
+
resource(users_url).post(
|
|
291
|
+
create_payload(user_representation), headers
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
273
296
|
def build(username, email, password, email_verified, locale, attributes={})
|
|
274
297
|
user = UserRepresentation.new
|
|
275
298
|
user.email = email
|
|
276
299
|
user.username = username
|
|
277
300
|
user.email_verified = email_verified
|
|
278
301
|
user.enabled = true
|
|
279
|
-
user.attributes = attributes || {}
|
|
302
|
+
user.attributes = (attributes || {}).dup
|
|
280
303
|
user.attributes[:locale] = locale if locale
|
|
281
304
|
user.add_credential(CredentialRepresentation.from_password(password)) if !password.nil?
|
|
282
305
|
user
|
|
@@ -1,13 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require "base64"
|
|
2
3
|
|
|
3
4
|
module KeycloakAdmin
|
|
4
5
|
class Configuration
|
|
5
|
-
attr_accessor :server_url, :server_domain, :client_id, :client_secret, :client_realm_name, :use_service_account, :username, :password, :logger, :faraday_options
|
|
6
|
+
attr_accessor :server_url, :server_domain, :client_id, :client_secret, :client_realm_name, :use_service_account, :username, :password, :logger, :faraday_options, :faraday_adapter
|
|
6
7
|
|
|
7
8
|
# Treat a cached token as expired this many seconds before its real expiry, so it
|
|
8
9
|
# cannot go stale between the check below and the request that relies on it.
|
|
9
10
|
TOKEN_EXPIRY_SAFETY_MARGIN_SECONDS = 10
|
|
10
11
|
|
|
12
|
+
FILTERED_ATTRIBUTES = %i[@client_secret @password @cached_token].freeze
|
|
13
|
+
FILTERED_PLACEHOLDER = "[FILTERED]".freeze
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@token_mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inspect
|
|
20
|
+
rendered = instance_variables.map do |name|
|
|
21
|
+
value = instance_variable_get(name)
|
|
22
|
+
shown = if FILTERED_ATTRIBUTES.include?(name) && !value.nil?
|
|
23
|
+
FILTERED_PLACEHOLDER
|
|
24
|
+
else
|
|
25
|
+
value.inspect
|
|
26
|
+
end
|
|
27
|
+
"#{name}=#{shown}"
|
|
28
|
+
end
|
|
29
|
+
"#<#{self.class.name} #{rendered.join(", ")}>"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the cached token, fetching one through the block if there is none.
|
|
33
|
+
# The block runs at most once across threads.
|
|
34
|
+
def fetch_token_once
|
|
35
|
+
token = cached_token
|
|
36
|
+
if token.nil?
|
|
37
|
+
@token_mutex.synchronize do
|
|
38
|
+
token = cached_token
|
|
39
|
+
token.nil? ? cache_token(yield) : token
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
token
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
11
46
|
# Returns the cached TokenRepresentation, or nil if there is none or it is (about to be) expired.
|
|
12
47
|
def cached_token
|
|
13
48
|
return nil if @token_expires_at.nil? || Time.now >= @token_expires_at
|
|
@@ -22,8 +57,8 @@ module KeycloakAdmin
|
|
|
22
57
|
end
|
|
23
58
|
|
|
24
59
|
def clear_cached_token!
|
|
25
|
-
@cached_token = nil
|
|
26
60
|
@token_expires_at = nil
|
|
61
|
+
@cached_token = nil
|
|
27
62
|
end
|
|
28
63
|
|
|
29
64
|
def body_for_token_retrieval
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module KeycloakAdmin
|
|
3
|
+
class Error < StandardError; end
|
|
4
|
+
|
|
5
|
+
# Raised when Keycloak answers with a non-2xx status.
|
|
6
|
+
#
|
|
7
|
+
# Callers can rescue the precise failure (KeycloakAdmin::NotFoundError), a whole family
|
|
8
|
+
# (KeycloakAdmin::ClientError for any 4xx) or every HTTP failure (KeycloakAdmin::ApiError),
|
|
9
|
+
# and read #status/#body off the exception rather than parsing the message.
|
|
10
|
+
class ApiError < Error
|
|
11
|
+
attr_reader :status, :body, :headers
|
|
12
|
+
|
|
13
|
+
def initialize(status, body, headers = {})
|
|
14
|
+
@status = status
|
|
15
|
+
@body = body
|
|
16
|
+
@headers = headers || {}
|
|
17
|
+
super("Keycloak: The request failed with response code #{status} and message: #{body}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_response(response)
|
|
21
|
+
response ||= {}
|
|
22
|
+
status = response[:status]
|
|
23
|
+
error_class_for(status).new(status, response[:body], response[:headers])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.error_class_for(status)
|
|
27
|
+
case status
|
|
28
|
+
when 400 then BadRequestError
|
|
29
|
+
when 401 then UnauthorizedError
|
|
30
|
+
when 403 then ForbiddenError
|
|
31
|
+
when 404 then NotFoundError
|
|
32
|
+
when 409 then ConflictError
|
|
33
|
+
when 400..499 then ClientError
|
|
34
|
+
when 500..599 then ServerError
|
|
35
|
+
else ApiError
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Any 4xx: the request itself was rejected.
|
|
41
|
+
class ClientError < ApiError; end
|
|
42
|
+
|
|
43
|
+
class BadRequestError < ClientError; end
|
|
44
|
+
class UnauthorizedError < ClientError; end
|
|
45
|
+
class ForbiddenError < ClientError; end
|
|
46
|
+
class NotFoundError < ClientError; end
|
|
47
|
+
class ConflictError < ClientError; end
|
|
48
|
+
|
|
49
|
+
# Any 5xx: Keycloak accepted the request but failed to serve it.
|
|
50
|
+
class ServerError < ApiError; end
|
|
51
|
+
|
|
52
|
+
# Raised when a create call succeeds at the HTTP level but answers something other than
|
|
53
|
+
# 201 Created, leaving no Location header to read the new resource's id from.
|
|
54
|
+
class UnexpectedResponseError < Error
|
|
55
|
+
attr_reader :status, :reason_phrase
|
|
56
|
+
|
|
57
|
+
def initialize(status, reason_phrase)
|
|
58
|
+
@status = status
|
|
59
|
+
@reason_phrase = reason_phrase
|
|
60
|
+
super("Create method returned status #{reason_phrase} (Code: #{status}); expected status: Created (201)")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
module KeycloakAdmin
|
|
5
|
+
# Percent-encoding for the identifiers interpolated into a URL path.
|
|
6
|
+
#
|
|
7
|
+
# Included by Client and BaseRoleContainingResource, which have no common ancestor but
|
|
8
|
+
# both build URLs out of caller-supplied values.
|
|
9
|
+
module PathEncoding
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Encodes '/' as %2F too, so an identifier can never open a path segment of its own.
|
|
13
|
+
# Values that are already URL-safe - the UUIDs Keycloak hands out - come out unchanged,
|
|
14
|
+
# so this is a no-op for the usual case.
|
|
15
|
+
#
|
|
16
|
+
# Without it, any identifier holding a character that is illegal in a URI (a space, most
|
|
17
|
+
# obviously) made Ruby's URI parser raise URI::InvalidURIError before the request was
|
|
18
|
+
# ever sent. That is reachable with values users choose themselves, identity provider
|
|
19
|
+
# aliases being the clearest case.
|
|
20
|
+
def encode_segment(value)
|
|
21
|
+
URI.encode_uri_component(value.to_s)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|