Skip to content

Commit c8fd863

Browse files
authored
chore(KeycloakHelper): Add Keycloak user and group creation / deletion in bulk and requireEnv util function (#55)
* Add keycloak configuration helper function Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Introduce requireEnv util Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Add bulk creation and deletion methods Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Revert deletion change Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Do not delete default users and groups Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Update changelog Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Fix type Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Bump package version Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> --------- Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
1 parent dc8699a commit c8fd863

10 files changed

Lines changed: 226 additions & 30 deletions

File tree

docs/api/deployment/keycloak-helper.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak";
1414
new KeycloakHelper(options?: KeycloakDeploymentOptions)
1515
```
1616

17-
| Parameter | Type | Description |
18-
|-----------|------|-------------|
17+
| Parameter | Type | Description |
18+
| --------- | --------------------------- | --------------------------------- |
1919
| `options` | `KeycloakDeploymentOptions` | Optional deployment configuration |
2020

2121
## Properties
@@ -102,6 +102,22 @@ async connect(config: KeycloakConnectionConfig): Promise<void>
102102

103103
Connect to an existing Keycloak instance.
104104

105+
### `createUsersAndGroups()`
106+
107+
```typescript
108+
async createUsersAndGroups(realm: string, options?: { users?: KeycloakUserConfig[]; groups?: KeycloakGroupConfig[]; }): Promise<void>
109+
```
110+
111+
Create new users and groups in a realm.
112+
113+
### `deleteUsersAndGroups()`
114+
115+
```typescript
116+
async deleteUsersAndGroups(realm: string, options?: { users?: Array<KeycloakUserConfig | string>; groups?: Array<KeycloakGroupConfig | string> }): Promise<void>
117+
```
118+
119+
Delete users and groups from a realm.
120+
105121
### `createRealm()`
106122

107123
```typescript
@@ -175,6 +191,10 @@ async deleteUser(realm: string, username: string): Promise<void>
175191

176192
Delete a user.
177193

194+
::: warning
195+
Deleting default Keycloak users (see DEFAULT_USERS in [constants](https://github.com/redhat-developer/rhdh-e2e-test-utils/blob/main/src/deployment/keycloak/constants.ts), e.g. `test1`, `test2`) is **not permitted** and will throw an error.
196+
:::
197+
178198
### `deleteGroup()`
179199

180200
```typescript
@@ -183,6 +203,10 @@ async deleteGroup(realm: string, groupName: string): Promise<void>
183203

184204
Delete a group.
185205

206+
::: warning
207+
Deleting default Keycloak groups (see `DEFAULT_GROUPS` in [constants](https://github.com/redhat-developer/rhdh-e2e-test-utils/blob/main/src/deployment/keycloak/constants.ts), e.g. `developers`, `admins`, `viewers`) is **not permitted** and will throw an error.
208+
:::
209+
186210
### `deleteRealm()`
187211

188212
```typescript

docs/changelog.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [1.1.17] - Current
5+
## [1.1.18] - Current
6+
7+
### Added
8+
9+
- **KeycloakHelper.createUsersAndGroups(realm: string, options?: { users?: KeycloakUserConfig[]; groups?: KeycloakGroupConfig[];})**: Create users and groups in a realm.
10+
- **KeycloakHelper.deleteUsersAndGroups(realm: string, options?: { users?: Array<KeycloakUserConfig | string>; groups?: Array<KeycloakGroupConfig | string>;})**: Delete users and groups in a realm by their usernames / names or by their KeycloakConfigs. Intended for user and group cleanup in bulk.
11+
12+
### Fixed
13+
14+
- **KeycloakHelper.deleteUser** and **KeycloakHelper.deleteGroup**: Default Keycloak users/groups (see `DEFAULT_USERS` / `DEFAULT_GROUPS`) can no longer be deleted; attempting to delete them throws an error.
15+
16+
## [1.1.17]
617

718
### Added
819

@@ -14,7 +25,7 @@ All notable changes to this project will be documented in this file.
1425

1526
- **Duplicate plugin when no user `dynamic-plugins.yaml` (Keycloak auth, PR build)**: When the workspace had no `dynamic-plugins.yaml`, auto-generated config (with OCI URL) was merged with auth config (with local path). Because merge used exact `package` string match, the same plugin appeared twice and the backend failed with `ExtensionPoint with ID 'keycloak.transformer' is already registered`. The merge now uses a normalized plugin key so OCI and local path for the same logical plugin are deduplicated; the metadata-derived entry (e.g. OCI URL) wins.
1627

17-
## [1.1.15] - Current
28+
## [1.1.15]
1829

1930
### Added
2031

@@ -228,7 +239,7 @@ All notable changes to this project will be documented in this file.
228239
1. **Update imports** - No changes required
229240
2. **Configure authentication** - Use the new `auth` option:
230241
```typescript
231-
await rhdh.configure({ auth: 'keycloak' });
242+
await rhdh.configure({ auth: "keycloak" });
232243
```
233244
3. **Keycloak auto-deployment** - Keycloak is now automatically deployed unless `SKIP_KEYCLOAK_DEPLOYMENT=true`
234245

@@ -245,6 +256,6 @@ After (1.1.x):
245256

246257
```typescript
247258
// Keycloak is auto-deployed and configured
248-
await rhdh.configure({ auth: 'keycloak' });
259+
await rhdh.configure({ auth: "keycloak" });
249260
await rhdh.deploy();
250261
```

docs/guide/deployment/authentication.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,18 @@ No additional environment variables required.
152152

153153
### Keycloak Auth
154154

155-
These are automatically set by `KeycloakHelper.configureForRHDH()`:
156-
157-
| Variable | Description |
158-
|----------|-------------|
159-
| `KEYCLOAK_BASE_URL` | Keycloak instance URL |
160-
| `KEYCLOAK_REALM` | Realm name |
161-
| `KEYCLOAK_CLIENT_ID` | OIDC client ID |
162-
| `KEYCLOAK_CLIENT_SECRET` | OIDC client secret |
163-
| `KEYCLOAK_METADATA_URL` | OIDC discovery URL |
164-
| `KEYCLOAK_LOGIN_REALM` | Login realm name |
155+
These are automatically set by `KeycloakHelper.configureForRHDH()` or populated from global workspace in the vault:
156+
157+
| Variable | Description |
158+
| ------------------------------- | --------------------- |
159+
| `KEYCLOAK_BASE_URL` | Keycloak instance URL |
160+
| `KEYCLOAK_REALM` | Realm name |
161+
| `KEYCLOAK_CLIENT_ID` | OIDC client ID |
162+
| `KEYCLOAK_CLIENT_SECRET` | OIDC client secret |
163+
| `KEYCLOAK_METADATA_URL` | OIDC discovery URL |
164+
| `KEYCLOAK_LOGIN_REALM` | Login realm name |
165+
| `VAULT_KEYCLOAK_ADMIN_USERNAME` | Admin username |
166+
| `VAULT_KEYCLOAK_ADMIN_PASSWORD` | Admin password |
165167

166168
### GitHub Auth
167169

docs/guide/deployment/keycloak-deployment.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ await keycloak.configureForRHDH({
9292
clientId: "my-client",
9393
clientSecret: "my-secret",
9494
},
95-
groups: ["developers", "admins"],
95+
groups: [{ name: "developers" }, { name: "admins" }],
9696
users: [
9797
{ username: "user1", password: "pass1", groups: ["developers"] },
9898
{ username: "user2", password: "pass2", groups: ["admins"] },
@@ -156,6 +156,18 @@ const users = await keycloak.getUsers("rhdh");
156156

157157
// Delete user
158158
await keycloak.deleteUser("rhdh", "username");
159+
160+
// Create multiple users
161+
await keycloak.createUsersAndGroups("rhdh", {
162+
users: [
163+
{ username: "user1", password: "pass1" },
164+
{ username: "user2", password: "pass2", groups: ["developers"] },
165+
],
166+
});
167+
168+
// Delete multiple users - by objects or by usernames
169+
await keycloak.deleteUsersAndGroups("rhdh", { users });
170+
await keycloak.deleteUsersAndGroups("rhdh", { users: ["user1", "user2"] });
159171
```
160172

161173
### Group Management
@@ -169,6 +181,15 @@ const groups = await keycloak.getGroups("rhdh");
169181

170182
// Delete group
171183
await keycloak.deleteGroup("rhdh", "testers");
184+
185+
// Create multiple groups
186+
await keycloak.createUsersAndGroups("rhdh", {
187+
groups: [{ name: "admins" }, { name: "viewers" }],
188+
});
189+
190+
// Delete multiple groups - by objects or by group names
191+
await keycloak.deleteUsersAndGroups("rhdh", { groups });
192+
await keycloak.deleteUsersAndGroups("rhdh", { groups: ["admins", "viewers"] });
172193
```
173194

174195
### Using getUsers and getGroupsOfUser in tests

docs/overlay/reference/environment-variables.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,10 @@ test.beforeAll(async ({ rhdh }) => {
215215
### Validating Required Variables
216216

217217
```typescript
218+
import { requireEnv } from "@red-hat-developer-hub/e2e-test-utils/utils";
219+
218220
test.beforeAll(async ({ rhdh }) => {
219-
const requiredVars = ["VAULT_API_KEY", "VAULT_SECRET"];
220-
for (const varName of requiredVars) {
221-
if (!process.env[varName]) {
222-
throw new Error(`Required variable ${varName} is not set`);
223-
}
224-
}
221+
requireEnv("VAULT_API_KEY", "VAULT_SECRET");
225222

226223
await rhdh.configure({ auth: "keycloak" });
227224
await rhdh.deploy();

docs/tutorials/keycloak-oidc-testing.md

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ test.beforeAll(async ({ rhdh }) => {
6666
// Connect to existing Keycloak
6767
await keycloak.connect({
6868
baseUrl: process.env.KEYCLOAK_BASE_URL!,
69-
username: "admin",
70-
password: "admin123",
69+
username: process.env.VAULT_KEYCLOAK_ADMIN_USERNAME!,
70+
password: process.env.VAULT_KEYCLOAK_ADMIN_PASSWORD!,
7171
});
7272

7373
// Create admin user
@@ -90,6 +90,56 @@ test.beforeAll(async ({ rhdh }) => {
9090
});
9191
```
9292

93+
## Creating Custom Users and Groups in Bulk
94+
95+
```typescript
96+
import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak";
97+
import type {
98+
KeycloakGroupConfig,
99+
KeycloakUserConfig,
100+
} from "@red-hat-developer-hub/e2e-test-utils/keycloak";
101+
102+
const TEST_GROUPS: KeycloakGroupConfig[] = [
103+
{ name: "writers" },
104+
{ name: "readers" },
105+
];
106+
107+
const TEST_USERS: Record<string, KeycloakUserConfig> = {
108+
reader: {
109+
username: "catalog-reader",
110+
password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"),
111+
groups: ["readers"],
112+
},
113+
writer: {
114+
username: "catalog-writer",
115+
password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"),
116+
groups: ["writers"],
117+
},
118+
};
119+
120+
test.beforeAll(async ({ rhdh }) => {
121+
const keycloak = new KeycloakHelper();
122+
await keycloak.connect({
123+
baseUrl: process.env.KEYCLOAK_BASE_URL!,
124+
username: process.env.VAULT_KEYCLOAK_ADMIN_USERNAME!,
125+
password: process.env.VAULT_KEYCLOAK_ADMIN_PASSWORD!,
126+
});
127+
await keycloak.createUsersAndGroups(process.env.KEYCLOAK_REALM!, {
128+
users: Object.values(TEST_USERS),
129+
groups: TEST_GROUPS,
130+
});
131+
132+
await rhdh.configure({ auth: "keycloak" });
133+
await rhdh.deploy();
134+
});
135+
136+
test.describe("Writer access", () => {
137+
test.beforeEach(async ({ page, loginHelper }) => {
138+
await page.goto("/");
139+
await loginHelper.loginAsKeycloakUser(TEST_USERS.writer.username, TEST_USERS.writer.password);
140+
});
141+
```
142+
93143
## Testing Role-Based Access
94144
95145
```typescript
@@ -160,8 +210,8 @@ test.afterAll(async () => {
160210
const keycloak = new KeycloakHelper();
161211
await keycloak.connect({
162212
baseUrl: process.env.KEYCLOAK_BASE_URL!,
163-
username: "admin",
164-
password: "admin123",
213+
username: process.env.VAULT_KEYCLOAK_ADMIN_USERNAME!,
214+
password: process.env.VAULT_KEYCLOAK_ADMIN_PASSWORD!,
165215
});
166216

167217
// Cleanup custom users
@@ -170,6 +220,27 @@ test.afterAll(async () => {
170220
});
171221
```
172222
223+
## Cleanup in Bulk
224+
225+
```typescript
226+
import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak";
227+
228+
test.afterAll(async () => {
229+
const keycloak = new KeycloakHelper();
230+
await keycloak.connect({
231+
baseUrl: process.env.KEYCLOAK_BASE_URL!,
232+
username: process.env.VAULT_KEYCLOAK_ADMIN_USERNAME!,
233+
password: process.env.VAULT_KEYCLOAK_ADMIN_PASSWORD!,
234+
});
235+
236+
// Cleanup custom users and groups
237+
await keycloak.deleteUsersAndGroups("rhdh", {
238+
users: TEST_USERS,
239+
groups: TEST_GROUPS,
240+
});
241+
});
242+
```
243+
173244
## Best Practices
174245
175246
1. **Use default users for simple tests** - `test1`, `test2`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@red-hat-developer-hub/e2e-test-utils",
3-
"version": "1.1.17",
3+
"version": "1.1.18",
44
"description": "Test utilities for RHDH E2E tests",
55
"license": "Apache-2.0",
66
"repository": {

src/deployment/keycloak/deployment.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,28 @@ export class KeycloakHelper {
270270
}
271271
}
272272

273+
/**
274+
* Create users and groups in a realm.
275+
*/
276+
async createUsersAndGroups(
277+
realm: string,
278+
options: {
279+
users?: KeycloakUserConfig[];
280+
groups?: KeycloakGroupConfig[];
281+
},
282+
): Promise<void> {
283+
await this._ensureAdminClient();
284+
const { groups = [], users = [] } = options;
285+
286+
for (const group of groups) {
287+
await this.createGroup(realm, group);
288+
}
289+
290+
for (const user of users) {
291+
await this.createUser(realm, user);
292+
}
293+
}
294+
273295
/**
274296
* Get all users in a realm
275297
*/
@@ -324,6 +346,12 @@ export class KeycloakHelper {
324346
* Delete a user from a realm
325347
*/
326348
async deleteUser(realm: string, username: string): Promise<void> {
349+
if (DEFAULT_USERS.some((u) => u.username === username)) {
350+
throw new Error(
351+
`Deleting default Keycloak user "${username}" is not permitted.`,
352+
);
353+
}
354+
327355
await this._ensureAdminClient();
328356
this._adminClient!.setConfig({ realmName: realm });
329357

@@ -338,6 +366,12 @@ export class KeycloakHelper {
338366
* Delete a group from a realm
339367
*/
340368
async deleteGroup(realm: string, groupName: string): Promise<void> {
369+
if (DEFAULT_GROUPS.some((g) => g.name === groupName)) {
370+
throw new Error(
371+
`Deleting default Keycloak group "${groupName}" is not permitted.`,
372+
);
373+
}
374+
341375
await this._ensureAdminClient();
342376
this._adminClient!.setConfig({ realmName: realm });
343377

@@ -349,6 +383,33 @@ export class KeycloakHelper {
349383
}
350384
}
351385

386+
/**
387+
* Delete users and groups from a realm.
388+
*/
389+
async deleteUsersAndGroups(
390+
realm: string,
391+
options: {
392+
users?: Array<KeycloakUserConfig | string>;
393+
groups?: Array<KeycloakGroupConfig | string>;
394+
},
395+
): Promise<void> {
396+
await this._ensureAdminClient();
397+
const { groups = [], users = [] } = options;
398+
399+
const usernames = users.map((u) =>
400+
typeof u === "string" ? u : u.username,
401+
);
402+
const groupNames = groups.map((g) => (typeof g === "string" ? g : g.name));
403+
404+
for (const username of usernames) {
405+
await this.deleteUser(realm, username);
406+
}
407+
408+
for (const groupName of groupNames) {
409+
await this.deleteGroup(realm, groupName);
410+
}
411+
}
412+
352413
/**
353414
* Delete a realm
354415
*/

0 commit comments

Comments
 (0)