keycloak-admin 1.1.5 → 1.1.6

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: dc70b33efe8f4293f7eb0f60eeb30f9468d7866a08e20180b6a3b66c35c96ea8
4
- data.tar.gz: f9cfd0bec183db13053ecd034c4c0c180a8564c116fdb6ace4271a086622f18b
3
+ metadata.gz: db97e606c0a299c2daff7be37a2df4a1c2a7261d2430b62dd196b8d29dc9117d
4
+ data.tar.gz: a1581e41e26b30d576273036ccf87abf463db0e4a80cd1f3a0961b87f874c373
5
5
  SHA512:
6
- metadata.gz: a3a4bf1e49ea91c845458186565f68636244e515a7ad78d12e69c50ad190de94fd38050a3222ed16eb33f0b5a13d5e1aa65e496ebb48fb732a5248ff6c227953
7
- data.tar.gz: b60ee39f400e84c57f1627042abbb07b0ca820471d3fe53448c09a2ef8b4cc1acb193ea4f11d63d4b60941f0ee9496ecb6a5fc89bc6007fa1c1eca69a5b37925
6
+ metadata.gz: 33416a0ad66f8f054a2192f560fbd72d6d68d1a29101521d8bfa2be5bd705888f46740d325ee265ef0e983bbd611901e7902db2a14f5693bd066c250692bb417
7
+ data.tar.gz: 6e17f0a84457d4bf8ce7332b756b0892fbe51a606a8895e5434fca458d9b936594006057820fb3e753d1713b5fdd895fe546f82f90a60fea948f3065e4905bed
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.6] - 2026-01-05
9
+
10
+ * [Feature] Support for Organizations (Multi-tenancy):
11
+ * **Organization Management**:
12
+ * Supported operations: `create!`, `update`, `get`, `delete`, `list`, and `count`.
13
+ * Supported searching and filtering via `exact`, `query`, and `search` parameters.
14
+ * **Member Management**:
15
+ * Added ability to list organization members with pagination and filtering (`members`).
16
+ * Added `members_count` to retrieve the total number of members.
17
+ * Added `get_member`, `add_member` (by user ID), and `delete_member`.
18
+ * Added helper to find all organizations associated with a specific user: `associated_with_member`.
19
+ * **Invitations**:
20
+ * Added `invite_user`: Invites a new user via email/name.
21
+ * Added `invite_existing_user`: Invites an existing Keycloak user to the organization by ID.
22
+ * **Identity Provider (IdP) Linking**:
23
+ * Added methods to manage IdPs linked to an organization: `identity_providers`, `get_identity_provider`, `add_identity_provider`, and `delete_identity_provider`.
24
+
8
25
  ## [1.1.5] - 2026-01-05
9
26
 
10
27
  * [Feature] Added the ability to list credentials for a given user.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- keycloak-admin (1.1.5)
4
+ keycloak-admin (1.1.6)
5
5
  http-cookie (~> 1.0, >= 1.0.3)
6
6
  rest-client (~> 2.0)
7
7
 
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.5"
15
+ gem "keycloak-admin", "1.1.6"
16
16
  ```
17
17
 
18
18
  ## Login
@@ -135,6 +135,11 @@ 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 organizations, create/update/get/delete an organization
139
+ * Get list of members of an organization, add/remove members
140
+ * Invite new or existing users to an organization
141
+ * List, add, and remove Identity Providers for an organization
142
+ * Get list of organizations associated with a specific user
138
143
 
139
144
  ### Get an access token
140
145
 
@@ -252,6 +257,12 @@ user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
252
257
  KeycloakAdmin.realm("a_realm").users.get_redirect_impersonation(user_id)
253
258
  ```
254
259
 
260
+ ### List all the organizations of a realm
261
+
262
+ ```ruby
263
+ KeycloakAdmin.realm("a_realm").organizations.list
264
+ ```
265
+
255
266
  ### Exchange a configurable token
256
267
 
