Skip to content

Commit 388a40e

Browse files
authored
Merge pull request #136 from fschmtt/organization-resource
feat: add organizations resource
2 parents cca201a + a62828e commit 388a40e

File tree

10 files changed

+372
-17
lines changed

10 files changed

+372
-17
lines changed

README.md

+17-7
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
106106
### [Attack Detection](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_attack_detection)
107107

108108
| Endpoint | Response | API |
109-
|----------------------------------------------------------------------------|-------------------------|-------------------------------------------------------------------|
109+
| -------------------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------- |
110110
| `DELETE /admin/realms/{realm}/attack-detection/brute-force/users` | `n/a` | [AttackDetection::clear()](src/Resource/AttackDetection.php) |
111111
| `GET /admin/realms/{realm}/attack-detection/brute-force/users/{userId}` | [Map](src/Type/Map.php) | [AttackDetection::userStatus()](src/Resource/AttackDetection.php) |
112112
| `DELETE /admin/realms/{realm}/attack-detection/brute-force/users/{userId}` | `n/a` | [AttackDetection::clearUser()](src/Resource/AttackDetection.php) |
113113

114114
### [Clients](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_clients)
115115

116116
| Endpoint | Response | API |
117-
|----------------------------------------------------------------|---------------------------------------------------------|--------------------------------------------------------|
117+
| -------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------ |
118118
| `GET /admin/realms/{realm}/clients` | [ClientCollection](src/Collection/ClientCollection.php) | [Clients::all()](src/Resource/Clients.php) |
119119
| `GET /admin/realms/{realm}/clients/{client-uuid}` | [Client](src/Representation/Client.php) | [Clients::get()](src/Resource/Clients.php) |
120120
| `PUT /admin/realms/{realm}/clients/{client-uuid}` | [Client](src/Representation/Client.php) | [Clients::update()](src/Resource/Clients.php) |
@@ -124,7 +124,7 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
124124
### [Groups](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_clients)
125125

126126
| Endpoint | Response | API |
127-
|---------------------------------------------------|-------------------------------------------------------|-----------------------------------------------|
127+
| ------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------- |
128128
| `GET /admin/realms/{realm}/groups` | [GroupCollection](src/Collection/GroupCollection.php) | [Groups::all()](src/Resource/Groups.php) |
129129
| `GET /admin/realms/{realm}/groups/{id}/children` | [GroupCollection](src/Collection/GroupCollection.php) | [Groups::children()](src/Resource/Groups.php) |
130130
| `GET /admin/realms/{realm}/groups/{id}` | [Group](src/Representation/Group.php) | [Groups::get()](src/Resource/Groups.php) |
@@ -133,10 +133,20 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
133133
| `POST /admin/realms/{realm}/groups/{id}/children` | `n/a` | [Groups::create()](src/Resource/Groups.php) |
134134
| `DELETE /admin/realms/{realm}/groups` | `n/a` | [Groups::delete()](src/Resource/Groups.php) |
135135

136+
### [Organizations](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_organizations)
137+
138+
| Endpoint | Response | API |
139+
| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- |
140+
| `GET /admin/realms/{realm}/organizations` | [OrganizationCollection](src/Collection/OrganizationCollection.php) | [Organizations::all()](src/Resource/Organizations.php) |
141+
| `GET /admin/realms/{realm}/organizations/{id}` | [Organization](src/Representation/Organization.php) | [Organizations::get()](src/Resource/Organizations.php) |
142+
| `POST /admin/realms/{realm}/organizations` | `n/a` | [Organizations::create()](src/Resource/Organizations.php) |
143+
| `DELETE /admin/realms/{realm}/organizations/{id}` | `n/a` | [Organizations::delete()](src/Resource/Organizations.php) |
144+
| `POST /admin/realms/{realm}/organizations/{id}/members/invite-user` | `n/a` | [Organizations::inviteUser()](src/Resource/Organizations.php) |
145+
136146
### [Realms Admin](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_realms_admin)
137147

