Skip to content

Commit 22ef166

Browse files
authored
Merge pull request #56 from fschmtt/origin/feature/user-realm-roles
Add user add/remove realm roles endpoints in Users
2 parents 1446ef2 + c245196 commit 22ef166

File tree

15 files changed

+617
-14
lines changed

15 files changed

+617
-14
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ More examples can be found in the [examples](examples) directory.
9999
| `PUT /{realm}/users/{id}/groups/{groupId}` | `n/a` | [Users::joinGroup()](src/Resource/Users.php) |
100100
| `DELETE /{realm}/users/{id}/groups/{groupId}` | `n/a` | [Users::leaveGroup()](src/Resource/Users.php) |
101101
| `GET /{realm}/users/{id}/groups` | [GroupCollection](src/Collection/GroupCollection.php) | [Users::retrieveGroups()](src/Resource/Users.php) |
102+
| `GET /{realm}/users/{id}/role-mappings/realm` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveRealmRoles()](src/Resource/Users.php) |
103+
| `GET /{realm}/users/{id}/role-mappings/realm/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableRealmRoles()](src/Resource/Users.php) |
104+
| `POST /{realm}/users/{id}/role-mappings/realm` | `n/a` | [Users::addRealmRoles()](src/Resource/Users.php) |
105+
| `DELETE /{realm}/users/{id}/role-mappings/realm` | `n/a` | [Users::removeRealmRoles()](src/Resource/Users.php) |
106+
107+
### [Roles](https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_roles_resource)
108+
| Endpoint | Response | API |
109+
|----------|----------|-----|
110+
| `GET /admin/realms/{realm}/roles` | [RoleCollection](src/Collection/RoleCollection.php) | [Roles::all()](src/Resource/Roles.php) |
111+
| `GET /admin/realms/{realm}/roles/{roleName}` | [Role](src/Representation/Role.php) | [Roles::get()](src/Resource/Roles.php) |
112+
| `POST /admin/realms/{realm}/roles` | `n/a` | [Roles::create()](src/Resource/Roles.php) |
113+
| `DELETE /admin/realms/{realm}/roles/{roleName}` | `n/a` | [Roles::delete()](src/Resource/Roles.php) |
102114

103115
### [Root](https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_root_resource)
104116
| Endpoint | Response | API |

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
"phpstan": "docker compose run --rm --no-deps php vendor/bin/phpstan analyze src tests examples",
4444
"psalm": "docker compose run --rm --no-deps php vendor/bin/psalm src tests examples",
4545
"test": [
46-
"@test-unit",
47-
"@test-integration"
46+
"@test:unit",
47+
"@test:integration"
4848
],
4949
"test:unit": "docker compose run --rm --no-deps php vendor/bin/phpunit --testsuite unit",
5050
"test:integration": "docker compose run --rm --no-deps php vendor/bin/phpunit --testsuite integration"

src/Collection/RoleCollection.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fschmtt\Keycloak\Collection;
6+
7+
use Fschmtt\Keycloak\Representation\Role;
8+
9+
/**
10+
* @method Role[] getIterator()
11+
* @codeCoverageIgnore
12+
*/
13+
class RoleCollection extends Collection
14+
{
15+
public static function getRepresentationClass(): string
16+
{
17+
return Role::class;
18+
}
19+
}

src/Http/Command.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Fschmtt\Keycloak\Http;
66

7+
use Fschmtt\Keycloak\Collection\Collection;
78
use Fschmtt\Keycloak\Representation\Representation;
89

910
class Command
@@ -12,7 +13,7 @@ public function __construct(
1213
private readonly string $path,
1314
private readonly Method $method,
1415
private readonly array $parameters = [],
15-
private readonly ?Representation $representation = null,
16+
private readonly Representation|Collection|null $payload = null,
1617
) {
1718
}
1819

@@ -37,8 +38,8 @@ public function getPath(): string
3738
);
3839
}
3940

40-
public function getRepresentation(): ?Representation
41+
public function getPayload(): Representation|Collection|null
4142
{
42-
return $this->representation;
43+
return $this->payload;
4344
}
4445
}

src/Http/CommandExecutor.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Fschmtt\Keycloak\Http;
66

77
use Fschmtt\Keycloak\Json\JsonEncoder;
8+
use Fschmtt\Keycloak\Representation\Representation;
89

910
class CommandExecutor
1011
{
@@ -20,13 +21,32 @@ public function executeCommand(Command $command): void
2021
$command->getMethod()->value,
2122
$command->getPath(),
2223
[
23-
'body' => $command->getRepresentation()
24-
? (new JsonEncoder())->encode($this->propertyFilter->filter($command->getRepresentation()))
25-
: null,
24+
'body' => $this->prepareBody($command),
2625
'headers' => [
2726
'Content-Type' => 'application/json',
2827
],
2928
]
3029
);
3130
}
31+
32+
protected function prepareBody(Command $command): ?string
33+
{
34+
if ($command->getPayload() === null) {
35+
return null;
36+
}
37+
38+
$jsonEncoder = new JsonEncoder();
39+
40+
if ($command->getPayload() instanceof Representation) {
41+
return $jsonEncoder->encode($this->propertyFilter->filter($command->getPayload()));
42+
}
43+
44+
$representations = [];
45+
46+
foreach ($command->getPayload() as $representation) {
47+
$representations[] = $this->propertyFilter->filter($representation);
48+
}
49+
50+
return $jsonEncoder->encode($representations);
51+
}
3252
}

src/Keycloak.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Fschmtt\Keycloak\Resource\Clients;
1313
use Fschmtt\Keycloak\Resource\Groups;
1414
use Fschmtt\Keycloak\Resource\Realms;
15+
use Fschmtt\Keycloak\Resource\Roles;
1516
use Fschmtt\Keycloak\Resource\ServerInfo;
1617
use Fschmtt\Keycloak\Resource\Users;
1718
use Fschmtt\Keycloak\Serializer\Factory as SerializerFactory;
@@ -91,6 +92,13 @@ public function groups(): Groups
9192
return new Groups($this->commandExecutor, $this->queryExecutor);
9293
}
9394

95+
public function roles(): Roles
96+
{
97+
$this->fetchVersion();
98+
99+
return new Roles($this->commandExecutor, $this->queryExecutor);
100+
}
101+
94102
private function fetchVersion(): void
95103
{
96104
if ($this->version) {

src/Resource/Roles.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fschmtt\Keycloak\Resource;
6+
7+
use Fschmtt\Keycloak\Collection\RoleCollection;
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\Role;
13+
14+
class Roles extends Resource
15+
{
16+
public function all(string $realm, ?Criteria $criteria = null): RoleCollection
17+
{
18+
return $this->queryExecutor->executeQuery(
19+
new Query(
20+
'/admin/realms/{realm}/roles',
21+
RoleCollection::class,
22+
[
23+
'realm' => $realm,
24+
],
25+
$criteria
26+
)
27+
);
28+
}
29+
30+
public function get(string $realm, string $roleName): Role
31+
{
32+
return $this->queryExecutor->executeQuery(
33+
new Query(
34+
'/admin/realms/{realm}/roles/{roleName}',
35+
Role::class,
36+
[
37+
'realm' => $realm,
38+
'roleName' => $roleName,
39+
]
40+
)
41+
);
42+
}
43+
44+
public function create(string $realm, Role $role): void
45+
{
46+
$this->commandExecutor->executeCommand(
47+
new Command(
48+
'/admin/realms/{realm}/roles',
49+
Method::POST,
50+
[
51+
'realm' => $realm,
52+
],
53+
$role,
54+
)
55+
);
56+
}
57+
58+
public function delete(string $realm, string $roleName): void
59+
{
60+
$this->commandExecutor->executeCommand(
61+
new Command(
62+
'/admin/realms/{realm}/roles/{roleName}',
63+
Method::DELETE,
64+
[
65+
'realm' => $realm,
66+
'roleName' => $roleName,
67+
],
68+
)
69+
);
70+
}
71+
}

src/Resource/Users.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Fschmtt\Keycloak\Resource;
66

77
use Fschmtt\Keycloak\Collection\GroupCollection;
8+
use Fschmtt\Keycloak\Collection\RoleCollection;
89
use Fschmtt\Keycloak\Collection\UserCollection;
910
use Fschmtt\Keycloak\Http\Command;
1011
use Fschmtt\Keycloak\Http\Criteria;
@@ -143,4 +144,62 @@ public function retrieveGroups(string $realm, string $userId, ?Criteria $criteri
143144
)
144145
);
145146
}
147+
148+
public function retrieveRealmRoles(string $realm, string $userId): RoleCollection
149+
{
150+
return $this->queryExecutor->executeQuery(
151+
new Query(
152+
'/admin/realms/{realm}/users/{userId}/role-mappings/realm',
153+
RoleCollection::class,
154+
[
155+
'realm' => $realm,
156+
'userId' => $userId,
157+
]
158+
)
159+
);
160+
}
161+
162+
public function retrieveAvailableRealmRoles(string $realm, string $userId): RoleCollection
163+
{
164+
return $this->queryExecutor->executeQuery(
165+
new Query(
166+
'/admin/realms/{realm}/users/{userId}/role-mappings/realm/available',
167+
RoleCollection::class,
168+
[
169+
'realm' => $realm,
170+
'userId' => $userId,
171+
]
172+
)
173+
);
174+
}
175+
176+
public function addRealmRoles(string $realm, string $userId, RoleCollection $roles): void
177+
{
178+
$this->commandExecutor->executeCommand(
179+
new Command(
180+
'/admin/realms/{realm}/users/{userId}/role-mappings/realm',
181+
Method::POST,
182+
[
183+
'realm' => $realm,
184+
'userId' => $userId,
185+
],
186+
$roles
187+
)
188+
);
189+
}
190+
191+
public function removeRealmRoles(string $realm, string $userId, RoleCollection $roles): void
192+
{
193+
$this->commandExecutor->executeCommand(
194+
new Command(
195+
'/admin/realms/{realm}/users/{userId}/role-mappings/realm',
196+
Method::DELETE,
197+
[
198+
'realm' => $realm,
199+
'userId' => $userId,
200+
],
201+
$roles
202+
)
203+
);
204+
}
146205
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fschmtt\Keycloak\Test\Integration\Resource;
6+
7+
use Exception;
8+
use Fschmtt\Keycloak\Http\Criteria;
9+
use Fschmtt\Keycloak\Representation\Role;
10+
use Fschmtt\Keycloak\Test\Integration\IntegrationTestBehaviour;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class RolesTest extends TestCase
14+
{
15+
use IntegrationTestBehaviour;
16+
17+
public function testCreateRetrieveDeleteRole(): void
18+
{
19+
$resource = $this->getKeycloak()->roles();
20+
21+
// Get all roles
22+
$allRoles = $resource->all('master');
23+
static::assertGreaterThanOrEqual(1, $allRoles->count());
24+
$role = $allRoles->first();
25+
static::assertInstanceOf(Role::class, $role);
26+
27+
// Create role
28+
$resource->create(
29+
'master',
30+
new Role(name: 'test-role', description: 'test-role-description')
31+
);
32+
33+
// Search (created) role
34+
$role = $resource->all('master', new Criteria([
35+
'search' => 'test-role',
36+
]))->first();
37+
static::assertInstanceOf(Role::class, $role);
38+
static::assertEquals('test-role', $role->getName());
39+
40+
// Get single (created) role
41+
$role = $resource->get('master', 'test-role');
42+
static::assertSame('test-role', $role->getName());
43+
44+
// Delete (created) role
45+
$resource->delete('master', 'test-role');
46+
47+
try {
48+
$resource->get('master', 'test-role');
49+
static::fail('Role should not exist anymore');
50+
} catch (Exception $e) {
51+
static::assertSame(404, $e->getCode());
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)