257
268
  *Requires your Keycloak server to have deployed the Custom REST API `configurable-token`* (https://github.com/looorent/keycloak-configurable-token-api)
@@ -0,0 +1,245 @@
1
+ module KeycloakAdmin
2
+ class OrganizationClient < Client
3
+ def initialize(configuration, realm_client)
4
+ super(configuration)
5
+ raise ArgumentError.new("realm must be defined") unless realm_client.name_defined?
6
+ @realm_client = realm_client
7
+ end
8
+
9
+ # This endpoint does not return members
10
+ def list(brief_representation=true, exact=nil, first=nil, max=nil, query=nil, search=nil)
11
+ response = execute_http do
12
+ RestClient::Resource.new(organizations_url_with_parameters(brief_representation, exact, first, max, query, search), @configuration.rest_client_options).get(headers)
13
+ end
14
+ JSON.parse(response).map { |organization_as_hash| OrganizationRepresentation.from_hash(organization_as_hash) }
15
+ end
16
+
17
+ def count(exact=nil, query=nil, search=nil)
18
+ response = execute_http do
19
+ RestClient::Resource.new(count_url(exact, query, search), @configuration.rest_client_options).get(headers)
20
+ end
21
+ response.to_i
22
+ end
23
+
24
+ def delete(organization_id)
25
+ execute_http do
26
+ RestClient::Resource.new(organization_url(organization_id), @configuration.rest_client_options).delete(headers)
27
+ end
28
+ true
29
+ end
30
+
31
+ def update(organization_representation)
32
+ execute_http do
33
+ RestClient::Resource.new(organization_url(organization_representation.id), @configuration.rest_client_options).put(
34
+ create_payload(organization_representation), headers
35
+ )
36
+ end
37
+
38
+ get(organization_representation.id)
39
+ end
40
+
41
+ def create!(name, alias_name, enabled, description, redirect_url=nil, domains=[], attributes={})
42
+ save(build(name, alias_name, enabled, description, redirect_url, domains, attributes))
43
+ end
44
+
45
+ # This operation does not associate members and identity providers
46
+ def save(organization_representation)
47
+ execute_http do
48
+ RestClient::Resource.new(organizations_url, @configuration.rest_client_options).post(
49
+ create_payload(organization_representation), headers
50
+ )
51
+ end
52
+ true
53
+ end
54
+
55
+ def get(organization_id)
56
+ response = execute_http do
57
+ RestClient::Resource.new(organization_url(organization_id), @configuration.rest_client_options).get(headers)
58
+ end
59
+ OrganizationRepresentation.from_hash(JSON.parse(response))
60
+ end
61
+
62
+ def identity_providers(organization_id)
63
+ response = execute_http do
64
+ RestClient::Resource.new(identity_providers_url(organization_id), @configuration.rest_client_options).get(headers)
65
+ end
66
+ JSON.parse(response).map { |idp_as_hash| IdentityProviderRepresentation.from_hash(idp_as_hash) }
67
+ end
68
+
69
+ def get_identity_provider(organization_id, identity_provider_alias)
70
+ raise ArgumentError.new("identity_provider_alias must be defined") if identity_provider_alias.nil?
71
+ response = execute_http do
72
+ RestClient::Resource.new("#{identity_providers_url(organization_id)}/#{identity_provider_alias}", @configuration.rest_client_options).get(headers)
73
+ end
74
+ IdentityProviderRepresentation.from_hash(JSON.parse(response))
75
+ end
76
+
77
+ def add_identity_provider(organization_id, identity_provider_alias)
78
+ raise ArgumentError.new("identity_provider_alias must be defined") if identity_provider_alias.nil?
79
+ execute_http do
80
+ RestClient::Resource.new(identity_providers_url(organization_id), @configuration.rest_client_options).post(identity_provider_alias, headers)
81
+ end
82
+ true
83
+ end
84
+
85
+ def delete_identity_provider(organization_id, identity_provider_alias)
86
+ execute_http do
87
+ RestClient::Resource.new(identity_provider_url(organization_id, identity_provider_alias), @configuration.rest_client_options).delete(headers)
88
+ end
89
+ true
90
+ end
91
+
92
+ def members_count(organization_id)
93
+ response = execute_http do
94
+ RestClient::Resource.new(members_count_url(organization_id), @configuration.rest_client_options).get(headers)
95
+ end
96
+ response.to_i
97
+ end
98
+
99
+ def members(organization_id, exact=nil, first=nil, max=nil, membership_type=nil, search=nil)
100
+ response = execute_http do
101
+ RestClient::Resource.new(members_url_with_query_parameters(organization_id, exact, first, max, membership_type, search), @configuration.rest_client_options).get(headers)
102
+ end
103
+ JSON.parse(response).map { |member_as_hash| MemberRepresentation.from_hash(member_as_hash) }
104
+ end
105
+
106
+ def invite_existing_user(organization_id, user_id)
107
+ raise ArgumentError.new("user_id must be defined") if user_id.nil?
108
+ execute_http do
109
+ RestClient::Resource.new(invite_existing_user_url(organization_id), @configuration.rest_client_options).post({id: user_id}, headers.merge(content_type: "application/x-www-form-urlencoded"))
110
+ end
111
+ true
112
+ end
113
+
114
+ def invite_user(organization_id, email, first_name, last_name)
115
+ execute_http do
116
+ RestClient::Resource.new(invite_user_url(organization_id), @configuration.rest_client_options).post({
117
+ email: email,
118
+ firstName: first_name,
119
+ lastName: last_name
120
+ }, headers.merge(content_type: "application/x-www-form-urlencoded"))
121
+ end
122
+ true
123
+ end
124
+
125
+ def add_member(organization_id, user_id)
126
+ raise ArgumentError.new("user_id must be defined") if user_id.nil?
127
+ execute_http do
128
+ RestClient::Resource.new(members_url(organization_id), @configuration.rest_client_options).post(user_id, headers)
129
+ end
130
+ true
131
+ end
132
+
133
+ def delete_member(organization_id, member_id)
134
+ execute_http do
135
+ RestClient::Resource.new(member_url(organization_id, member_id), @configuration.rest_client_options).delete(headers)
136
+ end
137
+ true
138
+ end
139
+
140
+ def get_member(organization_id, member_id)
141
+ response = execute_http do
142
+ RestClient::Resource.new(member_url(organization_id, member_id), @configuration.rest_client_options).get(headers)
143
+ end
144
+ MemberRepresentation.from_hash(JSON.parse(response))
145
+ end
146
+
147
+ def associated_with_member(member_id, brief_representation=true)
148
+ response = execute_http do
149
+ RestClient::Resource.new(associated_with_member_url(member_id, brief_representation), @configuration.rest_client_options).get(headers)
150
+ end
151
+ JSON.parse(response).map { |organization_as_hash| OrganizationRepresentation.from_hash(organization_as_hash) }
152
+ end
153
+
154
+ def organizations_url
155
+ "#{@realm_client.realm_admin_url}/organizations"
156
+ end
157
+
158
+ def organization_url(organization_id)
159
+ raise ArgumentError.new("organization_id must be defined") if organization_id.nil?
160
+ "#{organizations_url}/#{organization_id}"
161
+ end
162
+
163
+ def identity_providers_url(organization_id)
164
+ "#{organization_url(organization_id)}/identity-providers"
165
+ end
166
+
167
+ def identity_provider_url(organization_id, identity_provider_alias)
168
+ raise ArgumentError.new("identity_provider_alias must be defined") if identity_provider_alias.nil?
169
+ "#{identity_providers_url(organization_id)}/#{identity_provider_alias}"
170
+ end
171
+
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("&")
174
+ "#{organizations_url}/count?#{query_parameters}"
175
+ end
176
+
177
+ def organizations_url_with_parameters(brief_representation, exact, first, max, query, search)
178
+ query_parameters = {
179
+ briefRepresentation: brief_representation,
180
+ exact: exact,
181
+ first: first,
182
+ max: max,
183
+ q: query,
184
+ search: search
185
+ }.compact.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
186
+ "#{organizations_url}?#{query_parameters}"
187
+ end
188
+
189
+ def associated_with_member_url(member_id, brief_representation=true)
190
+ "#{organizations_url}/members/#{member_id}/organizations?briefRepresentation=#{brief_representation}"
191
+ end
192
+
193
+ def members_count_url(organization_id)
194
+ "#{organization_url(organization_id)}/members/count"
195
+ end
196
+
197
+ def member_url(organization_id, member_id)
198
+ raise ArgumentError.new("member_id must be defined") if member_id.nil?
199
+ "#{organization_url(organization_id)}/members/#{member_id}"
200
+ end
201
+
202
+ def invite_existing_user_url(organization_id)
203
+ "#{organization_url(organization_id)}/members/invite-existing-user"
204
+ end
205
+
206
+ def invite_user_url(organization_id)
207
+ "#{organization_url(organization_id)}/members/invite-user"
208
+ end
209
+
210
+ def members_url(organization_id)
211
+ "#{organization_url(organization_id)}/members"
212
+ end
213
+
214
+ def members_url_with_query_parameters(organization_id, exact, first, max, membership_type, search)
215
+ query_parameters = {
216
+ exact: exact,
217
+ first: first,
218
+ max: max,
219
+ membershipType: membership_type,
220
+ search: search
221
+ }.compact.to_a.map { |e| "#{e[0]}=#{e[1]}" }.join("&")
222
+ "#{organization_url(organization_id)}/members?#{query_parameters}"
223
+ end
224
+
225
+ def build(name, alias_name, enabled, description, redirect_url=nil, domains=[], attributes={})
226
+ unless domains.is_a?(Array)
227
+ raise ArgumentError.new("domains must be an Array, got #{new_domains.class}")
228
+ end
229
+
230
+ unless domains.all? { |domain| domain.is_a?(KeycloakAdmin::OrganizationDomainRepresentation) }
231
+ raise ArgumentError.new("All items in domains must be of type OrganizationDomainRepresentation")
232
+ end
233
+
234
+ organization = OrganizationRepresentation.new
235
+ organization.name = name
236
+ organization.alias = alias_name
237
+ organization.enabled = enabled
238
+ organization.description = description
239
+ organization.redirect_url = redirect_url
240
+ organization.domains = domains
241
+ organization.attributes = attributes
242
+ organization
243
+ end
244
+ end
245
+ end
@@ -95,6 +95,10 @@ module KeycloakAdmin
95
95
  IdentityProviderClient.new(@configuration, self)
96
96
  end
97
97
 
98
+ def organizations
99
+ OrganizationClient.new(@configuration, self)
100
+ end
101
+
98
102
  def user(user_id)
99
103
  UserResource.new(@configuration, self, user_id)
100
104
  end
@@ -11,6 +11,7 @@ module KeycloakAdmin
11
11
  :add_read_token_role_on_create,
12
12
  :authenticate_by_default,
13
13
  :link_only,
14
+ :organization_id,
14
15
  :first_broker_login_flow_alias,
15
16
  :config
16
17
 
@@ -30,6 +31,7 @@ module KeycloakAdmin
30
31
  hash["addReadTokenRoleOnCreate"],
31
32
  hash["authenticateByDefault"],
32
33
  hash["linkOnly"],
34
+ hash["organizationId"],
33
35
  hash["firstBrokerLoginFlowAlias"],
34
36
  hash["config"]
35
37
  )
