├── test ├── setup.js ├── constants.ts ├── auth.spec.ts ├── crossRealm.spec.ts ├── components.spec.ts ├── groupUser.spec.ts ├── roles.spec.ts ├── idp.spec.ts ├── realms.spec.ts ├── authenticationManagement.spec.ts ├── groups.spec.ts ├── users.spec.ts └── clientScopes.spec.ts ├── .prettierignore ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── prettier.config.js ├── src ├── utils │ ├── constants.ts │ └── auth.ts ├── defs │ ├── multivaluedHashMap.ts │ ├── userSessionRepresentation.ts │ ├── federatedIdentityRepresentation.ts │ ├── userConsentRepresentation.ts │ ├── mappingsRepresentation.ts │ ├── protocolMapperRepresentation.ts │ ├── identityProviderMapperRepresentation.ts │ ├── eventRepresentation.ts │ ├── componentRepresentation.ts │ ├── clientScopeRepresentation.ts │ ├── groupRepresentation.ts │ ├── scopeRepresentation.ts │ ├── resourceRepresentation.ts │ ├── credentialRepresentation.ts │ ├── identityProviderRepresentation.ts │ ├── roleRepresentation.ts │ ├── requiredActionProviderRepresentation.ts │ ├── policyRepresentation.ts │ ├── resourceServerRepresentation.ts │ ├── userRepresentation.ts │ ├── clientRepresentation.ts │ ├── realmRepresentation.ts │ └── eventTypes.ts ├── index.ts ├── resources │ ├── resource.ts │ ├── components.ts │ ├── roles.ts │ ├── authenticationManagement.ts │ ├── realms.ts │ ├── identityProviders.ts │ ├── groups.ts │ ├── agent.ts │ ├── clientScopes.ts │ ├── users.ts │ └── clients.ts └── client.ts ├── tsconfig.release.json ├── .editorconfig ├── .travis.yml ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── MAINTAINERS.md ├── tslint.json ├── package.json ├── LICENSE └── README.md /test/setup.js: -------------------------------------------------------------------------------- 1 | require('node-window-polyfill').register(); 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # NODE 2 | node_modules 3 | 4 | # IDE 5 | .vscode 6 | 7 | # GITHUB 8 | .github -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | lib 5 | yarn-error.log 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "eg2.tslint"] 3 | } 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const defaultBaseUrl = 'http://127.0.0.1:8080/auth'; 2 | 3 | export const defaultRealm = 'master'; 4 | -------------------------------------------------------------------------------- /src/defs/multivaluedHashMap.ts: -------------------------------------------------------------------------------- 1 | export default interface MultivaluedHashMap { 2 | empty?: boolean; 3 | loadFactor?: number; 4 | threshold?: number; 5 | } 6 | -------------------------------------------------------------------------------- /test/constants.ts: -------------------------------------------------------------------------------- 1 | export const credentials = { 2 | username: 'wwwy3y3', 3 | password: 'wwwy3y3', 4 | grantType: 'password', 5 | clientId: 'admin-cli', 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "exclude": ["test/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {RequiredActionAlias} from './defs/requiredActionProviderRepresentation'; 2 | import {KeycloakAdminClient} from './client'; 3 | 4 | export const requiredAction = RequiredActionAlias; 5 | export default KeycloakAdminClient; 6 | -------------------------------------------------------------------------------- /src/defs/userSessionRepresentation.ts: -------------------------------------------------------------------------------- 1 | export default interface UserSessionRepresentation { 2 | id?: string; 3 | clients?: Record; 4 | ipAddress?: string; 5 | lastAccess?: number; 6 | start?: number; 7 | userId?: string; 8 | username?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/defs/federatedIdentityRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_federatedidentityrepresentation 3 | */ 4 | 5 | export default interface FederatedIdentityRepresentation { 6 | identityProvider?: string; 7 | userId?: string; 8 | userName?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/defs/userConsentRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_userconsentrepresentation 3 | */ 4 | 5 | export default interface UserConsentRepresentation { 6 | clientId?: string; 7 | createDate?: string; 8 | grantedClientScopes?: string[]; 9 | lastUpdatedDate?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/defs/mappingsRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_mappingsrepresentation 3 | */ 4 | import RoleRepresentation from './roleRepresentation'; 5 | 6 | export default interface MappingsRepresentation { 7 | clientMappings?: Record; 8 | realmMappings?: RoleRepresentation[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/defs/protocolMapperRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_protocolmapperrepresentation 3 | */ 4 | 5 | export default interface ProtocolMapperRepresentation { 6 | config?: Record; 7 | id?: string; 8 | name?: string; 9 | protocol?: string; 10 | protocolMapper?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/defs/identityProviderMapperRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_identityprovidermapperrepresentation 3 | */ 4 | 5 | export default interface IdentityProviderMapperRepresentation { 6 | config?: any; 7 | id?: string; 8 | identityProviderAlias?: string; 9 | identityProviderMapper?: string; 10 | name?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/defs/eventRepresentation.ts: -------------------------------------------------------------------------------- 1 | import EventType from './eventTypes'; 2 | 3 | export default interface EventRepresentation { 4 | clientId?: string; 5 | details?: Record; 6 | error?: string; 7 | ipAddress?: string; 8 | realmId?: string; 9 | sessionId?: string; 10 | time?: number; 11 | type?: EventType; 12 | userId?: string; 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.tslintIntegration": true, 3 | "editor.formatOnSave": true, 4 | "javascript.format.enable": false, 5 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true, 6 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 7 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js}] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /src/defs/componentRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_componentrepresentation 3 | */ 4 | import MultivaluedHashMap from './multivaluedHashMap'; 5 | 6 | export default interface ComponentRepresentation { 7 | config?: MultivaluedHashMap; 8 | id?: string; 9 | name?: string; 10 | parentId?: string; 11 | providerId?: string; 12 | providerType?: string; 13 | subType?: string; 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "8" 7 | - "10" 8 | - "12" 9 | 10 | services: 11 | - docker 12 | 13 | before_install: 14 | - docker pull jboss/keycloak:7.0.1 15 | - docker run --name keycloak -d -p 127.0.0.1:8080:8080 -e KEYCLOAK_USER=wwwy3y3 -e KEYCLOAK_PASSWORD=wwwy3y3 jboss/keycloak:7.0.1 16 | - docker ps -a 17 | - docker logs keycloak 18 | - sleep 30 19 | - docker ps -a 20 | - docker logs keycloak 21 | -------------------------------------------------------------------------------- /src/defs/clientScopeRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_clientscoperepresentation 3 | */ 4 | import ProtocolMapperRepresentation from './protocolMapperRepresentation'; 5 | 6 | export default interface ClientScopeRepresentation { 7 | attributes?: Record; 8 | description?: string; 9 | id?: string; 10 | name?: string; 11 | protocol?: string; 12 | protocolMappers?: ProtocolMapperRepresentation[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/defs/groupRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_grouprepresentation 3 | */ 4 | 5 | export default interface GroupRepresentation { 6 | id?: string; 7 | name?: string; 8 | path?: string; 9 | subGroups?: GroupRepresentation[]; 10 | 11 | // optional in response 12 | access?: Record; 13 | attributes?: Record; 14 | clientRoles?: Record; 15 | realmRoles?: string[]; 16 | } 17 | -------------------------------------------------------------------------------- /src/defs/scopeRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_scoperepresentation 3 | */ 4 | import PolicyRepresentation from './policyRepresentation'; 5 | import ResourceRepresentation from './resourceRepresentation'; 6 | 7 | export default interface ScopeRepresentation { 8 | displayName?: string; 9 | iconUri?: string; 10 | id?: string; 11 | name?: string; 12 | policies?: PolicyRepresentation[]; 13 | resources?: ResourceRepresentation[]; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6", "dom"], 5 | "moduleResolution": "node", 6 | "removeComments": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src/**/*.ts", "docs/**/*.ts", "test/*"], 15 | "exclude": ["./node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /src/defs/resourceRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_resourcerepresentation 3 | */ 4 | import ScopeRepresentation from './scopeRepresentation'; 5 | 6 | export default interface ResourceRepresentation { 7 | id?: string; 8 | attributes?: Record; 9 | displayName?: string; 10 | icon_uri?: string; 11 | name?: string; 12 | ownerManagedAccess?: boolean; 13 | scopes?: ScopeRepresentation[]; 14 | type?: string; 15 | uri?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/defs/credentialRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_credentialrepresentation 3 | */ 4 | 5 | export default interface CredentialRepresentation { 6 | algorithm?: string; 7 | config?: Record; 8 | counter?: number; 9 | createdDate?: number; 10 | device?: string; 11 | digits?: number; 12 | hashIterations?: number; 13 | hashedSaltedValue?: string; 14 | period?: number; 15 | salt?: string; 16 | temporary?: boolean; 17 | type?: string; 18 | value?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/defs/identityProviderRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_identityproviderrepresentation 3 | */ 4 | 5 | export default interface IdentityProviderRepresentation { 6 | addReadTokenRoleOnCreate?: boolean; 7 | alias?: string; 8 | config?: Record; 9 | displayName?: string; 10 | enabled?: boolean; 11 | firstBrokerLoginFlowAlias?: string; 12 | internalId?: string; 13 | linkOnly?: boolean; 14 | postBrokerLoginFlowAlias?: string; 15 | providerId?: string; 16 | storeToken?: boolean; 17 | trustEmail?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/defs/roleRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_rolerepresentation 3 | */ 4 | 5 | export default interface RoleRepresentation { 6 | clientRole?: boolean; 7 | composite?: boolean; 8 | composites?: { 9 | client: Record; 10 | realm: string[]; 11 | }; 12 | containerId?: string; 13 | description?: string; 14 | id?: string; 15 | name?: string; 16 | } 17 | 18 | // when requesting to role-mapping api (create, delete), id and name are required 19 | export interface RoleMappingPayload extends RoleRepresentation { 20 | id: string; 21 | name: string; 22 | } 23 | -------------------------------------------------------------------------------- /test/auth.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import {getToken} from '../src/utils/auth'; 3 | import {credentials} from './constants'; 4 | 5 | const expect = chai.expect; 6 | 7 | describe('Authorization', () => { 8 | it('should get token from local keycloak', async () => { 9 | const data = await getToken({ 10 | credentials, 11 | }); 12 | 13 | expect(data).to.have.all.keys( 14 | 'accessToken', 15 | 'expiresIn', 16 | 'refreshExpiresIn', 17 | 'refreshToken', 18 | 'tokenType', 19 | 'notBeforePolicy', 20 | 'sessionState', 21 | 'scope', 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/defs/requiredActionProviderRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_requiredactionproviderrepresentation 3 | */ 4 | 5 | export enum RequiredActionAlias { 6 | VERIFY_EMAIL = 'VERIFY_EMAIL', 7 | UPDATE_PROFILE = 'UPDATE_PROFILE', 8 | CONFIGURE_TOTP = 'CONFIGURE_TOTP', 9 | UPDATE_PASSWORD = 'UPDATE_PASSWORD', 10 | terms_and_conditions = 'terms_and_conditions', 11 | } 12 | 13 | export default interface RequiredActionProviderRepresentation { 14 | alias?: string; 15 | config?: Record; 16 | defaultAction?: boolean; 17 | enabled?: boolean; 18 | name?: string; 19 | provider?: string; 20 | priority?: number; 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/defs/policyRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_policyrepresentation 3 | */ 4 | 5 | export enum DecisionStrategy { 6 | AFFIRMATIVE = 'AFFIRMATIVE', 7 | UNANIMOUS = 'UNANIMOUS', 8 | CONSENSUS = 'CONSENSUS', 9 | } 10 | 11 | export enum Logic { 12 | POSITIVE = 'POSITIVE', 13 | NEGATIVE = 'NEGATIVE', 14 | } 15 | 16 | export default interface PolicyRepresentation { 17 | config?: Record; 18 | decisionStrategy?: DecisionStrategy; 19 | description?: string; 20 | id?: string; 21 | logic?: Logic; 22 | name?: string; 23 | owner?: string; 24 | policies?: string[]; 25 | resources?: string[]; 26 | scopes?: string[]; 27 | type?: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/defs/resourceServerRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_policyrepresentation 3 | */ 4 | import PolicyRepresentation from './policyRepresentation'; 5 | import ResourceRepresentation from './resourceRepresentation'; 6 | import ScopeRepresentation from './scopeRepresentation'; 7 | 8 | export enum PolicyEnforcementMode { 9 | ENFORCING = 'ENFORCING', 10 | PERMISSIVE = 'PERMISSIVE', 11 | DISABLED = 'DISABLED', 12 | } 13 | 14 | export default interface ResourceServerRepresentation { 15 | allowRemoteResourceManagement?: boolean; 16 | clientId?: string; 17 | id?: string; 18 | name?: string; 19 | policies?: PolicyRepresentation[]; 20 | policyEnforcementMode?: PolicyEnforcementMode; 21 | resources?: ResourceRepresentation[]; 22 | scopes?: ScopeRepresentation[]; 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. What API did you use? 16 | 2. What error message did you see? 17 | 3. GIve us a minimum code example 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Enviroment (please complete the following information):** 26 | - OS: [e.g. Ubuntu] 27 | - Keycloak Version 28 | - Library Version [e.g. 1.9.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | ## The team 4 | 5 | - William Chang [@wwwy3y3](https://github.com/wwwy3y3) 6 | - Michael Schmid [@Schnitzel](https://github.com/Schnitzel) 7 | 8 | ## Originally developed by 9 | 10 | This repo is originally developed by [Canner](https://www.cannercms.com) and [InfuseAI](https://infuseai.io) before being transferred under Keycloak organization. 11 | 12 | 13 | 14 | 15 | 20 | 25 | 26 | 27 |
16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 |
28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "lib"], 3 | "extends": ["tslint:latest"], 4 | "rules": { 5 | "ordered-imports": [ 6 | true, 7 | { 8 | "import-sources-order": "any", 9 | "named-imports-order": "any" 10 | } 11 | ], 12 | "semicolon": [true, "always", "strict-bound-class-methods"], 13 | "quotemark": [true, "single"], 14 | "no-implicit-dependencies": [true, "dev"], 15 | "no-submodule-imports": false, 16 | "object-literal-sort-keys": false, 17 | "trailing-comma": [ 18 | true, 19 | { 20 | "multiline": { 21 | "objects": "always", 22 | "arrays": "always", 23 | "functions": "always", 24 | "typeLiterals": "always" 25 | }, 26 | "esSpecCompliant": true 27 | } 28 | ], 29 | "arrow-parens": [true, "ban-single-arg-parens"], 30 | "interface-name": [true, "never-prefix"] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/resources/resource.ts: -------------------------------------------------------------------------------- 1 | import {KeycloakAdminClient} from '../client'; 2 | import {Agent, RequestArgs} from './agent'; 3 | 4 | export default class Resource { 5 | private agent: Agent; 6 | constructor( 7 | client: KeycloakAdminClient, 8 | settings: { 9 | path?: string; 10 | getUrlParams?: () => Record; 11 | getBaseUrl?: () => string; 12 | } = {}, 13 | ) { 14 | this.agent = new Agent({ 15 | client, 16 | ...settings, 17 | }); 18 | } 19 | 20 | public makeRequest = ( 21 | args: RequestArgs, 22 | ): ((payload?: PayloadType & ParamType) => Promise) => { 23 | return this.agent.request(args); 24 | }; 25 | 26 | // update request will take three types: query, payload and response 27 | public makeUpdateRequest = < 28 | QueryType = any, 29 | PayloadType = any, 30 | ResponseType = any 31 | >( 32 | args: RequestArgs, 33 | ): (( 34 | query: QueryType & ParamType, 35 | payload: PayloadType, 36 | ) => Promise) => { 37 | return this.agent.updateRequest(args); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/defs/userRepresentation.ts: -------------------------------------------------------------------------------- 1 | import UserConsentRepresentation from './userConsentRepresentation'; 2 | import CredentialRepresentation from './credentialRepresentation'; 3 | import FederatedIdentityRepresentation from './federatedIdentityRepresentation'; 4 | import {RequiredActionAlias} from './requiredActionProviderRepresentation'; 5 | 6 | export default interface UserRepresentation { 7 | id?: string; 8 | createdTimestamp?: number; 9 | username?: string; 10 | enabled?: boolean; 11 | totp?: boolean; 12 | emailVerified?: boolean; 13 | disableableCredentialTypes?: string[]; 14 | requiredActions?: RequiredActionAlias[]; 15 | notBefore?: number; 16 | access?: Record; 17 | 18 | // optional from response 19 | attributes?: Record; 20 | clientConsents?: UserConsentRepresentation[]; 21 | clientRoles?: Record; 22 | credentials?: CredentialRepresentation[]; 23 | email?: string; 24 | federatedIdentities?: FederatedIdentityRepresentation[]; 25 | federationLink?: string; 26 | firstName?: string; 27 | groups?: string[]; 28 | lastName?: string; 29 | origin?: string; 30 | realmRoles?: string[]; 31 | self?: string; 32 | serviceAccountClientId?: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/defs/clientRepresentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_clientrepresentation 3 | */ 4 | import ResourceServerRepresentation from './resourceServerRepresentation'; 5 | import ProtocolMapperRepresentation from './protocolMapperRepresentation'; 6 | 7 | export default interface ClientRepresentation { 8 | access?: Record; 9 | adminUrl?: string; 10 | attributes?: Record; 11 | authenticationFlowBindingOverrides?: Record; 12 | authorizationServicesEnabled?: boolean; 13 | authorizationSettings?: ResourceServerRepresentation; 14 | baseUrl?: string; 15 | bearerOnly?: boolean; 16 | clientAuthenticatorType?: string; 17 | clientId?: string; 18 | consentRequired?: boolean; 19 | defaultClientScopes?: string[]; 20 | defaultRoles?: string[]; 21 | description?: string; 22 | directAccessGrantsEnabled?: boolean; 23 | enabled?: boolean; 24 | frontchannelLogout?: boolean; 25 | fullScopeAllowed?: boolean; 26 | id?: string; 27 | implicitFlowEnabled?: boolean; 28 | name?: string; 29 | nodeReRegistrationTimeout?: number; 30 | notBefore?: number; 31 | optionalClientScopes?: string[]; 32 | origin?: string; 33 | protocol?: string; 34 | protocolMappers?: ProtocolMapperRepresentation[]; 35 | publicClient?: boolean; 36 | redirectUris?: string[]; 37 | registeredNodes?: Record; 38 | registrationAccessToken?: string; 39 | rootUrl?: string; 40 | secret?: string; 41 | serviceAccountsEnabled?: boolean; 42 | standardFlowEnabled?: boolean; 43 | surrogateAuthRequired?: boolean; 44 | webOrigins?: string[]; 45 | } 46 | -------------------------------------------------------------------------------- /src/resources/components.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import ComponentRepresentation from '../defs/componentRepresentation'; 3 | import {KeycloakAdminClient} from '../client'; 4 | 5 | export interface ComponentQuery { 6 | name?: string; 7 | parent?: string; 8 | type?: string; 9 | } 10 | 11 | export class Components extends Resource<{realm?: string}> { 12 | /** 13 | * components 14 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_component_resource 15 | */ 16 | 17 | public find = this.makeRequest({ 18 | method: 'GET', 19 | }); 20 | 21 | public create = this.makeRequest({ 22 | method: 'POST', 23 | returnResourceIdInLocationHeader: {field: 'id'}, 24 | }); 25 | 26 | public findOne = this.makeRequest<{id: string}, ComponentRepresentation>({ 27 | method: 'GET', 28 | path: '/{id}', 29 | urlParamKeys: ['id'], 30 | catchNotFound: true, 31 | }); 32 | 33 | public update = this.makeUpdateRequest< 34 | {id: string}, 35 | ComponentRepresentation, 36 | void 37 | >({ 38 | method: 'PUT', 39 | path: '/{id}', 40 | urlParamKeys: ['id'], 41 | }); 42 | 43 | public del = this.makeRequest<{id: string}, void>({ 44 | method: 'DELETE', 45 | path: '/{id}', 46 | urlParamKeys: ['id'], 47 | }); 48 | 49 | constructor(client: KeycloakAdminClient) { 50 | super(client, { 51 | path: '/admin/realms/{realm}/components', 52 | getUrlParams: () => ({ 53 | realm: client.realmName, 54 | }), 55 | getBaseUrl: () => client.baseUrl, 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/crossRealm.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | const expect = chai.expect; 7 | 8 | declare module 'mocha' { 9 | // tslint:disable-next-line:interface-name 10 | interface ISuiteCallbackContext { 11 | kcAdminClient?: KeycloakAdminClient; 12 | currentRealmId?: string; 13 | } 14 | } 15 | 16 | describe('Realms', function() { 17 | before(async () => { 18 | this.kcAdminClient = new KeycloakAdminClient(); 19 | await this.kcAdminClient.auth(credentials); 20 | 21 | const realmId = faker.internet.userName(); 22 | const realm = await this.kcAdminClient.realms.create({ 23 | id: realmId, 24 | realm: realmId, 25 | }); 26 | expect(realm.realmName).to.be.ok; 27 | this.currentRealmId = realmId; 28 | }); 29 | 30 | after(async () => { 31 | await this.kcAdminClient.realms.del({realm: this.currentRealmId}); 32 | }); 33 | 34 | it('add a user to another realm', async () => { 35 | const username = faker.internet.userName().toLowerCase(); 36 | const user = await this.kcAdminClient.users.create({ 37 | realm: this.currentRealmId, 38 | username, 39 | email: 'wwwy3y3@canner.io', 40 | // enabled required to be true in order to send actions email 41 | emailVerified: true, 42 | enabled: true, 43 | }); 44 | const foundUser = await this.kcAdminClient.users.findOne({ 45 | realm: this.currentRealmId, 46 | id: user.id, 47 | }); 48 | expect(foundUser.username).to.be.eql(username); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keycloak-admin", 3 | "version": "1.12.0", 4 | "description": "keycloak admin client", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "clean": "rimraf lib", 11 | "lint": "tslint --force --format verbose \"src/**/*.ts\" \"test/*\"", 12 | "build": "npm run clean && npm run lint && echo Using TypeScript && tsc --version && tsc -p ./tsconfig.release.json --pretty", 13 | "test": "DEBUG=kc-admin mocha --require \"test/setup.js\" --require ts-node/register --recursive \"test/**/*.spec.ts\"", 14 | "test:grep": "DEBUG=kc-admin mocha --require ts-node/register", 15 | "coverage": "nyc npm run test", 16 | "watch": "npm run build -- --watch", 17 | "watch:test": "npm run test -- --watch", 18 | "prepublishOnly": "npm run clean && npm run build" 19 | }, 20 | "dependencies": { 21 | "@types/debug": "^0.0.30", 22 | "axios": "^0.18.0", 23 | "bluebird": "^3.5.1", 24 | "camelize": "^1.0.0", 25 | "debug": "^3.1.0", 26 | "keycloak-js": "^11.0.2", 27 | "lodash": "^4.17.10", 28 | "node-window-polyfill": "^1.0.0", 29 | "url-join": "^4.0.0", 30 | "url-template": "^2.0.8" 31 | }, 32 | "devDependencies": { 33 | "@types/axios": "^0.14.0", 34 | "@types/bluebird": "^3.5.20", 35 | "@types/chai": "^4.1.2", 36 | "@types/faker": "^4.1.2", 37 | "@types/lodash": "^4.14.108", 38 | "@types/mocha": "^2.2.48", 39 | "@types/node": "9.4.6", 40 | "@types/url-join": "^0.8.2", 41 | "@types/url-template": "^2.0.28", 42 | "chai": "^4.1.2", 43 | "faker": "^4.1.0", 44 | "mocha": "^5.2.0", 45 | "nyc": "^14.1.1", 46 | "prettier": "^1.14.3", 47 | "rimraf": "^2.5.4", 48 | "ts-node": "^7.0.0", 49 | "tslint": "^5.11.0", 50 | "typescript": "^2.9.2" 51 | }, 52 | "author": "wwwy3y3", 53 | "license": "Apache-2.0" 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosRequestConfig} from 'axios'; 2 | import camelize from 'camelize'; 3 | import querystring from 'querystring'; 4 | import {defaultBaseUrl, defaultRealm} from './constants'; 5 | 6 | export interface Credentials { 7 | username: string; 8 | password: string; 9 | grantType: string; 10 | clientId: string; 11 | clientSecret?: string; 12 | } 13 | 14 | export interface Settings { 15 | realmName?: string; 16 | baseUrl?: string; 17 | credentials: Credentials; 18 | requestConfig?: AxiosRequestConfig; 19 | } 20 | 21 | export interface TokenResponse { 22 | accessToken: string; 23 | expiresIn: string; 24 | refreshExpiresIn: number; 25 | refreshToken: string; 26 | tokenType: string; 27 | notBeforePolicy: number; 28 | sessionState: string; 29 | scope: string; 30 | } 31 | 32 | export const getToken = async (settings: Settings): Promise => { 33 | // Construct URL 34 | const baseUrl = settings.baseUrl || defaultBaseUrl; 35 | const realmName = settings.realmName || defaultRealm; 36 | const url = `${baseUrl}/realms/${realmName}/protocol/openid-connect/token`; 37 | 38 | // Prepare credentials for openid-connect token request 39 | // ref: http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint 40 | const credentials = settings.credentials || ({} as any); 41 | const payload = querystring.stringify({ 42 | username: credentials.username, 43 | password: credentials.password, 44 | grant_type: credentials.grantType, 45 | client_id: credentials.clientId, 46 | }); 47 | const config: AxiosRequestConfig = { 48 | ...settings.requestConfig, 49 | }; 50 | 51 | if (credentials.clientSecret) { 52 | config.auth = { 53 | username: credentials.clientId, 54 | password: credentials.clientSecret, 55 | }; 56 | } 57 | 58 | const {data} = await axios.post(url, payload, config); 59 | return camelize(data); 60 | }; 61 | -------------------------------------------------------------------------------- /src/resources/roles.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import RoleRepresentation from '../defs/roleRepresentation'; 3 | import UserRepresentation from '../defs/userRepresentation'; 4 | import {KeycloakAdminClient} from '../client'; 5 | 6 | export class Roles extends Resource<{realm?: string}> { 7 | /** 8 | * Realm roles 9 | */ 10 | 11 | public find = this.makeRequest({ 12 | method: 'GET', 13 | path: '/roles', 14 | }); 15 | 16 | public create = this.makeRequest({ 17 | method: 'POST', 18 | path: '/roles', 19 | returnResourceIdInLocationHeader: {field: 'roleName'}, 20 | }); 21 | 22 | /** 23 | * Roles by name 24 | */ 25 | 26 | public findOneByName = this.makeRequest<{name: string}, RoleRepresentation>({ 27 | method: 'GET', 28 | path: '/roles/{name}', 29 | urlParamKeys: ['name'], 30 | catchNotFound: true, 31 | }); 32 | 33 | public updateByName = this.makeUpdateRequest< 34 | {name: string}, 35 | RoleRepresentation, 36 | void 37 | >({ 38 | method: 'PUT', 39 | path: '/roles/{name}', 40 | urlParamKeys: ['name'], 41 | }); 42 | 43 | public delByName = this.makeRequest<{name: string}, void>({ 44 | method: 'DELETE', 45 | path: '/roles/{name}', 46 | urlParamKeys: ['name'], 47 | }); 48 | 49 | public findUsersWithRole = this.makeRequest<{name: string}, UserRepresentation[]>({ 50 | method: 'GET', 51 | path: '/roles/{name}/users', 52 | urlParamKeys: ['name'], 53 | catchNotFound: true, 54 | }); 55 | 56 | /** 57 | * Roles by id 58 | */ 59 | 60 | public findOneById = this.makeRequest<{id: string}, RoleRepresentation>({ 61 | method: 'GET', 62 | path: '/roles-by-id/{id}', 63 | urlParamKeys: ['id'], 64 | catchNotFound: true, 65 | }); 66 | 67 | public updateById = this.makeUpdateRequest< 68 | {id: string}, 69 | RoleRepresentation, 70 | void 71 | >({ 72 | method: 'PUT', 73 | path: '/roles-by-id/{id}', 74 | urlParamKeys: ['id'], 75 | }); 76 | 77 | public delById = this.makeRequest<{id: string}, void>({ 78 | method: 'DELETE', 79 | path: '/roles-by-id/{id}', 80 | urlParamKeys: ['id'], 81 | }); 82 | 83 | constructor(client: KeycloakAdminClient) { 84 | super(client, { 85 | path: '/admin/realms/{realm}', 86 | getUrlParams: () => ({ 87 | realm: client.realmName, 88 | }), 89 | getBaseUrl: () => client.baseUrl, 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/resources/authenticationManagement.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import RequiredActionProviderRepresentation from '../defs/requiredActionProviderRepresentation'; 3 | import {KeycloakAdminClient} from '../client'; 4 | 5 | export class AuthenticationManagement extends Resource { 6 | /** 7 | * Authentication Management 8 | * https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource 9 | */ 10 | 11 | // Register a new required action 12 | public registerRequiredAction = this.makeRequest>({ 13 | method: 'POST', 14 | path: '/register-required-action', 15 | }); 16 | 17 | // Get required actions. Returns a list of required actions. 18 | public getRequiredActions = this.makeRequest({ 19 | method: 'GET', 20 | path: '/required-actions', 21 | }); 22 | 23 | // Get required action for alias 24 | public getRequiredActionForAlias = this.makeRequest<{ 25 | alias: string; 26 | }>({ 27 | method: 'GET', 28 | path: '/required-actions/{alias}', 29 | urlParamKeys: ['alias'], 30 | catchNotFound: true, 31 | }); 32 | 33 | // Update required action 34 | public updateRequiredAction = this.makeUpdateRequest< 35 | {alias: string}, 36 | RequiredActionProviderRepresentation, 37 | void 38 | >({ 39 | method: 'PUT', 40 | path: '/required-actions/{alias}', 41 | urlParamKeys: ['alias'], 42 | }); 43 | 44 | // Delete required action 45 | public deleteRequiredAction = this.makeRequest<{alias: string}, void>({ 46 | method: 'DELETE', 47 | path: '/required-actions/{alias}', 48 | urlParamKeys: ['alias'], 49 | }); 50 | 51 | // Lower required action’s priority 52 | public lowerRequiredActionPriority = this.makeRequest<{ 53 | alias: string; 54 | }>({ 55 | method: 'POST', 56 | path: '/required-actions/{alias}/lower-priority', 57 | urlParamKeys: ['alias'], 58 | }); 59 | 60 | // Raise required action’s priority 61 | public raiseRequiredActionPriority = this.makeRequest<{ 62 | alias: string; 63 | }>({ 64 | method: 'POST', 65 | path: '/required-actions/{alias}/raise-priority', 66 | urlParamKeys: ['alias'], 67 | }); 68 | 69 | // Get unregistered required actions Returns a list of unregistered required actions. 70 | public getUnregisteredRequiredActions = this.makeRequest({ 71 | method: 'GET', 72 | path: '/unregistered-required-actions', 73 | }); 74 | 75 | constructor(client: KeycloakAdminClient) { 76 | super(client, { 77 | path: '/admin/realms/{realm}/authentication', 78 | getUrlParams: () => ({ 79 | realm: client.realmName, 80 | }), 81 | getBaseUrl: () => client.baseUrl, 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/components.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | import ComponentRepresentation from '../src/defs/componentRepresentation'; 7 | const expect = chai.expect; 8 | 9 | declare module 'mocha' { 10 | // tslint:disable-next-line:interface-name 11 | interface ISuiteCallbackContext { 12 | kcAdminClient?: KeycloakAdminClient; 13 | currentUserFed?: ComponentRepresentation; 14 | } 15 | } 16 | 17 | describe('User federation using component api', function() { 18 | before(async () => { 19 | this.kcAdminClient = new KeycloakAdminClient(); 20 | await this.kcAdminClient.auth(credentials); 21 | 22 | // create user fed 23 | const name = faker.internet.userName(); 24 | const component = await this.kcAdminClient.components.create({ 25 | name, 26 | parentId: 'master', 27 | providerId: 'ldap', 28 | providerType: 'org.keycloak.storage.UserStorageProvider', 29 | }); 30 | expect(component.id).to.be.ok; 31 | 32 | // assign current user fed 33 | const fed = await this.kcAdminClient.components.findOne({ 34 | id: component.id, 35 | }); 36 | this.currentUserFed = fed; 37 | }); 38 | 39 | after(async () => { 40 | await this.kcAdminClient.components.del({ 41 | id: this.currentUserFed.id, 42 | }); 43 | 44 | // check deleted 45 | const idp = await this.kcAdminClient.components.findOne({ 46 | id: this.currentUserFed.id, 47 | }); 48 | expect(idp).to.be.null; 49 | }); 50 | 51 | it('list user federations', async () => { 52 | const feds = await this.kcAdminClient.components.find({ 53 | parent: 'master', 54 | type: 'org.keycloak.storage.UserStorageProvider', 55 | }); 56 | expect(feds.length).to.be.least(1); 57 | }); 58 | 59 | it('get a user federation', async () => { 60 | const fed = await this.kcAdminClient.components.findOne({ 61 | id: this.currentUserFed.id, 62 | }); 63 | expect(fed).to.include({ 64 | id: this.currentUserFed.id, 65 | }); 66 | }); 67 | 68 | it('update a user federation', async () => { 69 | await this.kcAdminClient.components.update( 70 | {id: this.currentUserFed.id}, 71 | { 72 | // parentId, providerId, providerType required for update 73 | parentId: 'master', 74 | providerId: 'ldap', 75 | providerType: 'org.keycloak.storage.UserStorageProvider', 76 | name: 'cool-name', 77 | }, 78 | ); 79 | const updated = await this.kcAdminClient.components.findOne({ 80 | id: this.currentUserFed.id, 81 | }); 82 | 83 | expect(updated).to.include({ 84 | id: this.currentUserFed.id, 85 | name: 'cool-name', 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/resources/realms.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import RealmRepresentation from '../defs/realmRepresentation'; 3 | import EventRepresentation from '../defs/eventRepresentation'; 4 | import EventType from '../defs/eventTypes'; 5 | 6 | import {KeycloakAdminClient} from '../client'; 7 | 8 | export class Realms extends Resource { 9 | /** 10 | * Realm 11 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_realms_admin_resource 12 | */ 13 | 14 | public find = this.makeRequest({ 15 | method: 'GET', 16 | }); 17 | 18 | public create = this.makeRequest({ 19 | method: 'POST', 20 | returnResourceIdInLocationHeader: {field: 'realmName'}, 21 | }); 22 | 23 | public findOne = this.makeRequest<{realm: string}, RealmRepresentation>({ 24 | method: 'GET', 25 | path: '/{realm}', 26 | urlParamKeys: ['realm'], 27 | catchNotFound: true, 28 | }); 29 | 30 | public update = this.makeUpdateRequest< 31 | {realm: string}, 32 | RealmRepresentation, 33 | void 34 | >({ 35 | method: 'PUT', 36 | path: '/{realm}', 37 | urlParamKeys: ['realm'], 38 | }); 39 | 40 | public del = this.makeRequest<{realm: string}, void>({ 41 | method: 'DELETE', 42 | path: '/{realm}', 43 | urlParamKeys: ['realm'], 44 | }); 45 | 46 | /** 47 | * Get events Returns all events, or filters them based on URL query parameters listed here 48 | */ 49 | public findEvents = this.makeRequest< 50 | { 51 | realm: string; 52 | client?: string; 53 | dateFrom?: Date; 54 | dateTo?: Date; 55 | first?: number; 56 | ipAddress?: string; 57 | max?: number; 58 | type?: EventType; 59 | user?: string; 60 | }, 61 | EventRepresentation[] 62 | >({ 63 | method: 'GET', 64 | path: '/{realm}/events', 65 | urlParamKeys: ['realm'], 66 | queryParamKeys: [ 67 | 'client', 68 | 'dateFrom', 69 | 'dateTo', 70 | 'first', 71 | 'ipAddress', 72 | 'max', 73 | 'type', 74 | 'user', 75 | ], 76 | }); 77 | 78 | /** 79 | * Users management permissions 80 | */ 81 | public getUsersManagementPermissions = this.makeRequest< 82 | {realm: string}, 83 | void 84 | >({ 85 | method: 'GET', 86 | path: '/{realm}/users-management-permissions', 87 | urlParamKeys: ['realm'], 88 | }); 89 | 90 | public updateUsersManagementPermissions = this.makeRequest< 91 | {realm: string; enabled: boolean}, 92 | void 93 | >({ 94 | method: 'PUT', 95 | path: '/{realm}/users-management-permissions', 96 | urlParamKeys: ['realm'], 97 | }); 98 | 99 | constructor(client: KeycloakAdminClient) { 100 | super(client, { 101 | path: '/admin/realms', 102 | getBaseUrl: () => client.baseUrl, 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/groupUser.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {pick, omit} from 'lodash'; 4 | import {KeycloakAdminClient} from '../src/client'; 5 | import {credentials} from './constants'; 6 | import faker from 'faker'; 7 | import UserRepresentation from '../src/defs/userRepresentation'; 8 | import GroupRepresentation from '../src/defs/groupRepresentation'; 9 | 10 | const expect = chai.expect; 11 | 12 | declare module 'mocha' { 13 | // tslint:disable-next-line:interface-name 14 | interface ISuiteCallbackContext { 15 | kcAdminClient?: KeycloakAdminClient; 16 | currentGroup?: GroupRepresentation; 17 | currentUser?: UserRepresentation; 18 | } 19 | } 20 | 21 | describe('Group user integration', function() { 22 | before(async () => { 23 | const groupName = faker.internet.userName(); 24 | this.kcAdminClient = new KeycloakAdminClient(); 25 | await this.kcAdminClient.auth(credentials); 26 | // create group 27 | const group = await this.kcAdminClient.groups.create({ 28 | name: groupName, 29 | }); 30 | this.currentGroup = await this.kcAdminClient.groups.findOne({id: group.id}); 31 | 32 | // create user 33 | const username = faker.internet.userName(); 34 | const user = await this.kcAdminClient.users.create({ 35 | username, 36 | email: 'wwwy3y3@canner.io', 37 | enabled: true, 38 | }); 39 | this.currentUser = await this.kcAdminClient.users.findOne({id: user.id}); 40 | }); 41 | 42 | after(async () => { 43 | await this.kcAdminClient.groups.del({ 44 | id: this.currentGroup.id, 45 | }); 46 | await this.kcAdminClient.users.del({ 47 | id: this.currentUser.id, 48 | }); 49 | }); 50 | 51 | it("should list user's group and expect empty", async () => { 52 | const groups = await this.kcAdminClient.users.listGroups({ 53 | id: this.currentUser.id, 54 | }); 55 | expect(groups).to.be.eql([]); 56 | }); 57 | 58 | it('should add user to group', async () => { 59 | await this.kcAdminClient.users.addToGroup({ 60 | id: this.currentUser.id, 61 | groupId: this.currentGroup.id, 62 | }); 63 | 64 | const groups = await this.kcAdminClient.users.listGroups({ 65 | id: this.currentUser.id, 66 | }); 67 | // expect id,name,path to be the same 68 | expect(groups[0]).to.be.eql( 69 | pick(this.currentGroup, ['id', 'name', 'path']), 70 | ); 71 | }); 72 | 73 | it('should list members using group api', async () => { 74 | const members = await this.kcAdminClient.groups.listMembers({ 75 | id: this.currentGroup.id, 76 | }); 77 | // access will not returned from member api 78 | expect(members[0]).to.be.eql(omit(this.currentUser, ['access'])); 79 | }); 80 | 81 | it('should remove user from group', async () => { 82 | await this.kcAdminClient.users.delFromGroup({ 83 | id: this.currentUser.id, 84 | groupId: this.currentGroup.id, 85 | }); 86 | 87 | const groups = await this.kcAdminClient.users.listGroups({ 88 | id: this.currentUser.id, 89 | }); 90 | expect(groups).to.be.eql([]); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/roles.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import RoleRepresentation from '../src/defs/roleRepresentation'; 6 | 7 | const expect = chai.expect; 8 | 9 | declare module 'mocha' { 10 | // tslint:disable-next-line:interface-name 11 | interface ISuiteCallbackContext { 12 | client?: KeycloakAdminClient; 13 | currentRole?: RoleRepresentation; 14 | } 15 | } 16 | 17 | describe('Roles', function() { 18 | before(async () => { 19 | this.client = new KeycloakAdminClient(); 20 | await this.client.auth(credentials); 21 | }); 22 | 23 | it('list roles', async () => { 24 | const roles = await this.client.roles.find(); 25 | expect(roles).to.be.ok; 26 | }); 27 | 28 | it('create roles and get by name', async () => { 29 | const roleName = 'cool-role'; 30 | const createdRole = await this.client.roles.create({ 31 | name: roleName, 32 | }); 33 | 34 | expect(createdRole.roleName).to.be.equal(roleName); 35 | const role = await this.client.roles.findOneByName({name: roleName}); 36 | expect(role).to.be.ok; 37 | this.currentRole = role; 38 | }); 39 | 40 | it('get single roles by id', async () => { 41 | const roleId = this.currentRole.id; 42 | const role = await this.client.roles.findOneById({ 43 | id: roleId, 44 | }); 45 | expect(role).to.deep.include(this.currentRole); 46 | }); 47 | 48 | it('update single role by name & by id', async () => { 49 | await this.client.roles.updateByName( 50 | {name: this.currentRole.name}, 51 | { 52 | // dont know why if role name not exist in payload, role name will be overriden with empty string 53 | // todo: open an issue on keycloak 54 | name: 'cool-role', 55 | description: 'cool', 56 | }, 57 | ); 58 | 59 | const role = await this.client.roles.findOneByName({ 60 | name: this.currentRole.name, 61 | }); 62 | expect(role).to.include({ 63 | description: 'cool', 64 | }); 65 | 66 | await this.client.roles.updateById( 67 | {id: this.currentRole.id}, 68 | { 69 | description: 'another description', 70 | }, 71 | ); 72 | 73 | const roleById = await this.client.roles.findOneById({ 74 | id: this.currentRole.id, 75 | }); 76 | expect(roleById).to.include({ 77 | description: 'another description', 78 | }); 79 | }); 80 | 81 | it('delete single roles by id', async () => { 82 | const roleId = this.currentRole.id; 83 | await this.client.roles.create({ 84 | name: 'for-delete', 85 | }); 86 | 87 | await this.client.roles.delByName({ 88 | name: 'for-delete', 89 | }); 90 | 91 | // delete the currentRole with id 92 | await this.client.roles.delById({ 93 | id: roleId, 94 | }); 95 | 96 | // both should be null 97 | const role = await this.client.roles.findOneById({ 98 | id: roleId, 99 | }); 100 | expect(role).to.be.null; 101 | 102 | const roleDelByName = await this.client.roles.findOneByName({ 103 | name: 'for-delete', 104 | }); 105 | expect(roleDelByName).to.be.null; 106 | }); 107 | 108 | it('get users with role by name in realm', async () => { 109 | const users = await this.client.roles.findUsersWithRole({ 110 | name: 'admin', 111 | }); 112 | expect(users).to.be.ok; 113 | expect(users).to.be.an('array'); 114 | }) 115 | }); 116 | -------------------------------------------------------------------------------- /src/resources/identityProviders.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import IdentityProviderRepresentation from '../defs/identityProviderRepresentation'; 3 | import IdentityProviderMapperRepresentation from '../defs/identityProviderMapperRepresentation'; 4 | import {KeycloakAdminClient} from '../client'; 5 | 6 | export class IdentityProviders extends Resource<{realm?: string}> { 7 | /** 8 | * Identity provider 9 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_identity_providers_resource 10 | */ 11 | 12 | public find = this.makeRequest({ 13 | method: 'GET', 14 | path: '/instances', 15 | }); 16 | 17 | public create = this.makeRequest< 18 | IdentityProviderRepresentation, 19 | {id: string} 20 | >({ 21 | method: 'POST', 22 | path: '/instances', 23 | returnResourceIdInLocationHeader: {field: 'id'}, 24 | }); 25 | 26 | public findOne = this.makeRequest< 27 | {alias: string}, 28 | IdentityProviderRepresentation 29 | >({ 30 | method: 'GET', 31 | path: '/instances/{alias}', 32 | urlParamKeys: ['alias'], 33 | catchNotFound: true, 34 | }); 35 | 36 | public update = this.makeUpdateRequest< 37 | {alias: string}, 38 | IdentityProviderRepresentation, 39 | void 40 | >({ 41 | method: 'PUT', 42 | path: '/instances/{alias}', 43 | urlParamKeys: ['alias'], 44 | }); 45 | 46 | public del = this.makeRequest<{alias: string}, void>({ 47 | method: 'DELETE', 48 | path: '/instances/{alias}', 49 | urlParamKeys: ['alias'], 50 | }); 51 | 52 | public findFactory = this.makeRequest<{providerId: string}, any>({ 53 | method: 'GET', 54 | path: '/providers/{providerId}', 55 | urlParamKeys: ['providerId'], 56 | }); 57 | 58 | public findMappers = this.makeRequest< 59 | {alias: string}, 60 | IdentityProviderMapperRepresentation[] 61 | >({ 62 | method: 'GET', 63 | path: '/instances/{alias}/mappers', 64 | urlParamKeys: ['alias'], 65 | }); 66 | 67 | public findOneMapper = this.makeRequest< 68 | {alias: string; id: string}, 69 | IdentityProviderMapperRepresentation 70 | >({ 71 | method: 'GET', 72 | path: '/instances/{alias}/mappers/{id}', 73 | urlParamKeys: ['alias', 'id'], 74 | catchNotFound: true, 75 | }); 76 | 77 | public createMapper = this.makeRequest< 78 | { 79 | alias: string; 80 | identityProviderMapper: IdentityProviderMapperRepresentation; 81 | }, 82 | {id: string} 83 | >({ 84 | method: 'POST', 85 | path: '/instances/{alias}/mappers', 86 | urlParamKeys: ['alias'], 87 | payloadKey: 'identityProviderMapper', 88 | returnResourceIdInLocationHeader: {field: 'id'}, 89 | }); 90 | 91 | public updateMapper = this.makeUpdateRequest< 92 | {alias: string; id: string}, 93 | IdentityProviderMapperRepresentation, 94 | void 95 | >({ 96 | method: 'PUT', 97 | path: '/instances/{alias}/mappers/{id}', 98 | urlParamKeys: ['alias', 'id'], 99 | }); 100 | 101 | public delMapper = this.makeRequest<{alias: string; id: string}, void>({ 102 | method: 'DELETE', 103 | path: '/instances/{alias}/mappers/{id}', 104 | urlParamKeys: ['alias', 'id'], 105 | }); 106 | 107 | public findMapperTypes = this.makeRequest< 108 | {alias: string}, 109 | IdentityProviderMapperRepresentation[] 110 | >({ 111 | method: 'GET', 112 | path: '/instances/{alias}/mapper-types', 113 | urlParamKeys: ['alias'], 114 | }); 115 | 116 | constructor(client: KeycloakAdminClient) { 117 | super(client, { 118 | path: '/admin/realms/{realm}/identity-provider', 119 | getUrlParams: () => ({ 120 | realm: client.realmName, 121 | }), 122 | getBaseUrl: () => client.baseUrl, 123 | }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import {getToken, Credentials} from './utils/auth'; 2 | import {defaultBaseUrl, defaultRealm} from './utils/constants'; 3 | import {Users} from './resources/users'; 4 | import {Groups} from './resources/groups'; 5 | import {Roles} from './resources/roles'; 6 | import {Clients} from './resources/clients'; 7 | import {Realms} from './resources/realms'; 8 | import {ClientScopes} from './resources/clientScopes'; 9 | import {IdentityProviders} from './resources/identityProviders'; 10 | import {Components} from './resources/components'; 11 | import {AuthenticationManagement} from './resources/authenticationManagement'; 12 | import {AxiosRequestConfig} from 'axios'; 13 | import Keycloak, {KeycloakConfig, KeycloakInitOptions, KeycloakInstance} from 'keycloak-js'; 14 | 15 | export interface ConnectionConfig { 16 | baseUrl?: string; 17 | realmName?: string; 18 | requestConfig?: AxiosRequestConfig; 19 | } 20 | 21 | export class KeycloakAdminClient { 22 | // Resources 23 | public users: Users; 24 | public groups: Groups; 25 | public roles: Roles; 26 | public clients: Clients; 27 | public realms: Realms; 28 | public clientScopes: ClientScopes; 29 | public identityProviders: IdentityProviders; 30 | public components: Components; 31 | public authenticationManagement: AuthenticationManagement; 32 | 33 | // Members 34 | public baseUrl: string; 35 | public realmName: string; 36 | public accessToken: string; 37 | public refreshToken: string; 38 | private requestConfig?: AxiosRequestConfig; 39 | 40 | private keycloak: KeycloakInstance; 41 | 42 | constructor(connectionConfig?: ConnectionConfig) { 43 | this.baseUrl = 44 | (connectionConfig && connectionConfig.baseUrl) || defaultBaseUrl; 45 | this.realmName = 46 | (connectionConfig && connectionConfig.realmName) || defaultRealm; 47 | this.requestConfig = connectionConfig && connectionConfig.requestConfig; 48 | 49 | // Initialize resources 50 | this.users = new Users(this); 51 | this.groups = new Groups(this); 52 | this.roles = new Roles(this); 53 | this.clients = new Clients(this); 54 | this.realms = new Realms(this); 55 | this.clientScopes = new ClientScopes(this); 56 | this.identityProviders = new IdentityProviders(this); 57 | this.components = new Components(this); 58 | this.authenticationManagement = new AuthenticationManagement(this); 59 | 60 | } 61 | 62 | public async auth(credentials: Credentials) { 63 | const {accessToken, refreshToken} = await getToken({ 64 | baseUrl: this.baseUrl, 65 | realmName: this.realmName, 66 | credentials, 67 | requestConfig: this.requestConfig, 68 | }); 69 | this.accessToken = accessToken; 70 | this.refreshToken = refreshToken; 71 | } 72 | 73 | public async init(init?: KeycloakInitOptions, config?: KeycloakConfig) { 74 | this.keycloak = Keycloak(config); 75 | await this.keycloak.init(init); 76 | this.baseUrl = this.keycloak.authServerUrl; 77 | } 78 | 79 | public setAccessToken(token: string) { 80 | this.accessToken = token; 81 | } 82 | 83 | public async getAccessToken() { 84 | if (this.keycloak) { 85 | try { 86 | await this.keycloak.updateToken(5); 87 | } catch (error) { 88 | this.keycloak.login(); 89 | } 90 | return this.keycloak.token; 91 | } 92 | return this.accessToken; 93 | } 94 | 95 | public getRequestConfig() { 96 | return this.requestConfig; 97 | } 98 | 99 | public setConfig(connectionConfig: ConnectionConfig) { 100 | if ( 101 | typeof connectionConfig.baseUrl === 'string' && 102 | connectionConfig.baseUrl 103 | ) { 104 | this.baseUrl = connectionConfig.baseUrl; 105 | } 106 | 107 | if ( 108 | typeof connectionConfig.realmName === 'string' && 109 | connectionConfig.realmName 110 | ) { 111 | this.realmName = connectionConfig.realmName; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/defs/realmRepresentation.ts: -------------------------------------------------------------------------------- 1 | import ClientRepresentation from './clientRepresentation'; 2 | import MultivaluedHashMap from './multivaluedHashMap'; 3 | import UserRepresentation from './userRepresentation'; 4 | import GroupRepresentation from './groupRepresentation'; 5 | import IdentityProviderRepresentation from './identityProviderRepresentation'; 6 | import RequiredActionProviderRepresentation from './requiredActionProviderRepresentation'; 7 | import RoleRepresentation from './roleRepresentation'; 8 | 9 | /** 10 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_realmrepresentation 11 | */ 12 | 13 | export default interface RealmRepresentation { 14 | accessCodeLifespan?: number; 15 | accessCodeLifespanLogin?: number; 16 | accessCodeLifespanUserAction?: number; 17 | accessTokenLifespan?: number; 18 | accessTokenLifespanForImplicitFlow?: number; 19 | accountTheme?: string; 20 | actionTokenGeneratedByAdminLifespan?: number; 21 | actionTokenGeneratedByUserLifespan?: number; 22 | adminEventsDetailsEnabled?: boolean; 23 | adminEventsEnabled?: boolean; 24 | adminTheme?: string; 25 | attributes?: Record; 26 | // AuthenticationFlowRepresentation 27 | authenticationFlows?: any[]; 28 | // AuthenticatorConfigRepresentation 29 | authenticatorConfig?: any[]; 30 | browserFlow?: string; 31 | browserSecurityHeaders?: Record; 32 | bruteForceProtected?: boolean; 33 | clientAuthenticationFlow?: string; 34 | clientScopeMappings?: Record; 35 | // ClientScopeRepresentation 36 | clientScopes?: any[]; 37 | clients?: ClientRepresentation[]; 38 | components?: MultivaluedHashMap; 39 | defaultDefaultClientScopes?: string[]; 40 | defaultGroups?: string[]; 41 | defaultLocale?: string; 42 | defaultOptionalClientScopes?: string[]; 43 | defaultRoles?: string[]; 44 | directGrantFlow?: string; 45 | displayName?: string; 46 | displayNameHtml?: string; 47 | dockerAuthenticationFlow?: string; 48 | duplicateEmailsAllowed?: boolean; 49 | editUsernameAllowed?: boolean; 50 | emailTheme?: string; 51 | enabled?: boolean; 52 | enabledEventTypes?: string[]; 53 | eventsEnabled?: boolean; 54 | eventsExpiration?: number; 55 | eventsListeners?: string[]; 56 | failureFactor?: number; 57 | federatedUsers?: UserRepresentation[]; 58 | groups?: GroupRepresentation[]; 59 | id?: string; 60 | // IdentityProviderMapperRepresentation 61 | identityProviderMappers?: any[]; 62 | identityProviders?: IdentityProviderRepresentation[]; 63 | internationalizationEnabled?: boolean; 64 | keycloakVersion?: string; 65 | loginTheme?: string; 66 | loginWithEmailAllowed?: boolean; 67 | maxDeltaTimeSeconds?: number; 68 | maxFailureWaitSeconds?: number; 69 | minimumQuickLoginWaitSeconds?: number; 70 | notBefore?: number; 71 | offlineSessionIdleTimeout?: number; 72 | offlineSessionMaxLifespan?: number; 73 | offlineSessionMaxLifespanEnabled?: boolean; 74 | otpPolicyAlgorithm?: string; 75 | otpPolicyDigits?: number; 76 | otpPolicyInitialCounter?: number; 77 | otpPolicyLookAheadWindow?: number; 78 | otpPolicyPeriod?: number; 79 | otpPolicyType?: string; 80 | otpSupportedApplications?: string[]; 81 | passwordPolicy?: string; 82 | permanentLockout?: boolean; 83 | // ProtocolMapperRepresentation 84 | protocolMappers?: any[]; 85 | quickLoginCheckMilliSeconds?: number; 86 | realm?: string; 87 | refreshTokenMaxReuse?: number; 88 | registrationAllowed?: boolean; 89 | registrationEmailAsUsername?: boolean; 90 | registrationFlow?: string; 91 | rememberMe?: boolean; 92 | requiredActions?: RequiredActionProviderRepresentation[]; 93 | resetCredentialsFlow?: string; 94 | resetPasswordAllowed?: boolean; 95 | revokeRefreshToken?: boolean; 96 | roles?: RoleRepresentation; 97 | // ScopeMappingRepresentation 98 | scopeMappings?: any[]; 99 | smtpServer?: Record; 100 | sslRequired?: string; 101 | ssoSessionIdleTimeout?: number; 102 | ssoSessionMaxLifespan?: number; 103 | supportedLocales?: string[]; 104 | // UserFederationMapperRepresentation 105 | userFederationMappers?: any[]; 106 | // UserFederationProviderRepresentation 107 | userFederationProviders?: any[]; 108 | userManagedAccessAllowed?: boolean; 109 | users?: UserRepresentation[]; 110 | verifyEmail?: boolean; 111 | waitIncrementSeconds?: number; 112 | } 113 | -------------------------------------------------------------------------------- /src/defs/eventTypes.ts: -------------------------------------------------------------------------------- 1 | enum EventType { 2 | 3 | LOGIN = 'LOGIN', 4 | LOGIN_ERROR = 'LOGIN_ERROR', 5 | REGISTER = 'REGISTER', 6 | REGISTER_ERROR = 'REGISTER_ERROR', 7 | LOGOUT = 'LOGOUT', 8 | LOGOUT_ERROR = 'LOGOUT_ERROR', 9 | 10 | CODE_TO_TOKEN = 'CODE_TO_TOKEN', 11 | CODE_TO_TOKEN_ERROR = 'CODE_TO_TOKEN_ERROR', 12 | 13 | CLIENT_LOGIN = 'CLIENT_LOGIN', 14 | CLIENT_LOGIN_ERROR = 'CLIENT_LOGIN_ERROR', 15 | 16 | REFRESH_TOKEN = 'REFRESH_TOKEN', 17 | REFRESH_TOKEN_ERROR = 'REFRESH_TOKEN_ERROR', 18 | 19 | VALIDATE_ACCESS_TOKEN = 'VALIDATE_ACCESS_TOKEN', 20 | 21 | VALIDATE_ACCESS_TOKEN_ERROR = 'VALIDATE_ACCESS_TOKEN_ERROR', 22 | INTROSPECT_TOKEN = 'INTROSPECT_TOKEN', 23 | INTROSPECT_TOKEN_ERROR = 'INTROSPECT_TOKEN_ERROR', 24 | 25 | FEDERATED_IDENTITY_LINK = 'FEDERATED_IDENTITY_LINK', 26 | FEDERATED_IDENTITY_LINK_ERROR = 'FEDERATED_IDENTITY_LINK_ERROR', 27 | REMOVE_FEDERATED_IDENTITY = 'REMOVE_FEDERATED_IDENTITY', 28 | REMOVE_FEDERATED_IDENTITY_ERROR = 'REMOVE_FEDERATED_IDENTITY_ERROR', 29 | 30 | UPDATE_EMAIL = 'UPDATE_EMAIL', 31 | UPDATE_EMAIL_ERROR = 'UPDATE_EMAIL_ERROR', 32 | UPDATE_PROFILE = 'UPDATE_PROFILE', 33 | UPDATE_PROFILE_ERROR = 'UPDATE_PROFILE_ERROR', 34 | UPDATE_PASSWORD = 'UPDATE_PASSWORD', 35 | UPDATE_PASSWORD_ERROR = 'UPDATE_PASSWORD_ERROR', 36 | UPDATE_TOTP = 'UPDATE_TOTP', 37 | UPDATE_TOTP_ERROR = 'UPDATE_TOTP_ERROR', 38 | VERIFY_EMAIL = 'VERIFY_EMAIL', 39 | VERIFY_EMAIL_ERROR = 'VERIFY_EMAIL_ERROR', 40 | 41 | REMOVE_TOTP = 'REMOVE_TOTP', 42 | REMOVE_TOTP_ERROR = 'REMOVE_TOTP_ERROR', 43 | 44 | REVOKE_GRANT = 'REVOKE_GRANT', 45 | REVOKE_GRANT_ERROR = 'REVOKE_GRANT_ERROR', 46 | 47 | SEND_VERIFY_EMAIL = 'SEND_VERIFY_EMAIL', 48 | SEND_VERIFY_EMAIL_ERROR = 'SEND_VERIFY_EMAIL_ERROR', 49 | SEND_RESET_PASSWORD = 'SEND_RESET_PASSWORD', 50 | SEND_RESET_PASSWORD_ERROR = 'SEND_RESET_PASSWORD_ERROR', 51 | SEND_IDENTITY_PROVIDER_LINK = 'SEND_IDENTITY_PROVIDER_LINK', 52 | SEND_IDENTITY_PROVIDER_LINK_ERROR = 'SEND_IDENTITY_PROVIDER_LINK_ERROR', 53 | RESET_PASSWORD = 'RESET_PASSWORD', 54 | RESET_PASSWORD_ERROR = 'RESET_PASSWORD_ERROR', 55 | 56 | RESTART_AUTHENTICATION = 'RESTART_AUTHENTICATION', 57 | RESTART_AUTHENTICATION_ERROR = 'RESTART_AUTHENTICATION_ERROR', 58 | 59 | INVALID_SIGNATURE = 'INVALID_SIGNATURE', 60 | INVALID_SIGNATURE_ERROR = 'INVALID_SIGNATURE_ERROR', 61 | REGISTER_NODE = 'REGISTER_NODE', 62 | REGISTER_NODE_ERROR = 'REGISTER_NODE_ERROR', 63 | UNREGISTER_NODE = 'UNREGISTER_NODE', 64 | UNREGISTER_NODE_ERROR = 'UNREGISTER_NODE_ERROR', 65 | 66 | USER_INFO_REQUEST = 'USER_INFO_REQUEST', 67 | USER_INFO_REQUEST_ERROR = 'USER_INFO_REQUEST_ERROR', 68 | 69 | IDENTITY_PROVIDER_LINK_ACCOUNT = 'IDENTITY_PROVIDER_LINK_ACCOUNT', 70 | IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR = 'IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR', 71 | IDENTITY_PROVIDER_LOGIN = 'IDENTITY_PROVIDER_LOGIN', 72 | IDENTITY_PROVIDER_LOGIN_ERROR = 'IDENTITY_PROVIDER_LOGIN_ERROR', 73 | IDENTITY_PROVIDER_FIRST_LOGIN = 'IDENTITY_PROVIDER_FIRST_LOGIN', 74 | IDENTITY_PROVIDER_FIRST_LOGIN_ERROR = 'IDENTITY_PROVIDER_FIRST_LOGIN_ERROR', 75 | IDENTITY_PROVIDER_POST_LOGIN = 'IDENTITY_PROVIDER_POST_LOGIN', 76 | IDENTITY_PROVIDER_POST_LOGIN_ERROR = 'IDENTITY_PROVIDER_POST_LOGIN_ERROR', 77 | IDENTITY_PROVIDER_RESPONSE = 'IDENTITY_PROVIDER_RESPONSE', 78 | IDENTITY_PROVIDER_RESPONSE_ERROR = 'IDENTITY_PROVIDER_RESPONSE_ERROR', 79 | IDENTITY_PROVIDER_RETRIEVE_TOKEN = 'IDENTITY_PROVIDER_RETRIEVE_TOKEN', 80 | IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR = 'IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR', 81 | IMPERSONATE = 'IMPERSONATE', 82 | IMPERSONATE_ERROR = 'IMPERSONATE_ERROR', 83 | CUSTOM_REQUIRED_ACTION = 'CUSTOM_REQUIRED_ACTION', 84 | CUSTOM_REQUIRED_ACTION_ERROR = 'CUSTOM_REQUIRED_ACTION_ERROR', 85 | EXECUTE_ACTIONS = 'EXECUTE_ACTIONS', 86 | EXECUTE_ACTIONS_ERROR = 'EXECUTE_ACTIONS_ERROR', 87 | EXECUTE_ACTION_TOKEN = 'EXECUTE_ACTION_TOKEN', 88 | EXECUTE_ACTION_TOKEN_ERROR = 'EXECUTE_ACTION_TOKEN_ERROR', 89 | 90 | CLIENT_INFO = 'CLIENT_INFO', 91 | CLIENT_INFO_ERROR = 'CLIENT_INFO_ERROR', 92 | CLIENT_REGISTER = 'CLIENT_REGISTER', 93 | CLIENT_REGISTER_ERROR = 'CLIENT_REGISTER_ERROR', 94 | CLIENT_UPDATE = 'CLIENT_UPDATE', 95 | CLIENT_UPDATE_ERROR = 'CLIENT_UPDATE_ERROR', 96 | CLIENT_DELETE = 'CLIENT_DELETE', 97 | CLIENT_DELETE_ERROR = 'CLIENT_DELETE_ERROR', 98 | 99 | CLIENT_INITIATED_ACCOUNT_LINKING = 'CLIENT_INITIATED_ACCOUNT_LINKING', 100 | CLIENT_INITIATED_ACCOUNT_LINKING_ERROR = 'CLIENT_INITIATED_ACCOUNT_LINKING_ERROR', 101 | TOKEN_EXCHANGE = 'TOKEN_EXCHANGE', 102 | TOKEN_EXCHANGE_ERROR = 'TOKEN_EXCHANGE_ERROR', 103 | 104 | PERMISSION_TOKEN = 'PERMISSION_TOKEN', 105 | PERMISSION_TOKEN_ERROR = 'PERMISSION_TOKEN_ERROR', 106 | } 107 | 108 | export default EventType; 109 | -------------------------------------------------------------------------------- /test/idp.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | const expect = chai.expect; 7 | 8 | declare module 'mocha' { 9 | // tslint:disable-next-line:interface-name 10 | interface ISuiteCallbackContext { 11 | kcAdminClient?: KeycloakAdminClient; 12 | currentIdpAlias?: string; 13 | } 14 | } 15 | 16 | describe('Identity providers', function() { 17 | before(async () => { 18 | this.kcAdminClient = new KeycloakAdminClient(); 19 | await this.kcAdminClient.auth(credentials); 20 | 21 | // create idp 22 | const alias = faker.internet.userName(); 23 | const idp = await this.kcAdminClient.identityProviders.create({ 24 | alias, 25 | providerId: 'saml', 26 | }); 27 | expect(idp.id).to.be.ok; 28 | this.currentIdpAlias = alias; 29 | 30 | // create idp mapper 31 | const mapper = { 32 | name: 'First Name', 33 | identityProviderAlias: this.currentIdpAlias, 34 | identityProviderMapper: 'saml-user-attribute-idp-mapper', 35 | config: {}, 36 | }; 37 | const idpMapper = await this.kcAdminClient.identityProviders.createMapper({ 38 | alias: this.currentIdpAlias, 39 | identityProviderMapper: mapper, 40 | }); 41 | expect(idpMapper.id).to.be.ok; 42 | }); 43 | 44 | after(async () => { 45 | const idpMapper = await this.kcAdminClient.identityProviders.findMappers({ 46 | alias: this.currentIdpAlias, 47 | }); 48 | 49 | const idpMapperId = idpMapper[0].id; 50 | await this.kcAdminClient.identityProviders.delMapper({ 51 | alias: this.currentIdpAlias, 52 | id: idpMapperId, 53 | }); 54 | 55 | const idpMapperUpdated = await this.kcAdminClient.identityProviders.findOneMapper( 56 | { 57 | alias: this.currentIdpAlias, 58 | id: idpMapperId, 59 | }, 60 | ); 61 | 62 | // check idp mapper deleted 63 | expect(idpMapperUpdated).to.be.null; 64 | 65 | await this.kcAdminClient.identityProviders.del({ 66 | alias: this.currentIdpAlias, 67 | }); 68 | 69 | const idp = await this.kcAdminClient.identityProviders.findOne({ 70 | alias: this.currentIdpAlias, 71 | }); 72 | 73 | // check idp deleted 74 | expect(idp).to.be.null; 75 | }); 76 | 77 | it('list idp', async () => { 78 | const idps = await this.kcAdminClient.identityProviders.find(); 79 | expect(idps.length).to.be.least(1); 80 | }); 81 | 82 | it('get an idp', async () => { 83 | const idp = await this.kcAdminClient.identityProviders.findOne({ 84 | alias: this.currentIdpAlias, 85 | }); 86 | expect(idp).to.include({ 87 | alias: this.currentIdpAlias, 88 | }); 89 | }); 90 | 91 | it('update an idp', async () => { 92 | const idp = await this.kcAdminClient.identityProviders.findOne({ 93 | alias: this.currentIdpAlias, 94 | }); 95 | await this.kcAdminClient.identityProviders.update( 96 | {alias: this.currentIdpAlias}, 97 | { 98 | // alias and internalId are requried to update 99 | alias: idp.alias, 100 | internalId: idp.internalId, 101 | displayName: 'test', 102 | }, 103 | ); 104 | const updatedIdp = await this.kcAdminClient.identityProviders.findOne({ 105 | alias: this.currentIdpAlias, 106 | }); 107 | 108 | expect(updatedIdp).to.include({ 109 | alias: this.currentIdpAlias, 110 | displayName: 'test', 111 | }); 112 | }); 113 | 114 | it('list idp factory', async () => { 115 | const idpFactory = await this.kcAdminClient.identityProviders.findFactory({ 116 | providerId: 'saml', 117 | }); 118 | 119 | expect(idpFactory).to.include({ 120 | id: 'saml', 121 | }); 122 | }); 123 | 124 | it('get an idp mapper', async () => { 125 | const mappers = await this.kcAdminClient.identityProviders.findMappers({ 126 | alias: this.currentIdpAlias, 127 | }); 128 | expect(mappers.length).to.be.least(1); 129 | }); 130 | 131 | it('update an idp mapper', async () => { 132 | const idpMapper = await this.kcAdminClient.identityProviders.findMappers({ 133 | alias: this.currentIdpAlias, 134 | }); 135 | const idpMapperId = idpMapper[0].id; 136 | 137 | await this.kcAdminClient.identityProviders.updateMapper( 138 | {alias: this.currentIdpAlias, id: idpMapperId}, 139 | { 140 | id: idpMapperId, 141 | identityProviderAlias: this.currentIdpAlias, 142 | identityProviderMapper: 'saml-user-attribute-idp-mapper', 143 | config: { 144 | 'user.attribute': 'firstName', 145 | }, 146 | }, 147 | ); 148 | 149 | const updatedIdpMappers = await this.kcAdminClient.identityProviders.findOneMapper( 150 | { 151 | alias: this.currentIdpAlias, 152 | id: idpMapperId, 153 | }, 154 | ); 155 | 156 | const userAttribute = updatedIdpMappers.config['user.attribute']; 157 | expect(userAttribute).to.equal('firstName'); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/realms.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | const expect = chai.expect; 7 | 8 | declare module 'mocha' { 9 | // tslint:disable-next-line:interface-name 10 | interface ISuiteCallbackContext { 11 | kcAdminClient?: KeycloakAdminClient; 12 | currentRealmId?: string; 13 | currentRealmName?: string; 14 | } 15 | } 16 | 17 | describe('Realms', function() { 18 | before(async () => { 19 | this.kcAdminClient = new KeycloakAdminClient(); 20 | await this.kcAdminClient.auth(credentials); 21 | }); 22 | 23 | it('list realms', async () => { 24 | const realms = await this.kcAdminClient.realms.find(); 25 | expect(realms.length).to.be.least(1); 26 | }); 27 | 28 | it('create realm', async () => { 29 | const realmId = faker.internet.userName().toLowerCase(); 30 | const realmName = faker.internet.userName().toLowerCase(); 31 | const realm = await this.kcAdminClient.realms.create({ 32 | id: realmId, 33 | realm: realmName, 34 | }); 35 | expect(realm.realmName).to.be.equal(realmName); 36 | this.currentRealmId = realmId; 37 | this.currentRealmName = realmName; 38 | }); 39 | 40 | it('get a realm', async () => { 41 | const realm = await this.kcAdminClient.realms.findOne({ 42 | realm: this.currentRealmName, 43 | }); 44 | expect(realm).to.include({ 45 | id: this.currentRealmId, 46 | realm: this.currentRealmName, 47 | }); 48 | }); 49 | 50 | it('update a realm', async () => { 51 | await this.kcAdminClient.realms.update( 52 | {realm: this.currentRealmName}, 53 | { 54 | displayName: 'test', 55 | }, 56 | ); 57 | const realm = await this.kcAdminClient.realms.findOne({ 58 | realm: this.currentRealmName, 59 | }); 60 | expect(realm).to.include({ 61 | id: this.currentRealmId, 62 | realm: this.currentRealmName, 63 | displayName: 'test', 64 | }); 65 | }); 66 | 67 | it('delete a realm', async () => { 68 | await this.kcAdminClient.realms.del({realm: this.currentRealmName}); 69 | const realm = await this.kcAdminClient.realms.findOne({ 70 | realm: this.currentRealmName, 71 | }); 72 | expect(realm).to.be.null; 73 | }); 74 | 75 | describe('Realm Events', function() { 76 | before(async () => { 77 | this.kcAdminClient = new KeycloakAdminClient(); 78 | await this.kcAdminClient.auth(credentials); 79 | 80 | const realmId = faker.internet.userName().toLowerCase(); 81 | const realmName = faker.internet.userName().toLowerCase(); 82 | const realm = await this.kcAdminClient.realms.create({ 83 | id: realmId, 84 | realm: realmName, 85 | }); 86 | expect(realm.realmName).to.be.equal(realmName); 87 | this.currentRealmId = realmId; 88 | this.currentRealmName = realmName; 89 | }); 90 | 91 | it('list events of a realm', async () => { 92 | // @TODO: In order to test it, there have to be events 93 | const events = await this.kcAdminClient.realms.findEvents({ 94 | realm: this.currentRealmName, 95 | }); 96 | 97 | expect(events).to.be.ok; 98 | }); 99 | 100 | after(async () => { 101 | await this.kcAdminClient.realms.del({realm: this.currentRealmName}); 102 | const realm = await this.kcAdminClient.realms.findOne({ 103 | realm: this.currentRealmName, 104 | }); 105 | expect(realm).to.be.null; 106 | }); 107 | }); 108 | 109 | describe('Realm Users Management Permissions', function() { 110 | before(async () => { 111 | this.kcAdminClient = new KeycloakAdminClient(); 112 | await this.kcAdminClient.auth(credentials); 113 | 114 | const realmId = faker.internet.userName().toLowerCase(); 115 | const realmName = faker.internet.userName().toLowerCase(); 116 | const realm = await this.kcAdminClient.realms.create({ 117 | id: realmId, 118 | realm: realmName, 119 | }); 120 | expect(realm.realmName).to.be.equal(realmName); 121 | this.currentRealmId = realmId; 122 | this.currentRealmName = realmName; 123 | }); 124 | 125 | it('get users management permissions', async () => { 126 | const managementPermissions = await this.kcAdminClient.realms.getUsersManagementPermissions( 127 | { 128 | realm: this.currentRealmName, 129 | }, 130 | ); 131 | expect(managementPermissions).to.be.ok; 132 | }); 133 | 134 | it('enable users management permissions', async () => { 135 | const managementPermissions = await this.kcAdminClient.realms.updateUsersManagementPermissions( 136 | { 137 | realm: this.currentRealmName, 138 | enabled: true, 139 | }, 140 | ); 141 | expect(managementPermissions).to.include({enabled: true}); 142 | }); 143 | 144 | after(async () => { 145 | await this.kcAdminClient.realms.del({realm: this.currentRealmName}); 146 | const realm = await this.kcAdminClient.realms.findOne({ 147 | realm: this.currentRealmName, 148 | }); 149 | expect(realm).to.be.null; 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/authenticationManagement.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | import {RequiredActionAlias} from '../src/defs/requiredActionProviderRepresentation'; 7 | const expect = chai.expect; 8 | 9 | declare module 'mocha' { 10 | // tslint:disable-next-line:interface-name 11 | interface ISuiteCallbackContext { 12 | kcAdminClient?: KeycloakAdminClient; 13 | currentRealm?: string; 14 | requiredActionProvider?: Record; 15 | } 16 | } 17 | 18 | describe('Authentication management', function() { 19 | before(async () => { 20 | this.kcAdminClient = new KeycloakAdminClient(); 21 | await this.kcAdminClient.auth(credentials); 22 | const realmName = faker.internet.userName().toLowerCase(); 23 | await this.kcAdminClient.realms.create({ 24 | id: realmName, 25 | realm: realmName, 26 | enabled: true, 27 | }); 28 | this.currentRealm = realmName; 29 | this.kcAdminClient.setConfig({ 30 | realmName, 31 | }); 32 | }); 33 | 34 | after(async () => { 35 | // delete test realm 36 | await this.kcAdminClient.realms.del({realm: this.currentRealm}); 37 | const realm = await this.kcAdminClient.realms.findOne({ 38 | realm: this.currentRealm, 39 | }); 40 | expect(realm).to.be.null; 41 | }); 42 | 43 | /** 44 | * Required Actions 45 | */ 46 | describe('Required Actions', () => { 47 | it('should delete required action by alias', async () => { 48 | await this.kcAdminClient.authenticationManagement.deleteRequiredAction({ 49 | alias: RequiredActionAlias.UPDATE_PROFILE, 50 | }); 51 | }); 52 | 53 | it('should get unregistered required actions', async () => { 54 | const unregisteredReqActions = await this.kcAdminClient.authenticationManagement.getUnregisteredRequiredActions(); 55 | expect(unregisteredReqActions).to.be.an('array'); 56 | expect(unregisteredReqActions.length).to.be.least(1); 57 | this.requiredActionProvider = unregisteredReqActions[0]; 58 | }); 59 | 60 | it('should register new required action', async () => { 61 | const requiredAction = await this.kcAdminClient.authenticationManagement.registerRequiredAction( 62 | { 63 | providerId: this.requiredActionProvider.providerId, 64 | name: this.requiredActionProvider.name, 65 | }, 66 | ); 67 | expect(requiredAction).to.be.empty; 68 | }); 69 | 70 | it('should get required actions', async () => { 71 | const requiredActions = await this.kcAdminClient.authenticationManagement.getRequiredActions(); 72 | expect(requiredActions).to.be.an('array'); 73 | }); 74 | 75 | it('should get required action by alias', async () => { 76 | const requiredAction = await this.kcAdminClient.authenticationManagement.getRequiredActionForAlias( 77 | {alias: this.requiredActionProvider.providerId}, 78 | ); 79 | expect(requiredAction).to.be.ok; 80 | }); 81 | 82 | it('should update required action by alias', async () => { 83 | const requiredAction = await this.kcAdminClient.authenticationManagement.getRequiredActionForAlias( 84 | {alias: this.requiredActionProvider.providerId}, 85 | ); 86 | const response = await this.kcAdminClient.authenticationManagement.updateRequiredAction( 87 | {alias: this.requiredActionProvider.providerId}, 88 | { 89 | ...requiredAction, 90 | enabled: true, 91 | priority: 10, 92 | }, 93 | ); 94 | expect(response).to.be.empty; 95 | }); 96 | 97 | it('should lower required action priority', async () => { 98 | const requiredAction = await this.kcAdminClient.authenticationManagement.getRequiredActionForAlias( 99 | {alias: this.requiredActionProvider.providerId}, 100 | ); 101 | const response = await this.kcAdminClient.authenticationManagement.lowerRequiredActionPriority( 102 | {alias: this.requiredActionProvider.providerId}, 103 | ); 104 | expect(response).to.be.empty; 105 | const requiredActionUpdated = await this.kcAdminClient.authenticationManagement.getRequiredActionForAlias( 106 | {alias: this.requiredActionProvider.providerId}, 107 | ); 108 | expect(requiredActionUpdated.priority).to.be.greaterThan( 109 | requiredAction.priority, 110 | ); 111 | }); 112 | 113 | it('should raise required action priority', async () => { 114 | const requiredAction = await this.kcAdminClient.authenticationManagement.getRequiredActionForAlias( 115 | {alias: this.requiredActionProvider.providerId}, 116 | ); 117 | const response = await this.kcAdminClient.authenticationManagement.raiseRequiredActionPriority( 118 | {alias: this.requiredActionProvider.providerId}, 119 | ); 120 | expect(response).to.be.empty; 121 | const requiredActionUpdated = await this.kcAdminClient.authenticationManagement.getRequiredActionForAlias( 122 | {alias: this.requiredActionProvider.providerId}, 123 | ); 124 | expect(requiredActionUpdated.priority).to.be.lessThan( 125 | requiredAction.priority, 126 | ); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/resources/groups.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import GroupRepresentation from '../defs/groupRepresentation'; 3 | import {KeycloakAdminClient} from '../client'; 4 | import UserRepresentation from '../defs/userRepresentation'; 5 | import MappingsRepresentation from '../defs/mappingsRepresentation'; 6 | import RoleRepresentation, { 7 | RoleMappingPayload, 8 | } from '../defs/roleRepresentation'; 9 | 10 | export interface GroupQuery { 11 | first?: number; 12 | max?: number; 13 | search?: string; 14 | } 15 | 16 | export class Groups extends Resource<{realm?: string}> { 17 | public find = this.makeRequest({ 18 | method: 'GET', 19 | }); 20 | 21 | public create = this.makeRequest({ 22 | method: 'POST', 23 | returnResourceIdInLocationHeader: {field: 'id'}, 24 | }); 25 | 26 | /** 27 | * Single user 28 | */ 29 | 30 | public findOne = this.makeRequest<{id: string}, GroupRepresentation>({ 31 | method: 'GET', 32 | path: '/{id}', 33 | urlParamKeys: ['id'], 34 | catchNotFound: true, 35 | }); 36 | 37 | public update = this.makeUpdateRequest< 38 | {id: string}, 39 | GroupRepresentation, 40 | void 41 | >({ 42 | method: 'PUT', 43 | path: '/{id}', 44 | urlParamKeys: ['id'], 45 | }); 46 | 47 | public del = this.makeRequest<{id: string}, void>({ 48 | method: 'DELETE', 49 | path: '/{id}', 50 | urlParamKeys: ['id'], 51 | }); 52 | 53 | /** 54 | * Set or create child. 55 | * This will just set the parent if it exists. Create it and set the parent if the group doesn’t exist. 56 | */ 57 | 58 | public setOrCreateChild = this.makeUpdateRequest< 59 | {id: string}, 60 | GroupRepresentation, 61 | {id: string} 62 | >({ 63 | method: 'POST', 64 | path: '/{id}/children', 65 | urlParamKeys: ['id'], 66 | returnResourceIdInLocationHeader: {field: 'id'}, 67 | }); 68 | 69 | /** 70 | * Members 71 | */ 72 | 73 | public listMembers = this.makeRequest< 74 | {id: string; first?: number; max?: number}, 75 | UserRepresentation[] 76 | >({ 77 | method: 'GET', 78 | path: '/{id}/members', 79 | urlParamKeys: ['id'], 80 | catchNotFound: true, 81 | }); 82 | 83 | /** 84 | * Role mappings 85 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_role_mapper_resource 86 | */ 87 | 88 | public listRoleMappings = this.makeRequest< 89 | {id: string}, 90 | MappingsRepresentation 91 | >({ 92 | method: 'GET', 93 | path: '/{id}/role-mappings', 94 | urlParamKeys: ['id'], 95 | }); 96 | 97 | public addRealmRoleMappings = this.makeRequest< 98 | {id: string; roles: RoleMappingPayload[]}, 99 | void 100 | >({ 101 | method: 'POST', 102 | path: '/{id}/role-mappings/realm', 103 | urlParamKeys: ['id'], 104 | payloadKey: 'roles', 105 | }); 106 | 107 | public listRealmRoleMappings = this.makeRequest< 108 | {id: string}, 109 | RoleRepresentation[] 110 | >({ 111 | method: 'GET', 112 | path: '/{id}/role-mappings/realm', 113 | urlParamKeys: ['id'], 114 | }); 115 | 116 | public delRealmRoleMappings = this.makeRequest< 117 | {id: string; roles: RoleMappingPayload[]}, 118 | void 119 | >({ 120 | method: 'DELETE', 121 | path: '/{id}/role-mappings/realm', 122 | urlParamKeys: ['id'], 123 | payloadKey: 'roles', 124 | }); 125 | 126 | public listAvailableRealmRoleMappings = this.makeRequest< 127 | {id: string}, 128 | RoleRepresentation[] 129 | >({ 130 | method: 'GET', 131 | path: '/{id}/role-mappings/realm/available', 132 | urlParamKeys: ['id'], 133 | }); 134 | 135 | /** 136 | * Client role mappings 137 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_client_role_mappings_resource 138 | */ 139 | 140 | public listClientRoleMappings = this.makeRequest< 141 | {id: string; clientUniqueId: string}, 142 | RoleRepresentation[] 143 | >({ 144 | method: 'GET', 145 | path: '/{id}/role-mappings/clients/{clientUniqueId}', 146 | urlParamKeys: ['id', 'clientUniqueId'], 147 | }); 148 | 149 | public addClientRoleMappings = this.makeRequest< 150 | {id: string; clientUniqueId: string; roles: RoleMappingPayload[]}, 151 | void 152 | >({ 153 | method: 'POST', 154 | path: '/{id}/role-mappings/clients/{clientUniqueId}', 155 | urlParamKeys: ['id', 'clientUniqueId'], 156 | payloadKey: 'roles', 157 | }); 158 | 159 | public delClientRoleMappings = this.makeRequest< 160 | {id: string; clientUniqueId: string; roles: RoleMappingPayload[]}, 161 | void 162 | >({ 163 | method: 'DELETE', 164 | path: '/{id}/role-mappings/clients/{clientUniqueId}', 165 | urlParamKeys: ['id', 'clientUniqueId'], 166 | payloadKey: 'roles', 167 | }); 168 | 169 | public listAvailableClientRoleMappings = this.makeRequest< 170 | {id: string; clientUniqueId: string}, 171 | RoleRepresentation[] 172 | >({ 173 | method: 'GET', 174 | path: '/{id}/role-mappings/clients/{clientUniqueId}/available', 175 | urlParamKeys: ['id', 'clientUniqueId'], 176 | }); 177 | 178 | constructor(client: KeycloakAdminClient) { 179 | super(client, { 180 | path: '/admin/realms/{realm}/groups', 181 | getUrlParams: () => ({ 182 | realm: client.realmName, 183 | }), 184 | getBaseUrl: () => client.baseUrl, 185 | }); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/resources/agent.ts: -------------------------------------------------------------------------------- 1 | import urlJoin from 'url-join'; 2 | import template from 'url-template'; 3 | import axios, {AxiosRequestConfig} from 'axios'; 4 | import {pick, omit, isUndefined, last} from 'lodash'; 5 | import {KeycloakAdminClient} from '../client'; 6 | 7 | // constants 8 | const SLASH = '/'; 9 | 10 | // interface 11 | export interface RequestArgs { 12 | method: string; 13 | path?: string; 14 | // Keys of url params to be applied 15 | urlParamKeys?: string[]; 16 | // Keys of query parameters to be applied 17 | queryParamKeys?: string[]; 18 | // Mapping of key transformations to be performed on the payload 19 | keyTransform?: Record; 20 | // If responding with 404, catch it and return null instead 21 | catchNotFound?: boolean; 22 | // The key of the value to use from the payload of request. Only works for POST & PUT. 23 | payloadKey?: string; 24 | // Whether the response header have a location field with newly created resource id 25 | // if this value is set, we return the field with format: {[field]: resourceId} 26 | // to represent the newly created resource 27 | // detail: keycloak/keycloak-nodejs-admin-client issue #11 28 | returnResourceIdInLocationHeader?: {field: string}; 29 | } 30 | 31 | export class Agent { 32 | private client: KeycloakAdminClient; 33 | private basePath: string; 34 | private getBaseParams?: () => Record; 35 | private getBaseUrl?: () => string; 36 | private requestConfig?: AxiosRequestConfig; 37 | 38 | constructor({ 39 | client, 40 | path = '/', 41 | getUrlParams = () => ({}), 42 | getBaseUrl = () => client.baseUrl, 43 | }: { 44 | client: KeycloakAdminClient; 45 | path?: string; 46 | getUrlParams?: () => Record; 47 | getBaseUrl?: () => string; 48 | }) { 49 | this.client = client; 50 | this.getBaseParams = getUrlParams; 51 | this.getBaseUrl = getBaseUrl; 52 | this.basePath = path; 53 | this.requestConfig = client.getRequestConfig() || {}; 54 | } 55 | 56 | public request({ 57 | method, 58 | path = '', 59 | urlParamKeys = [], 60 | queryParamKeys = [], 61 | catchNotFound = false, 62 | keyTransform, 63 | payloadKey, 64 | returnResourceIdInLocationHeader, 65 | }: RequestArgs) { 66 | return async (payload: any = {}) => { 67 | const baseParams = this.getBaseParams(); 68 | 69 | // Filter query parameters by queryParamKeys 70 | const queryParams = queryParamKeys ? pick(payload, queryParamKeys) : null; 71 | 72 | // Add filtered payload parameters to base parameters 73 | const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys]; 74 | const urlParams = {...baseParams, ...pick(payload, allUrlParamKeys)}; 75 | 76 | // Omit url parameters and query parameters from payload 77 | payload = omit(payload, [...allUrlParamKeys, ...queryParamKeys]); 78 | 79 | // Transform keys of both payload and queryParams 80 | if (keyTransform) { 81 | this.transformKey(payload, keyTransform); 82 | this.transformKey(queryParams, keyTransform); 83 | } 84 | 85 | return this.requestWithParams({ 86 | method, 87 | path, 88 | payload, 89 | urlParams, 90 | queryParams, 91 | catchNotFound, 92 | payloadKey, 93 | returnResourceIdInLocationHeader, 94 | }); 95 | }; 96 | } 97 | 98 | public updateRequest({ 99 | method, 100 | path = '', 101 | urlParamKeys = [], 102 | queryParamKeys = [], 103 | catchNotFound = false, 104 | keyTransform, 105 | payloadKey, 106 | returnResourceIdInLocationHeader, 107 | }: RequestArgs) { 108 | return async (query: any = {}, payload: any = {}) => { 109 | const baseParams = this.getBaseParams(); 110 | 111 | // Filter query parameters by queryParamKeys 112 | const queryParams = queryParamKeys ? pick(query, queryParamKeys) : null; 113 | 114 | // Add filtered query parameters to base parameters 115 | const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys]; 116 | const urlParams = { 117 | ...baseParams, 118 | ...pick(query, allUrlParamKeys), 119 | }; 120 | 121 | // Transform keys of queryParams 122 | if (keyTransform) { 123 | this.transformKey(queryParams, keyTransform); 124 | } 125 | 126 | return this.requestWithParams({ 127 | method, 128 | path, 129 | payload, 130 | urlParams, 131 | queryParams, 132 | catchNotFound, 133 | payloadKey, 134 | returnResourceIdInLocationHeader, 135 | }); 136 | }; 137 | } 138 | 139 | private async requestWithParams({ 140 | method, 141 | path, 142 | payload, 143 | urlParams, 144 | queryParams, 145 | catchNotFound, 146 | payloadKey, 147 | returnResourceIdInLocationHeader, 148 | }: { 149 | method: string; 150 | path: string; 151 | payload: any; 152 | urlParams: any; 153 | queryParams?: Record | null; 154 | catchNotFound: boolean; 155 | payloadKey?: string; 156 | returnResourceIdInLocationHeader?: {field: string}; 157 | }) { 158 | const newPath = urlJoin(this.basePath, path); 159 | 160 | // Parse template and replace with values from urlParams 161 | const pathTemplate = template.parse(newPath); 162 | const parsedPath = pathTemplate.expand(urlParams); 163 | const url = `${this.getBaseUrl()}${parsedPath}`; 164 | 165 | // Prepare request config 166 | const requestConfig: AxiosRequestConfig = { 167 | ...this.requestConfig, 168 | method, 169 | url, 170 | headers: { 171 | Authorization: `bearer ${await this.client.getAccessToken()}`, 172 | }, 173 | }; 174 | 175 | // Put payload into querystring if method is GET 176 | if (method === 'GET') { 177 | requestConfig.params = payload; 178 | } else { 179 | // Set the request data to the payload, or the value corresponding to the payloadKey, if it's defined 180 | requestConfig.data = payloadKey ? payload[payloadKey] : payload; 181 | } 182 | 183 | // Concat to existing queryParams 184 | if (queryParams) { 185 | requestConfig.params = requestConfig.params 186 | ? { 187 | ...requestConfig.params, 188 | ...queryParams, 189 | } 190 | : queryParams; 191 | } 192 | 193 | try { 194 | const res = await axios(requestConfig); 195 | 196 | // now we get the response of the http request 197 | // if `resourceIdInLocationHeader` is true, we'll get the resourceId from the location header field 198 | // todo: find a better way to find the id in path, maybe some kind of pattern matching 199 | // for now, we simply split the last sub-path of the path returned in location header field 200 | if (returnResourceIdInLocationHeader) { 201 | const locationHeader = res.headers.location; 202 | if (!locationHeader) { 203 | throw new Error( 204 | `location header is not found in request: ${res.config.url}`, 205 | ); 206 | } 207 | const resourceId: string = last(locationHeader.split(SLASH)); 208 | if (!resourceId) { 209 | // throw an error to let users know the response is not expected 210 | throw new Error( 211 | `resourceId is not found in Location header from request: ${res.config.url 212 | }`, 213 | ); 214 | } 215 | 216 | // return with format {[field]: string} 217 | const {field} = returnResourceIdInLocationHeader; 218 | return {[field]: resourceId}; 219 | } 220 | return res.data; 221 | } catch (err) { 222 | if (err.response && err.response.status === 404 && catchNotFound) { 223 | return null; 224 | } 225 | throw err; 226 | } 227 | } 228 | 229 | private transformKey(payload: any, keyMapping: Record) { 230 | if (!payload) { 231 | return; 232 | } 233 | 234 | Object.keys(keyMapping).some(key => { 235 | if (isUndefined(payload[key])) { 236 | // Skip if undefined 237 | return false; 238 | } 239 | const newKey = keyMapping[key]; 240 | payload[newKey] = payload[key]; 241 | delete payload[key]; 242 | }); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /test/groups.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | import GroupRepresentation from '../src/defs/groupRepresentation'; 7 | import RoleRepresentation from '../src/defs/roleRepresentation'; 8 | import ClientRepresentation from '../src/defs/clientRepresentation'; 9 | 10 | const expect = chai.expect; 11 | 12 | declare module 'mocha' { 13 | // tslint:disable-next-line:interface-name 14 | interface ISuiteCallbackContext { 15 | kcAdminClient?: KeycloakAdminClient; 16 | currentClient?: ClientRepresentation; 17 | currentGroup?: GroupRepresentation; 18 | currentRole?: RoleRepresentation; 19 | } 20 | } 21 | 22 | describe('Groups', function() { 23 | before(async () => { 24 | this.kcAdminClient = new KeycloakAdminClient(); 25 | await this.kcAdminClient.auth(credentials); 26 | // initialize group 27 | const group = await this.kcAdminClient.groups.create({ 28 | name: 'cool-group', 29 | }); 30 | expect(group.id).to.be.ok; 31 | this.currentGroup = await this.kcAdminClient.groups.findOne({id: group.id}); 32 | }); 33 | 34 | after(async () => { 35 | const groupId = this.currentGroup.id; 36 | await this.kcAdminClient.groups.del({ 37 | id: groupId, 38 | }); 39 | 40 | const group = await this.kcAdminClient.groups.findOne({ 41 | id: groupId, 42 | }); 43 | expect(group).to.be.null; 44 | }); 45 | 46 | it('list groups', async () => { 47 | const groups = await this.kcAdminClient.groups.find(); 48 | expect(groups).to.be.ok; 49 | }); 50 | 51 | it('get single groups', async () => { 52 | const groupId = this.currentGroup.id; 53 | const group = await this.kcAdminClient.groups.findOne({ 54 | id: groupId, 55 | }); 56 | // get group from id will contains more fields than listing api 57 | expect(group).to.deep.include(this.currentGroup); 58 | }); 59 | 60 | it('update single groups', async () => { 61 | const groupId = this.currentGroup.id; 62 | await this.kcAdminClient.groups.update( 63 | {id: groupId}, 64 | {name: 'another-group-name'}, 65 | ); 66 | 67 | const group = await this.kcAdminClient.groups.findOne({ 68 | id: groupId, 69 | }); 70 | expect(group).to.include({ 71 | name: 'another-group-name', 72 | }); 73 | }); 74 | 75 | it('set or create child', async () => { 76 | const groupName = 'child-group'; 77 | const groupId = this.currentGroup.id; 78 | const childGroup = await this.kcAdminClient.groups.setOrCreateChild( 79 | {id: groupId}, 80 | {name: groupName}, 81 | ); 82 | 83 | expect(childGroup.id).to.be.ok; 84 | 85 | const group = await this.kcAdminClient.groups.findOne({ 86 | id: groupId, 87 | }); 88 | expect(group.subGroups[0]).to.deep.include({ 89 | id: childGroup.id, 90 | name: groupName, 91 | path: `/${group.name}/${groupName}`, 92 | }); 93 | }); 94 | 95 | /** 96 | * Role mappings 97 | */ 98 | describe('role-mappings', () => { 99 | before(async () => { 100 | // create new role 101 | const roleName = faker.internet.userName(); 102 | const {roleName: createdRoleName} = await this.kcAdminClient.roles.create( 103 | { 104 | name: roleName, 105 | }, 106 | ); 107 | expect(createdRoleName).to.be.equal(roleName); 108 | const role = await this.kcAdminClient.roles.findOneByName({ 109 | name: roleName, 110 | }); 111 | this.currentRole = role; 112 | }); 113 | 114 | after(async () => { 115 | await this.kcAdminClient.roles.delByName({name: this.currentRole.name}); 116 | }); 117 | 118 | it('add a role to group', async () => { 119 | // add role-mappings with role id 120 | await this.kcAdminClient.groups.addRealmRoleMappings({ 121 | id: this.currentGroup.id, 122 | 123 | // at least id and name should appear 124 | roles: [ 125 | { 126 | id: this.currentRole.id, 127 | name: this.currentRole.name, 128 | }, 129 | ], 130 | }); 131 | }); 132 | 133 | it('list available role-mappings', async () => { 134 | const roles = await this.kcAdminClient.groups.listAvailableRealmRoleMappings( 135 | { 136 | id: this.currentGroup.id, 137 | }, 138 | ); 139 | 140 | // admin, create-realm, offline_access, uma_authorization 141 | expect(roles.length).to.be.least(4); 142 | }); 143 | 144 | it('list role-mappings', async () => { 145 | const {realmMappings} = await this.kcAdminClient.groups.listRoleMappings({ 146 | id: this.currentGroup.id, 147 | }); 148 | 149 | expect(realmMappings).to.be.ok; 150 | // currentRole will have an empty `attributes`, but role-mappings do not 151 | expect(this.currentRole).to.deep.include(realmMappings[0]); 152 | }); 153 | 154 | it('list realm role-mappings of group', async () => { 155 | const roles = await this.kcAdminClient.groups.listRealmRoleMappings({ 156 | id: this.currentGroup.id, 157 | }); 158 | // currentRole will have an empty `attributes`, but role-mappings do not 159 | expect(this.currentRole).to.deep.include(roles[0]); 160 | }); 161 | 162 | it('del realm role-mappings from group', async () => { 163 | await this.kcAdminClient.groups.delRealmRoleMappings({ 164 | id: this.currentGroup.id, 165 | roles: [ 166 | { 167 | id: this.currentRole.id, 168 | name: this.currentRole.name, 169 | }, 170 | ], 171 | }); 172 | 173 | const roles = await this.kcAdminClient.groups.listRealmRoleMappings({ 174 | id: this.currentGroup.id, 175 | }); 176 | expect(roles).to.be.empty; 177 | }); 178 | }); 179 | 180 | /** 181 | * client Role mappings 182 | */ 183 | describe('client role-mappings', () => { 184 | before(async () => { 185 | // create new client 186 | const clientId = faker.internet.userName(); 187 | await this.kcAdminClient.clients.create({ 188 | clientId, 189 | }); 190 | 191 | const clients = await this.kcAdminClient.clients.find({clientId}); 192 | expect(clients[0]).to.be.ok; 193 | this.currentClient = clients[0]; 194 | 195 | // create new client role 196 | const roleName = faker.internet.userName(); 197 | await this.kcAdminClient.clients.createRole({ 198 | id: this.currentClient.id, 199 | name: roleName, 200 | }); 201 | 202 | // assign to currentRole 203 | this.currentRole = await this.kcAdminClient.clients.findRole({ 204 | id: this.currentClient.id, 205 | roleName, 206 | }); 207 | }); 208 | 209 | after(async () => { 210 | await this.kcAdminClient.clients.delRole({ 211 | id: this.currentClient.id, 212 | roleName: this.currentRole.name, 213 | }); 214 | await this.kcAdminClient.clients.del({id: this.currentClient.id}); 215 | }); 216 | 217 | it('add a client role to group', async () => { 218 | // add role-mappings with role id 219 | await this.kcAdminClient.groups.addClientRoleMappings({ 220 | id: this.currentGroup.id, 221 | clientUniqueId: this.currentClient.id, 222 | 223 | // at least id and name should appear 224 | roles: [ 225 | { 226 | id: this.currentRole.id, 227 | name: this.currentRole.name, 228 | }, 229 | ], 230 | }); 231 | }); 232 | 233 | it('list available client role-mappings for group', async () => { 234 | const roles = await this.kcAdminClient.groups.listAvailableClientRoleMappings( 235 | { 236 | id: this.currentGroup.id, 237 | clientUniqueId: this.currentClient.id, 238 | }, 239 | ); 240 | 241 | expect(roles).to.be.empty; 242 | }); 243 | 244 | it('list client role-mappings of group', async () => { 245 | const roles = await this.kcAdminClient.groups.listClientRoleMappings({ 246 | id: this.currentGroup.id, 247 | clientUniqueId: this.currentClient.id, 248 | }); 249 | 250 | // currentRole will have an empty `attributes`, but role-mappings do not 251 | expect(this.currentRole).to.deep.include(roles[0]); 252 | }); 253 | 254 | it('del client role-mappings from group', async () => { 255 | const roleName = faker.internet.userName(); 256 | await this.kcAdminClient.clients.createRole({ 257 | id: this.currentClient.id, 258 | name: roleName, 259 | }); 260 | const role = await this.kcAdminClient.clients.findRole({ 261 | id: this.currentClient.id, 262 | roleName, 263 | }); 264 | 265 | // delete the created role 266 | await this.kcAdminClient.groups.delClientRoleMappings({ 267 | id: this.currentGroup.id, 268 | clientUniqueId: this.currentClient.id, 269 | roles: [ 270 | { 271 | id: role.id, 272 | name: role.name, 273 | }, 274 | ], 275 | }); 276 | 277 | // check if mapping is successfully deleted 278 | const roles = await this.kcAdminClient.groups.listClientRoleMappings({ 279 | id: this.currentGroup.id, 280 | clientUniqueId: this.currentClient.id, 281 | }); 282 | 283 | // should only left the one we added in the previous test 284 | expect(roles.length).to.be.eql(1); 285 | }); 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /src/resources/clientScopes.ts: -------------------------------------------------------------------------------- 1 | import ClientScopeRepresentation from '../defs/clientScopeRepresentation'; 2 | import Resource from './resource'; 3 | import {KeycloakAdminClient} from '../client'; 4 | import ProtocolMapperRepresentation from '../defs/protocolMapperRepresentation'; 5 | import MappingsRepresentation from '../defs/mappingsRepresentation'; 6 | import RoleRepresentation from '../defs/roleRepresentation'; 7 | 8 | export class ClientScopes extends Resource<{realm?: string}> { 9 | public find = this.makeRequest<{}, ClientScopeRepresentation[]>({ 10 | method: 'GET', 11 | path: '/client-scopes', 12 | }); 13 | 14 | public create = this.makeRequest({ 15 | method: 'POST', 16 | path: '/client-scopes', 17 | }); 18 | 19 | /** 20 | * Client-Scopes by id 21 | */ 22 | 23 | public findOne = this.makeRequest<{id: string}, ClientScopeRepresentation>({ 24 | method: 'GET', 25 | path: '/client-scopes/{id}', 26 | urlParamKeys: ['id'], 27 | catchNotFound: true, 28 | }); 29 | 30 | public update = this.makeUpdateRequest< 31 | {id: string}, 32 | ClientScopeRepresentation, 33 | void 34 | >({ 35 | method: 'PUT', 36 | path: '/client-scopes/{id}', 37 | urlParamKeys: ['id'], 38 | }); 39 | 40 | public del = this.makeRequest<{id: string}, void>({ 41 | method: 'DELETE', 42 | path: '/client-scopes/{id}', 43 | urlParamKeys: ['id'], 44 | }); 45 | 46 | /** 47 | * Default Client-Scopes 48 | */ 49 | 50 | public listDefaultClientScopes = this.makeRequest< 51 | void, 52 | ClientScopeRepresentation[] 53 | >({ 54 | method: 'GET', 55 | path: '/default-default-client-scopes', 56 | }); 57 | 58 | public addDefaultClientScope = this.makeRequest<{id: string}, void>({ 59 | method: 'PUT', 60 | path: '/default-default-client-scopes/{id}', 61 | urlParamKeys: ['id'], 62 | }); 63 | 64 | public delDefaultClientScope = this.makeRequest<{id: string}, void>({ 65 | method: 'DELETE', 66 | path: '/default-default-client-scopes/{id}', 67 | urlParamKeys: ['id'], 68 | }); 69 | 70 | /** 71 | * Default Optional Client-Scopes 72 | */ 73 | 74 | public listDefaultOptionalClientScopes = this.makeRequest< 75 | void, 76 | ClientScopeRepresentation[] 77 | >({ 78 | method: 'GET', 79 | path: '/default-optional-client-scopes', 80 | }); 81 | 82 | public addDefaultOptionalClientScope = this.makeRequest<{id: string}, void>({ 83 | method: 'PUT', 84 | path: '/default-optional-client-scopes/{id}', 85 | urlParamKeys: ['id'], 86 | }); 87 | 88 | public delDefaultOptionalClientScope = this.makeRequest<{id: string}, void>({ 89 | method: 'DELETE', 90 | path: '/default-optional-client-scopes/{id}', 91 | urlParamKeys: ['id'], 92 | }); 93 | 94 | /** 95 | * Protocol Mappers 96 | */ 97 | 98 | public addMultipleProtocolMappers = this.makeUpdateRequest< 99 | {id: string}, 100 | ProtocolMapperRepresentation[], 101 | void 102 | >({ 103 | method: 'POST', 104 | path: '/client-scopes/{id}/protocol-mappers/add-models', 105 | urlParamKeys: ['id'], 106 | }); 107 | 108 | public addProtocolMapper = this.makeUpdateRequest< 109 | {id: string}, 110 | ProtocolMapperRepresentation, 111 | void 112 | >({ 113 | method: 'POST', 114 | path: '/client-scopes/{id}/protocol-mappers/models', 115 | urlParamKeys: ['id'], 116 | }); 117 | 118 | public listProtocolMappers = this.makeRequest< 119 | {id: string}, 120 | ProtocolMapperRepresentation[] 121 | >({ 122 | method: 'GET', 123 | path: '/client-scopes/{id}/protocol-mappers/models', 124 | urlParamKeys: ['id'], 125 | }); 126 | 127 | public findProtocolMapper = this.makeRequest< 128 | {id: string; mapperId: string}, 129 | ProtocolMapperRepresentation 130 | >({ 131 | method: 'GET', 132 | path: '/client-scopes/{id}/protocol-mappers/models/{mapperId}', 133 | urlParamKeys: ['id', 'mapperId'], 134 | catchNotFound: true, 135 | }); 136 | 137 | public findProtocolMappersByProtocol = this.makeRequest< 138 | {id: string; protocol: string}, 139 | ProtocolMapperRepresentation[] 140 | >({ 141 | method: 'GET', 142 | path: '/client-scopes/{id}/protocol-mappers/protocol/{protocol}', 143 | urlParamKeys: ['id', 'protocol'], 144 | catchNotFound: true, 145 | }); 146 | 147 | public updateProtocolMapper = this.makeUpdateRequest< 148 | {id: string; mapperId: string}, 149 | ProtocolMapperRepresentation, 150 | void 151 | >({ 152 | method: 'PUT', 153 | path: '/client-scopes/{id}/protocol-mappers/models/{mapperId}', 154 | urlParamKeys: ['id', 'mapperId'], 155 | }); 156 | 157 | public delProtocolMapper = this.makeRequest< 158 | {id: string; mapperId: string}, 159 | void 160 | >({ 161 | method: 'DELETE', 162 | path: '/client-scopes/{id}/protocol-mappers/models/{mapperId}', 163 | urlParamKeys: ['id', 'mapperId'], 164 | }); 165 | 166 | /** 167 | * Scope Mappings 168 | */ 169 | public listScopeMappings = this.makeRequest< 170 | {id: string}, 171 | MappingsRepresentation 172 | >({ 173 | method: 'GET', 174 | path: '/client-scopes/{id}/scope-mappings', 175 | urlParamKeys: ['id'], 176 | }); 177 | 178 | public addClientScopeMappings = this.makeUpdateRequest< 179 | {id: string; client: string}, 180 | RoleRepresentation[], 181 | void 182 | >({ 183 | method: 'POST', 184 | path: '/client-scopes/{id}/scope-mappings/clients/{client}', 185 | urlParamKeys: ['id', 'client'], 186 | }); 187 | 188 | public listClientScopeMappings = this.makeRequest< 189 | {id: string; client: string}, 190 | RoleRepresentation[] 191 | >({ 192 | method: 'GET', 193 | path: '/client-scopes/{id}/scope-mappings/clients/{client}', 194 | urlParamKeys: ['id', 'client'], 195 | }); 196 | 197 | public listAvailableClientScopeMappings = this.makeRequest< 198 | {id: string; client: string}, 199 | RoleRepresentation[] 200 | >({ 201 | method: 'GET', 202 | path: '/client-scopes/{id}/scope-mappings/clients/{client}/available', 203 | urlParamKeys: ['id', 'client'], 204 | }); 205 | 206 | public listCompositeClientScopeMappings = this.makeRequest< 207 | {id: string; client: string}, 208 | RoleRepresentation[] 209 | >({ 210 | method: 'GET', 211 | path: '/client-scopes/{id}/scope-mappings/clients/{client}/available', 212 | urlParamKeys: ['id', 'client'], 213 | }); 214 | 215 | public delClientScopeMappings = this.makeUpdateRequest< 216 | {id: string; client: string}, 217 | RoleRepresentation[], 218 | void 219 | >({ 220 | method: 'DELETE', 221 | path: '/client-scopes/{id}/scope-mappings/clients/{client}', 222 | urlParamKeys: ['id', 'client'], 223 | }); 224 | 225 | public addRealmScopeMappings = this.makeUpdateRequest< 226 | {id: string}, 227 | RoleRepresentation[], 228 | void 229 | >({ 230 | method: 'POST', 231 | path: '/client-scopes/{id}/scope-mappings/realm', 232 | urlParamKeys: ['id'], 233 | }); 234 | 235 | public listRealmScopeMappings = this.makeRequest< 236 | {id: string}, 237 | RoleRepresentation[] 238 | >({ 239 | method: 'GET', 240 | path: '/client-scopes/{id}/scope-mappings/realm', 241 | urlParamKeys: ['id'], 242 | }); 243 | 244 | public listAvailableRealmScopeMappings = this.makeRequest< 245 | {id: string}, 246 | RoleRepresentation[] 247 | >({ 248 | method: 'GET', 249 | path: '/client-scopes/{id}/scope-mappings/realm/available', 250 | urlParamKeys: ['id'], 251 | }); 252 | 253 | public listCompositeRealmScopeMappings = this.makeRequest< 254 | {id: string}, 255 | RoleRepresentation[] 256 | >({ 257 | method: 'GET', 258 | path: '/client-scopes/{id}/scope-mappings/realm/available', 259 | urlParamKeys: ['id'], 260 | }); 261 | 262 | public delRealmScopeMappings = this.makeUpdateRequest< 263 | {id: string}, 264 | RoleRepresentation[], 265 | void 266 | >({ 267 | method: 'DELETE', 268 | path: '/client-scopes/{id}/scope-mappings/realm', 269 | urlParamKeys: ['id'], 270 | }); 271 | 272 | constructor(client: KeycloakAdminClient) { 273 | super(client, { 274 | path: '/admin/realms/{realm}', 275 | getUrlParams: () => ({ 276 | realm: client.realmName, 277 | }), 278 | getBaseUrl: () => client.baseUrl, 279 | }); 280 | } 281 | 282 | /** 283 | * Find client scope by name. 284 | */ 285 | public async findOneByName(payload: { 286 | realm?: string; 287 | name: string; 288 | }): Promise { 289 | const allScopes = await this.find({ 290 | ...(payload.realm ? {realm: payload.realm} : {}), 291 | }); 292 | const scope = allScopes.find(item => item.name === payload.name); 293 | return scope ? scope : null; 294 | } 295 | 296 | /** 297 | * Delete client scope by name. 298 | */ 299 | public async delByName(payload: { 300 | realm?: string; 301 | name: string; 302 | }): Promise { 303 | const scope = await this.findOneByName(payload); 304 | 305 | if (!scope) { 306 | throw new Error('Scope not found.'); 307 | } 308 | 309 | await this.del({ 310 | ...(payload.realm ? {realm: payload.realm} : {}), 311 | id: scope.id, 312 | }); 313 | } 314 | 315 | /** 316 | * Find single protocol mapper by name. 317 | */ 318 | public async findProtocolMapperByName(payload: { 319 | realm?: string; 320 | id: string; 321 | name: string; 322 | }): Promise { 323 | const allProtocolMappers = await this.listProtocolMappers({ 324 | id: payload.id, 325 | ...(payload.realm ? {realm: payload.realm} : {}), 326 | }); 327 | const protocolMapper = allProtocolMappers.find( 328 | mapper => mapper.name === payload.name, 329 | ); 330 | return protocolMapper ? protocolMapper : null; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/resources/users.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import UserRepresentation from '../defs/userRepresentation'; 3 | import UserConsentRepresentation from '../defs/userConsentRepresentation'; 4 | import UserSessionRepresentation from '../defs/userSessionRepresentation'; 5 | import {KeycloakAdminClient} from '../client'; 6 | import MappingsRepresentation from '../defs/mappingsRepresentation'; 7 | import RoleRepresentation, { 8 | RoleMappingPayload, 9 | } from '../defs/roleRepresentation'; 10 | import {RequiredActionAlias} from '../defs/requiredActionProviderRepresentation'; 11 | import FederatedIdentityRepresentation from '../defs/federatedIdentityRepresentation'; 12 | import GroupRepresentation from '../defs/groupRepresentation'; 13 | import CredentialRepresentation from '../defs/credentialRepresentation'; 14 | 15 | export interface UserQuery { 16 | email?: string; 17 | first?: number; 18 | firstName?: string; 19 | lastName?: string; 20 | max?: number; 21 | search?: string; 22 | username?: string; 23 | } 24 | 25 | export class Users extends Resource<{realm?: string}> { 26 | public find = this.makeRequest({ 27 | method: 'GET', 28 | }); 29 | 30 | public create = this.makeRequest({ 31 | method: 'POST', 32 | returnResourceIdInLocationHeader: {field: 'id'}, 33 | }); 34 | 35 | /** 36 | * Single user 37 | */ 38 | 39 | public findOne = this.makeRequest<{id: string}, UserRepresentation>({ 40 | method: 'GET', 41 | path: '/{id}', 42 | urlParamKeys: ['id'], 43 | catchNotFound: true, 44 | }); 45 | 46 | public update = this.makeUpdateRequest< 47 | {id: string}, 48 | UserRepresentation, 49 | void 50 | >({ 51 | method: 'PUT', 52 | path: '/{id}', 53 | urlParamKeys: ['id'], 54 | }); 55 | 56 | public del = this.makeRequest<{id: string}, void>({ 57 | method: 'DELETE', 58 | path: '/{id}', 59 | urlParamKeys: ['id'], 60 | }); 61 | 62 | /** 63 | * role mappings 64 | */ 65 | 66 | public listRoleMappings = this.makeRequest< 67 | {id: string}, 68 | MappingsRepresentation 69 | >({ 70 | method: 'GET', 71 | path: '/{id}/role-mappings', 72 | urlParamKeys: ['id'], 73 | }); 74 | 75 | public addRealmRoleMappings = this.makeRequest< 76 | {id: string; roles: RoleMappingPayload[]}, 77 | void 78 | >({ 79 | method: 'POST', 80 | path: '/{id}/role-mappings/realm', 81 | urlParamKeys: ['id'], 82 | payloadKey: 'roles', 83 | }); 84 | 85 | public listRealmRoleMappings = this.makeRequest< 86 | {id: string}, 87 | RoleRepresentation[] 88 | >({ 89 | method: 'GET', 90 | path: '/{id}/role-mappings/realm', 91 | urlParamKeys: ['id'], 92 | }); 93 | 94 | public delRealmRoleMappings = this.makeRequest< 95 | {id: string; roles: RoleMappingPayload[]}, 96 | void 97 | >({ 98 | method: 'DELETE', 99 | path: '/{id}/role-mappings/realm', 100 | urlParamKeys: ['id'], 101 | payloadKey: 'roles', 102 | }); 103 | 104 | public listAvailableRealmRoleMappings = this.makeRequest< 105 | {id: string}, 106 | RoleRepresentation[] 107 | >({ 108 | method: 'GET', 109 | path: '/{id}/role-mappings/realm/available', 110 | urlParamKeys: ['id'], 111 | }); 112 | 113 | // Get effective realm-level role mappings This will recurse all composite roles to get the result. 114 | public listCompositeRealmRoleMappings = this.makeRequest< 115 | {id: string}, 116 | RoleRepresentation[] 117 | >({ 118 | method: 'GET', 119 | path: '/{id}/role-mappings/realm/composite', 120 | urlParamKeys: ['id'], 121 | }); 122 | 123 | /** 124 | * Client role mappings 125 | * https://www.keycloak.org/docs-api/4.1/rest-api/#_client_role_mappings_resource 126 | */ 127 | 128 | public listClientRoleMappings = this.makeRequest< 129 | {id: string; clientUniqueId: string}, 130 | RoleRepresentation[] 131 | >({ 132 | method: 'GET', 133 | path: '/{id}/role-mappings/clients/{clientUniqueId}', 134 | urlParamKeys: ['id', 'clientUniqueId'], 135 | }); 136 | 137 | public addClientRoleMappings = this.makeRequest< 138 | {id: string; clientUniqueId: string; roles: RoleMappingPayload[]}, 139 | void 140 | >({ 141 | method: 'POST', 142 | path: '/{id}/role-mappings/clients/{clientUniqueId}', 143 | urlParamKeys: ['id', 'clientUniqueId'], 144 | payloadKey: 'roles', 145 | }); 146 | 147 | public delClientRoleMappings = this.makeRequest< 148 | {id: string; clientUniqueId: string; roles: RoleMappingPayload[]}, 149 | void 150 | >({ 151 | method: 'DELETE', 152 | path: '/{id}/role-mappings/clients/{clientUniqueId}', 153 | urlParamKeys: ['id', 'clientUniqueId'], 154 | payloadKey: 'roles', 155 | }); 156 | 157 | public listAvailableClientRoleMappings = this.makeRequest< 158 | {id: string; clientUniqueId: string}, 159 | RoleRepresentation[] 160 | >({ 161 | method: 'GET', 162 | path: '/{id}/role-mappings/clients/{clientUniqueId}/available', 163 | urlParamKeys: ['id', 'clientUniqueId'], 164 | }); 165 | 166 | /** 167 | * Send a update account email to the user 168 | * an email contains a link the user can click to perform a set of required actions. 169 | */ 170 | 171 | public executeActionsEmail = this.makeRequest< 172 | { 173 | id: string; 174 | clientId?: string; 175 | lifespan?: number; 176 | redirectUri?: string; 177 | actions?: RequiredActionAlias[]; 178 | }, 179 | void 180 | >({ 181 | method: 'PUT', 182 | path: '/{id}/execute-actions-email', 183 | urlParamKeys: ['id'], 184 | payloadKey: 'actions', 185 | queryParamKeys: ['lifespan', 'redirectUri', 'clientId'], 186 | keyTransform: { 187 | clientId: 'client_id', 188 | redirectUri: 'redirect_uri', 189 | }, 190 | }); 191 | 192 | /** 193 | * Group 194 | */ 195 | 196 | public listGroups = this.makeRequest<{id: string}, GroupRepresentation[]>({ 197 | method: 'GET', 198 | path: '/{id}/groups', 199 | urlParamKeys: ['id'], 200 | }); 201 | 202 | public addToGroup = this.makeRequest< 203 | {id: string; groupId: string}, 204 | GroupRepresentation[] 205 | >({ 206 | method: 'PUT', 207 | path: '/{id}/groups/{groupId}', 208 | urlParamKeys: ['id', 'groupId'], 209 | }); 210 | 211 | public delFromGroup = this.makeRequest< 212 | {id: string; groupId: string}, 213 | GroupRepresentation[] 214 | >({ 215 | method: 'DELETE', 216 | path: '/{id}/groups/{groupId}', 217 | urlParamKeys: ['id', 'groupId'], 218 | }); 219 | 220 | /** 221 | * Federated Identity 222 | */ 223 | 224 | public listFederatedIdentities = this.makeRequest< 225 | {id: string}, 226 | FederatedIdentityRepresentation[] 227 | >({ 228 | method: 'GET', 229 | path: '/{id}/federated-identity', 230 | urlParamKeys: ['id'], 231 | }); 232 | 233 | public addToFederatedIdentity = this.makeRequest< 234 | { 235 | id: string; 236 | federatedIdentityId: string; 237 | federatedIdentity: FederatedIdentityRepresentation; 238 | }, 239 | void 240 | >({ 241 | method: 'POST', 242 | path: '/{id}/federated-identity/{federatedIdentityId}', 243 | urlParamKeys: ['id', 'federatedIdentityId'], 244 | payloadKey: 'federatedIdentity', 245 | }); 246 | 247 | public delFromFederatedIdentity = this.makeRequest< 248 | {id: string; federatedIdentityId: string}, 249 | void 250 | >({ 251 | method: 'DELETE', 252 | path: '/{id}/federated-identity/{federatedIdentityId}', 253 | urlParamKeys: ['id', 'federatedIdentityId'], 254 | }); 255 | 256 | /** 257 | * remove totp 258 | */ 259 | public removeTotp = this.makeRequest<{id: string}, void>({ 260 | method: 'PUT', 261 | path: '/{id}/remove-totp', 262 | urlParamKeys: ['id'], 263 | }); 264 | 265 | /** 266 | * reset password 267 | */ 268 | public resetPassword = this.makeRequest< 269 | {id: string; credential: CredentialRepresentation}, 270 | void 271 | >({ 272 | method: 'PUT', 273 | path: '/{id}/reset-password', 274 | urlParamKeys: ['id'], 275 | payloadKey: 'credential', 276 | }); 277 | 278 | /** 279 | * send verify email 280 | */ 281 | public sendVerifyEmail = this.makeRequest< 282 | {id: string; clientId?: string; redirectUri?: string}, 283 | void 284 | >({ 285 | method: 'PUT', 286 | path: '/{id}/send-verify-email', 287 | urlParamKeys: ['id'], 288 | queryParamKeys: ['clientId', 'redirectUri'], 289 | keyTransform: { 290 | clientId: 'client_id', 291 | redirectUri: 'redirect_uri', 292 | }, 293 | }); 294 | 295 | /** 296 | * list user sessions 297 | */ 298 | public listSessions = this.makeRequest< 299 | {id: string}, 300 | UserSessionRepresentation[] 301 | >({ 302 | method: 'GET', 303 | path: '/{id}/sessions', 304 | urlParamKeys: ['id'], 305 | }); 306 | 307 | /** 308 | * list offline sessions associated with the user and client 309 | */ 310 | public listOfflineSessions = this.makeRequest< 311 | {id: string, clientId: string}, 312 | UserSessionRepresentation[] 313 | >({ 314 | method: 'GET', 315 | path: '/{id}/offline-sessions/{clientId}', 316 | urlParamKeys: ['id', 'clientId'], 317 | }); 318 | 319 | /** 320 | * logout user from all sessions 321 | */ 322 | public logout = this.makeRequest< 323 | {id: string}, 324 | void 325 | >({ 326 | method: 'POST', 327 | path: '/{id}/logout', 328 | urlParamKeys: ['id'], 329 | }); 330 | 331 | /** 332 | * list consents granted by the user 333 | */ 334 | public listConsents = this.makeRequest< 335 | {id: string}, 336 | UserConsentRepresentation[] 337 | >({ 338 | method: 'GET', 339 | path: '/{id}/consents', 340 | urlParamKeys: ['id'], 341 | }); 342 | 343 | /** 344 | * revoke consent and offline tokens for particular client from user 345 | */ 346 | public revokeConsent = this.makeRequest< 347 | {id: string, clientId: string}, 348 | void 349 | >({ 350 | method: 'DELETE', 351 | path: '/{id}/consents/{clientId}', 352 | urlParamKeys: ['id', 'clientId'], 353 | }); 354 | 355 | constructor(client: KeycloakAdminClient) { 356 | super(client, { 357 | path: '/admin/realms/{realm}/users', 358 | getUrlParams: () => ({ 359 | realm: client.realmName, 360 | }), 361 | getBaseUrl: () => client.baseUrl, 362 | }); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/resources/clients.ts: -------------------------------------------------------------------------------- 1 | import Resource from './resource'; 2 | import ClientRepresentation from '../defs/clientRepresentation'; 3 | import {KeycloakAdminClient} from '../client'; 4 | import RoleRepresentation from '../defs/roleRepresentation'; 5 | import UserRepresentation from '../defs/userRepresentation'; 6 | import CredentialRepresentation from '../defs/credentialRepresentation'; 7 | import ClientScopeRepresentation from '../defs/clientScopeRepresentation'; 8 | import ProtocolMapperRepresentation from '../defs/protocolMapperRepresentation'; 9 | import MappingsRepresentation from '../defs/mappingsRepresentation'; 10 | import UserSessionRepresentation from '../defs/userSessionRepresentation'; 11 | 12 | export interface ClientQuery { 13 | clientId?: string; 14 | viewableOnly?: boolean; 15 | } 16 | 17 | export class Clients extends Resource<{realm?: string}> { 18 | public find = this.makeRequest({ 19 | method: 'GET', 20 | }); 21 | 22 | public create = this.makeRequest({ 23 | method: 'POST', 24 | returnResourceIdInLocationHeader: {field: 'id'}, 25 | }); 26 | 27 | /** 28 | * Single client 29 | */ 30 | 31 | public findOne = this.makeRequest<{id: string}, ClientRepresentation>({ 32 | method: 'GET', 33 | path: '/{id}', 34 | urlParamKeys: ['id'], 35 | catchNotFound: true, 36 | }); 37 | 38 | public update = this.makeUpdateRequest< 39 | {id: string}, 40 | ClientRepresentation, 41 | void 42 | >({ 43 | method: 'PUT', 44 | path: '/{id}', 45 | urlParamKeys: ['id'], 46 | }); 47 | 48 | public del = this.makeRequest<{id: string}, void>({ 49 | method: 'DELETE', 50 | path: '/{id}', 51 | urlParamKeys: ['id'], 52 | }); 53 | 54 | /** 55 | * Client roles 56 | */ 57 | 58 | public createRole = this.makeRequest({ 59 | method: 'POST', 60 | path: '/{id}/roles', 61 | urlParamKeys: ['id'], 62 | returnResourceIdInLocationHeader: {field: 'roleName'}, 63 | }); 64 | 65 | public listRoles = this.makeRequest<{id: string}, RoleRepresentation[]>({ 66 | method: 'GET', 67 | path: '/{id}/roles', 68 | urlParamKeys: ['id'], 69 | }); 70 | 71 | public findRole = this.makeRequest< 72 | {id: string; roleName: string}, 73 | RoleRepresentation 74 | >({ 75 | method: 'GET', 76 | path: '/{id}/roles/{roleName}', 77 | urlParamKeys: ['id', 'roleName'], 78 | catchNotFound: true, 79 | }); 80 | 81 | public updateRole = this.makeUpdateRequest< 82 | {id: string; roleName: string}, 83 | RoleRepresentation, 84 | void 85 | >({ 86 | method: 'PUT', 87 | path: '/{id}/roles/{roleName}', 88 | urlParamKeys: ['id', 'roleName'], 89 | }); 90 | 91 | public delRole = this.makeRequest<{id: string; roleName: string}, void>({ 92 | method: 'DELETE', 93 | path: '/{id}/roles/{roleName}', 94 | urlParamKeys: ['id', 'roleName'], 95 | }); 96 | 97 | public findUsersWithRole = this.makeRequest< 98 | {id: string; roleName: string; first?: number; max?: number}, 99 | UserRepresentation[] 100 | >({ 101 | method: 'GET', 102 | path: '/{id}/roles/{roleName}/users', 103 | urlParamKeys: ['id', 'roleName'], 104 | }); 105 | 106 | /** 107 | * Service account user 108 | */ 109 | 110 | public getServiceAccountUser = this.makeRequest< 111 | {id: string}, 112 | UserRepresentation 113 | >({ 114 | method: 'GET', 115 | path: '/{id}/service-account-user', 116 | urlParamKeys: ['id'], 117 | }); 118 | 119 | /** 120 | * Client secret 121 | */ 122 | 123 | public generateNewClientSecret = this.makeRequest<{id: string}, {id: string}>( 124 | { 125 | method: 'POST', 126 | path: '/{id}/client-secret', 127 | urlParamKeys: ['id'], 128 | }, 129 | ); 130 | 131 | public getClientSecret = this.makeRequest< 132 | {id: string}, 133 | CredentialRepresentation 134 | >({ 135 | method: 'GET', 136 | path: '/{id}/client-secret', 137 | urlParamKeys: ['id'], 138 | }); 139 | 140 | /** 141 | * Client Scopes 142 | */ 143 | public listDefaultClientScopes = this.makeRequest< 144 | {id: string}, 145 | ClientScopeRepresentation[] 146 | >({ 147 | method: 'GET', 148 | path: '/{id}/default-client-scopes', 149 | urlParamKeys: ['id'], 150 | }); 151 | 152 | public addDefaultClientScope = this.makeRequest< 153 | {id: string; clientScopeId: string}, 154 | void 155 | >({ 156 | method: 'PUT', 157 | path: '/{id}/default-client-scopes/{clientScopeId}', 158 | urlParamKeys: ['id', 'clientScopeId'], 159 | }); 160 | 161 | public delDefaultClientScope = this.makeRequest< 162 | {id: string; clientScopeId: string}, 163 | void 164 | >({ 165 | method: 'DELETE', 166 | path: '/{id}/default-client-scopes/{clientScopeId}', 167 | urlParamKeys: ['id', 'clientScopeId'], 168 | }); 169 | 170 | public listOptionalClientScopes = this.makeRequest< 171 | {id: string}, 172 | ClientScopeRepresentation[] 173 | >({ 174 | method: 'GET', 175 | path: '/{id}/optional-client-scopes', 176 | urlParamKeys: ['id'], 177 | }); 178 | 179 | public addOptionalClientScope = this.makeRequest< 180 | {id: string; clientScopeId: string}, 181 | void 182 | >({ 183 | method: 'PUT', 184 | path: '/{id}/optional-client-scopes/{clientScopeId}', 185 | urlParamKeys: ['id', 'clientScopeId'], 186 | }); 187 | 188 | public delOptionalClientScope = this.makeRequest< 189 | {id: string; clientScopeId: string}, 190 | void 191 | >({ 192 | method: 'DELETE', 193 | path: '/{id}/optional-client-scopes/{clientScopeId}', 194 | urlParamKeys: ['id', 'clientScopeId'], 195 | }); 196 | 197 | /** 198 | * Protocol Mappers 199 | */ 200 | 201 | public addMultipleProtocolMappers = this.makeUpdateRequest< 202 | {id: string}, 203 | ProtocolMapperRepresentation[], 204 | void 205 | >({ 206 | method: 'POST', 207 | path: '/{id}/protocol-mappers/add-models', 208 | urlParamKeys: ['id'], 209 | }); 210 | 211 | public addProtocolMapper = this.makeUpdateRequest< 212 | {id: string}, 213 | ProtocolMapperRepresentation, 214 | void 215 | >({ 216 | method: 'POST', 217 | path: '/{id}/protocol-mappers/models', 218 | urlParamKeys: ['id'], 219 | }); 220 | 221 | public listProtocolMappers = this.makeRequest< 222 | {id: string}, 223 | ProtocolMapperRepresentation[] 224 | >({ 225 | method: 'GET', 226 | path: '/{id}/protocol-mappers/models', 227 | urlParamKeys: ['id'], 228 | }); 229 | 230 | public findProtocolMapperById = this.makeRequest< 231 | {id: string; mapperId: string}, 232 | ProtocolMapperRepresentation 233 | >({ 234 | method: 'GET', 235 | path: '/{id}/protocol-mappers/models/{mapperId}', 236 | urlParamKeys: ['id', 'mapperId'], 237 | catchNotFound: true, 238 | }); 239 | 240 | public findProtocolMappersByProtocol = this.makeRequest< 241 | {id: string; protocol: string}, 242 | ProtocolMapperRepresentation[] 243 | >({ 244 | method: 'GET', 245 | path: '/{id}/protocol-mappers/protocol/{protocol}', 246 | urlParamKeys: ['id', 'protocol'], 247 | catchNotFound: true, 248 | }); 249 | 250 | public updateProtocolMapper = this.makeUpdateRequest< 251 | {id: string; mapperId: string}, 252 | ProtocolMapperRepresentation, 253 | void 254 | >({ 255 | method: 'PUT', 256 | path: '/{id}/protocol-mappers/models/{mapperId}', 257 | urlParamKeys: ['id', 'mapperId'], 258 | }); 259 | 260 | public delProtocolMapper = this.makeRequest< 261 | {id: string; mapperId: string}, 262 | void 263 | >({ 264 | method: 'DELETE', 265 | path: '/{id}/protocol-mappers/models/{mapperId}', 266 | urlParamKeys: ['id', 'mapperId'], 267 | }); 268 | 269 | /** 270 | * Scope Mappings 271 | */ 272 | public listScopeMappings = this.makeRequest< 273 | {id: string}, 274 | MappingsRepresentation 275 | >({ 276 | method: 'GET', 277 | path: '/{id}/scope-mappings', 278 | urlParamKeys: ['id'], 279 | }); 280 | 281 | public addClientScopeMappings = this.makeUpdateRequest< 282 | {id: string; client: string}, 283 | RoleRepresentation[], 284 | void 285 | >({ 286 | method: 'POST', 287 | path: '/{id}/scope-mappings/clients/{client}', 288 | urlParamKeys: ['id', 'client'], 289 | }); 290 | 291 | public listClientScopeMappings = this.makeRequest< 292 | {id: string; client: string}, 293 | RoleRepresentation[] 294 | >({ 295 | method: 'GET', 296 | path: '/{id}/scope-mappings/clients/{client}', 297 | urlParamKeys: ['id', 'client'], 298 | }); 299 | 300 | public listAvailableClientScopeMappings = this.makeRequest< 301 | {id: string; client: string}, 302 | RoleRepresentation[] 303 | >({ 304 | method: 'GET', 305 | path: '/{id}/scope-mappings/clients/{client}/available', 306 | urlParamKeys: ['id', 'client'], 307 | }); 308 | 309 | public listCompositeClientScopeMappings = this.makeRequest< 310 | {id: string; client: string}, 311 | RoleRepresentation[] 312 | >({ 313 | method: 'GET', 314 | path: '/{id}/scope-mappings/clients/{client}/available', 315 | urlParamKeys: ['id', 'client'], 316 | }); 317 | 318 | public delClientScopeMappings = this.makeUpdateRequest< 319 | {id: string; client: string}, 320 | RoleRepresentation[], 321 | void 322 | >({ 323 | method: 'DELETE', 324 | path: '/{id}/scope-mappings/clients/{client}', 325 | urlParamKeys: ['id', 'client'], 326 | }); 327 | 328 | public addRealmScopeMappings = this.makeUpdateRequest< 329 | {id: string}, 330 | RoleRepresentation[], 331 | void 332 | >({ 333 | method: 'POST', 334 | path: '/{id}/scope-mappings/realm', 335 | urlParamKeys: ['id', 'client'], 336 | }); 337 | 338 | public listRealmScopeMappings = this.makeRequest< 339 | {id: string}, 340 | RoleRepresentation[] 341 | >({ 342 | method: 'GET', 343 | path: '/{id}/scope-mappings/realm', 344 | urlParamKeys: ['id'], 345 | }); 346 | 347 | public listAvailableRealmScopeMappings = this.makeRequest< 348 | {id: string}, 349 | RoleRepresentation[] 350 | >({ 351 | method: 'GET', 352 | path: '/{id}/scope-mappings/realm/available', 353 | urlParamKeys: ['id'], 354 | }); 355 | 356 | public listCompositeRealmScopeMappings = this.makeRequest< 357 | {id: string}, 358 | RoleRepresentation[] 359 | >({ 360 | method: 'GET', 361 | path: '/{id}/scope-mappings/realm/available', 362 | urlParamKeys: ['id'], 363 | }); 364 | 365 | public delRealmScopeMappings = this.makeUpdateRequest< 366 | {id: string}, 367 | RoleRepresentation[], 368 | void 369 | >({ 370 | method: 'DELETE', 371 | path: '/{id}/scope-mappings/realm', 372 | urlParamKeys: ['id'], 373 | }); 374 | 375 | /** 376 | * Sessions 377 | */ 378 | public listSessions = this.makeRequest< 379 | {id: string, first?: number; max?: number}, 380 | UserSessionRepresentation[] 381 | >({ 382 | method: 'GET', 383 | path: '/{id}/user-sessions', 384 | urlParamKeys: ['id'], 385 | }); 386 | 387 | public listOfflineSessions = this.makeRequest< 388 | {id: string, first?: number; max?: number}, 389 | UserSessionRepresentation[] 390 | >({ 391 | method: 'GET', 392 | path: '/{id}/offline-sessions', 393 | urlParamKeys: ['id'], 394 | }); 395 | 396 | public getSessionCount = this.makeRequest< 397 | {id: string}, 398 | { "count": number } 399 | >({ 400 | method: 'GET', 401 | path: '/{id}/session-count', 402 | urlParamKeys: ['id'], 403 | }); 404 | 405 | public getOfflineSessionCount = this.makeRequest< 406 | {id: string}, 407 | { "count": number } 408 | >({ 409 | method: 'GET', 410 | path: '/{id}/offline-session-count', 411 | urlParamKeys: ['id'], 412 | }); 413 | 414 | constructor(client: KeycloakAdminClient) { 415 | super(client, { 416 | path: '/admin/realms/{realm}/clients', 417 | getUrlParams: () => ({ 418 | realm: client.realmName, 419 | }), 420 | getBaseUrl: () => client.baseUrl, 421 | }); 422 | } 423 | 424 | /** 425 | * Find single protocol mapper by name. 426 | */ 427 | public async findProtocolMapperByName(payload: { 428 | realm?: string; 429 | id: string; 430 | name: string; 431 | }): Promise { 432 | const allProtocolMappers = await this.listProtocolMappers({ 433 | id: payload.id, 434 | ...(payload.realm ? {realm: payload.realm} : {}), 435 | }); 436 | const protocolMapper = allProtocolMappers.find( 437 | mapper => mapper.name === payload.name, 438 | ); 439 | return protocolMapper ? protocolMapper : null; 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /test/users.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import faker from 'faker'; 6 | import UserRepresentation from '../src/defs/userRepresentation'; 7 | import UserSessionRepresentation from '../src/defs/userSessionRepresentation'; 8 | import RoleRepresentation from '../src/defs/roleRepresentation'; 9 | import ClientRepresentation from '../src/defs/clientRepresentation'; 10 | import {RequiredActionAlias} from '../src/defs/requiredActionProviderRepresentation'; 11 | import FederatedIdentityRepresentation from '../src/defs/federatedIdentityRepresentation'; 12 | import {omit} from 'lodash'; 13 | 14 | const expect = chai.expect; 15 | 16 | declare module 'mocha' { 17 | // tslint:disable-next-line:interface-name 18 | interface ISuiteCallbackContext { 19 | kcAdminClient?: KeycloakAdminClient; 20 | currentClient?: ClientRepresentation; 21 | currentUser?: UserRepresentation; 22 | currentRole?: RoleRepresentation; 23 | federatedIdentity?: FederatedIdentityRepresentation; 24 | } 25 | } 26 | 27 | describe('Users', function () { 28 | this.timeout(10000); 29 | 30 | before(async () => { 31 | this.kcAdminClient = new KeycloakAdminClient(); 32 | await this.kcAdminClient.auth(credentials); 33 | // initialize user 34 | const username = faker.internet.userName(); 35 | const user = await this.kcAdminClient.users.create({ 36 | username, 37 | email: 'wwwy3y3@canner.io', 38 | // enabled required to be true in order to send actions email 39 | emailVerified: true, 40 | enabled: true, 41 | }); 42 | 43 | expect(user.id).to.be.ok; 44 | this.currentUser = await this.kcAdminClient.users.findOne({id: user.id}); 45 | 46 | // add smtp to realm 47 | await this.kcAdminClient.realms.update( 48 | {realm: 'master'}, 49 | { 50 | smtpServer: { 51 | auth: true, 52 | from: '0830021730-07fb21@inbox.mailtrap.io', 53 | host: 'smtp.mailtrap.io', 54 | user: process.env.SMTP_USER, 55 | password: process.env.SMTP_PWD, 56 | }, 57 | }, 58 | ); 59 | }); 60 | 61 | after(async () => { 62 | const userId = this.currentUser.id; 63 | await this.kcAdminClient.users.del({ 64 | id: userId, 65 | }); 66 | 67 | const user = await this.kcAdminClient.users.findOne({ 68 | id: userId, 69 | }); 70 | expect(user).to.be.null; 71 | }); 72 | 73 | it('list users', async () => { 74 | const users = await this.kcAdminClient.users.find(); 75 | expect(users).to.be.ok; 76 | }); 77 | 78 | it('get single users', async () => { 79 | const userId = this.currentUser.id; 80 | const user = await this.kcAdminClient.users.findOne({ 81 | id: userId, 82 | }); 83 | expect(user).to.be.deep.include(this.currentUser); 84 | }); 85 | 86 | it('update single users', async () => { 87 | const userId = this.currentUser.id; 88 | await this.kcAdminClient.users.update( 89 | {id: userId}, 90 | { 91 | firstName: 'william', 92 | lastName: 'chang', 93 | requiredActions: [RequiredActionAlias.UPDATE_PASSWORD], 94 | emailVerified: true, 95 | }, 96 | ); 97 | 98 | const user = await this.kcAdminClient.users.findOne({ 99 | id: userId, 100 | }); 101 | expect(user).to.deep.include({ 102 | firstName: 'william', 103 | lastName: 'chang', 104 | requiredActions: [RequiredActionAlias.UPDATE_PASSWORD], 105 | emailVerified: true, 106 | }); 107 | }); 108 | 109 | /** 110 | * exeute actions email 111 | */ 112 | it('should send user exeute actions email', async () => { 113 | // if travis skip it, cause travis close smtp port 114 | if (process.env.TRAVIS) { 115 | return; 116 | } 117 | const userId = this.currentUser.id; 118 | await this.kcAdminClient.users.executeActionsEmail({ 119 | id: userId, 120 | lifespan: 43200, 121 | actions: [RequiredActionAlias.UPDATE_PASSWORD], 122 | }); 123 | }); 124 | 125 | /** 126 | * remove totp 127 | */ 128 | 129 | it('should remove totp', async () => { 130 | // todo: find a way to add totp from api 131 | const userId = this.currentUser.id; 132 | await this.kcAdminClient.users.removeTotp({ 133 | id: userId, 134 | }); 135 | }); 136 | 137 | /** 138 | * reset password 139 | */ 140 | 141 | it('should reset user password', async () => { 142 | // todo: find a way to validate the reset-password result 143 | const userId = this.currentUser.id; 144 | await this.kcAdminClient.users.resetPassword({ 145 | id: userId, 146 | credential: { 147 | temporary: false, 148 | type: 'password', 149 | value: 'test', 150 | }, 151 | }); 152 | }); 153 | 154 | /** 155 | * send verify email 156 | */ 157 | 158 | it('should send user verify email', async () => { 159 | // if travis skip it, cause travis close smtp port 160 | if (process.env.TRAVIS) { 161 | return; 162 | } 163 | const userId = this.currentUser.id; 164 | await this.kcAdminClient.users.sendVerifyEmail({ 165 | id: userId, 166 | }); 167 | }); 168 | 169 | /** 170 | * Role mappings 171 | */ 172 | describe('role-mappings', () => { 173 | before(async () => { 174 | // create new role 175 | const roleName = faker.internet.userName(); 176 | await this.kcAdminClient.roles.create({ 177 | name: roleName, 178 | }); 179 | const role = await this.kcAdminClient.roles.findOneByName({ 180 | name: roleName, 181 | }); 182 | this.currentRole = role; 183 | }); 184 | 185 | after(async () => { 186 | await this.kcAdminClient.roles.delByName({name: this.currentRole.name}); 187 | }); 188 | 189 | it('add a role to user', async () => { 190 | // add role-mappings with role id 191 | await this.kcAdminClient.users.addRealmRoleMappings({ 192 | id: this.currentUser.id, 193 | 194 | // at least id and name should appear 195 | roles: [ 196 | { 197 | id: this.currentRole.id, 198 | name: this.currentRole.name, 199 | }, 200 | ], 201 | }); 202 | }); 203 | 204 | it('list available role-mappings for user', async () => { 205 | const roles = await this.kcAdminClient.users.listAvailableRealmRoleMappings( 206 | { 207 | id: this.currentUser.id, 208 | }, 209 | ); 210 | 211 | // admin, create-realm 212 | // not sure why others like offline_access, uma_authorization not included 213 | expect(roles.length).to.be.least(2); 214 | }); 215 | 216 | it('list role-mappings of user', async () => { 217 | const res = await this.kcAdminClient.users.listRoleMappings({ 218 | id: this.currentUser.id, 219 | }); 220 | 221 | expect(res).have.all.keys('realmMappings', 'clientMappings'); 222 | }); 223 | 224 | it('list realm role-mappings of user', async () => { 225 | const roles = await this.kcAdminClient.users.listRealmRoleMappings({ 226 | id: this.currentUser.id, 227 | }); 228 | // currentRole will have an empty `attributes`, but role-mappings do not 229 | expect(roles).to.deep.include(omit(this.currentRole, 'attributes')); 230 | }); 231 | 232 | it('list realm composite role-mappings of user', async () => { 233 | const roles = await this.kcAdminClient.users.listCompositeRealmRoleMappings( 234 | { 235 | id: this.currentUser.id, 236 | }, 237 | ); 238 | // todo: add data integrity check later 239 | expect(roles).to.be.ok; 240 | }); 241 | 242 | it('del realm role-mappings from user', async () => { 243 | await this.kcAdminClient.users.delRealmRoleMappings({ 244 | id: this.currentUser.id, 245 | roles: [ 246 | { 247 | id: this.currentRole.id, 248 | name: this.currentRole.name, 249 | }, 250 | ], 251 | }); 252 | 253 | const roles = await this.kcAdminClient.users.listRealmRoleMappings({ 254 | id: this.currentUser.id, 255 | }); 256 | expect(roles).to.not.deep.include(this.currentRole); 257 | }); 258 | }); 259 | 260 | /** 261 | * client Role mappings 262 | */ 263 | describe('client role-mappings', () => { 264 | before(async () => { 265 | // create new client 266 | const clientId = faker.internet.userName(); 267 | await this.kcAdminClient.clients.create({ 268 | clientId, 269 | }); 270 | 271 | const clients = await this.kcAdminClient.clients.find({clientId}); 272 | expect(clients[0]).to.be.ok; 273 | this.currentClient = clients[0]; 274 | 275 | // create new client role 276 | const roleName = faker.internet.userName(); 277 | await this.kcAdminClient.clients.createRole({ 278 | id: this.currentClient.id, 279 | name: roleName, 280 | }); 281 | 282 | // assign to currentRole 283 | this.currentRole = await this.kcAdminClient.clients.findRole({ 284 | id: this.currentClient.id, 285 | roleName, 286 | }); 287 | }); 288 | 289 | after(async () => { 290 | await this.kcAdminClient.clients.delRole({ 291 | id: this.currentClient.id, 292 | roleName: this.currentRole.name, 293 | }); 294 | await this.kcAdminClient.clients.del({id: this.currentClient.id}); 295 | }); 296 | 297 | it('add a client role to user', async () => { 298 | // add role-mappings with role id 299 | await this.kcAdminClient.users.addClientRoleMappings({ 300 | id: this.currentUser.id, 301 | clientUniqueId: this.currentClient.id, 302 | 303 | // at least id and name should appear 304 | roles: [ 305 | { 306 | id: this.currentRole.id, 307 | name: this.currentRole.name, 308 | }, 309 | ], 310 | }); 311 | }); 312 | 313 | it('list available client role-mappings for user', async () => { 314 | const roles = await this.kcAdminClient.users.listAvailableClientRoleMappings( 315 | { 316 | id: this.currentUser.id, 317 | clientUniqueId: this.currentClient.id, 318 | }, 319 | ); 320 | 321 | expect(roles).to.be.empty; 322 | }); 323 | 324 | it('list client role-mappings of user', async () => { 325 | const roles = await this.kcAdminClient.users.listClientRoleMappings({ 326 | id: this.currentUser.id, 327 | clientUniqueId: this.currentClient.id, 328 | }); 329 | 330 | // currentRole will have an empty `attributes`, but role-mappings do not 331 | expect(this.currentRole).to.deep.include(roles[0]); 332 | }); 333 | 334 | it('del client role-mappings from user', async () => { 335 | const roleName = faker.internet.userName(); 336 | await this.kcAdminClient.clients.createRole({ 337 | id: this.currentClient.id, 338 | name: roleName, 339 | }); 340 | const role = await this.kcAdminClient.clients.findRole({ 341 | id: this.currentClient.id, 342 | roleName, 343 | }); 344 | 345 | // delete the created role 346 | await this.kcAdminClient.users.delClientRoleMappings({ 347 | id: this.currentUser.id, 348 | clientUniqueId: this.currentClient.id, 349 | roles: [ 350 | { 351 | id: role.id, 352 | name: role.name, 353 | }, 354 | ], 355 | }); 356 | 357 | // check if mapping is successfully deleted 358 | const roles = await this.kcAdminClient.users.listClientRoleMappings({ 359 | id: this.currentUser.id, 360 | clientUniqueId: this.currentClient.id, 361 | }); 362 | 363 | // should only left the one we added in the previous test 364 | expect(roles.length).to.be.eql(1); 365 | }); 366 | }); 367 | 368 | describe('User sessions', function () { 369 | before(async () => { 370 | this.kcAdminClient = new KeycloakAdminClient(); 371 | await this.kcAdminClient.auth(credentials); 372 | 373 | // create user 374 | const username = faker.internet.userName(); 375 | await this.kcAdminClient.users.create({ 376 | username, 377 | email: 'wwwy3y3-federated@canner.io', 378 | enabled: true, 379 | }); 380 | const users = await this.kcAdminClient.users.find({username}); 381 | expect(users[0]).to.be.ok; 382 | this.currentUser = users[0]; 383 | 384 | // create client 385 | const clientId = faker.internet.userName(); 386 | await this.kcAdminClient.clients.create({ 387 | clientId, consentRequired: true, 388 | }); 389 | 390 | const clients = await this.kcAdminClient.clients.find({clientId}); 391 | expect(clients[0]).to.be.ok; 392 | this.currentClient = clients[0]; 393 | }); 394 | 395 | after(async () => { 396 | await this.kcAdminClient.users.del({ 397 | id: this.currentUser.id, 398 | }); 399 | 400 | await this.kcAdminClient.clients.del({ 401 | id: this.currentClient.id, 402 | }); 403 | }); 404 | 405 | it('list user sessions', async () => { 406 | // @TODO: In order to test it, currentUser has to be logged in 407 | 408 | const userSessions = await this.kcAdminClient.users.listSessions({id: this.currentUser.id}); 409 | 410 | expect(userSessions).to.be.ok; 411 | }); 412 | 413 | it('list users off-line sessions', async () => { 414 | // @TODO: In order to test it, currentUser has to be logged in 415 | 416 | const userOfflineSessions = await this.kcAdminClient.users.listOfflineSessions( 417 | {id: this.currentUser.id, clientId: this.currentClient.id}, 418 | ); 419 | 420 | expect(userOfflineSessions).to.be.ok; 421 | }); 422 | 423 | it('logout user from all sessions', async () => { 424 | // @TODO: In order to test it, currentUser has to be logged in 425 | 426 | await this.kcAdminClient.users.logout({id: this.currentUser.id}); 427 | }); 428 | 429 | it('list consents granted by the user', async () => { 430 | const consents = await this.kcAdminClient.users.listConsents({id: this.currentUser.id}); 431 | 432 | expect(consents).to.be.ok; 433 | }); 434 | 435 | it('revoke consent and offline tokens for particular client', async () => { 436 | // @TODO: In order to test it, currentUser has to granted consent to client 437 | const consents = await this.kcAdminClient.users.listConsents({id: this.currentUser.id}); 438 | 439 | if (consents.length) { 440 | const consent = consents[0]; 441 | 442 | await this.kcAdminClient.users.revokeConsent({id: this.currentUser.id, clientId: consent.clientId}); 443 | } 444 | }); 445 | }); 446 | 447 | describe('Federated Identity user integration', function () { 448 | before(async () => { 449 | this.kcAdminClient = new KeycloakAdminClient(); 450 | await this.kcAdminClient.auth(credentials); 451 | 452 | // create user 453 | const username = faker.internet.userName(); 454 | await this.kcAdminClient.users.create({ 455 | username, 456 | email: 'wwwy3y3-federated@canner.io', 457 | enabled: true, 458 | }); 459 | const users = await this.kcAdminClient.users.find({username}); 460 | expect(users[0]).to.be.ok; 461 | this.currentUser = users[0]; 462 | this.federatedIdentity = { 463 | identityProvider: 'foobar', 464 | userId: 'userid1', 465 | userName: 'username1', 466 | }; 467 | }); 468 | 469 | after(async () => { 470 | await this.kcAdminClient.users.del({ 471 | id: this.currentUser.id, 472 | }); 473 | }); 474 | 475 | it("should list user's federated identities and expect empty", async () => { 476 | const federatedIdentities = await this.kcAdminClient.users.listFederatedIdentities( 477 | { 478 | id: this.currentUser.id, 479 | }, 480 | ); 481 | expect(federatedIdentities).to.be.eql([]); 482 | }); 483 | 484 | it('should add federated identity to user', async () => { 485 | await this.kcAdminClient.users.addToFederatedIdentity({ 486 | id: this.currentUser.id, 487 | federatedIdentityId: 'foobar', 488 | federatedIdentity: this.federatedIdentity, 489 | }); 490 | 491 | // @TODO: In order to test the integration with federated identities, the User Federation 492 | // would need to be created first, this is not implemented yet. 493 | // const federatedIdentities = await this.kcAdminClient.users.listFederatedIdentities({ 494 | // id: this.currentUser.id, 495 | // }); 496 | // expect(federatedIdentities[0]).to.be.eql(this.federatedIdentity); 497 | }); 498 | 499 | it('should remove federated identity from user', async () => { 500 | await this.kcAdminClient.users.delFromFederatedIdentity({ 501 | id: this.currentUser.id, 502 | federatedIdentityId: 'foobar', 503 | }); 504 | 505 | const federatedIdentities = await this.kcAdminClient.users.listFederatedIdentities( 506 | { 507 | id: this.currentUser.id, 508 | }, 509 | ); 510 | expect(federatedIdentities).to.be.eql([]); 511 | }); 512 | }); 513 | }); 514 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## keycloak-admin 2 | 3 | [![npm version](https://badge.fury.io/js/keycloak-admin.svg)](https://badge.fury.io/js/keycloak-admin) [![Travis (.org)](https://img.shields.io/travis/keycloak/keycloak-nodejs-admin-client.svg)](https://travis-ci.org/keycloak/keycloak-nodejs-admin-client) 4 | 5 | Node.js Keycloak admin client 6 | 7 | ## Features 8 | 9 | - TypeScript supported 10 | - Keycloak latest version (v4.1) supported 11 | - [Complete resource definitions](https://github.com/keycloak/keycloak-nodejs-admin-client/tree/master/src/defs) 12 | - [Well-tested for supported APIs](https://github.com/keycloak/keycloak-nodejs-admin-client/tree/master/test) 13 | 14 | ## Install 15 | 16 | ```sh 17 | yarn add keycloak-admin 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```js 23 | import KcAdminClient from 'keycloak-admin'; 24 | 25 | // To configure the client, pass an object to override any of these options: 26 | // { 27 | // baseUrl: 'http://127.0.0.1:8080/auth', 28 | // realmName: 'master', 29 | // requestConfig: { 30 | // /* Axios request config options https://github.com/axios/axios#request-config */ 31 | // }, 32 | // } 33 | const kcAdminClient = new KcAdminClient(); 34 | 35 | // Authorize with username / password 36 | await kcAdminClient.auth({ 37 | username: 'wwwy3y3', 38 | password: 'wwwy3y3', 39 | grantType: 'password', 40 | clientId: 'admin-cli', 41 | }); 42 | 43 | // List all users 44 | const users = await kcAdminClient.users.find(); 45 | 46 | // Override client configuration for all further requests: 47 | kcAdminClient.setConfig({ 48 | realmName: 'another-realm', 49 | }); 50 | 51 | // This operation will now be performed in 'another-realm' if the user has access. 52 | const groups = await kcAdminClient.groups.find(); 53 | 54 | // Set a `realm` property to override the realm for only a single operation. 55 | // For example, creating a user in another realm: 56 | await this.kcAdminClient.users.create({ 57 | realm: 'a-third-realm', 58 | username: 'username', 59 | email: 'user@example.com', 60 | }); 61 | ``` 62 | 63 | To refresh the access token provided by Keycloak, an OpenID client like [panva/node-openid-client](https://github.com/panva/node-openid-client) can be used like this: 64 | 65 | ```js 66 | import {Issuer} from 'openid-client'; 67 | 68 | const keycloakIssuer = await Issuer.discover( 69 | 'http://localhost:8080/auth/realms/master', 70 | ); 71 | 72 | const client = new keycloakIssuer.Client({ 73 | client_id: 'admin-cli', // Same as `clientId` passed to client.auth() 74 | }); 75 | 76 | // Use the grant type 'password' 77 | let tokenSet = await client.grant({ 78 | grant_type: 'password', 79 | username: 'wwwy3y3', 80 | password: 'wwwy3y3', 81 | }); 82 | 83 | // Periodically using refresh_token grant flow to get new access token here 84 | setInterval(async () => { 85 | const refreshToken = tokenSet.refresh_token; 86 | tokenSet = await client.refresh(refreshToken); 87 | kcAdminClient.setAccessToken(tokenSet.access_token); 88 | }, 58 * 1000); // 58 seconds 89 | ``` 90 | 91 | ## Supported APIs 92 | 93 | ### [Realm admin](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_realms_admin_resource) 94 | 95 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/realms.spec.ts 96 | 97 | - Import a realm from a full representation of that realm (`POST /`) 98 | - Get the top-level representation of the realm (`GET /{realm}`) 99 | - Update the top-level information of the realm (`PUT /{realm}`) 100 | - Delete the realm (`DELETE /{realm}`) 101 | - Get users management permissions (`GET /{realm}/users-management-permissions`) 102 | - Enable users management permissions (`PUT /{realm}/users-management-permissions`) 103 | 104 | ### [Role](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_roles_resource) 105 | 106 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/roles.spec.ts 107 | 108 | - Create a new role for the realm (`POST /{realm}/roles`) 109 | - Get all roles for the realm (`GET /{realm}/roles`) 110 | - Get a role by name (`GET /{realm}/roles/{role-name}`) 111 | - Update a role by name (`PUT /{realm}/roles/{role-name}`) 112 | - Delete a role by name (`DELETE /{realm}/roles/{role-name}`) 113 | - Get all users in a role by name for the realm (`GET /{realm}/roles/{role-name}/users`) 114 | 115 | ### [Roles (by ID)](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_roles_by_id_resource) 116 | 117 | - Get a specific role (`GET /{realm}/roles-by-id/{role-id}`) 118 | - Update the role (`PUT /{realm}/roles-by-id/{role-id}`) 119 | - Delete the role (`DELETE /{realm}/roles-by-id/{role-id}`) 120 | 121 | ### [User](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_users_resource) 122 | 123 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/users.spec.ts 124 | 125 | - Create a new user (`POST /{realm}/users`) 126 | - Get users Returns a list of users, filtered according to query parameters (`GET /{realm}/users`) 127 | - Get representation of the user (`GET /{realm}/users/{id}`) 128 | - Update the user (`PUT /{realm}/users/{id}`) 129 | - Delete the user (`DELETE /{realm}/users/{id}`) 130 | - Send a update account email to the user An email contains a link the user can click to perform a set of required actions. (`PUT /{realm}/users/{id}/execute-actions-email`) 131 | - Get user groups (`GET /{realm}/users/{id}/groups`) 132 | - Add user to group (`PUT /{realm}/users/{id}/groups/{groupId}`) 133 | - Delete user from group (`DELETE /{realm}/users/{id}/groups/{groupId}`) 134 | - Remove TOTP from the user (`PUT /{realm}/users/{id}/remove-totp`) 135 | - Set up a temporary password for the user User will have to reset the temporary password next time they log in. (`PUT /{realm}/users/{id}/reset-password`) 136 | - 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`) 137 | 138 | ### User role-mapping 139 | 140 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/users.spec.ts#L143 141 | 142 | - Get user role-mappings (`GET /{realm}/users/{id}/role-mappings`) 143 | - Add realm-level role mappings to the user (`POST /{realm}/users/{id}/role-mappings/realm`) 144 | - Get realm-level role mappings (`GET /{realm}/users/{id}/role-mappings/realm`) 145 | - Delete realm-level role mappings (`DELETE /{realm}/users/{id}/role-mappings/realm`) 146 | - Get realm-level roles that can be mapped (`GET /{realm}/users/{id}/role-mappings/realm/available`) 147 | - Get effective realm-level role mappings This will recurse all composite roles to get the result. (`GET /{realm}/users/{id}/role-mappings/realm/composite`) 148 | 149 | ### [Group](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_groups_resource) 150 | 151 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/groups.spec.ts 152 | 153 | - Create (`POST /{realm}/groups`) 154 | - List (`GET /{realm}/groups`) 155 | - Get one (`GET /{realm}/groups/{id}`) 156 | - Update (`PUT /{realm}/groups/{id}`) 157 | - Delete (`DELETE /{realm}/groups/{id}`) 158 | - List members (`GET /{realm}/groups/{id}/members`) 159 | - Set or create child (`POST /{realm}/groups/{id}/children`) 160 | 161 | ### Group role-mapping 162 | 163 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/groups.spec.ts#L76 164 | 165 | - Get group role-mappings (`GET /{realm}/groups/{id}/role-mappings`) 166 | - Add realm-level role mappings to the group (`POST /{realm}/groups/{id}/role-mappings/realm`) 167 | - Get realm-level role mappings (`GET /{realm}/groups/{id}/role-mappings/realm`) 168 | - Delete realm-level role mappings (`DELETE /{realm}/groups/{id}/role-mappings/realm`) 169 | - Get realm-level roles that can be mapped (`GET /{realm}/groups/{id}/role-mappings/realm/available`) 170 | 171 | ### [Client](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_clients_resource) 172 | 173 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clients.spec.ts 174 | 175 | - Create a new client (`POST /{realm}/clients`) 176 | - Get clients belonging to the realm (`GET /{realm}/clients`) 177 | - Get representation of the client (`GET /{realm}/clients/{id}`) 178 | - Update the client (`PUT /{realm}/clients/{id}`) 179 | - Delete the client (`DELETE /{realm}/clients/{id}`) 180 | 181 | ### [Client roles](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_roles_resource) 182 | 183 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clients.spec.ts 184 | 185 | - Create a new role for the client (`POST /{realm}/clients/{id}/roles`) 186 | - Get all roles for the client (`GET /{realm}/clients/{id}/roles`) 187 | - Get a role by name (`GET /{realm}/clients/{id}/roles/{role-name}`) 188 | - Update a role by name (`PUT /{realm}/clients/{id}/roles/{role-name}`) 189 | - Delete a role by name (`DELETE /{realm}/clients/{id}/roles/{role-name}`) 190 | 191 | ### [Client role-mapping for group](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_client_role_mappings_resource) 192 | 193 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/groups.spec.ts#L150 194 | 195 | - Add client-level roles to the group role mapping (`POST /{realm}/groups/{id}/role-mappings/clients/{client}`) 196 | - Get client-level role mappings for the group (`GET /{realm}/groups/{id}/role-mappings/clients/{client}`) 197 | - Delete client-level roles from group role mapping (`DELETE /{realm}/groups/{id}/role-mappings/clients/{client}`) 198 | - Get available client-level roles that can be mapped to the group (`GET /{realm}/groups/{id}/role-mappings/clients/{client}/available`) 199 | 200 | ### [Client role-mapping for user](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_client_role_mappings_resource) 201 | 202 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/users.spec.ts#L217 203 | 204 | - Add client-level roles to the user role mapping (`POST /{realm}/users/{id}/role-mappings/clients/{client}`) 205 | - Get client-level role mappings for the user (`GET /{realm}/users/{id}/role-mappings/clients/{client}`) 206 | - Delete client-level roles from user role mapping (`DELETE /{realm}/users/{id}/role-mappings/clients/{client}`) 207 | - Get available client-level roles that can be mapped to the user (`GET /{realm}/users/{id}/role-mappings/clients/{client}/available`) 208 | 209 | ### [Identity Providers](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_identity_providers_resource) 210 | 211 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/idp.spec.ts 212 | 213 | - Create a new identity provider (`POST /{realm}/identity-provider/instances`) 214 | - Get identity providers (`GET /{realm}/identity-provider/instances`) 215 | - Get the identity provider (`GET /{realm}/identity-provider/instances/{alias}`) 216 | - Update the identity provider (`PUT /{realm}/identity-provider/instances/{alias}`) 217 | - Delete the identity provider (`DELETE /{realm}/identity-provider/instances/{alias}`) 218 | - Find identity provider factory (`GET /{realm}/identity-provider/providers/{providerId}`) 219 | - Create a new identity provider mapper (`POST /{realm}/identity-provider/instances/{alias}/mappers`) 220 | - Get identity provider mappers (`GET /{realm}/identity-provider/instances/{alias}/mappers`) 221 | - Get the identity provider mapper (`GET /{realm}/identity-provider/instances/{alias}/mappers/{id}`) 222 | - Update the identity provider mapper (`PUT /{realm}/identity-provider/instances/{alias}/mappers/{id}`) 223 | - Delete the identity provider mapper (`DELETE /{realm}/identity-provider/instances/{alias}/mappers/{id}`) 224 | - Find the identity provider mapper types (`GET /{realm}/identity-provider/instances/{alias}/mapper-types`) 225 | 226 | ### [Client Scopes](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_client_scopes_resource) 227 | 228 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clientScopes.spec.ts 229 | 230 | - Create a new client scope (`POST /{realm}/client-scopes`) 231 | - Get client scopes belonging to the realm (`GET /{realm}/client-scopes`) 232 | - Get representation of the client scope (`GET /{realm}/client-scopes/{id}`) 233 | - Update the client scope (`PUT /{realm}/client-scopes/{id}`) 234 | - Delete the client scope (`DELETE /{realm}/client-scopes/{id}`) 235 | 236 | ### [Client Scopes for realm](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_client_scopes_resource) 237 | 238 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clientScopes.spec.ts 239 | 240 | - Get realm default client scopes (`GET /{realm}/default-default-client-scopes`) 241 | - Add realm default client scope (`PUT /{realm}/default-default-client-scopes/{id}`) 242 | - Delete realm default client scope (`DELETE /{realm}/default-default-client-scopes/{id}`) 243 | - Get realm optional client scopes (`GET /{realm}/default-optional-client-scopes`) 244 | - Add realm optional client scope (`PUT /{realm}/default-optional-client-scopes/{id}`) 245 | - Delete realm optional client scope (`DELETE /{realm}/default-optional-client-scopes/{id}`) 246 | 247 | ### [Client Scopes for client](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_client_scopes_resource) 248 | 249 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clientScopes.spec.ts 250 | 251 | - Get default client scopes (`GET /{realm}/clients/{id}/default-client-scopes`) 252 | - Add default client scope (`PUT /{realm}/clients/{id}/default-client-scopes/{clientScopeId}`) 253 | - Delete default client scope (`DELETE /{realm}/clients/{id}/default-client-scopes/{clientScopeId}`) 254 | - Get optional client scopes (`GET /{realm}/clients/{id}/optional-client-scopes`) 255 | - Add optional client scope (`PUT /{realm}/clients/{id}/optional-client-scopes/{clientScopeId}`) 256 | - Delete optional client scope (`DELETE /{realm}/clients/{id}/optional-client-scopes/{clientScopeId}`) 257 | 258 | ### [Scope Mappings for client scopes](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_scope_mappings_resource) 259 | 260 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clientScopes.spec.ts 261 | 262 | - Get all scope mappings for the client (`GET /{realm}/client-scopes/{id}/scope-mappings`) 263 | - Add client-level roles to the client’s scope (`POST /{realm}/client-scopes/{id}/scope-mappings/clients/{client}`) 264 | - Get the roles associated with a client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/clients/{client}`) 265 | - The available client-level roles (`GET /{realm}/client-scopes/{id}/scope-mappings/clients/{client}/available`) 266 | - Get effective client roles (`GET /{realm}/client-scopes/{id}/scope-mappings/clients/{client}/composite`) 267 | - Remove client-level roles from the client’s scope. (`DELETE /{realm}/client-scopes/{id}/scope-mappings/clients/{client}`) 268 | - Add a set of realm-level roles to the client’s scope (`POST /{realm}/client-scopes/{id}/scope-mappings/realm`) 269 | - Get realm-level roles associated with the client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/realm`) 270 | - Remove a set of realm-level roles from the client’s scope (`DELETE /{realm}/client-scopes/{id}/scope-mappings/realm`) 271 | - Get realm-level roles that are available to attach to this client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/realm/available`) 272 | - Get effective realm-level roles associated with the client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/realm/composite`) 273 | 274 | ### [Scope Mappings for clients](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_scope_mappings_resource) 275 | 276 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clientScopes.spec.ts 277 | 278 | - Get all scope mappings for the client (`GET /{realm}/clients/{id}/scope-mappings`) 279 | - Add client-level roles to the client’s scope (`POST /{realm}/clients/{id}/scope-mappings/clients/{client}`) 280 | - Get the roles associated with a client’s scope (`GET /{realm}/clients/{id}/scope-mappings/clients/{client}`) 281 | - Remove client-level roles from the client’s scope. (`DELETE /{realm}/clients/{id}/scope-mappings/clients/{client}`) 282 | - The available client-level roles (`GET /{realm}/clients/{id}/scope-mappings/clients/{client}/available`) 283 | - Get effective client roles (`GET /{realm}/clients/{id}/scope-mappings/clients/{client}/composite`) 284 | - Add a set of realm-level roles to the client’s scope (`POST /{realm}/clients/{id}/scope-mappings/realm`) 285 | - Get realm-level roles associated with the client’s scope (`GET /{realm}/clients/{id}/scope-mappings/realm`) 286 | - Remove a set of realm-level roles from the client’s scope (`DELETE /{realm}/clients/{id}/scope-mappings/realm`) 287 | - Get realm-level roles that are available to attach to this client’s scope (`GET /{realm}/clients/{id}/scope-mappings/realm/available`) 288 | - Get effective realm-level roles associated with the client’s scope (`GET /{realm}/clients/{id}/scope-mappings/realm/composite`) 289 | 290 | ### [Protocol Mappers for client scopes](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_protocol_mappers_resource) 291 | 292 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clientScopes.spec.ts 293 | 294 | - Create multiple mappers (`POST /{realm}/client-scopes/{id}/protocol-mappers/add-models`) 295 | - Create a mapper (`POST /{realm}/client-scopes/{id}/protocol-mappers/models`) 296 | - Get mappers (`GET /{realm}/client-scopes/{id}/protocol-mappers/models`) 297 | - Get mapper by id (`GET /{realm}/client-scopes/{id}/protocol-mappers/models/{mapperId}`) 298 | - Update the mapper (`PUT /{realm}/client-scopes/{id}/protocol-mappers/models/{mapperId}`) 299 | - Delete the mapper (`DELETE /{realm}/client-scopes/{id}/protocol-mappers/models/{mapperId}`) 300 | - Get mappers by name for a specific protocol (`GET /{realm}/client-scopes/{id}/protocol-mappers/protocol/{protocol}`) 301 | 302 | ### [Protocol Mappers for clients](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_protocol_mappers_resource) 303 | 304 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clients.spec.ts 305 | 306 | - Create multiple mappers (`POST /{realm}/clients/{id}/protocol-mappers/add-models`) 307 | - Create a mapper (`POST /{realm}/clients/{id}/protocol-mappers/models`) 308 | - Get mappers (`GET /{realm}/clients/{id}/protocol-mappers/models`) 309 | - Get mapper by id (`GET /{realm}/clients/{id}/protocol-mappers/models/{mapperId}`) 310 | - Update the mapper (`PUT /{realm}/clients/{id}/protocol-mappers/models/{mapperId}`) 311 | - Delete the mapper (`DELETE /{realm}/clients/{id}/protocol-mappers/models/{mapperId}`) 312 | - Get mappers by name for a specific protocol (`GET /{realm}/clients/{id}/protocol-mappers/protocol/{protocol}`) 313 | 314 | ### [Component]() 315 | 316 | Supported for [user federation](https://www.keycloak.org/docs/latest/server_admin/index.html#_user-storage-federation). Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/components.spec.ts 317 | 318 | - Create (`POST /{realm}/components`) 319 | - List (`GET /{realm}/components`) 320 | - Get (`GET /{realm}/components/{id}`) 321 | - Update (`PUT /{realm}/components/{id}`) 322 | - Delete (`DELETE /{realm}/components/{id}`) 323 | 324 | ### [Sessions for clients]() 325 | 326 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/clients.spec.ts 327 | 328 | - List user sessions for a specific client (`GET /{realm}/clients/{id}/user-sessions`) 329 | - List offline sessions for a specific client (`GET /{realm}/clients/{id}/offline-sessions`) 330 | - Get user session count for a specific client (`GET /{realm}/clients/{id}/session-count`) 331 | - List offline session count for a specific client (`GET /{realm}/clients/{id}/offline-session-count`) 332 | 333 | ### [Authentication Management: Required actions](https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource) 334 | 335 | Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/test/authenticationManagement.spec.ts 336 | 337 | - Register a new required action (`POST /{realm}/authentication/register-required-action`) 338 | - Get required actions. Returns a list of required actions. (`GET /{realm}/authentication/required-actions`) 339 | - Get required action for alias (`GET /{realm}/authentication/required-actions/{alias}`) 340 | - Update required action (`PUT /{realm}/authentication/required-actions/{alias}`) 341 | - Delete required action (`DELETE /{realm}/authentication/required-actions/{alias}`) 342 | - Lower required action’s priority (`POST /{realm}/authentication/required-actions/{alias}/lower-priority`) 343 | - Raise required action’s priority (`POST /{realm}/authentication/required-actions/{alias}/raise-priority`) 344 | - Get unregistered required actions Returns a list of unregistered required actions. (`GET /{realm}/authentication/unregistered-required-actions`) 345 | 346 | ## Not yet supported 347 | 348 | - [Attack Detection](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_attack_detection_resource) 349 | - [Authentication Management](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authentication_management_resource) 350 | - [Client Attribute Certificate](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_client_attribute_certificate_resource) 351 | - [Client Initial Access](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_client_initial_access_resource) 352 | - [Client Registration Policy](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_client_registration_policy_resource) 353 | - [Key](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_key_resource) 354 | - [User Storage Provider](https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_user_storage_provider_resource) 355 | 356 | ## Maintainers 357 | 358 | Checkout [MAINTAINERS.md](https://github.com/keycloak/keycloak-nodejs-admin-client/blob/master/MAINTAINERS.md) for detailed maintainers list. 359 | 360 | This repo is originally developed by [Canner](https://www.cannercms.com) and [InfuseAI](https://infuseai.io) before being transferred under keycloak organization. 361 | -------------------------------------------------------------------------------- /test/clientScopes.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unused-expression 2 | import * as chai from 'chai'; 3 | import {KeycloakAdminClient} from '../src/client'; 4 | import {credentials} from './constants'; 5 | import ClientScopeRepresentation from '../src/defs/clientScopeRepresentation'; 6 | import ProtocolMapperRepresentation from '../src/defs/protocolMapperRepresentation'; 7 | import ClientRepresentation from '../src/defs/clientRepresentation'; 8 | 9 | const expect = chai.expect; 10 | 11 | declare module 'mocha' { 12 | // tslint:disable-next-line:interface-name 13 | interface ISuiteCallbackContext { 14 | kcAdminClient?: KeycloakAdminClient; 15 | currentClientScope?: ClientScopeRepresentation; 16 | currentClientScopeName?: string; 17 | currentClient?: ClientRepresentation; 18 | } 19 | } 20 | 21 | describe('Client Scopes', () => { 22 | before(async () => { 23 | this.kcAdminClient = new KeycloakAdminClient(); 24 | await this.kcAdminClient.auth(credentials); 25 | }); 26 | 27 | beforeEach(async () => { 28 | this.currentClientScopeName = 'best-of-the-bests-scope'; 29 | await this.kcAdminClient.clientScopes.create({ 30 | name: this.currentClientScopeName, 31 | }); 32 | this.currentClientScope = await this.kcAdminClient.clientScopes.findOneByName( 33 | { 34 | name: this.currentClientScopeName, 35 | }, 36 | ); 37 | }); 38 | 39 | afterEach(async () => { 40 | // cleanup default client scopes 41 | try { 42 | await this.kcAdminClient.clientScopes.delDefaultClientScope({ 43 | id: this.currentClientScope.id, 44 | }); 45 | } catch (e) { 46 | // ignore 47 | } 48 | 49 | // cleanup optional client scopes 50 | try { 51 | await this.kcAdminClient.clientScopes.delDefaultOptionalClientScope({ 52 | id: this.currentClientScope.id, 53 | }); 54 | } catch (e) { 55 | // ignore 56 | } 57 | 58 | // cleanup client scopes 59 | try { 60 | await this.kcAdminClient.clientScopes.delByName({ 61 | name: this.currentClientScopeName, 62 | }); 63 | } catch (e) { 64 | // ignore 65 | } 66 | }); 67 | 68 | it('list client scopes', async () => { 69 | const scopes = await this.kcAdminClient.clientScopes.find(); 70 | expect(scopes).to.be.ok; 71 | }); 72 | 73 | it('create client scope and get by name', async () => { 74 | // ensure that the scope does not exist 75 | try { 76 | await this.kcAdminClient.clientScopes.delByName({ 77 | name: this.currentClientScopeName, 78 | }); 79 | } catch (e) { 80 | // ignore 81 | } 82 | 83 | await this.kcAdminClient.clientScopes.create({ 84 | name: this.currentClientScopeName, 85 | }); 86 | 87 | const scope = await this.kcAdminClient.clientScopes.findOneByName({ 88 | name: this.currentClientScopeName, 89 | }); 90 | expect(scope).to.be.ok; 91 | expect(scope.name).to.equal(this.currentClientScopeName); 92 | }); 93 | 94 | it('find scope by id', async () => { 95 | const scope = await this.kcAdminClient.clientScopes.findOne({ 96 | id: this.currentClientScope.id, 97 | }); 98 | expect(scope).to.be.ok; 99 | expect(scope).to.eql(this.currentClientScope); 100 | }); 101 | 102 | it('find scope by name', async () => { 103 | const scope = await this.kcAdminClient.clientScopes.findOneByName({ 104 | name: this.currentClientScopeName, 105 | }); 106 | expect(scope).to.be.ok; 107 | expect(scope.name).to.eql(this.currentClientScopeName); 108 | }); 109 | 110 | it('return null if scope not found by id', async () => { 111 | const scope = await this.kcAdminClient.clientScopes.findOne({ 112 | id: 'I do not exist', 113 | }); 114 | expect(scope).to.be.null; 115 | }); 116 | 117 | it('return null if scope not found by name', async () => { 118 | const scope = await this.kcAdminClient.clientScopes.findOneByName({ 119 | name: 'I do not exist', 120 | }); 121 | expect(scope).to.be.null; 122 | }); 123 | 124 | it('update client scope', async () => { 125 | const {id, description: oldDescription} = this.currentClientScope; 126 | const description = 'This scope is totally awesome.'; 127 | 128 | await this.kcAdminClient.clientScopes.update({id}, {description}); 129 | const updatedScope = await this.kcAdminClient.clientScopes.findOne({ 130 | id, 131 | }); 132 | expect(updatedScope).to.be.ok; 133 | expect(updatedScope).not.to.eql(this.currentClientScope); 134 | expect(updatedScope.description).to.eq(description); 135 | expect(updatedScope.description).not.to.eq(oldDescription); 136 | }); 137 | 138 | it('delete single client scope by id', async () => { 139 | await this.kcAdminClient.clientScopes.del({ 140 | id: this.currentClientScope.id, 141 | }); 142 | const scope = await this.kcAdminClient.clientScopes.findOne({ 143 | id: this.currentClientScope.id, 144 | }); 145 | expect(scope).not.to.be.ok; 146 | }); 147 | 148 | it('delete single client scope by name', async () => { 149 | await this.kcAdminClient.clientScopes.delByName({ 150 | name: this.currentClientScopeName, 151 | }); 152 | const scope = await this.kcAdminClient.clientScopes.findOneByName({ 153 | name: this.currentClientScopeName, 154 | }); 155 | expect(scope).not.to.be.ok; 156 | }); 157 | 158 | describe('default client scope', () => { 159 | it('list default client scopes', async () => { 160 | const defaultClientScopes = await this.kcAdminClient.clientScopes.listDefaultClientScopes(); 161 | expect(defaultClientScopes).to.be.ok; 162 | }); 163 | 164 | it('add default client scope', async () => { 165 | const {id} = this.currentClientScope; 166 | await this.kcAdminClient.clientScopes.addDefaultClientScope({id}); 167 | 168 | const defaultClientScopeList = await this.kcAdminClient.clientScopes.listDefaultClientScopes(); 169 | const defaultClientScope = defaultClientScopeList.find( 170 | scope => scope.id === id, 171 | ); 172 | 173 | expect(defaultClientScope).to.be.ok; 174 | expect(defaultClientScope.id).to.equal(this.currentClientScope.id); 175 | expect(defaultClientScope.name).to.equal(this.currentClientScope.name); 176 | }); 177 | 178 | it('delete default client scope', async () => { 179 | const {id} = this.currentClientScope; 180 | await this.kcAdminClient.clientScopes.addDefaultClientScope({id}); 181 | 182 | await this.kcAdminClient.clientScopes.delDefaultClientScope({id}); 183 | 184 | const defaultClientScopeList = await this.kcAdminClient.clientScopes.listDefaultClientScopes(); 185 | const defaultClientScope = defaultClientScopeList.find( 186 | scope => scope.id === id, 187 | ); 188 | 189 | expect(defaultClientScope).not.to.be.ok; 190 | }); 191 | }); 192 | 193 | describe('default optional client scopes', () => { 194 | it('list default optional client scopes', async () => { 195 | const defaultOptionalClientScopes = await this.kcAdminClient.clientScopes.listDefaultOptionalClientScopes(); 196 | expect(defaultOptionalClientScopes).to.be.ok; 197 | }); 198 | 199 | it('add default optional client scope', async () => { 200 | const {id} = this.currentClientScope; 201 | await this.kcAdminClient.clientScopes.addDefaultOptionalClientScope({id}); 202 | 203 | const defaultOptionalClientScopeList = await this.kcAdminClient.clientScopes.listDefaultOptionalClientScopes(); 204 | const defaultOptionalClientScope = defaultOptionalClientScopeList.find( 205 | scope => scope.id === id, 206 | ); 207 | 208 | expect(defaultOptionalClientScope).to.be.ok; 209 | expect(defaultOptionalClientScope.id).to.eq(this.currentClientScope.id); 210 | expect(defaultOptionalClientScope.name).to.eq( 211 | this.currentClientScope.name, 212 | ); 213 | }); 214 | 215 | it('delete default optional client scope', async () => { 216 | const {id} = this.currentClientScope; 217 | await this.kcAdminClient.clientScopes.addDefaultOptionalClientScope({id}); 218 | await this.kcAdminClient.clientScopes.delDefaultOptionalClientScope({id}); 219 | 220 | const defaultOptionalClientScopeList = await this.kcAdminClient.clientScopes.listDefaultOptionalClientScopes(); 221 | const defaultOptionalClientScope = defaultOptionalClientScopeList.find( 222 | scope => scope.id === id, 223 | ); 224 | 225 | expect(defaultOptionalClientScope).not.to.be.ok; 226 | }); 227 | }); 228 | 229 | describe('protocol mappers', () => { 230 | let dummyMapper: ProtocolMapperRepresentation; 231 | 232 | beforeEach(() => { 233 | dummyMapper = { 234 | name: 'mapping-maps-mapper', 235 | protocol: 'openid-connect', 236 | protocolMapper: 'oidc-audience-mapper', 237 | }; 238 | }); 239 | 240 | afterEach(async () => { 241 | try { 242 | const {id} = this.currentClientScope; 243 | const { 244 | id: mapperId, 245 | } = await this.kcAdminClient.clientScopes.findProtocolMapperByName({ 246 | id, 247 | name: dummyMapper.name, 248 | }); 249 | await this.kcAdminClient.clientScopes.delProtocolMapper({ 250 | id, 251 | mapperId, 252 | }); 253 | } catch (e) { 254 | // ignore 255 | } 256 | }); 257 | 258 | it('list protocol mappers', async () => { 259 | const {id} = this.currentClientScope; 260 | const mapperList = await this.kcAdminClient.clientScopes.listProtocolMappers( 261 | {id}, 262 | ); 263 | expect(mapperList).to.be.ok; 264 | }); 265 | 266 | it('add multiple protocol mappers', async () => { 267 | const {id} = this.currentClientScope; 268 | await this.kcAdminClient.clientScopes.addMultipleProtocolMappers({id}, [ 269 | dummyMapper, 270 | ]); 271 | 272 | const mapper = await this.kcAdminClient.clientScopes.findProtocolMapperByName( 273 | {id, name: dummyMapper.name}, 274 | ); 275 | expect(mapper).to.be.ok; 276 | expect(mapper.protocol).to.eq(dummyMapper.protocol); 277 | expect(mapper.protocolMapper).to.eq(dummyMapper.protocolMapper); 278 | }); 279 | 280 | it('add single protocol mapper', async () => { 281 | const {id} = this.currentClientScope; 282 | await this.kcAdminClient.clientScopes.addProtocolMapper( 283 | {id}, 284 | dummyMapper, 285 | ); 286 | 287 | const mapper = await this.kcAdminClient.clientScopes.findProtocolMapperByName( 288 | {id, name: dummyMapper.name}, 289 | ); 290 | expect(mapper).to.be.ok; 291 | expect(mapper.protocol).to.eq(dummyMapper.protocol); 292 | expect(mapper.protocolMapper).to.eq(dummyMapper.protocolMapper); 293 | }); 294 | 295 | it('find protocol mapper by id', async () => { 296 | const {id} = this.currentClientScope; 297 | await this.kcAdminClient.clientScopes.addProtocolMapper( 298 | {id}, 299 | dummyMapper, 300 | ); 301 | 302 | const { 303 | id: mapperId, 304 | } = await this.kcAdminClient.clientScopes.findProtocolMapperByName({ 305 | id, 306 | name: dummyMapper.name, 307 | }); 308 | 309 | const mapper = await this.kcAdminClient.clientScopes.findProtocolMapper({ 310 | id, 311 | mapperId, 312 | }); 313 | 314 | expect(mapper).to.be.ok; 315 | expect(mapper.id).to.eql(mapperId); 316 | }); 317 | 318 | it('find protocol mapper by name', async () => { 319 | const {id} = this.currentClientScope; 320 | await this.kcAdminClient.clientScopes.addProtocolMapper( 321 | {id}, 322 | dummyMapper, 323 | ); 324 | 325 | const mapper = await this.kcAdminClient.clientScopes.findProtocolMapperByName( 326 | {id, name: dummyMapper.name}, 327 | ); 328 | 329 | expect(mapper).to.be.ok; 330 | expect(mapper.name).to.eql(dummyMapper.name); 331 | }); 332 | 333 | it('find protocol mappers by protocol', async () => { 334 | const {id} = this.currentClientScope; 335 | await this.kcAdminClient.clientScopes.addProtocolMapper( 336 | {id}, 337 | dummyMapper, 338 | ); 339 | 340 | const mapperList = await this.kcAdminClient.clientScopes.findProtocolMappersByProtocol( 341 | {id, protocol: dummyMapper.protocol}, 342 | ); 343 | 344 | expect(mapperList).to.be.ok; 345 | expect(mapperList.length).to.be.gte(1); 346 | 347 | const mapper = mapperList.find(item => item.name === dummyMapper.name); 348 | expect(mapper).to.be.ok; 349 | }); 350 | 351 | it('update protocol mapper', async () => { 352 | const {id} = this.currentClientScope; 353 | 354 | dummyMapper.config = {'access.token.claim': 'true'}; 355 | await this.kcAdminClient.clientScopes.addProtocolMapper( 356 | {id}, 357 | dummyMapper, 358 | ); 359 | const mapper = await this.kcAdminClient.clientScopes.findProtocolMapperByName( 360 | {id, name: dummyMapper.name}, 361 | ); 362 | 363 | expect(mapper.config['access.token.claim']).to.eq('true'); 364 | 365 | mapper.config = {'access.token.claim': 'false'}; 366 | 367 | await this.kcAdminClient.clientScopes.updateProtocolMapper( 368 | {id, mapperId: mapper.id}, 369 | mapper, 370 | ); 371 | 372 | const updatedMapper = await this.kcAdminClient.clientScopes.findProtocolMapperByName( 373 | {id, name: dummyMapper.name}, 374 | ); 375 | 376 | expect(updatedMapper.config['access.token.claim']).to.eq('false'); 377 | }); 378 | 379 | it('delete protocol mapper', async () => { 380 | const {id} = this.currentClientScope; 381 | await this.kcAdminClient.clientScopes.addProtocolMapper( 382 | {id}, 383 | dummyMapper, 384 | ); 385 | 386 | const { 387 | id: mapperId, 388 | } = await this.kcAdminClient.clientScopes.findProtocolMapperByName({ 389 | id, 390 | name: dummyMapper.name, 391 | }); 392 | 393 | await this.kcAdminClient.clientScopes.delProtocolMapper({id, mapperId}); 394 | 395 | const mapper = await this.kcAdminClient.clientScopes.findProtocolMapperByName( 396 | {id, name: dummyMapper.name}, 397 | ); 398 | 399 | expect(mapper).not.to.be.ok; 400 | }); 401 | }); 402 | 403 | describe('scope mappings', () => { 404 | it('list client and realm scope mappings', async () => { 405 | const {id} = this.currentClientScope; 406 | const scopes = await this.kcAdminClient.clientScopes.listScopeMappings({ 407 | id, 408 | }); 409 | expect(scopes).to.be.ok; 410 | }); 411 | 412 | describe('client', () => { 413 | const dummyClientId = 'scopeMappings-dummy'; 414 | const dummyRoleName = 'scopeMappingsRole-dummy'; 415 | 416 | beforeEach(async () => { 417 | const {id} = await this.kcAdminClient.clients.create({ 418 | clientId: dummyClientId, 419 | }); 420 | this.currentClient = await this.kcAdminClient.clients.findOne({ 421 | id, 422 | }); 423 | 424 | await this.kcAdminClient.clients.createRole({ 425 | id, 426 | name: dummyRoleName, 427 | }); 428 | }); 429 | 430 | afterEach(async () => { 431 | try { 432 | const {id} = this.currentClient; 433 | await this.kcAdminClient.clients.delRole({ 434 | id, 435 | roleName: dummyRoleName, 436 | }); 437 | } catch (e) { 438 | // ignore 439 | } 440 | try { 441 | const {id} = this.currentClient; 442 | await this.kcAdminClient.clients.del({id}); 443 | } catch (e) { 444 | // ignore 445 | console.log(e); 446 | } 447 | }); 448 | 449 | it('add scope mappings', async () => { 450 | const {id} = this.currentClientScope; 451 | const {id: clientUniqueId} = this.currentClient; 452 | 453 | const availableRoles = await this.kcAdminClient.clientScopes.listAvailableClientScopeMappings( 454 | { 455 | id, 456 | client: clientUniqueId, 457 | }, 458 | ); 459 | 460 | const filteredRoles = availableRoles.filter(role => !role.composite); 461 | 462 | await this.kcAdminClient.clientScopes.addClientScopeMappings( 463 | { 464 | id, 465 | client: clientUniqueId, 466 | }, 467 | filteredRoles, 468 | ); 469 | 470 | const roles = await this.kcAdminClient.clientScopes.listClientScopeMappings( 471 | { 472 | id, 473 | client: clientUniqueId, 474 | }, 475 | ); 476 | 477 | expect(roles).to.be.ok; 478 | expect(roles).to.be.eql(filteredRoles); 479 | }); 480 | 481 | it('list scope mappings', async () => { 482 | const {id} = this.currentClientScope; 483 | const {id: clientUniqueId} = this.currentClient; 484 | const roles = await this.kcAdminClient.clientScopes.listClientScopeMappings( 485 | { 486 | id, 487 | client: clientUniqueId, 488 | }, 489 | ); 490 | expect(roles).to.be.ok; 491 | }); 492 | 493 | it('list available scope mappings', async () => { 494 | const {id} = this.currentClientScope; 495 | const {id: clientUniqueId} = this.currentClient; 496 | const roles = await this.kcAdminClient.clientScopes.listAvailableClientScopeMappings( 497 | { 498 | id, 499 | client: clientUniqueId, 500 | }, 501 | ); 502 | expect(roles).to.be.ok; 503 | }); 504 | 505 | it('list composite scope mappings', async () => { 506 | const {id} = this.currentClientScope; 507 | const {id: clientUniqueId} = this.currentClient; 508 | const roles = await this.kcAdminClient.clientScopes.listCompositeClientScopeMappings( 509 | { 510 | id, 511 | client: clientUniqueId, 512 | }, 513 | ); 514 | expect(roles).to.be.ok; 515 | }); 516 | 517 | it('delete scope mappings', async () => { 518 | const {id} = this.currentClientScope; 519 | const {id: clientUniqueId} = this.currentClient; 520 | 521 | const rolesBefore = await this.kcAdminClient.clientScopes.listClientScopeMappings( 522 | { 523 | id, 524 | client: clientUniqueId, 525 | }, 526 | ); 527 | 528 | await this.kcAdminClient.clientScopes.delClientScopeMappings( 529 | { 530 | id, 531 | client: clientUniqueId, 532 | }, 533 | rolesBefore, 534 | ); 535 | 536 | const rolesAfter = await this.kcAdminClient.clientScopes.listClientScopeMappings( 537 | { 538 | id, 539 | client: clientUniqueId, 540 | }, 541 | ); 542 | 543 | expect(rolesAfter).to.be.ok; 544 | expect(rolesAfter).to.eql([]); 545 | }); 546 | }); 547 | 548 | describe('realm', () => { 549 | const dummyRoleName = 'realmScopeMappingsRole-dummy'; 550 | 551 | beforeEach(async () => { 552 | await this.kcAdminClient.roles.create({ 553 | name: dummyRoleName, 554 | }); 555 | }); 556 | 557 | afterEach(async () => { 558 | try { 559 | await this.kcAdminClient.roles.delByName({ 560 | name: dummyRoleName, 561 | }); 562 | } catch (e) { 563 | // ignore 564 | } 565 | }); 566 | 567 | it('add scope mappings', async () => { 568 | const {id} = this.currentClientScope; 569 | 570 | const availableRoles = await this.kcAdminClient.clientScopes.listAvailableRealmScopeMappings( 571 | {id}, 572 | ); 573 | 574 | const filteredRoles = availableRoles.filter(role => !role.composite); 575 | 576 | await this.kcAdminClient.clientScopes.addRealmScopeMappings( 577 | {id}, 578 | filteredRoles, 579 | ); 580 | 581 | const roles = await this.kcAdminClient.clientScopes.listRealmScopeMappings( 582 | {id}, 583 | ); 584 | 585 | expect(roles).to.be.ok; 586 | expect(roles).to.include.deep.members(filteredRoles); 587 | }); 588 | 589 | it('list scope mappings', async () => { 590 | const {id} = this.currentClientScope; 591 | const roles = await this.kcAdminClient.clientScopes.listRealmScopeMappings( 592 | { 593 | id, 594 | }, 595 | ); 596 | expect(roles).to.be.ok; 597 | }); 598 | 599 | it('list available scope mappings', async () => { 600 | const {id} = this.currentClientScope; 601 | const roles = await this.kcAdminClient.clientScopes.listAvailableRealmScopeMappings( 602 | { 603 | id, 604 | }, 605 | ); 606 | expect(roles).to.be.ok; 607 | }); 608 | 609 | it('list composite scope mappings', async () => { 610 | const {id} = this.currentClientScope; 611 | const roles = await this.kcAdminClient.clientScopes.listCompositeRealmScopeMappings( 612 | { 613 | id, 614 | }, 615 | ); 616 | expect(roles).to.be.ok; 617 | }); 618 | 619 | it('delete scope mappings', async () => { 620 | const {id} = this.currentClientScope; 621 | 622 | const rolesBefore = await this.kcAdminClient.clientScopes.listRealmScopeMappings( 623 | { 624 | id, 625 | }, 626 | ); 627 | 628 | await this.kcAdminClient.clientScopes.delRealmScopeMappings( 629 | { 630 | id, 631 | }, 632 | rolesBefore, 633 | ); 634 | 635 | const rolesAfter = await this.kcAdminClient.clientScopes.listRealmScopeMappings( 636 | { 637 | id, 638 | }, 639 | ); 640 | 641 | expect(rolesAfter).to.be.ok; 642 | expect(rolesAfter).to.eql([]); 643 | }); 644 | }); 645 | }); 646 | }); 647 | --------------------------------------------------------------------------------