diff --git a/README.md b/README.md index 41941dd3..752f3a1c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,15 @@ setInterval(async () => { }, 58 * 1000); // 58 seconds ``` +In cases where you don't have a refresh token, eg. in a client credentials flow, you can simply call `kcAdminClient.auth` to get a new access token, like this: + +```js +const credentials = { grantType: 'client_credentials', clientId: 'clientId', clientSecret: 'some-client-secret-uuid' }; +await kcAdminClient.auth(credentials); + +setInterval(() => kcAdminClient.auth(credentials), 58 * 1000); // 58 seconds +``` + ## Supported APIs ### [Realm admin](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_realms_admin_resource) @@ -102,6 +111,7 @@ Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/ - Get the top-level representation of the realm (`GET /{realm}`) - Update the top-level information of the realm (`PUT /{realm}`) - Delete the realm (`DELETE /{realm}`) +- Partial export of existing realm into a JSON file (`POST /{realm}/partial-export`) - Get users management permissions (`GET /{realm}/users-management-permissions`) - Enable users management permissions (`PUT /{realm}/users-management-permissions`) - Get events (`GET /{realm}/events`) @@ -150,7 +160,9 @@ Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/ - Send an email-verification email to the user An email contains a link the user can click to verify their email address. (`PUT /{realm}/users/{id}/send-verify-email`) ### User group-mapping + Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/users.spec.ts#L178 + - Add user to group (`PUT /{id}/groups/{groupId}`) - List all user groups (`GET /{id}/groups`) - Count user groups (`GET /{id}/groups/count`) diff --git a/package-lock.json b/package-lock.json index d88e4df3..7de9d84b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "keycloak-admin", - "version": "1.14.1", + "name": "@keycloak/keycloak-admin-client", + "version": "1.14.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 752eee8d..ee209624 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@keycloak/keycloak-admin-client", - "version": "1.14.5", + "version": "1.14.9", "description": "keycloak admin client", "main": "lib/index.js", "files": [ diff --git a/src/client.ts b/src/client.ts index 5e4ee8df..b1e20bc7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,6 @@ import {getToken, Credentials} from './utils/auth'; import {defaultBaseUrl, defaultRealm} from './utils/constants'; +import {Cache} from './resources/cache'; import {Users} from './resources/users'; import {Groups} from './resources/groups'; import {Roles} from './resources/roles'; @@ -13,13 +14,9 @@ import {ServerInfo} from './resources/serverInfo'; import {WhoAmI} from './resources/whoAmI'; import {AttackDetection} from './resources/attackDetection'; import {AxiosRequestConfig} from 'axios'; -import './utils/window-polyfill'; -import Keycloak, { - KeycloakConfig, - KeycloakInitOptions, - KeycloakInstance, -} from 'keycloak-js'; + import {Sessions} from './resources/sessions'; +import {UserStorageProvider} from './resources/userStorageProvider'; export interface ConnectionConfig { baseUrl?: string; @@ -30,6 +27,7 @@ export interface ConnectionConfig { export class KeycloakAdminClient { // Resources public users: Users; + public userStorageProvider: UserStorageProvider; public groups: Groups; public roles: Roles; public clients: Clients; @@ -42,13 +40,14 @@ export class KeycloakAdminClient { public attackDetection: AttackDetection; public sessions: Sessions; public authenticationManagement: AuthenticationManagement; + public cache: Cache; // Members public baseUrl: string; public realmName: string; public accessToken: string; public refreshToken: string; - public keycloak: KeycloakInstance; + public keycloak: any; private requestConfig?: AxiosRequestConfig; @@ -61,6 +60,7 @@ export class KeycloakAdminClient { // Initialize resources this.users = new Users(this); + this.userStorageProvider = new UserStorageProvider(this); this.groups = new Groups(this); this.roles = new Roles(this); this.clients = new Clients(this); @@ -73,6 +73,7 @@ export class KeycloakAdminClient { this.whoAmI = new WhoAmI(this); this.sessions = new Sessions(this); this.attackDetection = new AttackDetection(this); + this.cache = new Cache(this); } public async auth(credentials: Credentials) { @@ -86,10 +87,13 @@ export class KeycloakAdminClient { this.refreshToken = refreshToken; } - public async init(init?: KeycloakInitOptions, config?: KeycloakConfig) { - this.keycloak = Keycloak(config); - await this.keycloak.init(init); - this.baseUrl = this.keycloak.authServerUrl; + public async init(init?, config?) { + if (window) { + const Keycloak = (await import('keycloak-js')).default; + this.keycloak = Keycloak(config); + await this.keycloak.init(init); + this.baseUrl = this.keycloak.authServerUrl; + } } public setAccessToken(token: string) { diff --git a/src/defs/clientInitialAccessPresentation.ts b/src/defs/clientInitialAccessPresentation.ts new file mode 100644 index 00000000..dc3cf94d --- /dev/null +++ b/src/defs/clientInitialAccessPresentation.ts @@ -0,0 +1,11 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clientinitialaccesspresentation + */ +export default interface ClientInitialAccessPresentation { + id?: string; + token?: string; + timestamp?: number; + expiration?: number; + count?: number; + remainingCount?: number; +} diff --git a/src/defs/componentRepresentation.ts b/src/defs/componentRepresentation.ts index 3b1f7aac..21f1f2e0 100644 --- a/src/defs/componentRepresentation.ts +++ b/src/defs/componentRepresentation.ts @@ -9,5 +9,5 @@ export default interface ComponentRepresentation { providerType?: string; parentId?: string; subType?: string; - config?: {[index: string]: string}; + config?: {[index: string]: string | string[]}; } diff --git a/src/defs/globalRequestResult.ts b/src/defs/globalRequestResult.ts new file mode 100644 index 00000000..bb650bc1 --- /dev/null +++ b/src/defs/globalRequestResult.ts @@ -0,0 +1,7 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_globalrequestresult + */ +export default interface GlobalRequestResult { + successRequests?: string[]; + failedRequests?: string[]; +} diff --git a/src/defs/keyMetadataRepresentation.ts b/src/defs/keyMetadataRepresentation.ts new file mode 100644 index 00000000..f0b0528f --- /dev/null +++ b/src/defs/keyMetadataRepresentation.ts @@ -0,0 +1,18 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_keysmetadatarepresentation-keymetadatarepresentation + */ +export default interface KeysMetadataRepresentation { + active?: {[index: string]: string}; + keys?: KeyMetadataRepresentation[]; +} + +export interface KeyMetadataRepresentation { + providerId?: string; + providerPriority?: number; + kid?: string; + status?: string; + type?: string; + algorithm?: string; + publicKey?: string; + certificate?: string; +} diff --git a/src/defs/synchronizationResultRepresentation.ts b/src/defs/synchronizationResultRepresentation.ts new file mode 100644 index 00000000..6efb54fa --- /dev/null +++ b/src/defs/synchronizationResultRepresentation.ts @@ -0,0 +1,12 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_synchronizationresult + */ + +export default interface SynchronizationResultRepresentation { + ignored?: boolean; + added?: number; + updated?: number; + removed?: number; + failed?: number; + status?: string; +} diff --git a/src/resources/authenticationManagement.ts b/src/resources/authenticationManagement.ts index 38f0fa15..17ade6c5 100644 --- a/src/resources/authenticationManagement.ts +++ b/src/resources/authenticationManagement.ts @@ -120,12 +120,19 @@ export class AuthenticationManagement extends Resource { urlParamKeys: ['flow'], }); - public addExecutionToFlow = this.makeRequest<{flow: string, provider: string}>({ + public addExecutionToFlow = this.makeRequest<{flow: string, provider: string}, AuthenticationExecutionInfoRepresentation>({ method: 'POST', path: '/flows/{flow}/executions/execution', urlParamKeys: ['flow'], returnResourceIdInLocationHeader: {field: 'id'}, - }) + }); + + public addFlowToFlow = this.makeRequest<{flow: string, alias: string, type: string, provider: string, description: string}, AuthenticationFlowRepresentation>({ + method: 'POST', + path: '/flows/{flow}/executions/flow', + urlParamKeys: ['flow'], + returnResourceIdInLocationHeader: {field: 'id'}, + }); public updateExecution = this.makeUpdateRequest<{flow: string}, AuthenticationExecutionInfoRepresentation>({ method: 'PUT', diff --git a/src/resources/cache.ts b/src/resources/cache.ts new file mode 100644 index 00000000..8db6dd2f --- /dev/null +++ b/src/resources/cache.ts @@ -0,0 +1,20 @@ +import Resource from './resource'; +import {KeycloakAdminClient} from '../client'; + +export class Cache extends Resource<{realm?: string}> { + + public clearUserCache = this.makeRequest<{}, void>({ + method: 'POST', + path: '/clear-user-cache', + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: '/admin/realms/{realm}', + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/src/resources/clients.ts b/src/resources/clients.ts index 2f304f88..202b450f 100644 --- a/src/resources/clients.ts +++ b/src/resources/clients.ts @@ -10,6 +10,7 @@ import RoleRepresentation from '../defs/roleRepresentation'; import UserRepresentation from '../defs/userRepresentation'; import UserSessionRepresentation from '../defs/userSessionRepresentation'; import ResourceEvaluation from '../defs/resourceEvaluation'; +import GlobalRequestResult from '../defs/globalRequestResult'; import Resource from './resource'; export interface ClientQuery { @@ -20,7 +21,16 @@ export interface ClientQuery { } export interface PolicyQuery { - name: string; + id?: string; + name?: string; + type?: string; + resource?: string; + scope?: string; + permission?: string; + owner?: string; + fields?: string; + first?: number; + max?: number; } export class Clients extends Resource<{realm?: string}> { @@ -500,7 +510,7 @@ export class Clients extends Resource<{realm?: string}> { * Policy */ public listPolicies = this.makeRequest< - {id: string, name: string}, + PolicyQuery, PolicyRepresentation[] >({ method: 'GET', @@ -508,7 +518,7 @@ export class Clients extends Resource<{realm?: string}> { urlParamKeys: ['id'], }); - public findByName = this.makeRequest< + public findPolicyByName = this.makeRequest< {id: string; name: string}, PolicyRepresentation >({ @@ -558,7 +568,7 @@ export class Clients extends Resource<{realm?: string}> { policyName: string; policy: PolicyRepresentation; }): Promise { - const policyFound = await this.findByName({ + const policyFound = await this.findPolicyByName({ id: payload.id, name: payload.policyName, }); @@ -567,7 +577,7 @@ export class Clients extends Resource<{realm?: string}> { {id: payload.id, policyId: policyFound.id, type: payload.policy.type}, payload.policy, ); - return this.findByName({id: payload.id, name: payload.policyName}); + return this.findPolicyByName({id: payload.id, name: payload.policyName}); } else { return this.createPolicy( {id: payload.id, type: payload.policy.type}, @@ -673,6 +683,30 @@ export class Clients extends Resource<{realm?: string}> { urlParamKeys: ['id', 'providerId'], }); + public pushRevocation = this.makeRequest<{id: string}, void>({ + method: 'POST', + path: '/{id}/push-revocation', + urlParamKeys: ['id'], + }); + + public addClusterNode = this.makeRequest<{id: string, node: string}, void>({ + method: 'POST', + path: '/{id}/nodes', + urlParamKeys: ['id'], + }); + + public deleteClusterNode = this.makeRequest<{id: string, node: string}, void>({ + method: 'DELETE', + path: '/{id}/nodes/{node}', + urlParamKeys: ['id', 'node'], + }); + + public testNodesAvailable = this.makeRequest<{id: string}, GlobalRequestResult>({ + method: 'GET', + path: '/{id}/test-nodes-available', + urlParamKeys: ['id'], + }); + constructor(client: KeycloakAdminClient) { super(client, { path: '/admin/realms/{realm}/clients', diff --git a/src/resources/realms.ts b/src/resources/realms.ts index 2a109826..f4d6a1e3 100644 --- a/src/resources/realms.ts +++ b/src/resources/realms.ts @@ -3,6 +3,8 @@ import AdminEventRepresentation from '../defs/adminEventRepresentation'; import RealmRepresentation from '../defs/realmRepresentation'; import EventRepresentation from '../defs/eventRepresentation'; import EventType from '../defs/eventTypes'; +import KeysMetadataRepresentation from '../defs/keyMetadataRepresentation'; +import ClientInitialAccessPresentation from '../defs/clientInitialAccessPresentation'; import {KeycloakAdminClient} from '../client'; @@ -44,6 +46,23 @@ export class Realms extends Resource { urlParamKeys: ['realm'], }); + public export = this.makeRequest< + { + realm: string, + exportClients?: boolean, + exportGroupsAndRoles?: boolean + }, + RealmRepresentation + >({ + method: 'POST', + path: '/{realm}/partial-export', + urlParamKeys: ['realm'], + queryParamKeys: [ + 'exportClients', + 'exportGroupsAndRoles' + ] + }); + /** * Get events Returns all events, or filters them based on URL query parameters listed here */ @@ -76,17 +95,46 @@ export class Realms extends Resource { ], }); + public getClientsInitialAccess = this.makeRequest< + {realm: string}, + ClientInitialAccessPresentation[] + >({ + method: 'GET', + path: '/{realm}/clients-initial-access', + urlParamKeys: ['realm'], + }); + + public createClientsInitialAccess = this.makeUpdateRequest< + {realm: string}, + {count?: number; expiration?: number}, + ClientInitialAccessPresentation + >({ + method: 'POST', + path: '/{realm}/clients-initial-access', + urlParamKeys: ['realm'], + }); + + public delClientsInitialAccess = this.makeRequest< + {realm: string, id: string}, void + >({ + method: 'DELETE', + path: '/{realm}/clients-initial-access/{id}', + urlParamKeys: ['realm', 'id'], + }); + /** * Remove a specific user session. */ - public removeSession = this.makeRequest<{realm: string, sessionId: string}, void>({ + public removeSession = this.makeRequest< + {realm: string; sessionId: string}, + void + >({ method: 'DELETE', path: '/{realm}/sessions/{session}', urlParamKeys: ['realm', 'session'], catchNotFound: true, }); - /** * Get admin events Returns all admin events, or filters events based on URL query parameters listed here */ @@ -149,10 +197,7 @@ export class Realms extends Resource { /** * Sessions */ - public logoutAll = this.makeRequest< - {realm: string}, - void - >({ + public logoutAll = this.makeRequest<{realm: string}, void>({ method: 'POST', path: '/{realm}/logout-all', urlParamKeys: ['realm'], @@ -167,6 +212,15 @@ export class Realms extends Resource { urlParamKeys: ['realm', 'session'], }); + public getKeys = this.makeRequest< + {realm: string}, + KeysMetadataRepresentation + >({ + method: 'GET', + path: '/{realm}/keys', + urlParamKeys: ['realm'], + }); + constructor(client: KeycloakAdminClient) { super(client, { path: '/admin/realms', diff --git a/src/resources/roles.ts b/src/resources/roles.ts index 1f088ff3..541e9826 100644 --- a/src/resources/roles.ts +++ b/src/resources/roles.ts @@ -47,7 +47,7 @@ export class Roles extends Resource<{realm?: string}> { }); public findUsersWithRole = this.makeRequest< - {name: string}, + {name: string; first?: number; max?: number}, UserRepresentation[] >({ method: 'GET', diff --git a/src/resources/userStorageProvider.ts b/src/resources/userStorageProvider.ts new file mode 100644 index 00000000..ad443332 --- /dev/null +++ b/src/resources/userStorageProvider.ts @@ -0,0 +1,60 @@ +import {KeycloakAdminClient} from '../client'; +import SynchronizationResultRepresentation from '../defs/synchronizationResultRepresentation'; +import Resource from './resource'; + +type ActionType = 'triggerFullSync' | 'triggerChangedUsersSync'; +type DirectionType = 'fedToKeycloak' | 'keycloakToFed'; +type NameResponse = { + id: string; + name: string; +}; + +export class UserStorageProvider extends Resource<{realm?: string}> { + public name = this.makeRequest<{id: string}, NameResponse>({ + method: 'GET', + path: '/{id}/name', + urlParamKeys: ['id'], + }); + + public removeImportedUsers = this.makeRequest<{id: string}, void>({ + method: 'POST', + path: '/{id}/remove-imported-users', + urlParamKeys: ['id'], + }); + + public sync = this.makeRequest< + {id: string; action?: ActionType}, + SynchronizationResultRepresentation + >({ + method: 'POST', + path: '/{id}/sync', + urlParamKeys: ['id'], + queryParamKeys: ['action'], + }); + + public unlinkUsers = this.makeRequest<{id: string}, void>({ + method: 'POST', + path: '/{id}/unlink-users', + urlParamKeys: ['id'], + }); + + public mappersSync = this.makeRequest< + {id: string; parentId: string; direction?: DirectionType}, + SynchronizationResultRepresentation + >({ + method: 'POST', + path: '/{parentId}/mappers/{id}/sync', + urlParamKeys: ['id', 'parentId'], + queryParamKeys: ['direction'], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: '/admin/realms/{realm}/user-storage', + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 8e259048..adeb4d5a 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -6,13 +6,14 @@ import {defaultBaseUrl, defaultRealm} from './constants'; export type GrantTypes = 'client_credentials' | 'password'; export interface Credentials { - username: string; - password: string; + username?: string; + password?: string; grantType: GrantTypes; clientId: string; clientSecret?: string; totp?: string; offlineToken?: boolean; + refreshToken?: string; } export interface Settings { @@ -49,7 +50,12 @@ export const getToken = async (settings: Settings): Promise => { client_id: credentials.clientId, totp: credentials.totp, ...(credentials.offlineToken ? {scope: 'offline_access'} : {}), + ...(credentials.refreshToken ? { + refresh_token: credentials.refreshToken, + client_secret: credentials.clientSecret, + } : {}), }); + const config: AxiosRequestConfig = { ...settings.requestConfig, }; diff --git a/src/utils/window-polyfill.ts b/src/utils/window-polyfill.ts deleted file mode 100644 index 6b3579ce..00000000 --- a/src/utils/window-polyfill.ts +++ /dev/null @@ -1,4 +0,0 @@ -const globalObject = typeof global !== "undefined" ? global as any : {}; -if (!globalObject.window) { - globalObject.window = {}; -} diff --git a/test/authenticationManagement.spec.ts b/test/authenticationManagement.spec.ts index ba23321b..41d0893d 100644 --- a/test/authenticationManagement.spec.ts +++ b/test/authenticationManagement.spec.ts @@ -145,11 +145,11 @@ describe('Authentication management', () => { }); it('should create new authentication flow', async () => { - const flowName = 'test'; - await kcAdminClient.authenticationManagement.createFlow({alias: flowName, providerId: 'basic-flow', description: '', topLevel: true, builtIn: false}); + const flow = 'test'; + await kcAdminClient.authenticationManagement.createFlow({alias: flow, providerId: 'basic-flow', description: '', topLevel: true, builtIn: false}); const flows = await kcAdminClient.authenticationManagement.getFlows(); - expect(flows.find(f => f.alias === flowName)).to.be.ok; + expect(flows.find(f => f.alias === flow)).to.be.ok; }); const flowName = 'copy of browser'; @@ -194,6 +194,14 @@ describe('Authentication management', () => { expect(execution.id).to.be.ok; }); + it('should add flow to a flow', async () => { + const flow = await kcAdminClient.authenticationManagement.addFlowToFlow({flow: flowName, alias: 'subFlow', description: '', provider: 'registration-page-form', type: 'basic-flow'}); + const executions = await kcAdminClient.authenticationManagement.getExecutions({flow: flowName}); + expect(flow.id).to.be.ok; + + expect(executions.map(execution => execution.displayName)).includes('subFlow'); + }); + it('should update execution to a flow', async () => { let executions = await kcAdminClient.authenticationManagement.getExecutions({flow: flowName}); let execution = executions[executions.length - 1]; diff --git a/test/clients.spec.ts b/test/clients.spec.ts index cc191888..1b5eb68c 100644 --- a/test/clients.spec.ts +++ b/test/clients.spec.ts @@ -886,4 +886,21 @@ describe('Clients', () => { expect(userSessions).to.be.ok; }); }); + + describe('nodes', () => { + const host = '127.0.0.1'; + it('register a node manually', async () => { + await kcAdminClient.clients.addClusterNode({id: currentClient.id, node: host}); + const client = await kcAdminClient.clients.findOne({id: currentClient.id}); + + expect(Object.keys(client.registeredNodes)).to.be.eql([host]); + }); + + it('remove registered host', async () => { + await kcAdminClient.clients.deleteClusterNode({id: currentClient.id, node: host}); + const client = await kcAdminClient.clients.findOne({id: currentClient.id}); + + expect(client.registeredNodes).to.be.undefined; + }); + }); }); diff --git a/test/groupUser.spec.ts b/test/groupUser.spec.ts index d8e24057..6f98dbf5 100644 --- a/test/groupUser.spec.ts +++ b/test/groupUser.spec.ts @@ -138,12 +138,15 @@ describe('Group user integration', () => { resourceName: permissions.resource, }); + const policies = await kcAdminClient.clients.listPolicies({id: managementClient.id, resource: permissions.resource, max: 2}); + expect(policies).to.have.length(2); + expect(scopes).to.have.length(5); // Search for the id of the management role const roleId = scopes.find(scope => scope.name === 'manage').id; - const userPolicy = await kcAdminClient.clients.findByName({id: managementClient.id, name: `policy.manager.${currentGroup.id}`}); + const userPolicy = await kcAdminClient.clients.findPolicyByName({id: managementClient.id, name: `policy.manager.${currentGroup.id}`}); expect(userPolicy).to.deep.include({ name: `policy.manager.${currentGroup.id}`, diff --git a/test/realms.spec.ts b/test/realms.spec.ts index 4af7d2f5..9f0f00e4 100644 --- a/test/realms.spec.ts +++ b/test/realms.spec.ts @@ -5,6 +5,27 @@ import {credentials} from './constants'; import faker from 'faker'; const expect = chai.expect; + +const createRealm = async (kcAdminClient: KeycloakAdminClient) => { + const realmId = faker.internet.userName().toLowerCase(); + const realmName = faker.internet.userName().toLowerCase(); + const realm = await kcAdminClient.realms.create({ + id: realmId, + realm: realmName, + }); + expect(realm.realmName).to.be.equal(realmName); + + return {realmId, realmName}; +}; + +const deleteRealm = async (kcAdminClient: KeycloakAdminClient, currentRealmName: string) => { + await kcAdminClient.realms.del({realm: currentRealmName}); + const realm = await kcAdminClient.realms.findOne({ + realm: currentRealmName, + }); + expect(realm).to.be.null; +}; + describe('Realms', () => { let kcAdminClient: KeycloakAdminClient; let currentRealmId: string; @@ -42,6 +63,18 @@ describe('Realms', () => { }); }); + it('export a realm', async () => { + const realm = await kcAdminClient.realms.export({ + realm: currentRealmName, + exportClients: true, + exportGroupsAndRoles: true, + }); + expect(realm).to.include({ + id: currentRealmId, + realm: currentRealmName, + }); + }); + it('update a realm', async () => { await kcAdminClient.realms.update( {realm: currentRealmName}, @@ -67,71 +100,70 @@ describe('Realms', () => { expect(realm).to.be.null; }); - describe('Realm Events', () => { + describe('Realm Client Initial Access', () => { before(async () => { kcAdminClient = new KeycloakAdminClient(); await kcAdminClient.auth(credentials); - const realmId = faker.internet.userName().toLowerCase(); - const realmName = faker.internet.userName().toLowerCase(); - const realm = await kcAdminClient.realms.create({ - id: realmId, - realm: realmName, - }); - expect(realm.realmName).to.be.equal(realmName); - currentRealmId = realmId; - currentRealmName = realmName; + const created = await createRealm(kcAdminClient); + currentRealmName = created.realmName; + + await kcAdminClient.realms.createClientsInitialAccess({realm: currentRealmName}, {count: 1, expiration: 0}); }); - it('list events of a realm', async () => { - // @TODO: In order to test it, there have to be events - const events = await kcAdminClient.realms.findEvents({ - realm: currentRealmName, - }); + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); + }); - expect(events).to.be.ok; + it('list client initial access', async () => { + const initialAccess = await kcAdminClient.realms.getClientsInitialAccess({realm: currentRealmName}); + expect(initialAccess).to.be.ok; + expect(initialAccess[0].count).to.be.eq(1); }); - after(async () => { - await kcAdminClient.realms.del({realm: currentRealmName}); - const realm = await kcAdminClient.realms.findOne({ - realm: currentRealmName, - }); - expect(realm).to.be.null; + it('del client initial access', async () => { + const access = await kcAdminClient.realms.createClientsInitialAccess({realm: currentRealmName}, {count: 1, expiration: 0}); + expect((await kcAdminClient.realms.getClientsInitialAccess({realm: currentRealmName})).length).to.be.eq(2); + + await kcAdminClient.realms.delClientsInitialAccess({realm: currentRealmName, id: access.id!}); + + const initialAccess = await kcAdminClient.realms.getClientsInitialAccess({realm: currentRealmName}); + expect(initialAccess).to.be.ok; + expect(initialAccess[0].count).to.be.eq(1); }); + }); - describe('Realm Admin Events', () => { + describe('Realm Events', () => { before(async () => { kcAdminClient = new KeycloakAdminClient(); await kcAdminClient.auth(credentials); - const realmId = faker.internet.userName().toLowerCase(); - const realmName = faker.internet.userName().toLowerCase(); - const realm = await kcAdminClient.realms.create({ - id: realmId, - realm: realmName, - }); - expect(realm.realmName).to.be.equal(realmName); - currentRealmId = realmId; - currentRealmName = realmName; + const created = await createRealm(kcAdminClient); + currentRealmId = created.realmId; + currentRealmName = created.realmName; }); it('list events of a realm', async () => { // @TODO: In order to test it, there have to be events - const events = await kcAdminClient.realms.findAdminEvents({ + const events = await kcAdminClient.realms.findEvents({ realm: currentRealmName, }); expect(events).to.be.ok; }); - after(async () => { - await kcAdminClient.realms.del({realm: currentRealmName}); - const realm = await kcAdminClient.realms.findOne({ + it('list admin events of a realm', async () => { + // @TODO: In order to test it, there have to be events + const events = await kcAdminClient.realms.findAdminEvents({ realm: currentRealmName, }); - expect(realm).to.be.null; + + expect(events).to.be.ok; + }); + + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); }); }); @@ -140,15 +172,9 @@ describe('Realms', () => { kcAdminClient = new KeycloakAdminClient(); await kcAdminClient.auth(credentials); - const realmId = faker.internet.userName().toLowerCase(); - const realmName = faker.internet.userName().toLowerCase(); - const realm = await kcAdminClient.realms.create({ - id: realmId, - realm: realmName, - }); - expect(realm.realmName).to.be.equal(realmName); - currentRealmId = realmId; - currentRealmName = realmName; + const created = await createRealm(kcAdminClient); + currentRealmId = created.realmId; + currentRealmName = created.realmName; }); it('get users management permissions', async () => { @@ -170,12 +196,13 @@ describe('Realms', () => { expect(managementPermissions).to.include({enabled: true}); }); + it('get realm keys', async () => { + const keys = await kcAdminClient.realms.getKeys({realm: currentRealmName}); + expect(keys.active).to.be.ok; + }); + after(async () => { - await kcAdminClient.realms.del({realm: currentRealmName}); - const realm = await kcAdminClient.realms.findOne({ - realm: currentRealmName, - }); - expect(realm).to.be.null; + deleteRealm(kcAdminClient, currentRealmName); }); }); @@ -184,15 +211,9 @@ describe('Realms', () => { kcAdminClient = new KeycloakAdminClient(); await kcAdminClient.auth(credentials); - const realmId = faker.internet.userName().toLowerCase(); - const realmName = faker.internet.userName().toLowerCase(); - const realm = await kcAdminClient.realms.create({ - id: realmId, - realm: realmName, - }); - expect(realm.realmName).to.be.equal(realmName); - currentRealmId = realmId; - currentRealmName = realmName; + const created = await createRealm(kcAdminClient); + currentRealmId = created.realmId; + currentRealmName = created.realmName; }); it('log outs all sessions', async () => { @@ -203,12 +224,7 @@ describe('Realms', () => { }); after(async () => { - await kcAdminClient.realms.del({realm: currentRealmName}); - const realm = await kcAdminClient.realms.findOne({ - realm: currentRealmName, - }); - expect(realm).to.be.null; + deleteRealm(kcAdminClient, currentRealmName); }); }); - }); diff --git a/test/userStorageProvider.spec.ts b/test/userStorageProvider.spec.ts new file mode 100644 index 00000000..f39989ad --- /dev/null +++ b/test/userStorageProvider.spec.ts @@ -0,0 +1,52 @@ +// tslint:disable:no-unused-expression +import * as chai from 'chai'; +import {KeycloakAdminClient} from '../src/client'; +import {credentials} from './constants'; +import faker from 'faker'; + +import ComponentRepresentation from '../src/defs/componentRepresentation'; + +const expect = chai.expect; + +describe('Users federation provider', () => { + let kcAdminClient: KeycloakAdminClient; + let currentUserFed: ComponentRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const name = faker.internet.userName(); + currentUserFed = await kcAdminClient.components.create({ + name, + parentId: 'master', + providerId: 'ldap', + providerType: 'org.keycloak.storage.UserStorageProvider', + }); + }); + + after(async () => { + await kcAdminClient.components.del({ + id: currentUserFed.id, + }); + }); + + it('list storage provider', async () => { + const name = await kcAdminClient.userStorageProvider.name({ + id: currentUserFed.id, + }); + expect(name).to.be.ok; + }); + + it('remove imported users', async () => { + await kcAdminClient.userStorageProvider.removeImportedUsers({ + id: currentUserFed.id, + }); + }); + + it('unlink users', async () => { + await kcAdminClient.userStorageProvider.unlinkUsers({ + id: currentUserFed.id, + }); + }); +}); diff --git a/test/users.spec.ts b/test/users.spec.ts index d7d89ca4..e1e713c1 100644 --- a/test/users.spec.ts +++ b/test/users.spec.ts @@ -81,8 +81,8 @@ describe('Users', function () { if (process.env.KEYCLOAK_VERSION && ( - process.env.KEYCLOAK_VERSION.startsWith('7.') - || process.env.KEYCLOAK_VERSION.startsWith('8.') + process.env.KEYCLOAK_VERSION.startsWith('7.') + || process.env.KEYCLOAK_VERSION.startsWith('8.') )) { // should be 1, but it seems it doesn't work issue: KEYCLOAK-16081 expect(numUsers).to.equal(2);