@@ -47,6 +49,7 @@ module KeycloakAdmin
47
49
  add_read_token_role_on_create,
48
50
  authenticate_by_default,
49
51
  link_only,
52
+ organization_id,
50
53
  first_broker_login_flow_alias,
51
54
  config)
52
55
  @alias = alias_name
@@ -60,6 +63,7 @@ module KeycloakAdmin
60
63
  @add_read_token_role_on_create = add_read_token_role_on_create
61
64
  @authenticate_by_default = authenticate_by_default
62
65
  @link_only = link_only
66
+ @organization_id = organization_id
63
67
  @first_broker_login_flow_alias = first_broker_login_flow_alias
64
68
  @config = config || {}
65
69
  end
@@ -0,0 +1,11 @@
1
+ module KeycloakAdmin
2
+ class MemberRepresentation < UserRepresentation
3
+ attr_accessor :membership_type
4
+
5
+ def self.from_hash(hash)
6
+ member = super(hash)
7
+ member.membership_type = hash["membershipType"]
8
+ member
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module KeycloakAdmin
2
+ class OrganizationDomainRepresentation < Representation
3
+ attr_accessor :name, :verified
4
+
5
+ def initialize(name, verified)
6
+ @name = name
7
+ @verified = verified
8
+ end
9
+
10
+ def self.from_hash(hash)
11
+ new(hash["name"], hash["verified"])
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+
18
+
@@ -0,0 +1,30 @@
1
+ module KeycloakAdmin
2
+ class OrganizationRepresentation < Representation
3
+ attr_accessor :id,
4
+ :name,
5
+ :alias,
6
+ :enabled,
7
+ :description,
8
+ :redirect_url,
9
+ :attributes,
10
+ :domains,
11
+ :members,
12
+ :attributes,
13
+ :identity_providers
14
+
15
+ def self.from_hash(hash)
16
+ role = new
17
+ role.id = hash["id"]
18
+ role.name = hash["name"]
19
+ role.alias = hash["alias"]
20
+ role.enabled = hash["enabled"]
21
+ role.description = hash["description"]
22
+ role.redirect_url = hash["redirectUrl"]
23
+ role.attributes = hash["attributes"] || {}
24
+ role.domains = hash["domains"].nil? ? [] : hash["domains"].map { |domain| KeycloakAdmin::OrganizationDomainRepresentation.from_hash(domain) }
25
+ role.members = hash["members"].nil? ? [] : hash["members"].map { |member| KeycloakAdmin::MemberRepresentation.from_hash(member) }
26
+ role.identity_providers = hash["identityProviders"].nil? ? [] : hash["identityProviders"].map { |provider| KeycloakAdmin::IdentityProviderRepresentation.from_hash(provider) }
27
+ role
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module KeycloakAdmin
2
- VERSION = "1.1.5"
2
+ VERSION = "1.1.6"
3
3
  end