138148
| Endpoint | Response | API |
139-
|------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------|
149+
| ---------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------ |
140150
| `POST /admin/realms` | [Realm](src/Representation/Realm.php) | [Realms::import()](src/Resource/Realms.php) |
141151
| `GET /admin/realms` | [RealmCollection](src/Collection/RealmCollection.php) | [Realms::all()](src/Resource/Realms.php) |
142152
| `PUT /admin/realms/{realm}` | [Realm](src/Representation/Realm.php) | [Realms::update()](src/Resource/Realms.php) |
@@ -151,7 +161,7 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
151161
### [Users](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_users)
152162

153163
| Endpoint | Response | API |
154-
|---------------------------------------------------------|-----------------------------------------------------------------|----------------------------------------------------------------|
164+
| ------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------- |
155165
| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::all()](src/Resource/Users.php) |
156166
| `POST /admin/realms/{realm}/users` | `n/a` | [Users::create()](src/Resource/Users.php) |
157167
| `GET /admin/realms/{realm}/users/{userId}` | [User](src/Representation/User.php) | [Users::get()](src/Resource/Users.php) |
@@ -171,7 +181,7 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
171181
### [Roles](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_roles)
172182

173183
| Endpoint | Response | API |
174-
|-------------------------------------------------|-----------------------------------------------------|-------------------------------------------|
184+
| ----------------------------------------------- | --------------------------------------------------- | ----------------------------------------- |
175185
| `GET /admin/realms/{realm}/roles` | [RoleCollection](src/Collection/RoleCollection.php) | [Roles::all()](src/Resource/Roles.php) |
176186
| `GET /admin/realms/{realm}/roles/{roleName}` | [Role](src/Representation/Role.php) | [Roles::get()](src/Resource/Roles.php) |
177187
| `POST /admin/realms/{realm}/roles` | `n/a` | [Roles::create()](src/Resource/Roles.php) |
@@ -180,7 +190,7 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
180190
### [Root](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_root)
181191

182192
| Endpoint | Response | API |
183-
|-------------------------|-------------------------------------------------|--------------------------------------------------|
193+
| ----------------------- | ----------------------------------------------- | ------------------------------------------------ |
184194
| `GET /admin/serverinfo` | [ServerInfo](src/Representation/ServerInfo.php) | [ServerInfo::get()](src/Resource/ServerInfo.php) |
185195

186196
## Local development and testing

src/Http/CommandExecutor.php

+16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Fschmtt\Keycloak\Serializer\Serializer;
88

