diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d9ccec5..4f496df7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "prettier.tslintIntegration": true, "editor.formatOnSave": true, - "javascript.format.enable": false + "javascript.format.enable": false, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, } diff --git a/src/defs/eventRepresentation.ts b/src/defs/eventRepresentation.ts new file mode 100644 index 00000000..ce557eba --- /dev/null +++ b/src/defs/eventRepresentation.ts @@ -0,0 +1,13 @@ +import EventType from './eventTypes'; + +export default interface EventRepresentation { + clientId?: string; + details?: Record; + error?: string; + ipAddress?: string; + realmId?: string; + sessionId?: string; + time?: number; + type?: EventType; + userId?: string; +} diff --git a/src/defs/eventTypes.ts b/src/defs/eventTypes.ts new file mode 100644 index 00000000..b7a92b71 --- /dev/null +++ b/src/defs/eventTypes.ts @@ -0,0 +1,108 @@ +enum EventType { + + LOGIN = 'LOGIN', + LOGIN_ERROR = 'LOGIN_ERROR', + REGISTER = 'REGISTER', + REGISTER_ERROR = 'REGISTER_ERROR', + LOGOUT = 'LOGOUT', + LOGOUT_ERROR = 'LOGOUT_ERROR', + + CODE_TO_TOKEN = 'CODE_TO_TOKEN', + CODE_TO_TOKEN_ERROR = 'CODE_TO_TOKEN_ERROR', + + CLIENT_LOGIN = 'CLIENT_LOGIN', + CLIENT_LOGIN_ERROR = 'CLIENT_LOGIN_ERROR', + + REFRESH_TOKEN = 'REFRESH_TOKEN', + REFRESH_TOKEN_ERROR = 'REFRESH_TOKEN_ERROR', + + VALIDATE_ACCESS_TOKEN = 'VALIDATE_ACCESS_TOKEN', + + VALIDATE_ACCESS_TOKEN_ERROR = 'VALIDATE_ACCESS_TOKEN_ERROR', + INTROSPECT_TOKEN = 'INTROSPECT_TOKEN', + INTROSPECT_TOKEN_ERROR = 'INTROSPECT_TOKEN_ERROR', + + FEDERATED_IDENTITY_LINK = 'FEDERATED_IDENTITY_LINK', + FEDERATED_IDENTITY_LINK_ERROR = 'FEDERATED_IDENTITY_LINK_ERROR', + REMOVE_FEDERATED_IDENTITY = 'REMOVE_FEDERATED_IDENTITY', + REMOVE_FEDERATED_IDENTITY_ERROR = 'REMOVE_FEDERATED_IDENTITY_ERROR', + + UPDATE_EMAIL = 'UPDATE_EMAIL', + UPDATE_EMAIL_ERROR = 'UPDATE_EMAIL_ERROR', + UPDATE_PROFILE = 'UPDATE_PROFILE', + UPDATE_PROFILE_ERROR = 'UPDATE_PROFILE_ERROR', + UPDATE_PASSWORD = 'UPDATE_PASSWORD', + UPDATE_PASSWORD_ERROR = 'UPDATE_PASSWORD_ERROR', + UPDATE_TOTP = 'UPDATE_TOTP', + UPDATE_TOTP_ERROR = 'UPDATE_TOTP_ERROR', + VERIFY_EMAIL = 'VERIFY_EMAIL', + VERIFY_EMAIL_ERROR = 'VERIFY_EMAIL_ERROR', + + REMOVE_TOTP = 'REMOVE_TOTP', + REMOVE_TOTP_ERROR = 'REMOVE_TOTP_ERROR', + + REVOKE_GRANT = 'REVOKE_GRANT', + REVOKE_GRANT_ERROR = 'REVOKE_GRANT_ERROR', + + SEND_VERIFY_EMAIL = 'SEND_VERIFY_EMAIL', + SEND_VERIFY_EMAIL_ERROR = 'SEND_VERIFY_EMAIL_ERROR', + SEND_RESET_PASSWORD = 'SEND_RESET_PASSWORD', + SEND_RESET_PASSWORD_ERROR = 'SEND_RESET_PASSWORD_ERROR', + SEND_IDENTITY_PROVIDER_LINK = 'SEND_IDENTITY_PROVIDER_LINK', + SEND_IDENTITY_PROVIDER_LINK_ERROR = 'SEND_IDENTITY_PROVIDER_LINK_ERROR', + RESET_PASSWORD = 'RESET_PASSWORD', + RESET_PASSWORD_ERROR = 'RESET_PASSWORD_ERROR', + + RESTART_AUTHENTICATION = 'RESTART_AUTHENTICATION', + RESTART_AUTHENTICATION_ERROR = 'RESTART_AUTHENTICATION_ERROR', + + INVALID_SIGNATURE = 'INVALID_SIGNATURE', + INVALID_SIGNATURE_ERROR = 'INVALID_SIGNATURE_ERROR', + REGISTER_NODE = 'REGISTER_NODE', + REGISTER_NODE_ERROR = 'REGISTER_NODE_ERROR', + UNREGISTER_NODE = 'UNREGISTER_NODE', + UNREGISTER_NODE_ERROR = 'UNREGISTER_NODE_ERROR', + + USER_INFO_REQUEST = 'USER_INFO_REQUEST', + USER_INFO_REQUEST_ERROR = 'USER_INFO_REQUEST_ERROR', + + IDENTITY_PROVIDER_LINK_ACCOUNT = 'IDENTITY_PROVIDER_LINK_ACCOUNT', + IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR = 'IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR', + IDENTITY_PROVIDER_LOGIN = 'IDENTITY_PROVIDER_LOGIN', + IDENTITY_PROVIDER_LOGIN_ERROR = 'IDENTITY_PROVIDER_LOGIN_ERROR', + IDENTITY_PROVIDER_FIRST_LOGIN = 'IDENTITY_PROVIDER_FIRST_LOGIN', + IDENTITY_PROVIDER_FIRST_LOGIN_ERROR = 'IDENTITY_PROVIDER_FIRST_LOGIN_ERROR', + IDENTITY_PROVIDER_POST_LOGIN = 'IDENTITY_PROVIDER_POST_LOGIN', + IDENTITY_PROVIDER_POST_LOGIN_ERROR = 'IDENTITY_PROVIDER_POST_LOGIN_ERROR', + IDENTITY_PROVIDER_RESPONSE = 'IDENTITY_PROVIDER_RESPONSE', + IDENTITY_PROVIDER_RESPONSE_ERROR = 'IDENTITY_PROVIDER_RESPONSE_ERROR', + IDENTITY_PROVIDER_RETRIEVE_TOKEN = 'IDENTITY_PROVIDER_RETRIEVE_TOKEN', + IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR = 'IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR', + IMPERSONATE = 'IMPERSONATE', + IMPERSONATE_ERROR = 'IMPERSONATE_ERROR', + CUSTOM_REQUIRED_ACTION = 'CUSTOM_REQUIRED_ACTION', + CUSTOM_REQUIRED_ACTION_ERROR = 'CUSTOM_REQUIRED_ACTION_ERROR', + EXECUTE_ACTIONS = 'EXECUTE_ACTIONS', + EXECUTE_ACTIONS_ERROR = 'EXECUTE_ACTIONS_ERROR', + EXECUTE_ACTION_TOKEN = 'EXECUTE_ACTION_TOKEN', + EXECUTE_ACTION_TOKEN_ERROR = 'EXECUTE_ACTION_TOKEN_ERROR', + + CLIENT_INFO = 'CLIENT_INFO', + CLIENT_INFO_ERROR = 'CLIENT_INFO_ERROR', + CLIENT_REGISTER = 'CLIENT_REGISTER', + CLIENT_REGISTER_ERROR = 'CLIENT_REGISTER_ERROR', + CLIENT_UPDATE = 'CLIENT_UPDATE', + CLIENT_UPDATE_ERROR = 'CLIENT_UPDATE_ERROR', + CLIENT_DELETE = 'CLIENT_DELETE', + CLIENT_DELETE_ERROR = 'CLIENT_DELETE_ERROR', + + CLIENT_INITIATED_ACCOUNT_LINKING = 'CLIENT_INITIATED_ACCOUNT_LINKING', + CLIENT_INITIATED_ACCOUNT_LINKING_ERROR = 'CLIENT_INITIATED_ACCOUNT_LINKING_ERROR', + TOKEN_EXCHANGE = 'TOKEN_EXCHANGE', + TOKEN_EXCHANGE_ERROR = 'TOKEN_EXCHANGE_ERROR', + + PERMISSION_TOKEN = 'PERMISSION_TOKEN', + PERMISSION_TOKEN_ERROR = 'PERMISSION_TOKEN_ERROR', +} + +export default EventType; diff --git a/src/defs/userSessionRepresentation.ts b/src/defs/userSessionRepresentation.ts new file mode 100644 index 00000000..18dffd3a --- /dev/null +++ b/src/defs/userSessionRepresentation.ts @@ -0,0 +1,9 @@ +export default interface UserSessionRepresentation { + id?: string; + clients?: Record; + ipAddress?: string; + lastAccess?: number; + start?: number; + userId?: string; + username?: string; +} diff --git a/src/resources/realms.ts b/src/resources/realms.ts index 5b022a69..b24b12e9 100644 --- a/src/resources/realms.ts +++ b/src/resources/realms.ts @@ -1,5 +1,8 @@ import Resource from './resource'; import RealmRepresentation from '../defs/realmRepresentation'; +import EventRepresentation from '../defs/eventRepresentation'; +import EventType from '../defs/eventTypes'; + import {KeycloakAdminClient} from '../client'; export class Realms extends Resource { @@ -40,6 +43,21 @@ export class Realms extends Resource { urlParamKeys: ['realm'], }); + /** + * Get events Returns all events, or filters them based on URL query parameters listed here + */ + public findEvents = this.makeRequest<{ + realm: string, + client?: string, dateFrom?: Date, dateTo?: Date, + first?: number, ipAddress?: string, max?: number, + type?: EventType, user?: string, + }, EventRepresentation[]>({ + method: 'GET', + path: '/{realm}/events', + urlParamKeys: ['realm'], + queryParamKeys: ['client', 'dateFrom', 'dateTo', 'first', 'ipAddress', 'max', 'type', 'user'], + }); + constructor(client: KeycloakAdminClient) { super(client, { path: '/admin/realms', diff --git a/src/resources/users.ts b/src/resources/users.ts index 70ffdd7a..c63f9f88 100644 --- a/src/resources/users.ts +++ b/src/resources/users.ts @@ -1,5 +1,7 @@ import Resource from './resource'; import UserRepresentation from '../defs/userRepresentation'; +import UserConsentRepresentation from '../defs/userConsentRepresentation'; +import UserSessionRepresentation from '../defs/userSessionRepresentation'; import {KeycloakAdminClient} from '../client'; import MappingsRepresentation from '../defs/mappingsRepresentation'; import RoleRepresentation, { @@ -290,6 +292,66 @@ export class Users extends Resource<{realm?: string}> { }, }); + /** + * list user sessions + */ + public listSessions = this.makeRequest< + {id: string}, + UserSessionRepresentation[] + >({ + method: 'GET', + path: '/{id}/sessions', + urlParamKeys: ['id'], + }); + + /** + * list offline sessions associated with the user and client + */ + public listOfflineSessions = this.makeRequest< + {id: string, clientId: string}, + UserSessionRepresentation[] + >({ + method: 'GET', + path: '/{id}/offline-sessions/{clientId}', + urlParamKeys: ['id', 'clientId'], + }); + + /** + * logout user from all sessions + */ + public logout = this.makeRequest< + {id: string}, + void + >({ + method: 'POST', + path: '/{id}/logout', + urlParamKeys: ['id'], + }); + + /** + * list consents granted by the user + */ + public listConsents = this.makeRequest< + {id: string}, + UserConsentRepresentation[] + >({ + method: 'GET', + path: '/{id}/consents', + urlParamKeys: ['id'], + }); + + /** + * revoke consent and offline tokens for particular client from user + */ + public revokeConsent = this.makeRequest< + {id: string, clientId: string}, + void + >({ + method: 'DELETE', + path: '/{id}/consents/{clientId}', + urlParamKeys: ['id', 'clientId'], + }); + constructor(client: KeycloakAdminClient) { super(client, { path: '/admin/realms/{realm}/users', diff --git a/test/realms.spec.ts b/test/realms.spec.ts index 900afeed..448103dc 100644 --- a/test/realms.spec.ts +++ b/test/realms.spec.ts @@ -14,7 +14,7 @@ declare module 'mocha' { } } -describe('Realms', function() { +describe('Realms', function () { before(async () => { this.kcAdminClient = new KeycloakAdminClient(); await this.kcAdminClient.auth(credentials); @@ -71,4 +71,36 @@ describe('Realms', function() { }); expect(realm).to.be.null; }); + + describe('Realm Events', function () { + before(async () => { + this.kcAdminClient = new KeycloakAdminClient(); + await this.kcAdminClient.auth(credentials); + + const realmId = faker.internet.userName().toLowerCase(); + const realmName = faker.internet.userName().toLowerCase(); + const realm = await this.kcAdminClient.realms.create({ + id: realmId, + realm: realmName, + }); + expect(realm.realmName).to.be.equal(realmName); + this.currentRealmId = realmId; + this.currentRealmName = realmName; + }); + + it('list events of a realm', async () => { + // @TODO: In order to test it, there have to be events + const events = await this.kcAdminClient.realms.findEvents({realm: this.currentRealmName}); + + expect(events).to.be.ok; + }); + + after(async () => { + await this.kcAdminClient.realms.del({realm: this.currentRealmName}); + const realm = await this.kcAdminClient.realms.findOne({ + realm: this.currentRealmName, + }); + expect(realm).to.be.null; + }); + }); }); diff --git a/test/users.spec.ts b/test/users.spec.ts index 3656fb34..0a64414f 100644 --- a/test/users.spec.ts +++ b/test/users.spec.ts @@ -4,6 +4,7 @@ import {KeycloakAdminClient} from '../src/client'; import {credentials} from './constants'; import faker from 'faker'; import UserRepresentation from '../src/defs/userRepresentation'; +import UserSessionRepresentation from '../src/defs/userSessionRepresentation'; import RoleRepresentation from '../src/defs/roleRepresentation'; import ClientRepresentation from '../src/defs/clientRepresentation'; import {RequiredActionAlias} from '../src/defs/requiredActionProviderRepresentation'; @@ -23,7 +24,7 @@ declare module 'mocha' { } } -describe('Users', function() { +describe('Users', function () { this.timeout(10000); before(async () => { @@ -364,7 +365,86 @@ describe('Users', function() { }); }); - describe('Federated Identity user integration', function() { + describe('User sessions', function () { + before(async () => { + this.kcAdminClient = new KeycloakAdminClient(); + await this.kcAdminClient.auth(credentials); + + // create user + const username = faker.internet.userName(); + await this.kcAdminClient.users.create({ + username, + email: 'wwwy3y3-federated@canner.io', + enabled: true, + }); + const users = await this.kcAdminClient.users.find({username}); + expect(users[0]).to.be.ok; + this.currentUser = users[0]; + + // create client + const clientId = faker.internet.userName(); + await this.kcAdminClient.clients.create({ + clientId, consentRequired: true, + }); + + const clients = await this.kcAdminClient.clients.find({clientId}); + expect(clients[0]).to.be.ok; + this.currentClient = clients[0]; + }); + + after(async () => { + await this.kcAdminClient.users.del({ + id: this.currentUser.id, + }); + + await this.kcAdminClient.clients.del({ + id: this.currentClient.id, + }); + }); + + it('list user sessions', async () => { + // @TODO: In order to test it, currentUser has to be logged in + + const userSessions = await this.kcAdminClient.users.listSessions({id: this.currentUser.id}); + + expect(userSessions).to.be.ok; + }); + + it('list users off-line sessions', async () => { + // @TODO: In order to test it, currentUser has to be logged in + + const userOfflineSessions = await this.kcAdminClient.users.listOfflineSessions( + {id: this.currentUser.id, clientId: this.currentClient.id}, + ); + + expect(userOfflineSessions).to.be.ok; + }); + + it('logout user from all sessions', async () => { + // @TODO: In order to test it, currentUser has to be logged in + + await this.kcAdminClient.users.logout({id: this.currentUser.id}); + }); + + it('list consents granted by the user', async () => { + const consents = await this.kcAdminClient.users.listConsents({id: this.currentUser.id}); + + expect(consents).to.be.ok; + }); + + it('revoke consent and offline tokens for particular client', async () => { + // @TODO: In order to test it, currentUser has to granted consent to client + const consents = await this.kcAdminClient.users.listConsents({id: this.currentUser.id}); + + if (consents.length) { + const consent = consents[0]; + + await this.kcAdminClient.users.revokeConsent({id: this.currentUser.id, clientId: consent.clientId}); + } + }); + }); + + describe('Federated Identity user integration', function () { before(async () => { this.kcAdminClient = new KeycloakAdminClient(); await this.kcAdminClient.auth(credentials);