@@ -7,6 +7,7 @@ require_relative "keycloak-admin/client/client_role_client"
7
7
  require_relative "keycloak-admin/client/client_role_mappings_client"
8
8
  require_relative "keycloak-admin/client/group_client"
9
9
  require_relative "keycloak-admin/client/realm_client"
10
+ require_relative "keycloak-admin/client/organization_client"
10
11
  require_relative "keycloak-admin/client/role_client"
11
12
  require_relative "keycloak-admin/client/role_mapper_client"
12
13
  require_relative "keycloak-admin/client/token_client"
@@ -27,10 +28,13 @@ require_relative "keycloak-admin/representation/token_representation"
27
28
  require_relative "keycloak-admin/representation/impersonation_redirection_representation"
28
29
  require_relative "keycloak-admin/representation/impersonation_representation"
29
30
  require_relative "keycloak-admin/representation/credential_representation"
31
+ require_relative "keycloak-admin/representation/organization_domain_representation"
32
+ require_relative "keycloak-admin/representation/organization_representation"
30
33
  require_relative "keycloak-admin/representation/realm_representation"
31
34
  require_relative "keycloak-admin/representation/role_representation"
32
35
  require_relative "keycloak-admin/representation/federated_identity_representation"
33
36
  require_relative "keycloak-admin/representation/user_representation"
37
+ require_relative "keycloak-admin/representation/member_representation"
34
38
  require_relative "keycloak-admin/representation/identity_provider_mapper_representation"
35
39
  require_relative "keycloak-admin/representation/identity_provider_representation"
36
40
  require_relative "keycloak-admin/representation/attack_detection_representation"