9+
use function is_array;
10+
911
/**
1012
* @internal
1113
*/
@@ -18,6 +20,20 @@ public function __construct(
1820

1921
public function executeCommand(Command $command): void
2022
{
23+
$payload = $command->getPayload();
24+
25+
if (is_array($payload)) {
26+
$this->client->request(
27+
$command->getMethod()->value,
28+
$command->getPath(),
29+
[
30+
'form_params' => $payload,
31+
],
32+
);
33+
34+
return;
35+
}
36+
2137
$this->client->request(
2238
$command->getMethod()->value,
2339
$command->getPath(),

src/Keycloak.php

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Fschmtt\Keycloak\Resource\AttackDetection;
1313
use Fschmtt\Keycloak\Resource\Clients;
1414
use Fschmtt\Keycloak\Resource\Groups;
15+
use Fschmtt\Keycloak\Resource\Organizations;
1516
use Fschmtt\Keycloak\Resource\Realms;
1617
use Fschmtt\Keycloak\Resource\Resource;
1718
use Fschmtt\Keycloak\Resource\Roles;
@@ -114,6 +115,13 @@ public function roles(): Roles
114115
return new Roles($this->commandExecutor, $this->queryExecutor);
115116
}
116117

118+
public function organizations(): Organizations
119+
{
120+
$this->fetchVersion();
121+
122+
return new Organizations($this->commandExecutor, $this->queryExecutor);
123+
}
124+
117125
/**
118126
* @param class-string<Resource> $resource
119127
* @return Resource

src/Representation/Realm.php

+6
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@
278278
* @method self withWebAuthnPolicySignatureAlgorithms(?string[] $value)
279279
* @method self withWebAuthnPolicyUserVerificationRequirement(?string $value)
280280
*
281+
* @method OrganizationCollection|null getOrganizations()
282+
* @method self withOrganizations(?OrganizationCollection $organizations)
283+
*
284+
* @method bool|null getOrganizationsEnabled()
285+
* @method self withOrganizationsEnabled(?bool $organizationsEnabled)
286+
*
281287
* @codeCoverageIgnore
282288
*/
283289
class Realm extends Representation

src/Resource/Organizations.php

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fschmtt\Keycloak\Resource;
6+
7+
use Fschmtt\Keycloak\Collection\OrganizationCollection;
8+
use Fschmtt\Keycloak\Http\Command;
9+
use Fschmtt\Keycloak\Http\Criteria;
10+
use Fschmtt\Keycloak\Http\Method;
11+
use Fschmtt\Keycloak\Http\Query;
12+
use Fschmtt\Keycloak\Representation\Organization;
13+
14+
class Organizations extends Resource
15+
{
16+
public function all(string $realm, ?Criteria $criteria = null): OrganizationCollection
17+
{
18+
return $this->queryExecutor->executeQuery(
19+
new Query(
20+
'/admin/realms/{realm}/organizations',
21+
OrganizationCollection::class,
22+
['realm' => $realm],
23+
$criteria,
24+
),
25+
);
26+
}
27+
28+
public function get(string $realm, string $id): Organization
29+
{
30+
return $this->queryExecutor->executeQuery(
31+
new Query(
32+
'/admin/realms/{realm}/organizations/{id}',
33+
Organization::class,
34+
['realm' => $realm, 'id' => $id],
35+
),
36+
);
37+
}
38+
39+
public function create(string $realm, Organization $organization): void
40+
{
41+
$this->commandExecutor->executeCommand(
42+
new Command(
43+
'/admin/realms/{realm}/organizations',
44+
Method::POST,
45+
['realm' => $realm],
46+
$organization,
47+
),
48+
);
49+
}
50+
51+
public function delete(string $realm, string $id): void
52+
{
53+
$this->commandExecutor->executeCommand(
54+
new Command(
55+
'/admin/realms/{realm}/organizations/{id}',
56+
Method::DELETE,
57+
['realm' => $realm, 'id' => $id],
58+
),
59+
);
60+
}
61+
62+
public function inviteUser(string $realm, string $id, string $email, string $firstName, string $lastName): void
63+
{
64+
$this->commandExecutor->executeCommand(
65+
new Command(
66+
'/admin/realms/{realm}/organizations/{id}/members/invite-user',
67+
Method::POST,
68+
['realm' => $realm, 'id' => $id],
69+
payload: [
70+
'email' => $email,
71+
'firstName' => $firstName,
72+
'lastName' => $lastName,
73+
],
74+
),
75+
);
76+
}
77+
}

src/Resource/Realms.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ public function import(Realm $realm): Realm
4747
new Command(
4848
'/admin/realms',
4949
Method::POST,
50-
[],
51-
$realm,
50+
payload: $realm,
5251
),
5352
);
5453

tests/Integration/KeycloakTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ class KeycloakTest extends TestCase
1414
public function testFetchesKeycloakVersionBeforeResourceIsAccessedForTheFirstTime(): void
1515
{
1616
$reflection = new ReflectionClass($this->getKeycloak());
17-
$version = $reflection->getProperty('version')->getValue($this->keycloak);
17+
$version = $reflection->getProperty('version')->getValue($this->getKeycloak());
1818

1919
static::assertNull($version);
2020

21-
$this->keycloak->realms();
21+
$this->getKeycloak()->realms();
2222

23-
$version = $reflection->getProperty('version')->getValue($this->keycloak);
23+
$version = $reflection->getProperty('version')->getValue($this->getKeycloak());
2424
static::assertIsString($version);
2525
}
2626
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fschmtt\Keycloak\Test\Integration\Resource;
6+
7+
use Fschmtt\Keycloak\Collection\OrganizationDomainCollection;
8+
use Fschmtt\Keycloak\Representation\Organization;
9+
use Fschmtt\Keycloak\Representation\OrganizationDomain;
10+
use Fschmtt\Keycloak\Representation\Realm;
11+
use Fschmtt\Keycloak\Test\Integration\IntegrationTestBehaviour;
12+
use GuzzleHttp\Exception\ServerException;
13+
use PHPUnit\Framework\Attributes\AfterClass;
14+
use PHPUnit\Framework\Attributes\BeforeClass;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class OrganizationsTest extends TestCase
18+
{
19+
use IntegrationTestBehaviour;
20+
private const REALM = 'organizations-tests';
21+
22+
public function testOrganizations(): void
23+
{
24+
$this->skipIfKeycloakVersionIsLessThan('26.0.0');
25+
26+
// Create realm
27+
$this->getKeycloak()->realms()->import(new Realm(realm: self::REALM, organizationsEnabled: true));
28+
29+
// No organizations exist yet in realm
30+
$organizations = $this->getKeycloak()->organizations()->all(self::REALM);
31+
static::assertCount(0, $organizations);
32+
33+
// Create a new organization in realm
34+
$createdOrganization = new Organization(
35+
name: 'created-organization',
36+
domains: new OrganizationDomainCollection([
37+
new OrganizationDomain('foo.bar', true),
38+
new OrganizationDomain('bar.foo', false),
39+
]),
40+
);
41+
$this->getKeycloak()->organizations()->create(self::REALM, $createdOrganization);
42+
43+
$organizations = $this->getKeycloak()->organizations()->all(self::REALM);
44+
static::assertCount(1, $organizations);
45+
static::assertSame($createdOrganization->getName(), $organizations->first()->getName());
46+
47+
// Get newly created organization
48+
$organization = $this->getKeycloak()->organizations()->get(self::REALM, $organizations->first()->getId());
49+
static::assertSame($createdOrganization->getName(), $organization->getName());
50+
51+
try {
52+
// Invite user to newly created organization
53+
$this->getKeycloak()->organizations()->inviteUser(
54+
self::REALM,
55+
$organizations->first()->getId(),
56+
57+
'John',
58+
'Doe',
59+
);
60+
} catch (ServerException $e) {
61+
// Error is expected as SMTP is not configured
62+
static::assertSame(500, $e->getCode());
63+
static::assertSame(
64+
['errorMessage' => 'Failed to send invite email'],
65+
json_decode($e->getResponse()->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR),
66+
);
67+
}
68+
69+
// Delete newly created organization
70+
$this->getKeycloak()->organizations()->delete(self::REALM, $organizations->first()->getId());
71+
$organizations = $this->getKeycloak()->organizations()->all(self::REALM);
72+
static::assertCount(0, $organizations);
73+
74+
// Delete realm
75+
$this->getKeycloak()->realms()->delete(self::REALM);
76+
}
77+
}

tests/Unit/Http/CommandExecutorTest.php

+2-5
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public function testCallsClientWithBodyIfCommandHasCollection(): void
101101
$executor->executeCommand($command);
102102
}
103103

104-
public function testCallsClientWithBodyIfCommandHasArrayPayload(): void
104+
public function testCallsClientWithFormParamsIfCommandHasArrayPayload(): void
105105
{
106106
$command = new Command(
107107
'/path/to/resource',
@@ -117,10 +117,7 @@ public function testCallsClientWithBodyIfCommandHasArrayPayload(): void
117117
Method::PUT->value,
118118
'/path/to/resource',
119119
[
120-
'body' => (new JsonEncoder())->encode($payload),
121-
'headers' => [
122-
'Content-Type' => 'application/json',
123-
],
120+
'form_params' => $payload,
124121
],
125122
);
126123

0 commit comments

Comments
 (0)