@@ -0,0 +1,595 @@
1
+ RSpec.describe KeycloakAdmin::OrganizationClient do
2
+ describe "#organization_url" do
3
+ let(:realm_name) { "valid-realm" }
4
+ let(:organization_id) { nil }
5
+
6
+ before(:each) do
7
+ @built_url = KeycloakAdmin.realm(realm_name).organizations.organization_url(organization_id)
8
+ end
9
+
10
+ context "when organization_id is defined" do
11
+ let(:organization_id) { "95985b21-d884-4bbd-b852-cb8cd365afc2" }
12
+ it "return a proper url with the organization id" do
13
+ expect(@built_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/95985b21-d884-4bbd-b852-cb8cd365afc2"
14
+ end
15
+ end
16
+ end
17
+
18
+ describe "#list" do
19
+ let(:realm_name) { "valid-realm" }
20
+
21
+ before(:each) do
22
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
23
+ stub_token_client
24
+ json_payload = <<-'payload'
25
+ [
26
+ {
27
+ "id": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
28
+ "name": "My organization",
29
+ "alias": "myorg",
30
+ "enabled": true,
31
+ "description": "A single organization",
32
+ "domains": [
33
+ {
34
+ "name": "hello.com",
35
+ "verified": false
36
+ },
37
+ {
38
+ "name": "gmail.com",
39
+ "verified": true
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ payload
45
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
46
+ end
47
+
48
+ it "lists organizations" do
49
+ organizations = @organization_client.list
50
+ expect(organizations.length).to eq 1
51
+ expect(organizations[0].id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
52
+ expect(organizations[0].name).to eq "My organization"
53
+ end
54
+
55
+ it "passes rest client options" do
56
+ rest_client_options = {timeout: 10}
57
+ allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:rest_client_options).and_return rest_client_options
58
+
59
+ expect(RestClient::Resource).to receive(:new).with(
60
+ "http://auth.service.io/auth/admin/realms/valid-realm/organizations?briefRepresentation=true", rest_client_options).and_call_original
61
+
62
+ organizations = @organization_client.list
63
+ expect(organizations.length).to eq 1
64
+ expect(organizations[0]).to be_a KeycloakAdmin::OrganizationRepresentation
65
+ expect(organizations[0].id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
66
+ expect(organizations[0].name).to eq "My organization"
67
+ end
68
+ end
69
+
70
+ describe "#count" do
71
+ let(:realm_name) { "valid-realm" }
72
+
73
+ before(:each) do
74
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
75
+ stub_token_client
76
+ json_payload = "2"
77
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
78
+ end
79
+
80
+ it "count organizations" do
81
+ count = @organization_client.count(false, nil, "test")
82
+ expect(count).to eq 2
83
+ end
84
+
85
+ context "when building the count url" do
86
+ let(:exact) { nil }
87
+ let(:query) { nil }
88
+ let(:search) { nil }
89
+
90
+ before(:each) do
91
+ @count_url = @organization_client.count_url(exact, query, search)
92
+ end
93
+
94
+ context "with everything null" do
95
+ it "return a proper url" do
96
+ expect(@count_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/count?"
97
+ end
98
+ end
99
+
100
+ context "with exact=false" do
101
+ let(:exact) { false }
102
+ it "return a proper url" do
103
+ expect(@count_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/count?exact=false"
104
+ end
105
+ end
106
+
107
+ context "with exact=true" do
108
+ let(:exact) { true }
109
+ it "return a proper url" do
110
+ expect(@count_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/count?exact=true"
111
+ end
112
+ end
113
+
114
+ context "with query=test" do
115
+ let(:query) { "test" }
116
+ it "return a proper url" do
117
+ expect(@count_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/count?q=test"
118
+ end
119
+ end
120
+
121
+ context "with search=nameoforg" do
122
+ let(:search) { "nameoforg" }
123
+ it "return a proper url" do
124
+ expect(@count_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/count?search=nameoforg"
125
+ end
126
+ end
127
+
128
+ context "with every argument setup" do
129
+ let(:exact) { true }
130
+ let(:query) { "a query" }
131
+ let(:search) { "a name" }
132
+ it "return a proper url" do
133
+ expect(@count_url).to eq "http://auth.service.io/auth/admin/realms/valid-realm/organizations/count?exact=true&q=a query&search=a name"
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ describe "#associated_with_member" do
140
+ let(:realm_name) { "valid-realm" }
141
+
142
+ before(:each) do
143
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
144
+ stub_token_client
145
+ json_payload = <<-'payload'
146
+ [
147
+ {
148
+ "id": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
149
+ "name": "My organization",
150
+ "alias": "myorg",
151
+ "enabled": true,
152
+ "description": "A single organization",
153
+ "domains": [
154
+ {
155
+ "name": "hello.com",
156
+ "verified": false
157
+ },
158
+ {
159
+ "name": "gmail.com",
160
+ "verified": true
161
+ }
162
+ ]
163
+ }
164
+ ]
165
+ payload
166
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
167
+ end
168
+
169
+ it "list organizations of members organizations" do
170
+ organizations = @organization_client.associated_with_member("648ebe7f-e4ba-4d82-a87d-c585c866d0e7")
171
+ expect(organizations.size).to eq 1
172
+ expect(organizations[0]).to be_a KeycloakAdmin::OrganizationRepresentation
173
+ expect(organizations[0].id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
174
+ end
175
+ end
176
+
177
+ describe "#delete" do
178
+ let(:realm_name) { "valid-realm" }
179
+
180
+ before(:each) do
181
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
182
+ stub_token_client
183
+ allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return ""
184
+ end
185
+
186
+ it "deletes an organization" do
187
+ result = @organization_client.delete("2904e1a1-e5f4-4143-8725-003e54cc8b58")
188
+ expect(result).to be(true)
189
+ end
190
+
191
+ it "raises a delete error" do
192
+ rest_client_options = {timeout: 10}
193
+ allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:rest_client_options).and_return rest_client_options
194
+
195
+ expect(RestClient::Resource).to receive(:new).with(
196
+ "http://auth.service.io/auth/admin/realms/valid-realm/organizations/2904e1a1-e5f4-4143-8725-003e54cc8b58", rest_client_options).and_raise("error")
197
+
198
+ expect { @organization_client.delete("2904e1a1-e5f4-4143-8725-003e54cc8b58") }.to raise_error("error")
199
+ end
200
+ end
201
+
202
+ describe "#identity_providers" do
203
+ let(:realm_name) { "valid-realm" }
204
+
205
+ before(:each) do
206
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
207
+ stub_token_client
208
+ json_payload = <<-'payload'
209
+ [
210
+ {
211
+ "alias": "google",
212
+ "displayName": "",
213
+ "internalId": "59b28b03-07db-4281-b637-4040368df082",
214
+ "providerId": "google",
215
+ "enabled": true,
216
+ "updateProfileFirstLoginMode": "on",
217
+ "trustEmail": false,
218
+ "storeToken": false,
219
+ "addReadTokenRoleOnCreate": false,
220
+ "authenticateByDefault": false,
221
+ "linkOnly": false,
222
+ "hideOnLogin": true,
223
+ "organizationId": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
224
+ "config": {
225
+ "syncMode": "LEGACY",
226
+ "clientSecret": "**********",
227
+ "clientId": "test",
228
+ "kc.org.broker.redirect.mode.email-matches": "false"
229
+ }
230
+ }
231
+ ]
232
+ payload
233
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
234
+ end
235
+
236
+ it "get identity providers" do
237
+ identity_providers = @organization_client.identity_providers("8f6e474e-e688-4bec-99ba-5dc862594f4b")
238
+ expect(identity_providers.size).to eq 1
239
+ expect(identity_providers[0]).to be_a KeycloakAdmin::IdentityProviderRepresentation
240
+ expect(identity_providers[0].alias).to eq "google"
241
+ expect(identity_providers[0].organization_id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
242
+ end
243
+ end
244
+
245
+ describe "#add_identity_provider" do
246
+ let(:realm_name) { "valid-realm" }
247
+
248
+ before(:each) do
249
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
250
+ stub_token_client
251
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
252
+ end
253
+
254
+ it "adds one identity provider" do
255
+ @organization_client.add_identity_provider("8f6e474e-e688-4bec-99ba-5dc862594f4b", "google")
256
+ end
257
+ end
258
+
259
+ describe "#get_identity_provider" do
260
+ let(:realm_name) { "valid-realm" }
261
+
262
+ before(:each) do
263
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
264
+ json_payload = <<-'payload'
265
+ {
266
+ "alias": "google",
267
+ "displayName": "",
268
+ "internalId": "59b28b03-07db-4281-b637-4040368df082",
269
+ "providerId": "google",
270
+ "enabled": true,
271
+ "updateProfileFirstLoginMode": "on",
272
+ "trustEmail": false,
273
+ "storeToken": false,
274
+ "addReadTokenRoleOnCreate": false,
275
+ "authenticateByDefault": false,
276
+ "linkOnly": false,
277
+ "hideOnLogin": true,
278
+ "organizationId": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
279
+ "config": {
280
+ "syncMode": "LEGACY",
281
+ "clientSecret": "**********",
282
+ "clientId": "test",
283
+ "kc.org.broker.redirect.mode.email-matches": "false"
284
+ }
285
+ }
286
+ payload
287
+ stub_token_client
288
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
289
+ end
290
+
291
+ it "get identity provider" do
292
+ idp = @organization_client.get_identity_provider("8f6e474e-e688-4bec-99ba-5dc862594f4b", "google")
293
+ expect(idp).to be_a KeycloakAdmin::IdentityProviderRepresentation
294
+ expect(idp.alias).to eq "google"
295
+ end
296
+ end
297
+
298
+ describe "#get" do
299
+ let(:realm_name) { "valid-realm" }
300
+
301
+ before(:each) do
302
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
303
+ stub_token_client
304
+ json_payload = <<-'payload'
305
+ {
306
+ "id": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
307
+ "name": "My organization",
308
+ "alias": "myorg",
309
+ "enabled": true,
310
+ "description": "A single organization",
311
+ "domains": [
312
+ {
313
+ "name": "hello.com",
314
+ "verified": false
315
+ },
316
+ {
317
+ "name": "gmail.com",
318
+ "verified": true
319
+ }
320
+ ]
321
+ }
322
+ payload
323
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
324
+ end
325
+
326
+ it "get organization" do
327
+ organization = @organization_client.get("8f6e474e-e688-4bec-99ba-5dc862594f4b")
328
+ expect(organization).to be
329
+ expect(organization).to be_a KeycloakAdmin::OrganizationRepresentation
330
+ expect(organization.id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
331
+ expect(organization.name).to eq "My organization"
332
+ end
333
+
334
+ it "passes rest client options" do
335
+ rest_client_options = {timeout: 10}
336
+ allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:rest_client_options).and_return rest_client_options
337
+
338
+ expect(RestClient::Resource).to receive(:new).with(
339
+ "http://auth.service.io/auth/admin/realms/valid-realm/organizations/8f6e474e-e688-4bec-99ba-5dc862594f4b", rest_client_options).and_call_original
340
+
341
+ organization = @organization_client.get("8f6e474e-e688-4bec-99ba-5dc862594f4b")
342
+ expect(organization).to be
343
+ expect(organization).to be_a KeycloakAdmin::OrganizationRepresentation
344
+ expect(organization.id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
345
+ end
346
+ end
347
+
348
+ describe "#members_count" do
349
+ let(:realm_name) { "valid-realm" }
350
+
351
+ before(:each) do
352
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
353
+ stub_token_client
354
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return "2"
355
+ end
356
+
357
+ it "get count of members" do
358
+ count = @organization_client.members_count("8f6e474e-e688-4bec-99ba-5dc862594f4b")
359
+ expect(count).to eq 2
360
+ end
361
+ end
362
+
363
+ describe "#members" do
364
+ let(:realm_name) { "valid-realm" }
365
+
366
+ before(:each) do
367
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
368
+ json_payload = <<-'payload'
369
+ [
370
+ {
371
+ "id": "648ebe7f-e4ba-4d82-a87d-c585c866d0e7",
372
+ "username": "admin",
373
+ "emailVerified": false,
374
+ "attributes": {
375
+ "is_temporary_admin": [
376
+ "true"
377
+ ]
378
+ },
379
+ "enabled": true,
380
+ "createdTimestamp": 1767600090489,
381
+ "totp": false,
382
+ "disableableCredentialTypes": [],
383
+ "requiredActions": [],
384
+ "notBefore": 0,
385
+ "membershipType": "UNMANAGED"
386
+ },
387
+ {
388
+ "id": "2167481a-6a08-44f3-aa9a-42e33afa6834",
389
+ "username": "client",
390
+ "emailVerified": true,
391
+ "enabled": true,
392
+ "createdTimestamp": 1767601626861,
393
+ "totp": false,
394
+ "disableableCredentialTypes": [],
395
+ "requiredActions": [],
396
+ "notBefore": 0,
397
+ "membershipType": "MANAGED"
398
+ }
399
+ ]
400
+ payload
401
+ stub_token_client
402
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
403
+ end
404
+
405
+ it "get members" do
406
+ members = @organization_client.members("8f6e474e-e688-4bec-99ba-5dc862594f4b")
407
+ expect(members.size).to eq 2
408
+ expect(members[0]).to be_a KeycloakAdmin::MemberRepresentation
409
+ expect(members[0].id).to eq "648ebe7f-e4ba-4d82-a87d-c585c866d0e7"
410
+ expect(members[0].membership_type).to eq "UNMANAGED"
411
+ expect(members[1]).to be_a KeycloakAdmin::MemberRepresentation
412
+ expect(members[1].id).to eq "2167481a-6a08-44f3-aa9a-42e33afa6834"
413
+ expect(members[1].membership_type).to eq "MANAGED"
414
+ end
415
+ end
416
+
417
+ describe "#invite_existing_user" do
418
+ let(:realm_name) { "valid-realm" }
419
+
420
+ before(:each) do
421
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
422
+ stub_token_client
423
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
424
+ end
425
+
426
+ it "invites an existing user" do
427
+ @organization_client.invite_existing_user("8f6e474e-e688-4bec-99ba-5dc862594f4b", "9a2ff47d-759c-4126-a281-8e4a7c6465e4")
428
+ end
429
+ end
430
+
431
+ describe "#invite_user" do
432
+ let(:realm_name) { "valid-realm" }
433
+
434
+ before(:each) do
435
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
436
+ stub_token_client
437
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
438
+ end
439
+
440
+ it "invites an existing user" do
441
+ @organization_client.invite_user("8f6e474e-e688-4bec-99ba-5dc862594f4b", "hello@acme.com", "John", "Doe")
442
+ end
443
+ end
444
+
445
+ describe "#delete_member" do
446
+ let(:realm_name) { "valid-realm" }
447
+
448
+ before(:each) do
449
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
450
+ stub_token_client
451
+ allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return ""
452
+ end
453
+
454
+ it "deletes a member" do
455
+ result = @organization_client.delete_member("8f6e474e-e688-4bec-99ba-5dc862594f4b", "e226d9d3-868e-453b-9bfc-d9d9cc534526")
456
+ expect(result).to be(true)
457
+ end
458
+ end
459
+
460
+ describe "#get_member" do
461
+ let(:realm_name) { "valid-realm" }
462
+
463
+ before(:each) do
464
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
465
+ json_payload = <<-'payload'
466
+ {
467
+ "id": "9a2ff47d-759c-4126-a281-8e4a7c6465e4",
468
+ "username": "hello",
469
+ "email": "hello@gmail.com",
470
+ "emailVerified": true,
471
+ "enabled": true,
472
+ "createdTimestamp": 1767608088024,
473
+ "totp": false,
474
+ "disableableCredentialTypes": [],
475
+ "requiredActions": [],
476
+ "notBefore": 0,
477
+ "membershipType": "UNMANAGED"
478
+ }
479
+ payload
480
+ stub_token_client
481
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
482
+ end
483
+
484
+ it "gets a member" do
485
+ member = @organization_client.get_member("8f6e474e-e688-4bec-99ba-5dc862594f4b", "9a2ff47d-759c-4126-a281-8e4a7c6465e4")
486
+ expect(member).to be
487
+ expect(member).to be_a KeycloakAdmin::MemberRepresentation
488
+ expect(member.id).to eq "9a2ff47d-759c-4126-a281-8e4a7c6465e4"
489
+ expect(member.membership_type).to eq "UNMANAGED"
490
+ end
491
+ end
492
+
493
+ describe "#add_member" do
494
+ let(:realm_name) { "valid-realm" }
495
+
496
+ before(:each) do
497
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
498
+ stub_token_client
499
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
500
+ end
501
+
502
+ it "creates a member from an existing user" do
503
+ result = @organization_client.add_member("8f6e474e-e688-4bec-99ba-5dc862594f4b", "9a2ff47d-759c-4126-a281-8e4a7c6465e4")
504
+ expect(result).to be true
505
+ end
506
+ end
507
+
508
+ describe "#add_member" do
509
+ let(:realm_name) { "valid-realm" }
510
+
511
+ before(:each) do
512
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
513
+ stub_token_client
514
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
515
+ end
516
+
517
+ it "creates a member from an existing user" do
518
+ result = @organization_client.add_member("8f6e474e-e688-4bec-99ba-5dc862594f4b", "9a2ff47d-759c-4126-a281-8e4a7c6465e4")
519
+ expect(result).to be true
520
+ end
521
+ end
522
+
523
+ describe "#update" do
524
+ let(:realm_name) { "valid-realm" }
525
+ let(:json_payload) do
526
+ <<-'payload'
527
+ {
528
+ "id": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
529
+ "name": "Company",
530
+ "alias": "company",
531
+ "enabled": true,
532
+ "description": "A test organization",
533
+ "redirectUrl": "https://myapp.acme.com/redirect",
534
+ "attributes": {
535
+ "hello": [
536
+ "yes"
537
+ ]
538
+ },
539
+ "domains": [
540
+ {
541
+ "name": "hello.com",
542
+ "verified": false
543
+ },
544
+ {
545
+ "name": "help.com",
546
+ "verified": false
547
+ }
548
+ ]
549
+ }
550
+ payload
551
+ end
552
+
553
+ before(:each) do
554
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
555
+ stub_token_client
556
+ allow_any_instance_of(RestClient::Resource).to receive(:put).and_return ""
557
+ allow_any_instance_of(RestClient::Resource).to receive(:get).and_return json_payload
558
+ end
559
+
560
+ it "updates an organization" do
561
+ organization = KeycloakAdmin::OrganizationRepresentation.from_json(json_payload)
562
+ updated_organization = @organization_client.update(organization)
563
+ expect(updated_organization).to be
564
+ expect(updated_organization).to be_a KeycloakAdmin::OrganizationRepresentation
565
+ expect(updated_organization.id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
566
+ end
567
+ end
568
+
569
+ describe "#create!" do
570
+ let(:realm_name) { "valid-realm" }
571
+ before(:each) do
572
+ @organization_client = KeycloakAdmin.realm(realm_name).organizations
573
+ stub_token_client
574
+ allow_any_instance_of(RestClient::Resource).to receive(:post).and_return ""
575
+ end
576
+
577
+ it "creates an organization" do
578
+ @organization_client.create!(
579
+ "new name",
580
+ "alias_name",
581
+ "enabled",
582
+ "description",
583
+ "http://redirect_url",
584
+ [
585
+ KeycloakAdmin::OrganizationDomainRepresentation.new("acme.com", true),
586
+ KeycloakAdmin::OrganizationDomainRepresentation.new("doe.com", false)
587
+ ],
588
+ {
589
+ "advanced": ["yes", "no"],
590
+ "hello": ["maybe"]
591
+ }
592
+ )
593
+ end
594
+ end
595
+ end
@@ -0,0 +1,64 @@
1
+
2
+ RSpec.describe KeycloakAdmin::OrganizationRepresentation do
3
+ describe ".from_json" do
4
+ it "parse a single organization" do
5
+ json_payload = <<-'payload'
6
+ {
7
+ "id": "8f6e474e-e688-4bec-99ba-5dc862594f4b",
8
+ "name": "My organization",
9
+ "alias": "myorg",
10
+ "enabled": true,
11
+ "description": "A single organization",
12
+ "redirectUrl": "https://myapp.acme.com",
13
+ "attributes": {
14
+ "advanced": [
15
+ "yes"
16
+ ],
17
+ "days": [
18
+ "monday",
19
+ "friday"
20
+ ]
21
+ },
22
+ "domains": [
23
+ {
24
+ "name": "hello.com",
25
+ "verified": false
26
+ },
27
+ {
28
+ "name": "gmail.com",
29
+ "verified": true
30
+ }
31
+ ]
32
+ }
33
+ payload
34
+
35
+ organization = described_class.from_json(json_payload)
36
+ expect(organization).to be
37
+ expect(organization).to be_a described_class
38
+ expect(organization.id).to eq "8f6e474e-e688-4bec-99ba-5dc862594f4b"
39
+ expect(organization.name).to eq "My organization"
40
+ expect(organization.alias).to eq "myorg"
41
+ expect(organization.description).to eq "A single organization"
42
+ expect(organization.redirect_url).to eq "https://myapp.acme.com"
43
+ expect(organization.enabled).to be true
44
+
45
+ expect(organization.domains.size).to eq 2
46
+ expect(organization.domains[0]).to be_a KeycloakAdmin::OrganizationDomainRepresentation
47
+ expect(organization.domains[0].name).to eq "hello.com"
48
+ expect(organization.domains[0].verified).to be false
49
+ expect(organization.domains[1]).to be_a KeycloakAdmin::OrganizationDomainRepresentation
50
+ expect(organization.domains[1].name).to eq "gmail.com"
51
+ expect(organization.domains[1].verified).to be true
52
+
53
+ expect(organization.attributes.size).to eq 2
54
+ expect(organization.attributes["advanced"].size).to eq 1
55
+ expect(organization.attributes["advanced"][0]).to eq "yes"
56
+ expect(organization.attributes["days"].size).to eq 2
57
+ expect(organization.attributes["days"][0]).to eq "monday"
58
+ expect(organization.attributes["days"][1]).to eq "friday"
59
+
60
+ expect(organization.members.size).to eq 0
61
+ expect(organization.identity_providers.size).to eq 0
62
+ end
63
+ end
64
+ end
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: 1.1.5
4
+ version: 1.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
@@ -104,6 +104,7 @@ files:
104
104
  - lib/keycloak-admin/client/configurable_token_client.rb
105
105
  - lib/keycloak-admin/client/group_client.rb
106
106
  - lib/keycloak-admin/client/identity_provider_client.rb
107
+ - lib/keycloak-admin/client/organization_client.rb
107
108
  - lib/keycloak-admin/client/realm_client.rb
108
109
  - lib/keycloak-admin/client/role_client.rb
109
110
  - lib/keycloak-admin/client/role_mapper_client.rb
@@ -125,6 +126,9 @@ files:
125
126
  - lib/keycloak-admin/representation/identity_provider_representation.rb
126
127
  - lib/keycloak-admin/representation/impersonation_redirection_representation.rb
127
128
  - lib/keycloak-admin/representation/impersonation_representation.rb
129
+ - lib/keycloak-admin/representation/member_representation.rb
130
+ - lib/keycloak-admin/representation/organization_domain_representation.rb
131
+ - lib/keycloak-admin/representation/organization_representation.rb
128
132
  - lib/keycloak-admin/representation/protocol_mapper_representation.rb
129
133
  - lib/keycloak-admin/representation/realm_representation.rb
130
134
  - lib/keycloak-admin/representation/representation.rb
@@ -147,6 +151,7 @@ files:
147
151
  - spec/client/configurable_token_client_spec.rb
148
152
  - spec/client/group_client_spec.rb
149
153
  - spec/client/identity_provider_client_spec.rb
154
+ - spec/client/organization_client_spec.rb
150
155
  - spec/client/realm_client_spec.rb
151
156
  - spec/client/role_client_spec.rb
152
157
  - spec/client/role_mapper_client_spec.rb
@@ -165,6 +170,7 @@ files:
165
170
  - spec/representation/identity_provider_mapper_representation_spec.rb
166
171
  - spec/representation/identity_provider_representation_spec.rb
167
172
  - spec/representation/impersonation_representation_spec.rb
173
+ - spec/representation/organization_representation_spec.rb
168
174
  - spec/representation/protocol_mapper_representation_spec.rb
169
175
  - spec/representation/role_representation_spec.rb
170
176
  - spec/representation/session_representation_spec.rb