diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 36a7ad94d8..453d77085a 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -102,6 +102,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Compile Admin Client + run: npm run build --workspace=@keycloak/keycloak-admin-client + - name: Restore Keycloak server uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 09fe138be0..c95ae9c097 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,6 +49,11 @@ jobs: command: lint - workspace: account-ui command: build + # Keycloak Admin Client + - workspace: "@keycloak/keycloak-admin-client" + command: lint + - workspace: "@keycloak/keycloak-admin-client" + command: build # Keycloak Masthead - workspace: keycloak-masthead command: lint diff --git a/apps/admin-ui/package.json b/apps/admin-ui/package.json index e66e674f81..5a5a4dfdc3 100644 --- a/apps/admin-ui/package.json +++ b/apps/admin-ui/package.json @@ -8,7 +8,7 @@ "test": "wireit", "cy:open": "cypress open --e2e --browser chrome", "cy:run": "cypress run --browser chrome", - "cy:check-types": "tsc --project cypress/tsconfig.json", + "cy:check-types": "wireit", "cy:ldap-server": "ldap-server-mock --conf=./cypress/fixtures/ldap/server.json --database=./cypress/fixtures/ldap/users.json", "server:start": "./scripts/start-server.mjs", "server:import-client": "./scripts/import-client.mjs" @@ -17,36 +17,47 @@ "dev": { "command": "vite --host", "dependencies": [ - "../../libs/keycloak-js:build" + "../../libs/keycloak-js:build", + "../../libs/keycloak-admin-client:build" ] }, "preview": { "command": "vite preview", "dependencies": [ - "../../libs/keycloak-js:build" + "../../libs/keycloak-js:build", + "../../libs/keycloak-admin-client:build" ] }, "build": { "command": "vite build", "dependencies": [ - "../../libs/keycloak-js:build" + "../../libs/keycloak-js:build", + "../../libs/keycloak-admin-client:build" ] }, "lint": { "command": "eslint . --ext js,jsx,mjs,ts,tsx", "dependencies": [ - "../../libs/keycloak-js:build" + "../../libs/keycloak-js:build", + "../../libs/keycloak-admin-client:build" ] }, "test": { "command": "vitest", "dependencies": [ - "../../libs/keycloak-js:build" + "../../libs/keycloak-js:build", + "../../libs/keycloak-admin-client:build" + ] + }, + "cy:check-types": { + "command": "tsc --project cypress/tsconfig.json", + "dependencies": [ + "../../libs/keycloak-admin-client:build" ] } }, "dependencies": { - "@keycloak/keycloak-admin-client": "^21.0.0-dev.3", + "@keycloak/keycloak-admin-client": "999.0.0-dev", "@patternfly/patternfly": "^4.219.2", "@patternfly/react-code-editor": "^4.82.55", "@patternfly/react-core": "^4.258.3", diff --git a/libs/admin-client/.gitkeep b/libs/admin-client/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/keycloak-admin-client/.gitignore b/libs/keycloak-admin-client/.gitignore new file mode 100644 index 0000000000..f1ff06d608 --- /dev/null +++ b/libs/keycloak-admin-client/.gitignore @@ -0,0 +1 @@ +lib/ \ No newline at end of file diff --git a/libs/keycloak-admin-client/.mocharc.json b/libs/keycloak-admin-client/.mocharc.json new file mode 100644 index 0000000000..4e7c141a16 --- /dev/null +++ b/libs/keycloak-admin-client/.mocharc.json @@ -0,0 +1,5 @@ +{ + "node-option": [ + "loader=ts-node/esm" + ] +} diff --git a/libs/keycloak-admin-client/LICENSE b/libs/keycloak-admin-client/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/libs/keycloak-admin-client/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/libs/keycloak-admin-client/README.md b/libs/keycloak-admin-client/README.md new file mode 100644 index 0000000000..76ca6f2d0f --- /dev/null +++ b/libs/keycloak-admin-client/README.md @@ -0,0 +1,455 @@ +## Keycloak Admin Client + +## Features + +- TypeScript supported +- Latest Keycloak version supported +- [Complete resource definitions](https://github.com/keycloak/keycloak-ui/tree/main/libs/keycloak-admin-client/src/defs) +- [Well-tested for supported APIs](https://github.com/keycloak/keycloak-ui/tree/main/libs/keycloak-admin-client/test) + +## Install + +```sh +npm install @keycloak/keycloak-admin-client +``` + +## Usage + +```js +import KcAdminClient from '@keycloak/keycloak-admin-client'; + +// To configure the client, pass an object to override any of these options: +// { +// baseUrl: 'http://127.0.0.1:8080', +// realmName: 'master', +// requestConfig: { +// /* Axios request config options https://github.com/axios/axios#request-config */ +// }, +// } +const kcAdminClient = new KcAdminClient(); + +// Authorize with username / password +await kcAdminClient.auth({ + username: 'admin', + password: 'admin', + grantType: 'password', + clientId: 'admin-cli', + totp: '123456', // optional Time-based One-time Password if OTP is required in authentication flow +}); + +// List all users +const users = await kcAdminClient.users.find(); + +// Override client configuration for all further requests: +kcAdminClient.setConfig({ + realmName: 'another-realm', +}); + +// This operation will now be performed in 'another-realm' if the user has access. +const groups = await kcAdminClient.groups.find(); + +// Set a `realm` property to override the realm for only a single operation. +// For example, creating a user in another realm: +await this.kcAdminClient.users.create({ + realm: 'a-third-realm', + username: 'username', + email: 'user@example.com', +}); +``` + +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: + +```js +import {Issuer} from 'openid-client'; + +const keycloakIssuer = await Issuer.discover( + 'http://localhost:8080/realms/master', +); + +const client = new keycloakIssuer.Client({ + client_id: 'admin-cli', // Same as `clientId` passed to client.auth() + token_endpoint_auth_method: 'none', // to send only client_id in the header +}); + +// Use the grant type 'password' +let tokenSet = await client.grant({ + grant_type: 'password', + username: 'admin', + password: 'admin', +}); + +// Periodically using refresh_token grant flow to get new access token here +setInterval(async () => { + const refreshToken = tokenSet.refresh_token; + tokenSet = await client.refresh(refreshToken); + kcAdminClient.setAccessToken(tokenSet.access_token); +}, 58 * 1000); // 58 seconds +``` + +In cases where you don't have a refresh token, eg. in a client credentials flow, you can simply call `kcAdminClient.auth` to get a new access token, like this: + +```js +const credentials = { + grantType: 'client_credentials', + clientId: 'clientId', + clientSecret: 'some-client-secret-uuid', +}; +await kcAdminClient.auth(credentials); + +setInterval(() => kcAdminClient.auth(credentials), 58 * 1000); // 58 seconds +``` + +## Building and running the tests + +To build the source do a build: + +```bash +npm run build +``` + +Start the Keycloak server: + +```bash +npm run server:start +``` + +If you started your container manually make sure there is an admin user named 'admin' with password 'admin'. +Then start the tests with: + +```bash +npm test +``` + +## Supported APIs + +### [Realm admin](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_realms_admin_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/realms.spec.ts + +- Import a realm from a full representation of that realm (`POST /`) +- Get the top-level representation of the realm (`GET /{realm}`) +- Update the top-level information of the realm (`PUT /{realm}`) +- Delete the realm (`DELETE /{realm}`) +- Partial export of existing realm into a JSON file (`POST /{realm}/partial-export`) +- Get users management permissions (`GET /{realm}/users-management-permissions`) +- Enable users management permissions (`PUT /{realm}/users-management-permissions`) +- Get events (`GET /{realm}/events`) +- Get admin events (`GET /{realm}/admin-events`) +- Remove all user sessions (`POST /{realm}/logout-all`) +- Remove a specific user session (`DELETE /{realm}/sessions/{session}`) +- Get client policies policies (`GET /{realm}/client-policies/policies`) +- Update client policies policies (`PUT /{realm}/client-policies/policies`) +- Get client policies profiles (`GET /{realm}/client-policies/profiles`) +- Update client policies profiles (`PUT /{realm}/client-policies/profiles`) +- Get a group by path (`GET /{realm}/group-by-path/{path}`) +### [Role](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/roles.spec.ts + +- Create a new role for the realm (`POST /{realm}/roles`) +- Get all roles for the realm (`GET /{realm}/roles`) +- Get a role by name (`GET /{realm}/roles/{role-name}`) +- Update a role by name (`PUT /{realm}/roles/{role-name}`) +- Delete a role by name (`DELETE /{realm}/roles/{role-name}`) +- Get all users in a role by name for the realm (`GET /{realm}/roles/{role-name}/users`) + +### [Roles (by ID)](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_by_id_resource) + +- Get a specific role (`GET /{realm}/roles-by-id/{role-id}`) +- Update the role (`PUT /{realm}/roles-by-id/{role-id}`) +- Delete the role (`DELETE /{realm}/roles-by-id/{role-id}`) +- Make the role a composite role by associating some child roles(`POST /{realm}/roles-by-id/{role-id}/composites`) +- Get role’s children Returns a set of role’s children provided the role is a composite. (`GET /{realm}/roles-by-id/{role-id}/composites`) +- Remove a set of roles from the role’s composite (`DELETE /{realm}/roles-by-id/{role-id}/composites`) +- Get client-level roles for the client that are in the role’s composite (`GET /{realm}/roles-by-id/{role-id}/composites/clients/{client}`) +- Get realm-level roles that are in the role’s composite (`GET /{realm}/roles-by-id/{role-id}/composites/realm`) + +### [User](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_users_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/users.spec.ts + +- Create a new user (`POST /{realm}/users`) +- Get users Returns a list of users, filtered according to query parameters (`GET /{realm}/users`) +- Get representation of the user (`GET /{realm}/users/{id}`) +- Update the user (`PUT /{realm}/users/{id}`) +- Delete the user (`DELETE /{realm}/users/{id}`) +- Count users (`GET /{realm}/users/count`) +- 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`) +- Get user groups (`GET /{realm}/users/{id}/groups`) +- Add user to group (`PUT /{realm}/users/{id}/groups/{groupId}`) +- Delete user from group (`DELETE /{realm}/users/{id}/groups/{groupId}`) +- Remove TOTP from the user (`PUT /{realm}/users/{id}/remove-totp`) +- 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`) +- 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`) +- Update a credential label for a user (`PUT /{realm}/users/{id}/credentials/{credentialId}/userLabel`) +- Move a credential to a position behind another credential (`POST /{realm}/users/{id}/credentials/{credentialId}/moveAfter/{newPreviousCredentialId}`) +- Move a credential to a first position in the credentials list of the user (`PUT /{realm}/users/{id}/credentials/{credentialId}/moveToFirst`) + +### User group-mapping + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/users.spec.ts#L178 + +- Add user to group (`PUT /{id}/groups/{groupId}`) +- List all user groups (`GET /{id}/groups`) +- Count user groups (`GET /{id}/groups/count`) +- Remove user from group (`DELETE /{id}/groups/{groupId}`) + +### User role-mapping + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/users.spec.ts#L143 + +- Get user role-mappings (`GET /{realm}/users/{id}/role-mappings`) +- Add realm-level role mappings to the user (`POST /{realm}/users/{id}/role-mappings/realm`) +- Get realm-level role mappings (`GET /{realm}/users/{id}/role-mappings/realm`) +- Delete realm-level role mappings (`DELETE /{realm}/users/{id}/role-mappings/realm`) +- Get realm-level roles that can be mapped (`GET /{realm}/users/{id}/role-mappings/realm/available`) +- Get effective realm-level role mappings This will recurse all composite roles to get the result. (`GET /{realm}/users/{id}/role-mappings/realm/composite`) + +### [Group](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_groups_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/groups.spec.ts + +- Create (`POST /{realm}/groups`) +- List (`GET /{realm}/groups`) +- Get one (`GET /{realm}/groups/{id}`) +- Update (`PUT /{realm}/groups/{id}`) +- Delete (`DELETE /{realm}/groups/{id}`) +- Count (`GET /{realm}/groups/count`) +- List members (`GET /{realm}/groups/{id}/members`) +- Set or create child (`POST /{realm}/groups/{id}/children`) + +### Group role-mapping + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/groups.spec.ts#L76 + +- Get group role-mappings (`GET /{realm}/groups/{id}/role-mappings`) +- Add realm-level role mappings to the group (`POST /{realm}/groups/{id}/role-mappings/realm`) +- Get realm-level role mappings (`GET /{realm}/groups/{id}/role-mappings/realm`) +- Delete realm-level role mappings (`DELETE /{realm}/groups/{id}/role-mappings/realm`) +- Get realm-level roles that can be mapped (`GET /{realm}/groups/{id}/role-mappings/realm/available`) +- Get effective realm-level role mappings This will recurse all composite roles to get the result. (`GET /{realm}/groups/{id}/role-mappings/realm/composite`) + +### [Client](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clients.spec.ts + +- Create a new client (`POST /{realm}/clients`) +- Get clients belonging to the realm (`GET /{realm}/clients`) +- Get representation of the client (`GET /{realm}/clients/{id}`) +- Update the client (`PUT /{realm}/clients/{id}`) +- Delete the client (`DELETE /{realm}/clients/{id}`) + +### [Client roles](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clients.spec.ts + +- Create a new role for the client (`POST /{realm}/clients/{id}/roles`) +- Get all roles for the client (`GET /{realm}/clients/{id}/roles`) +- Get a role by name (`GET /{realm}/clients/{id}/roles/{role-name}`) +- Update a role by name (`PUT /{realm}/clients/{id}/roles/{role-name}`) +- Delete a role by name (`DELETE /{realm}/clients/{id}/roles/{role-name}`) + +### [Client role-mapping for group](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_client_role_mappings_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/groups.spec.ts#L150 + +- Add client-level roles to the group role mapping (`POST /{realm}/groups/{id}/role-mappings/clients/{client}`) +- Get client-level role mappings for the group (`GET /{realm}/groups/{id}/role-mappings/clients/{client}`) +- Delete client-level roles from group role mapping (`DELETE /{realm}/groups/{id}/role-mappings/clients/{client}`) +- Get available client-level roles that can be mapped to the group (`GET /{realm}/groups/{id}/role-mappings/clients/{client}/available`) +- Get effective client-level role mappings This will recurse all composite roles to get the result. (`GET /{realm}/groups/{id}/role-mappings/clients/{client}/composite`) + +### [Client role-mapping for user](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_client_role_mappings_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/users.spec.ts#L217 + +- Add client-level roles to the user role mapping (`POST /{realm}/users/{id}/role-mappings/clients/{client}`) +- Get client-level role mappings for the user (`GET /{realm}/users/{id}/role-mappings/clients/{client}`) +- Delete client-level roles from user role mapping (`DELETE /{realm}/users/{id}/role-mappings/clients/{client}`) +- Get available client-level roles that can be mapped to the user (`GET /{realm}/users/{id}/role-mappings/clients/{client}/available`) +- Get effective client-level role mappings This will recurse all composite roles to get the result. (`GET /{realm}/users/{id}/role-mappings/clients/{client}/composite`) + +### [Client Attribute Certificate](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_client_attribute_certificate_resource) + +- Get key info (`GET /{realm}/clients/{id}/certificates/{attr}`) +- Get a keystore file for the client, containing private key and public certificate (`POST /{realm}/clients/{id}/certificates/{attr}/download`) +- Generate a new certificate with new key pair (`POST /{realm}/clients/{id}/certificates/{attr}/generate`) +- Generate a new keypair and certificate, and get the private key file Generates a keypair and certificate and serves the private key in a specified keystore format. (`POST /{realm}/clients/{id}/certificates/{attr}/generate-and-download`) +- Upload certificate and eventually private key (`POST /{realm}/clients/{id}/certificates/{attr}/upload`) +- Upload only certificate, not private key (`POST /{realm}/clients/{id}/certificates/{attr}/upload-certificate`) + +### [Identity Providers](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_identity_providers_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/idp.spec.ts + +- Create a new identity provider (`POST /{realm}/identity-provider/instances`) +- Get identity providers (`GET /{realm}/identity-provider/instances`) +- Get the identity provider (`GET /{realm}/identity-provider/instances/{alias}`) +- Update the identity provider (`PUT /{realm}/identity-provider/instances/{alias}`) +- Delete the identity provider (`DELETE /{realm}/identity-provider/instances/{alias}`) +- Find identity provider factory (`GET /{realm}/identity-provider/providers/{providerId}`) +- Create a new identity provider mapper (`POST /{realm}/identity-provider/instances/{alias}/mappers`) +- Get identity provider mappers (`GET /{realm}/identity-provider/instances/{alias}/mappers`) +- Get the identity provider mapper (`GET /{realm}/identity-provider/instances/{alias}/mappers/{id}`) +- Update the identity provider mapper (`PUT /{realm}/identity-provider/instances/{alias}/mappers/{id}`) +- Delete the identity provider mapper (`DELETE /{realm}/identity-provider/instances/{alias}/mappers/{id}`) +- Find the identity provider mapper types (`GET /{realm}/identity-provider/instances/{alias}/mapper-types`) + +### [Client Scopes](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_client_scopes_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clientScopes.spec.ts + +- Create a new client scope (`POST /{realm}/client-scopes`) +- Get client scopes belonging to the realm (`GET /{realm}/client-scopes`) +- Get representation of the client scope (`GET /{realm}/client-scopes/{id}`) +- Update the client scope (`PUT /{realm}/client-scopes/{id}`) +- Delete the client scope (`DELETE /{realm}/client-scopes/{id}`) + +### [Client Scopes for realm](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_client_scopes_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clientScopes.spec.ts + +- Get realm default client scopes (`GET /{realm}/default-default-client-scopes`) +- Add realm default client scope (`PUT /{realm}/default-default-client-scopes/{id}`) +- Delete realm default client scope (`DELETE /{realm}/default-default-client-scopes/{id}`) +- Get realm optional client scopes (`GET /{realm}/default-optional-client-scopes`) +- Add realm optional client scope (`PUT /{realm}/default-optional-client-scopes/{id}`) +- Delete realm optional client scope (`DELETE /{realm}/default-optional-client-scopes/{id}`) + +### [Client Scopes for client](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_client_scopes_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clientScopes.spec.ts + +- Get default client scopes (`GET /{realm}/clients/{id}/default-client-scopes`) +- Add default client scope (`PUT /{realm}/clients/{id}/default-client-scopes/{clientScopeId}`) +- Delete default client scope (`DELETE /{realm}/clients/{id}/default-client-scopes/{clientScopeId}`) +- Get optional client scopes (`GET /{realm}/clients/{id}/optional-client-scopes`) +- Add optional client scope (`PUT /{realm}/clients/{id}/optional-client-scopes/{clientScopeId}`) +- Delete optional client scope (`DELETE /{realm}/clients/{id}/optional-client-scopes/{clientScopeId}`) + +### [Scope Mappings for client scopes](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_scope_mappings_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clientScopes.spec.ts + +- Get all scope mappings for the client (`GET /{realm}/client-scopes/{id}/scope-mappings`) +- Add client-level roles to the client’s scope (`POST /{realm}/client-scopes/{id}/scope-mappings/clients/{client}`) +- Get the roles associated with a client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/clients/{client}`) +- The available client-level roles (`GET /{realm}/client-scopes/{id}/scope-mappings/clients/{client}/available`) +- Get effective client roles (`GET /{realm}/client-scopes/{id}/scope-mappings/clients/{client}/composite`) +- Remove client-level roles from the client’s scope. (`DELETE /{realm}/client-scopes/{id}/scope-mappings/clients/{client}`) +- Add a set of realm-level roles to the client’s scope (`POST /{realm}/client-scopes/{id}/scope-mappings/realm`) +- Get realm-level roles associated with the client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/realm`) +- Remove a set of realm-level roles from the client’s scope (`DELETE /{realm}/client-scopes/{id}/scope-mappings/realm`) +- Get realm-level roles that are available to attach to this client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/realm/available`) +- Get effective realm-level roles associated with the client’s scope (`GET /{realm}/client-scopes/{id}/scope-mappings/realm/composite`) + +### [Scope Mappings for clients](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_scope_mappings_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clientScopes.spec.ts + +- Get all scope mappings for the client (`GET /{realm}/clients/{id}/scope-mappings`) +- Add client-level roles to the client’s scope (`POST /{realm}/clients/{id}/scope-mappings/clients/{client}`) +- Get the roles associated with a client’s scope (`GET /{realm}/clients/{id}/scope-mappings/clients/{client}`) +- Remove client-level roles from the client’s scope. (`DELETE /{realm}/clients/{id}/scope-mappings/clients/{client}`) +- The available client-level roles (`GET /{realm}/clients/{id}/scope-mappings/clients/{client}/available`) +- Get effective client roles (`GET /{realm}/clients/{id}/scope-mappings/clients/{client}/composite`) +- Add a set of realm-level roles to the client’s scope (`POST /{realm}/clients/{id}/scope-mappings/realm`) +- Get realm-level roles associated with the client’s scope (`GET /{realm}/clients/{id}/scope-mappings/realm`) +- Remove a set of realm-level roles from the client’s scope (`DELETE /{realm}/clients/{id}/scope-mappings/realm`) +- Get realm-level roles that are available to attach to this client’s scope (`GET /{realm}/clients/{id}/scope-mappings/realm/available`) +- Get effective realm-level roles associated with the client’s scope (`GET /{realm}/clients/{id}/scope-mappings/realm/composite`) + +### [Protocol Mappers for client scopes](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_protocol_mappers_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clientScopes.spec.ts + +- Create multiple mappers (`POST /{realm}/client-scopes/{id}/protocol-mappers/add-models`) +- Create a mapper (`POST /{realm}/client-scopes/{id}/protocol-mappers/models`) +- Get mappers (`GET /{realm}/client-scopes/{id}/protocol-mappers/models`) +- Get mapper by id (`GET /{realm}/client-scopes/{id}/protocol-mappers/models/{mapperId}`) +- Update the mapper (`PUT /{realm}/client-scopes/{id}/protocol-mappers/models/{mapperId}`) +- Delete the mapper (`DELETE /{realm}/client-scopes/{id}/protocol-mappers/models/{mapperId}`) +- Get mappers by name for a specific protocol (`GET /{realm}/client-scopes/{id}/protocol-mappers/protocol/{protocol}`) + +### [Protocol Mappers for clients](https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_protocol_mappers_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clients.spec.ts + +- Create multiple mappers (`POST /{realm}/clients/{id}/protocol-mappers/add-models`) +- Create a mapper (`POST /{realm}/clients/{id}/protocol-mappers/models`) +- Get mappers (`GET /{realm}/clients/{id}/protocol-mappers/models`) +- Get mapper by id (`GET /{realm}/clients/{id}/protocol-mappers/models/{mapperId}`) +- Update the mapper (`PUT /{realm}/clients/{id}/protocol-mappers/models/{mapperId}`) +- Delete the mapper (`DELETE /{realm}/clients/{id}/protocol-mappers/models/{mapperId}`) +- Get mappers by name for a specific protocol (`GET /{realm}/clients/{id}/protocol-mappers/protocol/{protocol}`) + +### [Component]() + +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/main/libs/keycloak-admin-client/test/components.spec.ts + +- Create (`POST /{realm}/components`) +- List (`GET /{realm}/components`) +- Get (`GET /{realm}/components/{id}`) +- Update (`PUT /{realm}/components/{id}`) +- Delete (`DELETE /{realm}/components/{id}`) + +### [Sessions for clients]() + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clients.spec.ts + +- List user sessions for a specific client (`GET /{realm}/clients/{id}/user-sessions`) +- List offline sessions for a specific client (`GET /{realm}/clients/{id}/offline-sessions`) +- Get user session count for a specific client (`GET /{realm}/clients/{id}/session-count`) +- List offline session count for a specific client (`GET /{realm}/clients/{id}/offline-session-count`) + +### [Authentication Management: Required actions](https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/authenticationManagement.spec.ts + +- Register a new required action (`POST /{realm}/authentication/register-required-action`) +- Get required actions. Returns a list of required actions. (`GET /{realm}/authentication/required-actions`) +- Get required action for alias (`GET /{realm}/authentication/required-actions/{alias}`) +- Update required action (`PUT /{realm}/authentication/required-actions/{alias}`) +- Delete required action (`DELETE /{realm}/authentication/required-actions/{alias}`) +- Lower required action’s priority (`POST /{realm}/authentication/required-actions/{alias}/lower-priority`) +- Raise required action’s priority (`POST /{realm}/authentication/required-actions/{alias}/raise-priority`) +- Get unregistered required actions Returns a list of unregistered required actions. (`GET /{realm}/authentication/unregistered-required-actions`) + +### [Authorization: Permission](https://www.keycloak.org/docs/8.0/authorization_services/#_overview) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clients.spec.ts + +- Create permission (`POST /{realm}/clients/{id}/authz/resource-server/permission/{type}`) +- Get permission (`GET /{realm}/clients/{id}/authz/resource-server/permission/{type}/{permissionId}`) +- Update permission (`PUT /{realm}/clients/{id}/authz/resource-server/permission/{type}/{permissionId}`) +- Delete permission (`DELETE /{realm}/clients/{id}/authz/resource-server/permission/{type}/{permissionId}`) + +### [Authorization: Policy](https://www.keycloak.org/docs/8.0/authorization_services/#_overview) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/clients.spec.ts + +- Create policy (`POST /{realm}/clients/{id}/authz/resource-server/policy/{type}`) +- Get policy (`GET /{realm}/clients/{id}/authz/resource-server/policy/{type}/{policyId}`) +- Get policy by name (`GET /{realm}/clients/{id}/authz/resource-server/policy/search`) +- Update policy (`PUT /{realm}/clients/{id}/authz/resource-server/policy/{type}/{policyId}`) +- Delete policy (`DELETE /{realm}/clients/{id}/authz/resource-server/policy/{policyId}`) + +### [Attack Detection](https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_attack_detection_resource) + +Demo code: https://github.com/keycloak/keycloak-nodejs-admin-client/blob/main/libs/keycloak-admin-client/test/attackDetection.spec.ts + +- Clear any user login failures for all users This can release temporary disabled users (`DELETE /{realm}/attack-detection/brute-force/users`) +- Get status of a username in brute force detection (`GET /{realm}/attack-detection/brute-force/users/{userId}`) +- Clear any user login failures for the user This can release temporary disabled user (`DELETE /{realm}/attack-detection/brute-force/users/{userId}`) + +## Not yet supported + +- [Authentication Management](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_authentication_management_resource) +- [Client Initial Access](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_client_initial_access_resource) +- [Client Registration Policy](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_client_registration_policy_resource) +- [Key](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_key_resource) +- [User Storage Provider](https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_user_storage_provider_resource) + +## Maintainers + +This repo is originally developed by [Canner](https://www.cannercms.com) and [InfuseAI](https://infuseai.io) before being transferred under keycloak organization. diff --git a/libs/keycloak-admin-client/package.json b/libs/keycloak-admin-client/package.json new file mode 100644 index 0000000000..7edd5628ab --- /dev/null +++ b/libs/keycloak-admin-client/package.json @@ -0,0 +1,63 @@ +{ + "name": "@keycloak/keycloak-admin-client", + "version": "999.0.0-dev", + "description": "keycloak admin client", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib" + ], + "types": "lib/index.d.ts", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "scripts": { + "build": "wireit", + "lint": "wireit", + "test": "wireit", + "prepublishOnly": "npm run build" + }, + "wireit": { + "build": { + "command": "tsc --pretty", + "files": [ + "src/**", + "package.json", + "tsconfig.json" + ], + "output": [ + "lib/**" + ] + }, + "lint": { + "command": "eslint . --ext js,jsx,mjs,ts,tsx" + }, + "test": { + "command": "TS_NODE_PROJECT=tsconfig.test.json mocha --recursive \"test/**/*.spec.ts\" --timeout 10000" + } + }, + "dependencies": { + "axios": "^0.27.2", + "camelize-ts": "^2.1.1", + "lodash-es": "^4.17.21", + "url-join": "^5.0.0", + "url-template": "^3.0.0" + }, + "devDependencies": { + "@faker-js/faker": "^7.1.0", + "@types/chai": "^4.2.14", + "@types/lodash-es": "^4.17.5", + "@types/mocha": "^10.0.0", + "@types/node": "^18.0.3", + "chai": "^4.1.2", + "mocha": "^10.0.0", + "ts-node": "^10.2.1" + }, + "author": "wwwy3y3", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/keycloak/keycloak-ui.git" + }, + "homepage": "https://www.keycloak.org/" +} diff --git a/libs/keycloak-admin-client/src/client.ts b/libs/keycloak-admin-client/src/client.ts new file mode 100644 index 0000000000..37ef23b12d --- /dev/null +++ b/libs/keycloak-admin-client/src/client.ts @@ -0,0 +1,144 @@ +import type { AxiosRequestConfig } from "axios"; +import type { RequestArgs } from "./resources/agent.js"; +import { AttackDetection } from "./resources/attackDetection.js"; +import { AuthenticationManagement } from "./resources/authenticationManagement.js"; +import { Cache } from "./resources/cache.js"; +import { ClientPolicies } from "./resources/clientPolicies.js"; +import { Clients } from "./resources/clients.js"; +import { ClientScopes } from "./resources/clientScopes.js"; +import { Components } from "./resources/components.js"; +import { Groups } from "./resources/groups.js"; +import { IdentityProviders } from "./resources/identityProviders.js"; +import { Realms } from "./resources/realms.js"; +import { Roles } from "./resources/roles.js"; +import { ServerInfo } from "./resources/serverInfo.js"; +import { Sessions } from "./resources/sessions.js"; +import { Users } from "./resources/users.js"; +import { UserStorageProvider } from "./resources/userStorageProvider.js"; +import { WhoAmI } from "./resources/whoAmI.js"; +import { Credentials, getToken } from "./utils/auth.js"; +import { defaultBaseUrl, defaultRealm } from "./utils/constants.js"; + +export interface TokenProvider { + getAccessToken: () => Promise; +} + +export interface ConnectionConfig { + baseUrl?: string; + realmName?: string; + requestConfig?: AxiosRequestConfig; + requestArgOptions?: Pick; +} + +export class KeycloakAdminClient { + // Resources + public users: Users; + public userStorageProvider: UserStorageProvider; + public groups: Groups; + public roles: Roles; + public clients: Clients; + public realms: Realms; + public clientScopes: ClientScopes; + public clientPolicies: ClientPolicies; + public identityProviders: IdentityProviders; + public components: Components; + public serverInfo: ServerInfo; + public whoAmI: WhoAmI; + public attackDetection: AttackDetection; + public sessions: Sessions; + public authenticationManagement: AuthenticationManagement; + public cache: Cache; + + // Members + public baseUrl: string; + public realmName: string; + public accessToken?: string; + public refreshToken?: string; + + private requestConfig?: AxiosRequestConfig; + private globalRequestArgOptions?: Pick; + private tokenProvider?: TokenProvider; + + constructor(connectionConfig?: ConnectionConfig) { + this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl; + this.realmName = connectionConfig?.realmName || defaultRealm; + this.requestConfig = connectionConfig?.requestConfig; + this.globalRequestArgOptions = connectionConfig?.requestArgOptions; + + // Initialize resources + this.users = new Users(this); + this.userStorageProvider = new UserStorageProvider(this); + this.groups = new Groups(this); + this.roles = new Roles(this); + this.clients = new Clients(this); + this.realms = new Realms(this); + this.clientScopes = new ClientScopes(this); + this.clientPolicies = new ClientPolicies(this); + this.identityProviders = new IdentityProviders(this); + this.components = new Components(this); + this.authenticationManagement = new AuthenticationManagement(this); + this.serverInfo = new ServerInfo(this); + this.whoAmI = new WhoAmI(this); + this.sessions = new Sessions(this); + this.attackDetection = new AttackDetection(this); + this.cache = new Cache(this); + } + + public async auth(credentials: Credentials) { + const { accessToken, refreshToken } = await getToken({ + baseUrl: this.baseUrl, + realmName: this.realmName, + credentials, + requestConfig: this.requestConfig, + }); + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public registerTokenProvider(provider: TokenProvider) { + if (this.tokenProvider) { + throw new Error("An existing token provider was already registered."); + } + + this.tokenProvider = provider; + } + + public setAccessToken(token: string) { + this.accessToken = token; + } + + public async getAccessToken() { + if (this.tokenProvider) { + return this.tokenProvider.getAccessToken(); + } + + return this.accessToken; + } + + public getRequestConfig() { + return this.requestConfig; + } + + public getGlobalRequestArgOptions(): + | Pick + | undefined { + return this.globalRequestArgOptions; + } + + public setConfig(connectionConfig: ConnectionConfig) { + if ( + typeof connectionConfig.baseUrl === "string" && + connectionConfig.baseUrl + ) { + this.baseUrl = connectionConfig.baseUrl; + } + + if ( + typeof connectionConfig.realmName === "string" && + connectionConfig.realmName + ) { + this.realmName = connectionConfig.realmName; + } + this.requestConfig = connectionConfig.requestConfig; + } +} diff --git a/libs/keycloak-admin-client/src/defs/AccessTokenAccess.ts b/libs/keycloak-admin-client/src/defs/AccessTokenAccess.ts new file mode 100644 index 0000000000..02af3a2e20 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/AccessTokenAccess.ts @@ -0,0 +1,4 @@ +export default interface AccessTokenAccess { + roles?: string[]; + verify_caller?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/PermissonRepresentation.ts b/libs/keycloak-admin-client/src/defs/PermissonRepresentation.ts new file mode 100644 index 0000000000..272da529c1 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/PermissonRepresentation.ts @@ -0,0 +1,6 @@ +export default interface PermissionRepresentation { + claims?: { [index: string]: string }; + rsid?: string; + rsname?: string; + scopes?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/accessTokenCertConf.ts b/libs/keycloak-admin-client/src/defs/accessTokenCertConf.ts new file mode 100644 index 0000000000..3d271b67e1 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/accessTokenCertConf.ts @@ -0,0 +1,3 @@ +export default interface AccessTokenCertConf { + "x5t#S256"?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/accessTokenRepresentation.ts b/libs/keycloak-admin-client/src/defs/accessTokenRepresentation.ts new file mode 100644 index 0000000000..fbc8db24ae --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/accessTokenRepresentation.ts @@ -0,0 +1,50 @@ +import type AccessTokenAccess from "./AccessTokenAccess.js"; +import type AccessTokenCertConf from "./accessTokenCertConf.js"; +import type AddressClaimSet from "./addressClaimSet.js"; +import type { Category } from "./resourceServerRepresentation.js"; + +export default interface AccessTokenRepresentation { + acr?: string; + address?: AddressClaimSet; + "allowed-origins"?: string[]; + at_hash?: string; + auth_time?: number; + authorization?: AccessTokenRepresentation; + azp?: string; + birthdate?: string; + c_hash?: string; + category?: Category; + claims_locales?: string; + cnf?: AccessTokenCertConf; + email?: string; + email_verified?: boolean; + exp?: number; + family_name?: string; + gender: string; + given_name?: string; + iat?: number; + iss?: string; + jti?: string; + locale?: string; + middle_name?: string; + name?: string; + nbf?: number; + nickname?: string; + nonce?: string; + otherClaims?: { [index: string]: string }; + phone_number?: string; + phone_number_verified?: boolean; + picture?: string; + preferred_username?: string; + profile?: string; + realm_access?: AccessTokenAccess; + s_hash?: string; + scope?: string; + session_state?: string; + sub?: string; + "trusted-certs"?: string[]; + typ?: string; + updated_at?: number; + website?: string; + zoneinfo?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/addressClaimSet.ts b/libs/keycloak-admin-client/src/defs/addressClaimSet.ts new file mode 100644 index 0000000000..0b8ef628da --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/addressClaimSet.ts @@ -0,0 +1,8 @@ +export default interface AddressClaimSet { + country?: string; + formatted?: string; + locality?: string; + postal_code?: string; + region?: string; + street_address?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/adminEventRepresentation.ts b/libs/keycloak-admin-client/src/defs/adminEventRepresentation.ts new file mode 100644 index 0000000000..d546abe01d --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/adminEventRepresentation.ts @@ -0,0 +1,12 @@ +import type AuthDetailsRepresentation from "./authDetailsRepresentation.js"; + +export default interface AdminEventRepresentation { + authDetails?: AuthDetailsRepresentation; + error?: string; + operationType?: string; + realmId?: string; + representation?: string; + resourcePath?: string; + resourceType?: string; + time?: number; +} diff --git a/libs/keycloak-admin-client/src/defs/authDetailsRepresentation.ts b/libs/keycloak-admin-client/src/defs/authDetailsRepresentation.ts new file mode 100644 index 0000000000..2d225e43dd --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/authDetailsRepresentation.ts @@ -0,0 +1,6 @@ +export default interface AuthDetailsRepresentation { + clientId?: string; + ipAddress?: string; + realmId?: string; + userId?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/authenticationExecutionExportRepresentation.ts b/libs/keycloak-admin-client/src/defs/authenticationExecutionExportRepresentation.ts new file mode 100644 index 0000000000..16a8426d8e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/authenticationExecutionExportRepresentation.ts @@ -0,0 +1,12 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_authenticationexecutionexportrepresentation + */ +export default interface AuthenticationExecutionExportRepresentation { + flowAlias?: string; + userSetupAllowed?: boolean; + authenticatorConfig?: string; + authenticator?: string; + requirement?: string; + priority?: number; + autheticatorFlow?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/authenticationExecutionInfoRepresentation.ts b/libs/keycloak-admin-client/src/defs/authenticationExecutionInfoRepresentation.ts new file mode 100644 index 0000000000..3fa5db2ba9 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/authenticationExecutionInfoRepresentation.ts @@ -0,0 +1,18 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_authenticationexecutioninforepresentation + */ +export default interface AuthenticationExecutionInfoRepresentation { + id?: string; + requirement?: string; + displayName?: string; + alias?: string; + description?: string; + requirementChoices?: string[]; + configurable?: boolean; + authenticationFlow?: boolean; + providerId?: string; + authenticationConfig?: string; + flowId?: string; + level?: number; + index?: number; +} diff --git a/libs/keycloak-admin-client/src/defs/authenticationFlowRepresentation.ts b/libs/keycloak-admin-client/src/defs/authenticationFlowRepresentation.ts new file mode 100644 index 0000000000..c24d4907b0 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/authenticationFlowRepresentation.ts @@ -0,0 +1,14 @@ +import type AuthenticationExecutionExportRepresentation from "./authenticationExecutionExportRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_authenticationflowrepresentation + */ +export default interface AuthenticationFlowRepresentation { + id?: string; + alias?: string; + description?: string; + providerId?: string; + topLevel?: boolean; + builtIn?: boolean; + authenticationExecutions?: AuthenticationExecutionExportRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/authenticatorConfigInfoRepresentation.ts b/libs/keycloak-admin-client/src/defs/authenticatorConfigInfoRepresentation.ts new file mode 100644 index 0000000000..96cd28477e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/authenticatorConfigInfoRepresentation.ts @@ -0,0 +1,19 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_authenticatorconfiginforepresentation + */ +export default interface AuthenticatorConfigInfoRepresentation { + name?: string; + providerId?: string; + helpText?: string; + properties?: ConfigPropertyRepresentation[]; +} + +export interface ConfigPropertyRepresentation { + name?: string; + label?: string; + helpText?: string; + type?: string; + defaultValue?: any; + options?: string[]; + secret?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/authenticatorConfigRepresentation.ts b/libs/keycloak-admin-client/src/defs/authenticatorConfigRepresentation.ts new file mode 100644 index 0000000000..00830a86a4 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/authenticatorConfigRepresentation.ts @@ -0,0 +1,16 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_authenticatorconfigrepresentation + */ +export default interface AuthenticatorConfigRepresentation { + id?: string; + alias?: string; + config?: { [index: string]: string }; +} + +// we defined this type ourself as the original is just `{[index: string]: any}[]` +// but the admin console does assume these properties are there. +export interface AuthenticationProviderRepresentation { + id?: string; + displayName?: string; + description?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/certificateRepresentation.ts b/libs/keycloak-admin-client/src/defs/certificateRepresentation.ts new file mode 100644 index 0000000000..9abf28ea32 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/certificateRepresentation.ts @@ -0,0 +1,9 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/#_certificaterepresentation + */ +export default interface CertificateRepresentation { + privateKey?: string; + publicKey?: string; + certificate?: string; + kid?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/clientInitialAccessPresentation.ts b/libs/keycloak-admin-client/src/defs/clientInitialAccessPresentation.ts new file mode 100644 index 0000000000..dc3cf94dbf --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientInitialAccessPresentation.ts @@ -0,0 +1,11 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clientinitialaccesspresentation + */ +export default interface ClientInitialAccessPresentation { + id?: string; + token?: string; + timestamp?: number; + expiration?: number; + count?: number; + remainingCount?: number; +} diff --git a/libs/keycloak-admin-client/src/defs/clientPoliciesRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientPoliciesRepresentation.ts new file mode 100644 index 0000000000..06b7b597ac --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientPoliciesRepresentation.ts @@ -0,0 +1,8 @@ +import type ClientPolicyRepresentation from "./clientPolicyRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpoliciesrepresentation + */ +export default interface ClientPoliciesRepresentation { + policies?: ClientPolicyRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/clientPolicyConditionRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientPolicyConditionRepresentation.ts new file mode 100644 index 0000000000..03ff4ebb6e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientPolicyConditionRepresentation.ts @@ -0,0 +1,7 @@ +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpolicyconditionrepresentation + */ +export default interface ClientPolicyConditionRepresentation { + condition?: string; + configuration?: object; +} diff --git a/libs/keycloak-admin-client/src/defs/clientPolicyExecutorRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientPolicyExecutorRepresentation.ts new file mode 100644 index 0000000000..80517c2a35 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientPolicyExecutorRepresentation.ts @@ -0,0 +1,7 @@ +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpolicyexecutorrepresentation + */ +export default interface ClientPolicyExecutorRepresentation { + configuration?: object; + executor?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/clientPolicyRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientPolicyRepresentation.ts new file mode 100644 index 0000000000..f979bfe45e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientPolicyRepresentation.ts @@ -0,0 +1,12 @@ +import type ClientPolicyConditionRepresentation from "./clientPolicyConditionRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpolicyrepresentation + */ +export default interface ClientPolicyRepresentation { + conditions?: ClientPolicyConditionRepresentation[]; + description?: string; + enabled?: boolean; + name?: string; + profiles?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/clientProfileRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientProfileRepresentation.ts new file mode 100644 index 0000000000..7e16675d81 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientProfileRepresentation.ts @@ -0,0 +1,10 @@ +import type ClientPolicyExecutorRepresentation from "./clientPolicyExecutorRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_clientprofilerepresentation + */ +export default interface ClientProfileRepresentation { + description?: string; + executors?: ClientPolicyExecutorRepresentation[]; + name?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/clientProfilesRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientProfilesRepresentation.ts new file mode 100644 index 0000000000..13063b0d4b --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientProfilesRepresentation.ts @@ -0,0 +1,9 @@ +import type ClientProfileRepresentation from "./clientProfileRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_clientprofilesrepresentation + */ +export default interface ClientProfilesRepresentation { + globalProfiles?: ClientProfileRepresentation[]; + profiles?: ClientProfileRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/clientRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientRepresentation.ts new file mode 100644 index 0000000000..627a914b8c --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientRepresentation.ts @@ -0,0 +1,46 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clientrepresentation + */ +import type ResourceServerRepresentation from "./resourceServerRepresentation.js"; +import type ProtocolMapperRepresentation from "./protocolMapperRepresentation.js"; + +export default interface ClientRepresentation { + access?: Record; + adminUrl?: string; + attributes?: Record; + authenticationFlowBindingOverrides?: Record; + authorizationServicesEnabled?: boolean; + authorizationSettings?: ResourceServerRepresentation; + baseUrl?: string; + bearerOnly?: boolean; + clientAuthenticatorType?: string; + clientId?: string; + consentRequired?: boolean; + defaultClientScopes?: string[]; + defaultRoles?: string[]; + description?: string; + directAccessGrantsEnabled?: boolean; + enabled?: boolean; + alwaysDisplayInConsole?: boolean; + frontchannelLogout?: boolean; + fullScopeAllowed?: boolean; + id?: string; + implicitFlowEnabled?: boolean; + name?: string; + nodeReRegistrationTimeout?: number; + notBefore?: number; + optionalClientScopes?: string[]; + origin?: string; + protocol?: string; + protocolMappers?: ProtocolMapperRepresentation[]; + publicClient?: boolean; + redirectUris?: string[]; + registeredNodes?: Record; + registrationAccessToken?: string; + rootUrl?: string; + secret?: string; + serviceAccountsEnabled?: boolean; + standardFlowEnabled?: boolean; + surrogateAuthRequired?: boolean; + webOrigins?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/clientScopeRepresentation.ts b/libs/keycloak-admin-client/src/defs/clientScopeRepresentation.ts new file mode 100644 index 0000000000..008229f18d --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/clientScopeRepresentation.ts @@ -0,0 +1,13 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clientscoperepresentation + */ +import type ProtocolMapperRepresentation from "./protocolMapperRepresentation.js"; + +export default interface ClientScopeRepresentation { + attributes?: Record; + description?: string; + id?: string; + name?: string; + protocol?: string; + protocolMappers?: ProtocolMapperRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/componentExportRepresentation.ts b/libs/keycloak-admin-client/src/defs/componentExportRepresentation.ts new file mode 100644 index 0000000000..4cf865ddc1 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/componentExportRepresentation.ts @@ -0,0 +1,12 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_componentexportrepresentation + */ + +export default interface ComponentExportRepresentation { + id?: string; + name?: string; + providerId?: string; + subType?: string; + subComponents?: { [index: string]: ComponentExportRepresentation }; + config?: { [index: string]: string }; +} diff --git a/libs/keycloak-admin-client/src/defs/componentRepresentation.ts b/libs/keycloak-admin-client/src/defs/componentRepresentation.ts new file mode 100644 index 0000000000..dd9cb4c055 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/componentRepresentation.ts @@ -0,0 +1,13 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_componentrepresentation + */ + +export default interface ComponentRepresentation { + id?: string; + name?: string; + providerId?: string; + providerType?: string; + parentId?: string; + subType?: string; + config?: { [index: string]: string | string[] }; +} diff --git a/libs/keycloak-admin-client/src/defs/componentTypeRepresentation.ts b/libs/keycloak-admin-client/src/defs/componentTypeRepresentation.ts new file mode 100644 index 0000000000..36ebfb8b7e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/componentTypeRepresentation.ts @@ -0,0 +1,11 @@ +import type { ConfigPropertyRepresentation } from "./configPropertyRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_componenttyperepresentation + */ +export default interface ComponentTypeRepresentation { + id: string; + helpText: string; + properties: ConfigPropertyRepresentation[]; + metadata: { [index: string]: any }; +} diff --git a/libs/keycloak-admin-client/src/defs/configPropertyRepresentation.ts b/libs/keycloak-admin-client/src/defs/configPropertyRepresentation.ts new file mode 100644 index 0000000000..1eb322a1c1 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/configPropertyRepresentation.ts @@ -0,0 +1,12 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_configpropertyrepresentation + */ +export interface ConfigPropertyRepresentation { + name?: string; + label?: string; + helpText?: string; + type?: string; + defaultValue?: object; + options?: string[]; + secret?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/credentialRepresentation.ts b/libs/keycloak-admin-client/src/defs/credentialRepresentation.ts new file mode 100644 index 0000000000..c5438f1165 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/credentialRepresentation.ts @@ -0,0 +1,15 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_credentialrepresentation + */ + +export default interface CredentialRepresentation { + createdDate?: number; + credentialData?: string; + id?: string; + priority?: number; + secretData?: string; + temporary?: boolean; + type?: string; + userLabel?: string; + value?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/evaluationResultRepresentation.ts b/libs/keycloak-admin-client/src/defs/evaluationResultRepresentation.ts new file mode 100644 index 0000000000..689af48f75 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/evaluationResultRepresentation.ts @@ -0,0 +1,12 @@ +import type { DecisionEffect } from "./policyRepresentation.js"; +import type PolicyResultRepresentation from "./policyResultRepresentation.js"; +import type ResourceRepresentation from "./resourceRepresentation.js"; +import type ScopeRepresentation from "./scopeRepresentation.js"; + +export default interface EvaluationResultRepresentation { + resource?: ResourceRepresentation; + scopes?: ScopeRepresentation[]; + policies?: PolicyResultRepresentation[]; + status?: DecisionEffect; + allowedScopes?: ScopeRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/eventRepresentation.ts b/libs/keycloak-admin-client/src/defs/eventRepresentation.ts new file mode 100644 index 0000000000..6f0feaae18 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/eventRepresentation.ts @@ -0,0 +1,16 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_eventrepresentation + */ +import type EventType from "./eventTypes.js"; + +export default interface EventRepresentation { + clientId?: string; + details?: Record; + error?: string; + ipAddress?: string; + realmId?: string; + sessionId?: string; + time?: number; + type?: EventType; + userId?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/eventTypes.ts b/libs/keycloak-admin-client/src/defs/eventTypes.ts new file mode 100644 index 0000000000..288692bb42 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/eventTypes.ts @@ -0,0 +1,89 @@ +type EventType = + | "LOGIN" + | "LOGIN_ERROR" + | "REGISTER" + | "REGISTER_ERROR" + | "LOGOUT" + | "LOGOUT_ERROR" + | "CODE_TO_TOKEN" + | "CODE_TO_TOKEN_ERROR" + | "CLIENT_LOGIN" + | "CLIENT_LOGIN_ERROR" + | "REFRESH_TOKEN" + | "REFRESH_TOKEN_ERROR" + | "VALIDATE_ACCESS_TOKEN" + | "VALIDATE_ACCESS_TOKEN_ERROR" + | "INTROSPECT_TOKEN" + | "INTROSPECT_TOKEN_ERROR" + | "FEDERATED_IDENTITY_LINK" + | "FEDERATED_IDENTITY_LINK_ERROR" + | "REMOVE_FEDERATED_IDENTITY" + | "REMOVE_FEDERATED_IDENTITY_ERROR" + | "UPDATE_EMAIL" + | "UPDATE_EMAIL_ERROR" + | "UPDATE_PROFILE" + | "UPDATE_PROFILE_ERROR" + | "UPDATE_PASSWORD" + | "UPDATE_PASSWORD_ERROR" + | "UPDATE_TOTP" + | "UPDATE_TOTP_ERROR" + | "VERIFY_EMAIL" + | "VERIFY_EMAIL_ERROR" + | "REMOVE_TOTP" + | "REMOVE_TOTP_ERROR" + | "REVOKE_GRANT" + | "REVOKE_GRANT_ERROR" + | "SEND_VERIFY_EMAIL" + | "SEND_VERIFY_EMAIL_ERROR" + | "SEND_RESET_PASSWORD" + | "SEND_RESET_PASSWORD_ERROR" + | "SEND_IDENTITY_PROVIDER_LINK" + | "SEND_IDENTITY_PROVIDER_LINK_ERROR" + | "RESET_PASSWORD" + | "RESET_PASSWORD_ERROR" + | "RESTART_AUTHENTICATION" + | "RESTART_AUTHENTICATION_ERROR" + | "INVALID_SIGNATURE" + | "INVALID_SIGNATURE_ERROR" + | "REGISTER_NODE" + | "REGISTER_NODE_ERROR" + | "UNREGISTER_NODE" + | "UNREGISTER_NODE_ERROR" + | "USER_INFO_REQUEST" + | "USER_INFO_REQUEST_ERROR" + | "IDENTITY_PROVIDER_LINK_ACCOUNT" + | "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR" + | "IDENTITY_PROVIDER_LOGIN" + | "IDENTITY_PROVIDER_LOGIN_ERROR" + | "IDENTITY_PROVIDER_FIRST_LOGIN" + | "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" + | "IDENTITY_PROVIDER_POST_LOGIN" + | "IDENTITY_PROVIDER_POST_LOGIN_ERROR" + | "IDENTITY_PROVIDER_RESPONSE" + | "IDENTITY_PROVIDER_RESPONSE_ERROR" + | "IDENTITY_PROVIDER_RETRIEVE_TOKEN" + | "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR" + | "IMPERSONATE" + | "IMPERSONATE_ERROR" + | "CUSTOM_REQUIRED_ACTION" + | "CUSTOM_REQUIRED_ACTION_ERROR" + | "EXECUTE_ACTIONS" + | "EXECUTE_ACTIONS_ERROR" + | "EXECUTE_ACTION_TOKEN" + | "EXECUTE_ACTION_TOKEN_ERROR" + | "CLIENT_INFO" + | "CLIENT_INFO_ERROR" + | "CLIENT_REGISTER" + | "CLIENT_REGISTER_ERROR" + | "CLIENT_UPDATE" + | "CLIENT_UPDATE_ERROR" + | "CLIENT_DELETE" + | "CLIENT_DELETE_ERROR" + | "CLIENT_INITIATED_ACCOUNT_LINKING" + | "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR" + | "TOKEN_EXCHANGE" + | "TOKEN_EXCHANGE_ERROR" + | "PERMISSION_TOKEN" + | "PERMISSION_TOKEN_ERROR"; + +export default EventType; diff --git a/libs/keycloak-admin-client/src/defs/federatedIdentityRepresentation.ts b/libs/keycloak-admin-client/src/defs/federatedIdentityRepresentation.ts new file mode 100644 index 0000000000..f9ace144fc --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/federatedIdentityRepresentation.ts @@ -0,0 +1,9 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_federatedidentityrepresentation + */ + +export default interface FederatedIdentityRepresentation { + identityProvider?: string; + userId?: string; + userName?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/globalRequestResult.ts b/libs/keycloak-admin-client/src/defs/globalRequestResult.ts new file mode 100644 index 0000000000..bb650bc1fd --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/globalRequestResult.ts @@ -0,0 +1,7 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_globalrequestresult + */ +export default interface GlobalRequestResult { + successRequests?: string[]; + failedRequests?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/groupRepresentation.ts b/libs/keycloak-admin-client/src/defs/groupRepresentation.ts new file mode 100644 index 0000000000..7ebee981c3 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/groupRepresentation.ts @@ -0,0 +1,16 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_grouprepresentation + */ + +export default interface GroupRepresentation { + id?: string; + name?: string; + path?: string; + subGroups?: GroupRepresentation[]; + + // optional in response + access?: Record; + attributes?: Record; + clientRoles?: Record; + realmRoles?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/identityProviderMapperRepresentation.ts b/libs/keycloak-admin-client/src/defs/identityProviderMapperRepresentation.ts new file mode 100644 index 0000000000..9912b0aaf1 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/identityProviderMapperRepresentation.ts @@ -0,0 +1,11 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_identityprovidermapperrepresentation + */ + +export default interface IdentityProviderMapperRepresentation { + config?: any; + id?: string; + identityProviderAlias?: string; + identityProviderMapper?: string; + name?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/identityProviderMapperTypeRepresentation.ts b/libs/keycloak-admin-client/src/defs/identityProviderMapperTypeRepresentation.ts new file mode 100644 index 0000000000..e75d65d0c1 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/identityProviderMapperTypeRepresentation.ts @@ -0,0 +1,9 @@ +import type { ConfigPropertyRepresentation } from "./configPropertyRepresentation.js"; + +export interface IdentityProviderMapperTypeRepresentation { + id?: string; + name?: string; + category?: string; + helpText?: string; + properties?: ConfigPropertyRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/identityProviderRepresentation.ts b/libs/keycloak-admin-client/src/defs/identityProviderRepresentation.ts new file mode 100644 index 0000000000..7e8ba1dca6 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/identityProviderRepresentation.ts @@ -0,0 +1,18 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_identityproviderrepresentation + */ + +export default interface IdentityProviderRepresentation { + addReadTokenRoleOnCreate?: boolean; + alias?: string; + config?: Record; + displayName?: string; + enabled?: boolean; + firstBrokerLoginFlowAlias?: string; + internalId?: string; + linkOnly?: boolean; + postBrokerLoginFlowAlias?: string; + providerId?: string; + storeToken?: boolean; + trustEmail?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/keyMetadataRepresentation.ts b/libs/keycloak-admin-client/src/defs/keyMetadataRepresentation.ts new file mode 100644 index 0000000000..cff2624899 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/keyMetadataRepresentation.ts @@ -0,0 +1,18 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_keysmetadatarepresentation-keymetadatarepresentation + */ +export default interface KeysMetadataRepresentation { + active?: { [index: string]: string }; + keys?: KeyMetadataRepresentation[]; +} + +export interface KeyMetadataRepresentation { + providerId?: string; + providerPriority?: number; + kid?: string; + status?: string; + type?: string; + algorithm?: string; + publicKey?: string; + certificate?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/keystoreConfig.ts b/libs/keycloak-admin-client/src/defs/keystoreConfig.ts new file mode 100644 index 0000000000..5c11410ed8 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/keystoreConfig.ts @@ -0,0 +1,11 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/#_keystoreconfig + */ +export default interface KeyStoreConfig { + realmCertificate?: boolean; + storePassword?: string; + keyPassword?: string; + keyAlias?: string; + realmAlias?: string; + format?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/managementPermissionReference.ts b/libs/keycloak-admin-client/src/defs/managementPermissionReference.ts new file mode 100644 index 0000000000..e4424ac6c4 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/managementPermissionReference.ts @@ -0,0 +1,5 @@ +export interface ManagementPermissionReference { + enabled?: boolean; + resource?: string; + scopePermissions?: Record; +} diff --git a/libs/keycloak-admin-client/src/defs/mappingsRepresentation.ts b/libs/keycloak-admin-client/src/defs/mappingsRepresentation.ts new file mode 100644 index 0000000000..23d144b019 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/mappingsRepresentation.ts @@ -0,0 +1,9 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_mappingsrepresentation + */ +import type RoleRepresentation from "./roleRepresentation.js"; + +export default interface MappingsRepresentation { + clientMappings?: Record; + realmMappings?: RoleRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/passwordPolicyTypeRepresentation.ts b/libs/keycloak-admin-client/src/defs/passwordPolicyTypeRepresentation.ts new file mode 100644 index 0000000000..1b7464644f --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/passwordPolicyTypeRepresentation.ts @@ -0,0 +1,10 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_passwordpolicytyperepresentation + */ +export default interface PasswordPolicyTypeRepresentation { + id?: string; + displayName?: string; + configType?: string; + defaultValue?: string; + multipleSupported?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/policyEvaluationResponse.ts b/libs/keycloak-admin-client/src/defs/policyEvaluationResponse.ts new file mode 100644 index 0000000000..e123872237 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/policyEvaluationResponse.ts @@ -0,0 +1,10 @@ +import type AccessTokenRepresentation from "./accessTokenRepresentation.js"; +import type EvaluationResultRepresentation from "./evaluationResultRepresentation.js"; +import type { DecisionEffect } from "./policyRepresentation.js"; + +export default interface PolicyEvaluationResponse { + results?: EvaluationResultRepresentation[]; + entitlements?: boolean; + status?: DecisionEffect; + rpt?: AccessTokenRepresentation; +} diff --git a/libs/keycloak-admin-client/src/defs/policyProviderRepresentation.ts b/libs/keycloak-admin-client/src/defs/policyProviderRepresentation.ts new file mode 100644 index 0000000000..468ec183d7 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/policyProviderRepresentation.ts @@ -0,0 +1,5 @@ +export default interface PolicyProviderRepresentation { + type?: string; + name?: string; + group?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/policyRepresentation.ts b/libs/keycloak-admin-client/src/defs/policyRepresentation.ts new file mode 100644 index 0000000000..7b4158989e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/policyRepresentation.ts @@ -0,0 +1,40 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_policyrepresentation + */ + +export enum DecisionStrategy { + AFFIRMATIVE = "AFFIRMATIVE", + UNANIMOUS = "UNANIMOUS", + CONSENSUS = "CONSENSUS", +} + +export enum DecisionEffect { + Permit = "PERMIT", + Deny = "DENY", +} + +export enum Logic { + POSITIVE = "POSITIVE", + NEGATIVE = "NEGATIVE", +} + +export interface PolicyRoleRepresentation { + id: string; + required?: boolean; +} + +export default interface PolicyRepresentation { + config?: Record; + decisionStrategy?: DecisionStrategy; + description?: string; + id?: string; + logic?: Logic; + name?: string; + owner?: string; + policies?: string[]; + resources?: string[]; + scopes?: string[]; + type?: string; + users?: string[]; + roles?: PolicyRoleRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/policyResultRepresentation.ts b/libs/keycloak-admin-client/src/defs/policyResultRepresentation.ts new file mode 100644 index 0000000000..02d4f24422 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/policyResultRepresentation.ts @@ -0,0 +1,9 @@ +import type PolicyRepresentation from "./policyRepresentation.js"; +import type { DecisionEffect } from "./policyRepresentation.js"; + +export default interface PolicyResultRepresentation { + policy?: PolicyRepresentation; + status?: DecisionEffect; + associatedPolicies?: PolicyResultRepresentation[]; + scopes?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/profileInfoRepresentation.ts b/libs/keycloak-admin-client/src/defs/profileInfoRepresentation.ts new file mode 100644 index 0000000000..24caa8b979 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/profileInfoRepresentation.ts @@ -0,0 +1,9 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_profileinforepresentation + */ +export default interface ProfileInfoRepresentation { + name?: string; + disabledFeatures?: string[]; + previewFeatures?: string[]; + experimentalFeatures?: string[]; +} diff --git a/libs/keycloak-admin-client/src/defs/protocolMapperRepresentation.ts b/libs/keycloak-admin-client/src/defs/protocolMapperRepresentation.ts new file mode 100644 index 0000000000..835db76ca2 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/protocolMapperRepresentation.ts @@ -0,0 +1,11 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_protocolmapperrepresentation + */ + +export default interface ProtocolMapperRepresentation { + config?: Record; + id?: string; + name?: string; + protocol?: string; + protocolMapper?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/realmEventsConfigRepresentation.ts b/libs/keycloak-admin-client/src/defs/realmEventsConfigRepresentation.ts new file mode 100644 index 0000000000..01b46fe4bc --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/realmEventsConfigRepresentation.ts @@ -0,0 +1,12 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/#_realmeventsconfigrepresentation + */ + +export interface RealmEventsConfigRepresentation { + eventsEnabled?: boolean; + eventsExpiration?: number; + eventsListeners?: string[]; + enabledEventTypes?: string[]; + adminEventsEnabled?: boolean; + adminEventsDetailsEnabled?: boolean; +} diff --git a/libs/keycloak-admin-client/src/defs/realmRepresentation.ts b/libs/keycloak-admin-client/src/defs/realmRepresentation.ts new file mode 100644 index 0000000000..b062d20ad6 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/realmRepresentation.ts @@ -0,0 +1,141 @@ +import type ClientRepresentation from "./clientRepresentation.js"; +import type ComponentExportRepresentation from "./componentExportRepresentation.js"; +import type UserRepresentation from "./userRepresentation.js"; +import type GroupRepresentation from "./groupRepresentation.js"; +import type IdentityProviderRepresentation from "./identityProviderRepresentation.js"; +import type RequiredActionProviderRepresentation from "./requiredActionProviderRepresentation.js"; +import type RolesRepresentation from "./rolesRepresentation.js"; +import type ClientProfilesRepresentation from "./clientProfilesRepresentation.js"; +import type ClientPoliciesRepresentation from "./clientPoliciesRepresentation.js"; +import type RoleRepresentation from "./roleRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_realmrepresentation + */ + +export default interface RealmRepresentation { + accessCodeLifespan?: number; + accessCodeLifespanLogin?: number; + accessCodeLifespanUserAction?: number; + accessTokenLifespan?: number; + accessTokenLifespanForImplicitFlow?: number; + accountTheme?: string; + actionTokenGeneratedByAdminLifespan?: number; + actionTokenGeneratedByUserLifespan?: number; + adminEventsDetailsEnabled?: boolean; + adminEventsEnabled?: boolean; + adminTheme?: string; + attributes?: Record; + // AuthenticationFlowRepresentation + authenticationFlows?: any[]; + // AuthenticatorConfigRepresentation + authenticatorConfig?: any[]; + browserFlow?: string; + browserSecurityHeaders?: Record; + bruteForceProtected?: boolean; + clientAuthenticationFlow?: string; + clientScopeMappings?: Record; + // ClientScopeRepresentation + clientScopes?: any[]; + clients?: ClientRepresentation[]; + clientPolicies?: ClientPoliciesRepresentation; + clientProfiles?: ClientProfilesRepresentation; + components?: { [index: string]: ComponentExportRepresentation }; + defaultDefaultClientScopes?: string[]; + defaultGroups?: string[]; + defaultLocale?: string; + defaultOptionalClientScopes?: string[]; + defaultRoles?: string[]; + defaultRole?: RoleRepresentation; + defaultSignatureAlgorithm?: string; + directGrantFlow?: string; + displayName?: string; + displayNameHtml?: string; + dockerAuthenticationFlow?: string; + duplicateEmailsAllowed?: boolean; + editUsernameAllowed?: boolean; + emailTheme?: string; + enabled?: boolean; + enabledEventTypes?: string[]; + eventsEnabled?: boolean; + eventsExpiration?: number; + eventsListeners?: string[]; + failureFactor?: number; + federatedUsers?: UserRepresentation[]; + groups?: GroupRepresentation[]; + id?: string; + // IdentityProviderMapperRepresentation + identityProviderMappers?: any[]; + identityProviders?: IdentityProviderRepresentation[]; + internationalizationEnabled?: boolean; + keycloakVersion?: string; + loginTheme?: string; + loginWithEmailAllowed?: boolean; + maxDeltaTimeSeconds?: number; + maxFailureWaitSeconds?: number; + minimumQuickLoginWaitSeconds?: number; + notBefore?: number; + offlineSessionIdleTimeout?: number; + offlineSessionMaxLifespan?: number; + offlineSessionMaxLifespanEnabled?: boolean; + otpPolicyAlgorithm?: string; + otpPolicyDigits?: number; + otpPolicyInitialCounter?: number; + otpPolicyLookAheadWindow?: number; + otpPolicyPeriod?: number; + otpPolicyType?: string; + otpSupportedApplications?: string[]; + passwordPolicy?: string; + permanentLockout?: boolean; + // ProtocolMapperRepresentation + protocolMappers?: any[]; + quickLoginCheckMilliSeconds?: number; + realm?: string; + refreshTokenMaxReuse?: number; + registrationAllowed?: boolean; + registrationEmailAsUsername?: boolean; + registrationFlow?: string; + rememberMe?: boolean; + requiredActions?: RequiredActionProviderRepresentation[]; + resetCredentialsFlow?: string; + resetPasswordAllowed?: boolean; + revokeRefreshToken?: boolean; + roles?: RolesRepresentation; + // ScopeMappingRepresentation + scopeMappings?: any[]; + smtpServer?: Record; + sslRequired?: string; + ssoSessionIdleTimeout?: number; + ssoSessionIdleTimeoutRememberMe?: number; + ssoSessionMaxLifespan?: number; + ssoSessionMaxLifespanRememberMe?: number; + clientSessionIdleTimeout?: number; + clientSessionMaxLifespan?: number; + supportedLocales?: string[]; + // UserFederationMapperRepresentation + userFederationMappers?: any[]; + // UserFederationProviderRepresentation + userFederationProviders?: any[]; + userManagedAccessAllowed?: boolean; + users?: UserRepresentation[]; + verifyEmail?: boolean; + waitIncrementSeconds?: number; +} + +export type PartialImportRealmRepresentation = RealmRepresentation & { + ifResourceExists: "FAIL" | "SKIP" | "OVERWRITE"; +}; + +export type PartialImportResponse = { + overwritten: number; + added: number; + skipped: number; + results: PartialImportResult[]; +}; + +export type PartialImportResult = { + action: string; + resourceType: string; + resourceName: string; + id: string; +}; diff --git a/libs/keycloak-admin-client/src/defs/requiredActionProviderRepresentation.ts b/libs/keycloak-admin-client/src/defs/requiredActionProviderRepresentation.ts new file mode 100644 index 0000000000..90fec730e5 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/requiredActionProviderRepresentation.ts @@ -0,0 +1,21 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_requiredactionproviderrepresentation + */ + +export enum RequiredActionAlias { + VERIFY_EMAIL = "VERIFY_EMAIL", + UPDATE_PROFILE = "UPDATE_PROFILE", + CONFIGURE_TOTP = "CONFIGURE_TOTP", + UPDATE_PASSWORD = "UPDATE_PASSWORD", + terms_and_conditions = "terms_and_conditions", +} + +export default interface RequiredActionProviderRepresentation { + alias?: string; + config?: Record; + defaultAction?: boolean; + enabled?: boolean; + name?: string; + providerId?: string; + priority?: number; +} diff --git a/libs/keycloak-admin-client/src/defs/requiredActionProviderSimpleRepresentation.ts b/libs/keycloak-admin-client/src/defs/requiredActionProviderSimpleRepresentation.ts new file mode 100644 index 0000000000..8968145824 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/requiredActionProviderSimpleRepresentation.ts @@ -0,0 +1,5 @@ +export default interface RequiredActionProviderSimpleRepresentation { + id?: string; + name?: string; + providerId?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/resourceEvaluation.ts b/libs/keycloak-admin-client/src/defs/resourceEvaluation.ts new file mode 100644 index 0000000000..e3f02fad89 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/resourceEvaluation.ts @@ -0,0 +1,14 @@ +import type ResourceRepresentation from "./resourceRepresentation.js"; + +export default interface ResourceEvaluation { + roleIds?: string[]; + clientId: string; + userId: string; + resources?: ResourceRepresentation[]; + entitlements: boolean; + context: { + attributes: { + [key: string]: string; + }; + }; +} diff --git a/libs/keycloak-admin-client/src/defs/resourceRepresentation.ts b/libs/keycloak-admin-client/src/defs/resourceRepresentation.ts new file mode 100644 index 0000000000..0abaa365f8 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/resourceRepresentation.ts @@ -0,0 +1,18 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_resourcerepresentation + */ +import type { ResourceOwnerRepresentation } from "./resourceServerRepresentation.js"; +import type ScopeRepresentation from "./scopeRepresentation.js"; + +export default interface ResourceRepresentation { + name?: string; + type?: string; + owner?: ResourceOwnerRepresentation; + ownerManagedAccess?: boolean; + displayName?: string; + attributes?: { [index: string]: string[] }; + _id?: string; + uris?: string[]; + scopes?: ScopeRepresentation[]; + icon_uri?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/resourceServerRepresentation.ts b/libs/keycloak-admin-client/src/defs/resourceServerRepresentation.ts new file mode 100644 index 0000000000..eb0cc90543 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/resourceServerRepresentation.ts @@ -0,0 +1,44 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_policyrepresentation + */ +import type PolicyRepresentation from "./policyRepresentation.js"; +import type ResourceRepresentation from "./resourceRepresentation.js"; +import type ScopeRepresentation from "./scopeRepresentation.js"; + +export default interface ResourceServerRepresentation { + id?: string; + clientId?: string; + name?: string; + allowRemoteResourceManagement?: boolean; + policyEnforcementMode?: PolicyEnforcementMode; + resources?: ResourceRepresentation[]; + policies?: PolicyRepresentation[]; + scopes?: ScopeRepresentation[]; + decisionStrategy?: DecisionStrategy; +} +export interface ResourceOwnerRepresentation { + id?: string; + name?: string; +} +export interface AbstractPolicyRepresentation { + id?: string; + name?: string; + description?: string; + type?: string; + policies?: string[]; + resources?: string[]; + scopes?: string[]; + logic?: Logic; + decisionStrategy?: DecisionStrategy; + owner?: string; + resourcesData?: ResourceRepresentation[]; + scopesData?: ScopeRepresentation[]; +} + +export type PolicyEnforcementMode = "ENFORCING" | "PERMISSIVE" | "DISABLED"; + +export type DecisionStrategy = "AFFIRMATIVE" | "UNANIMOUS" | "CONSENSUS"; + +export type Logic = "POSITIVE" | "NEGATIVE"; + +export type Category = "INTERNAL" | "ACCESS" | "ID" | "ADMIN" | "USERINFO"; diff --git a/libs/keycloak-admin-client/src/defs/roleRepresentation.ts b/libs/keycloak-admin-client/src/defs/roleRepresentation.ts new file mode 100644 index 0000000000..39aeb95b11 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/roleRepresentation.ts @@ -0,0 +1,27 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_rolerepresentation + */ + +export default interface RoleRepresentation { + id?: string; + name?: string; + description?: string; + scopeParamRequired?: boolean; + composite?: boolean; + composites?: Composites; + clientRole?: boolean; + containerId?: string; + attributes?: { [index: string]: string[] }; +} + +export interface Composites { + realm?: string[]; + client?: { [index: string]: string[] }; + application?: { [index: string]: string[] }; +} + +// when requesting to role-mapping api (create, delete), id and name are required +export interface RoleMappingPayload extends RoleRepresentation { + id: string; + name: string; +} diff --git a/libs/keycloak-admin-client/src/defs/rolesRepresentation.ts b/libs/keycloak-admin-client/src/defs/rolesRepresentation.ts new file mode 100644 index 0000000000..8959ad0ea0 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/rolesRepresentation.ts @@ -0,0 +1,11 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_rolesrepresentation + */ + +import type RoleRepresentation from "./roleRepresentation.js"; + +export default interface RolesRepresentation { + realm?: RoleRepresentation[]; + client?: { [index: string]: RoleRepresentation[] }; + application?: { [index: string]: RoleRepresentation[] }; +} diff --git a/libs/keycloak-admin-client/src/defs/scopeRepresentation.ts b/libs/keycloak-admin-client/src/defs/scopeRepresentation.ts new file mode 100644 index 0000000000..c32e9cb145 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/scopeRepresentation.ts @@ -0,0 +1,14 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_scoperepresentation + */ +import type PolicyRepresentation from "./policyRepresentation.js"; +import type ResourceRepresentation from "./resourceRepresentation.js"; + +export default interface ScopeRepresentation { + displayName?: string; + iconUri?: string; + id?: string; + name?: string; + policies?: PolicyRepresentation[]; + resources?: ResourceRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts b/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts new file mode 100644 index 0000000000..d56e940d20 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts @@ -0,0 +1,70 @@ +import type ComponentTypeRepresentation from "./componentTypeRepresentation.js"; +import type { ConfigPropertyRepresentation } from "./configPropertyRepresentation.js"; +import type PasswordPolicyTypeRepresentation from "./passwordPolicyTypeRepresentation.js"; +import type ProfileInfoRepresentation from "./profileInfoRepresentation.js"; +import type ProtocolMapperRepresentation from "./protocolMapperRepresentation.js"; +import type SystemInfoRepresentation from "./systemInfoRepersantation.js"; + +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_serverinforepresentation + */ +export interface ServerInfoRepresentation { + systemInfo?: SystemInfoRepresentation; + memoryInfo?: MemoryInfoRepresentation; + profileInfo?: ProfileInfoRepresentation; + themes?: { [index: string]: ThemeInfoRepresentation[] }; + socialProviders?: { [index: string]: string }[]; + identityProviders?: { [index: string]: string }[]; + clientImporters?: { [index: string]: string }[]; + providers?: { [index: string]: SpiInfoRepresentation }; + protocolMapperTypes?: { [index: string]: ProtocolMapperTypeRepresentation[] }; + builtinProtocolMappers?: { [index: string]: ProtocolMapperRepresentation[] }; + clientInstallations?: { [index: string]: ClientInstallationRepresentation[] }; + componentTypes?: { [index: string]: ComponentTypeRepresentation[] }; + passwordPolicies?: PasswordPolicyTypeRepresentation[]; + enums?: { [index: string]: string[] }; +} + +export interface ThemeInfoRepresentation { + name: string; + locales?: string[]; +} + +export interface SpiInfoRepresentation { + internal: boolean; + providers: { [index: string]: ProviderRepresentation }; +} + +export interface ProviderRepresentation { + order: number; + operationalInfo?: Record; +} + +export interface ClientInstallationRepresentation { + id: string; + protocol: string; + downloadOnly: boolean; + displayType: string; + helpText: string; + filename: string; + mediaType: string; +} + +export interface MemoryInfoRepresentation { + total: number; + totalFormated: string; + used: number; + usedFormated: string; + free: number; + freePercentage: number; + freeFormated: string; +} + +export interface ProtocolMapperTypeRepresentation { + id: string; + name: string; + category: string; + helpText: string; + priority: number; + properties: ConfigPropertyRepresentation[]; +} diff --git a/libs/keycloak-admin-client/src/defs/synchronizationResultRepresentation.ts b/libs/keycloak-admin-client/src/defs/synchronizationResultRepresentation.ts new file mode 100644 index 0000000000..6efb54fa2e --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/synchronizationResultRepresentation.ts @@ -0,0 +1,12 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_synchronizationresult + */ + +export default interface SynchronizationResultRepresentation { + ignored?: boolean; + added?: number; + updated?: number; + removed?: number; + failed?: number; + status?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/systemInfoRepersantation.ts b/libs/keycloak-admin-client/src/defs/systemInfoRepersantation.ts new file mode 100644 index 0000000000..38c694ba2a --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/systemInfoRepersantation.ts @@ -0,0 +1,24 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_systeminforepresentation + */ + +export default interface SystemInfoRepresentation { + version?: string; + serverTime?: string; + uptime?: string; + uptimeMillis?: number; + javaVersion?: string; + javaVendor?: string; + javaVm?: string; + javaVmVersion?: string; + javaRuntime?: string; + javaHome?: string; + osName?: string; + osArchitecture?: string; + osVersion?: string; + fileEncoding?: string; + userName?: string; + userDir?: string; + userTimezone?: string; + userLocale?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/testLdapConnection.ts b/libs/keycloak-admin-client/src/defs/testLdapConnection.ts new file mode 100644 index 0000000000..2cd79167c5 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/testLdapConnection.ts @@ -0,0 +1,15 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/#_testldapconnectionrepresentation + */ + +export default interface TestLdapConnectionRepresentation { + action?: string; + connectionUrl?: string; + bindDn?: string; + bindCredential?: string; + useTruststoreSpi?: string; + connectionTimeout?: string; + componentId?: string; + startTls?: string; + authType?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/userConsentRepresentation.ts b/libs/keycloak-admin-client/src/defs/userConsentRepresentation.ts new file mode 100644 index 0000000000..50d96fd987 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/userConsentRepresentation.ts @@ -0,0 +1,10 @@ +/** + * https://www.keycloak.org/docs-api/11.0/rest-api/#_userconsentrepresentation + */ + +export default interface UserConsentRepresentation { + clientId?: string; + createDate?: string; + grantedClientScopes?: string[]; + lastUpdatedDate?: number; +} diff --git a/libs/keycloak-admin-client/src/defs/userProfileConfig.ts b/libs/keycloak-admin-client/src/defs/userProfileConfig.ts new file mode 100644 index 0000000000..47ad4c22af --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/userProfileConfig.ts @@ -0,0 +1,42 @@ +// See: https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/config/UPConfig.java +export default interface UserProfileConfig { + attributes?: UserProfileAttribute[]; + groups?: UserProfileGroup[]; +} + +// See: https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/config/UPAttribute.java +export interface UserProfileAttribute { + name?: string; + validations?: Record>; + annotations?: Record[]; + required?: UserProfileAttributeRequired; + permissions?: UserProfileAttributePermissions; + selector?: UserProfileAttributeSelector; + displayName?: string; + group?: string; +} + +// See: https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/config/UPAttributeRequired.java +export interface UserProfileAttributeRequired { + roles?: string[]; + scopes?: string[]; +} + +// See: https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/config/UPAttributePermissions.java +export interface UserProfileAttributePermissions { + view?: string[]; + edit?: string[]; +} + +// See: https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/config/UPAttributeSelector.java +export interface UserProfileAttributeSelector { + scopes?: string[]; +} + +// See: https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/config/UPGroup.java +export interface UserProfileGroup { + name?: string; + displayHeader?: string; + displayDescription?: string; + annotations?: Record; +} diff --git a/libs/keycloak-admin-client/src/defs/userRepresentation.ts b/libs/keycloak-admin-client/src/defs/userRepresentation.ts new file mode 100644 index 0000000000..4215dc9c4d --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/userRepresentation.ts @@ -0,0 +1,33 @@ +import type UserConsentRepresentation from "./userConsentRepresentation.js"; +import type CredentialRepresentation from "./credentialRepresentation.js"; +import type FederatedIdentityRepresentation from "./federatedIdentityRepresentation.js"; +import type { RequiredActionAlias } from "./requiredActionProviderRepresentation.js"; + +export default interface UserRepresentation { + id?: string; + createdTimestamp?: number; + username?: string; + enabled?: boolean; + totp?: boolean; + emailVerified?: boolean; + disableableCredentialTypes?: string[]; + requiredActions?: (RequiredActionAlias | string)[]; + notBefore?: number; + access?: Record; + + // optional from response + attributes?: Record; + clientConsents?: UserConsentRepresentation[]; + clientRoles?: Record; + credentials?: CredentialRepresentation[]; + email?: string; + federatedIdentities?: FederatedIdentityRepresentation[]; + federationLink?: string; + firstName?: string; + groups?: string[]; + lastName?: string; + origin?: string; + realmRoles?: string[]; + self?: string; + serviceAccountClientId?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/userSessionRepresentation.ts b/libs/keycloak-admin-client/src/defs/userSessionRepresentation.ts new file mode 100644 index 0000000000..7611fa29ae --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/userSessionRepresentation.ts @@ -0,0 +1,9 @@ +export default interface UserSessionRepresentation { + id?: string; + clients?: Record; + ipAddress?: string; + lastAccess?: number; + start?: number; + userId?: string; + username?: string; +} diff --git a/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts b/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts new file mode 100644 index 0000000000..1086f93a82 --- /dev/null +++ b/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts @@ -0,0 +1,29 @@ +export type AccessType = + | "view-realm" + | "view-identity-providers" + | "manage-identity-providers" + | "impersonation" + | "create-client" + | "manage-users" + | "query-realms" + | "view-authorization" + | "query-clients" + | "query-users" + | "manage-events" + | "manage-realm" + | "view-events" + | "view-users" + | "view-clients" + | "manage-authorization" + | "manage-clients" + | "query-groups" + | "anyone"; + +export default interface WhoAmIRepresentation { + userId: string; + realm: string; + displayName: string; + locale: string; + createRealm: boolean; + realm_access: { [key: string]: AccessType[] }; +} diff --git a/libs/keycloak-admin-client/src/index.ts b/libs/keycloak-admin-client/src/index.ts new file mode 100644 index 0000000000..03b575c6a1 --- /dev/null +++ b/libs/keycloak-admin-client/src/index.ts @@ -0,0 +1,5 @@ +import { RequiredActionAlias } from "./defs/requiredActionProviderRepresentation.js"; +import { KeycloakAdminClient } from "./client.js"; + +export const requiredAction = RequiredActionAlias; +export default KeycloakAdminClient; diff --git a/libs/keycloak-admin-client/src/resources/agent.ts b/libs/keycloak-admin-client/src/resources/agent.ts new file mode 100644 index 0000000000..d3550a4886 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/agent.ts @@ -0,0 +1,275 @@ +import axios, { AxiosRequestConfig, AxiosRequestHeaders, Method } from "axios"; +import { isUndefined, last, omit, pick } from "lodash-es"; +import urlJoin from "url-join"; +import { parseTemplate } from "url-template"; +import type { KeycloakAdminClient } from "../client.js"; +import { stringifyQueryParams } from "../utils/stringifyQueryParams.js"; + +// constants +const SLASH = "/"; + +// interface +export interface RequestArgs { + method: Method; + path?: string; + // Keys of url params to be applied + urlParamKeys?: string[]; + // Keys of query parameters to be applied + queryParamKeys?: string[]; + // Mapping of key transformations to be performed on the payload + keyTransform?: Record; + // If responding with 404, catch it and return null instead + catchNotFound?: boolean; + // The key of the value to use from the payload of request. Only works for POST & PUT. + payloadKey?: string; + // Whether the response header have a location field with newly created resource id + // if this value is set, we return the field with format: {[field]: resourceId} + // to represent the newly created resource + // detail: keycloak/keycloak-nodejs-admin-client issue #11 + returnResourceIdInLocationHeader?: { field: string }; + /** + * Keys to be ignored, meaning that they will not be filtered out of the request payload even if they are a part of `urlParamKeys` or `queryParamKeys`, + */ + ignoredKeys?: string[]; + headers?: AxiosRequestHeaders; +} + +export class Agent { + private client: KeycloakAdminClient; + private basePath: string; + private getBaseParams?: () => Record; + private getBaseUrl?: () => string; + + constructor({ + client, + path = "/", + getUrlParams = () => ({}), + getBaseUrl = () => client.baseUrl, + }: { + client: KeycloakAdminClient; + path?: string; + getUrlParams?: () => Record; + getBaseUrl?: () => string; + }) { + this.client = client; + this.getBaseParams = getUrlParams; + this.getBaseUrl = getBaseUrl; + this.basePath = path; + } + + public request({ + method, + path = "", + urlParamKeys = [], + queryParamKeys = [], + catchNotFound = false, + keyTransform, + payloadKey, + returnResourceIdInLocationHeader, + ignoredKeys, + headers, + }: RequestArgs) { + return async ( + payload: any = {}, + options?: Pick + ) => { + const baseParams = this.getBaseParams?.() ?? {}; + + // Filter query parameters by queryParamKeys + const queryParams = queryParamKeys ? pick(payload, queryParamKeys) : null; + + // Add filtered payload parameters to base parameters + const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys]; + const urlParams = { ...baseParams, ...pick(payload, allUrlParamKeys) }; + + // Omit url parameters and query parameters from payload + const omittedKeys = ignoredKeys + ? [...allUrlParamKeys, ...queryParamKeys].filter( + (key) => !ignoredKeys.includes(key) + ) + : [...allUrlParamKeys, ...queryParamKeys]; + + payload = omit(payload, omittedKeys); + + // Transform keys of both payload and queryParams + if (keyTransform) { + this.transformKey(payload, keyTransform); + this.transformKey(queryParams, keyTransform); + } + + return this.requestWithParams({ + method, + path, + payload, + urlParams, + queryParams, + // catchNotFound precedence: global > local > default + catchNotFound, + ...(this.client.getGlobalRequestArgOptions() ?? options ?? {}), + payloadKey, + returnResourceIdInLocationHeader, + headers, + }); + }; + } + + public updateRequest({ + method, + path = "", + urlParamKeys = [], + queryParamKeys = [], + catchNotFound = false, + keyTransform, + payloadKey, + returnResourceIdInLocationHeader, + headers, + }: RequestArgs) { + return async (query: any = {}, payload: any = {}) => { + const baseParams = this.getBaseParams?.() ?? {}; + + // Filter query parameters by queryParamKeys + const queryParams = queryParamKeys ? pick(query, queryParamKeys) : null; + + // Add filtered query parameters to base parameters + const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys]; + const urlParams = { + ...baseParams, + ...pick(query, allUrlParamKeys), + }; + + // Transform keys of queryParams + if (keyTransform) { + this.transformKey(queryParams, keyTransform); + } + + return this.requestWithParams({ + method, + path, + payload, + urlParams, + queryParams, + catchNotFound, + payloadKey, + returnResourceIdInLocationHeader, + headers, + }); + }; + } + + private async requestWithParams({ + method, + path, + payload, + urlParams, + queryParams, + catchNotFound, + payloadKey, + returnResourceIdInLocationHeader, + headers, + }: { + method: Method; + path: string; + payload: any; + urlParams: any; + queryParams?: Record | null; + catchNotFound: boolean; + payloadKey?: string; + returnResourceIdInLocationHeader?: { field: string }; + headers?: AxiosRequestHeaders; + }) { + const newPath = urlJoin(this.basePath, path); + + // Parse template and replace with values from urlParams + const pathTemplate = parseTemplate(newPath); + const parsedPath = pathTemplate.expand(urlParams); + const url = `${this.getBaseUrl?.() ?? ""}${parsedPath}`; + + // Prepare request config + const requestConfig: AxiosRequestConfig = { + paramsSerializer: (params) => stringifyQueryParams(params), + ...(this.client.getRequestConfig() || {}), + method, + url, + }; + + // Headers + requestConfig.headers = { + ...requestConfig.headers, + Authorization: `bearer ${await this.client.getAccessToken()}`, + ...headers, + }; + + // Put payload into querystring if method is GET + if (method === "GET") { + requestConfig.params = payload; + } else { + // Set the request data to the payload, or the value corresponding to the payloadKey, if it's defined + requestConfig.data = payloadKey ? payload[payloadKey] : payload; + } + + // Concat to existing queryParams + if (queryParams) { + requestConfig.params = requestConfig.params + ? { + ...requestConfig.params, + ...queryParams, + } + : queryParams; + } + + try { + const res = await axios.default(requestConfig); + // now we get the response of the http request + // if `resourceIdInLocationHeader` is true, we'll get the resourceId from the location header field + // todo: find a better way to find the id in path, maybe some kind of pattern matching + // for now, we simply split the last sub-path of the path returned in location header field + if (returnResourceIdInLocationHeader) { + const locationHeader = res.headers.location; + + if (typeof locationHeader !== "string") { + throw new Error( + `location header is not found in request: ${res.config.url}` + ); + } + + const resourceId = last(locationHeader.split(SLASH)); + if (!resourceId) { + // throw an error to let users know the response is not expected + throw new Error( + `resourceId is not found in Location header from request: ${res.config.url}` + ); + } + + // return with format {[field]: string} + const { field } = returnResourceIdInLocationHeader; + return { [field]: resourceId }; + } + return res.data; + } catch (err) { + if ( + axios.default.isAxiosError(err) && + err.response?.status === 404 && + catchNotFound + ) { + return null; + } + throw err; + } + } + + private transformKey(payload: any, keyMapping: Record) { + if (!payload) { + return; + } + + Object.keys(keyMapping).some((key) => { + if (isUndefined(payload[key])) { + // Skip if undefined + return false; + } + const newKey = keyMapping[key]; + payload[newKey] = payload[key]; + delete payload[key]; + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/attackDetection.ts b/libs/keycloak-admin-client/src/resources/attackDetection.ts new file mode 100644 index 0000000000..384608b564 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/attackDetection.ts @@ -0,0 +1,35 @@ +import Resource from "./resource.js"; +import type KeycloakAdminClient from "../index.js"; + +export class AttackDetection extends Resource<{ realm?: string }> { + public findOne = this.makeRequest< + { id: string }, + Record | undefined + >({ + method: "GET", + path: "/users/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public del = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/users/{id}", + urlParamKeys: ["id"], + }); + + public delAll = this.makeRequest<{}, void>({ + method: "DELETE", + path: "/users", + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/attack-detection/brute-force", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/authenticationManagement.ts b/libs/keycloak-admin-client/src/resources/authenticationManagement.ts new file mode 100644 index 0000000000..f7235b9d74 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/authenticationManagement.ts @@ -0,0 +1,286 @@ +import Resource from "./resource.js"; +import type RequiredActionProviderRepresentation from "../defs/requiredActionProviderRepresentation.js"; +import type { KeycloakAdminClient } from "../client.js"; +import type AuthenticationExecutionInfoRepresentation from "../defs/authenticationExecutionInfoRepresentation.js"; +import type AuthenticationFlowRepresentation from "../defs/authenticationFlowRepresentation.js"; +import type AuthenticatorConfigRepresentation from "../defs/authenticatorConfigRepresentation.js"; +import type { AuthenticationProviderRepresentation } from "../defs/authenticatorConfigRepresentation.js"; +import type AuthenticatorConfigInfoRepresentation from "../defs/authenticatorConfigInfoRepresentation.js"; +import type RequiredActionProviderSimpleRepresentation from "../defs/requiredActionProviderSimpleRepresentation.js"; + +export class AuthenticationManagement extends Resource { + /** + * Authentication Management + * https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource + */ + + // Register a new required action + public registerRequiredAction = this.makeRequest>({ + method: "POST", + path: "/register-required-action", + }); + + // Get required actions. Returns a list of required actions. + public getRequiredActions = this.makeRequest< + void, + RequiredActionProviderRepresentation[] + >({ + method: "GET", + path: "/required-actions", + }); + + // Get required action for alias + public getRequiredActionForAlias = this.makeRequest<{ + alias: string; + }>({ + method: "GET", + path: "/required-actions/{alias}", + urlParamKeys: ["alias"], + catchNotFound: true, + }); + + public getClientAuthenticatorProviders = this.makeRequest< + void, + AuthenticationProviderRepresentation[] + >({ + method: "GET", + path: "/client-authenticator-providers", + }); + + public getAuthenticatorProviders = this.makeRequest< + void, + AuthenticationProviderRepresentation[] + >({ + method: "GET", + path: "/authenticator-providers", + }); + + public getFormActionProviders = this.makeRequest< + void, + AuthenticationProviderRepresentation[] + >({ + method: "GET", + path: "/form-action-providers", + }); + + // Update required action + public updateRequiredAction = this.makeUpdateRequest< + { alias: string }, + RequiredActionProviderRepresentation, + void + >({ + method: "PUT", + path: "/required-actions/{alias}", + urlParamKeys: ["alias"], + }); + + // Delete required action + public deleteRequiredAction = this.makeRequest<{ alias: string }, void>({ + method: "DELETE", + path: "/required-actions/{alias}", + urlParamKeys: ["alias"], + }); + + // Lower required action’s priority + public lowerRequiredActionPriority = this.makeRequest<{ + alias: string; + }>({ + method: "POST", + path: "/required-actions/{alias}/lower-priority", + urlParamKeys: ["alias"], + }); + + // Raise required action’s priority + public raiseRequiredActionPriority = this.makeRequest<{ + alias: string; + }>({ + method: "POST", + path: "/required-actions/{alias}/raise-priority", + urlParamKeys: ["alias"], + }); + + // Get unregistered required actions Returns a list of unregistered required actions. + public getUnregisteredRequiredActions = this.makeRequest< + void, + RequiredActionProviderSimpleRepresentation[] + >({ + method: "GET", + path: "/unregistered-required-actions", + }); + + public getFlows = this.makeRequest<{}, AuthenticationFlowRepresentation[]>({ + method: "GET", + path: "/flows", + }); + + public getFlow = this.makeRequest< + { flowId: string }, + AuthenticationFlowRepresentation + >({ + method: "GET", + path: "/flows/{flowId}", + urlParamKeys: ["flowId"], + }); + + public getFormProviders = this.makeRequest< + void, + AuthenticationProviderRepresentation[] + >({ + method: "GET", + path: "/form-providers", + }); + + public createFlow = this.makeRequest< + AuthenticationFlowRepresentation, + AuthenticationFlowRepresentation + >({ + method: "POST", + path: "/flows", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public copyFlow = this.makeRequest<{ flow: string; newName: string }>({ + method: "POST", + path: "/flows/{flow}/copy", + urlParamKeys: ["flow"], + }); + + public deleteFlow = this.makeRequest<{ flowId: string }>({ + method: "DELETE", + path: "/flows/{flowId}", + urlParamKeys: ["flowId"], + }); + + public updateFlow = this.makeUpdateRequest< + { flowId: string }, + AuthenticationFlowRepresentation + >({ + method: "PUT", + path: "/flows/{flowId}", + urlParamKeys: ["flowId"], + }); + + public getExecutions = this.makeRequest< + { flow: string }, + AuthenticationExecutionInfoRepresentation[] + >({ + method: "GET", + path: "/flows/{flow}/executions", + urlParamKeys: ["flow"], + }); + + public addExecution = this.makeUpdateRequest< + { flow: string }, + AuthenticationExecutionInfoRepresentation + >({ + method: "POST", + path: "/flows/{flow}/executions", + urlParamKeys: ["flow"], + }); + + public addExecutionToFlow = this.makeRequest< + { flow: string; provider: string }, + AuthenticationExecutionInfoRepresentation + >({ + method: "POST", + path: "/flows/{flow}/executions/execution", + urlParamKeys: ["flow"], + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public addFlowToFlow = this.makeRequest< + { + flow: string; + alias: string; + type: string; + provider: string; + description: string; + }, + AuthenticationFlowRepresentation + >({ + method: "POST", + path: "/flows/{flow}/executions/flow", + urlParamKeys: ["flow"], + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public updateExecution = this.makeUpdateRequest< + { flow: string }, + AuthenticationExecutionInfoRepresentation + >({ + method: "PUT", + path: "/flows/{flow}/executions", + urlParamKeys: ["flow"], + }); + + public delExecution = this.makeRequest<{ id: string }>({ + method: "DELETE", + path: "/executions/{id}", + urlParamKeys: ["id"], + }); + + public lowerPriorityExecution = this.makeRequest<{ id: string }>({ + method: "POST", + path: "/executions/{id}/lower-priority", + urlParamKeys: ["id"], + }); + + public raisePriorityExecution = this.makeRequest<{ id: string }>({ + method: "POST", + path: "/executions/{id}/raise-priority", + urlParamKeys: ["id"], + }); + + public getConfigDescription = this.makeRequest< + { providerId: string }, + AuthenticatorConfigInfoRepresentation + >({ + method: "GET", + path: "config-description/{providerId}", + urlParamKeys: ["providerId"], + }); + + public createConfig = this.makeRequest< + AuthenticatorConfigRepresentation, + AuthenticatorConfigRepresentation + >({ + method: "POST", + path: "/executions/{id}/config", + urlParamKeys: ["id"], + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public updateConfig = this.makeRequest< + AuthenticatorConfigRepresentation, + void + >({ + method: "PUT", + path: "/config/{id}", + urlParamKeys: ["id"], + }); + + public getConfig = this.makeRequest< + { id: string }, + AuthenticatorConfigRepresentation + >({ + method: "GET", + path: "/config/{id}", + urlParamKeys: ["id"], + }); + + public delConfig = this.makeRequest<{ id: string }>({ + method: "DELETE", + path: "/config/{id}", + urlParamKeys: ["id"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/authentication", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/cache.ts b/libs/keycloak-admin-client/src/resources/cache.ts new file mode 100644 index 0000000000..ce6baf81d5 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/cache.ts @@ -0,0 +1,19 @@ +import Resource from "./resource.js"; +import type { KeycloakAdminClient } from "../client.js"; + +export class Cache extends Resource<{ realm?: string }> { + public clearUserCache = this.makeRequest<{}, void>({ + method: "POST", + path: "/clear-user-cache", + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/clientPolicies.ts b/libs/keycloak-admin-client/src/resources/clientPolicies.ts new file mode 100644 index 0000000000..23bb5b4393 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/clientPolicies.ts @@ -0,0 +1,50 @@ +import Resource from "./resource.js"; +import type { KeycloakAdminClient } from "../client.js"; +import type ClientProfilesRepresentation from "../defs/clientProfilesRepresentation.js"; +import type ClientPoliciesRepresentation from "../defs/clientPoliciesRepresentation.js"; + +/** + * https://www.keycloak.org/docs-api/15.0/rest-api/#_client_registration_policy_resource + */ +export class ClientPolicies extends Resource<{ realm?: string }> { + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/client-policies", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } + + /* Client Profiles */ + + public listProfiles = this.makeRequest< + { includeGlobalProfiles?: boolean }, + ClientProfilesRepresentation + >({ + method: "GET", + path: "/profiles", + queryParamKeys: ["include-global-profiles"], + keyTransform: { + includeGlobalProfiles: "include-global-profiles", + }, + }); + + public createProfiles = this.makeRequest({ + method: "PUT", + path: "/profiles", + }); + + /* Client Policies */ + + public listPolicies = this.makeRequest<{}, ClientPoliciesRepresentation>({ + method: "GET", + path: "/policies", + }); + + public updatePolicy = this.makeRequest({ + method: "PUT", + path: "/policies", + }); +} diff --git a/libs/keycloak-admin-client/src/resources/clientScopes.ts b/libs/keycloak-admin-client/src/resources/clientScopes.ts new file mode 100644 index 0000000000..e08f66415b --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/clientScopes.ts @@ -0,0 +1,336 @@ +import type ClientScopeRepresentation from "../defs/clientScopeRepresentation.js"; +import Resource from "./resource.js"; +import type { KeycloakAdminClient } from "../client.js"; +import type ProtocolMapperRepresentation from "../defs/protocolMapperRepresentation.js"; +import type MappingsRepresentation from "../defs/mappingsRepresentation.js"; +import type RoleRepresentation from "../defs/roleRepresentation.js"; + +export class ClientScopes extends Resource<{ realm?: string }> { + public find = this.makeRequest<{}, ClientScopeRepresentation[]>({ + method: "GET", + path: "/client-scopes", + }); + + public create = this.makeRequest({ + method: "POST", + path: "/client-scopes", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + /** + * Client-Scopes by id + */ + + public findOne = this.makeRequest< + { id: string }, + ClientScopeRepresentation | undefined + >({ + method: "GET", + path: "/client-scopes/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { id: string }, + ClientScopeRepresentation, + void + >({ + method: "PUT", + path: "/client-scopes/{id}", + urlParamKeys: ["id"], + }); + + public del = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/client-scopes/{id}", + urlParamKeys: ["id"], + }); + + /** + * Default Client-Scopes + */ + + public listDefaultClientScopes = this.makeRequest< + void, + ClientScopeRepresentation[] + >({ + method: "GET", + path: "/default-default-client-scopes", + }); + + public addDefaultClientScope = this.makeRequest<{ id: string }, void>({ + method: "PUT", + path: "/default-default-client-scopes/{id}", + urlParamKeys: ["id"], + }); + + public delDefaultClientScope = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/default-default-client-scopes/{id}", + urlParamKeys: ["id"], + }); + + /** + * Default Optional Client-Scopes + */ + + public listDefaultOptionalClientScopes = this.makeRequest< + void, + ClientScopeRepresentation[] + >({ + method: "GET", + path: "/default-optional-client-scopes", + }); + + public addDefaultOptionalClientScope = this.makeRequest<{ id: string }, void>( + { + method: "PUT", + path: "/default-optional-client-scopes/{id}", + urlParamKeys: ["id"], + } + ); + + public delDefaultOptionalClientScope = this.makeRequest<{ id: string }, void>( + { + method: "DELETE", + path: "/default-optional-client-scopes/{id}", + urlParamKeys: ["id"], + } + ); + + /** + * Protocol Mappers + */ + + public addMultipleProtocolMappers = this.makeUpdateRequest< + { id: string }, + ProtocolMapperRepresentation[], + void + >({ + method: "POST", + path: "/client-scopes/{id}/protocol-mappers/add-models", + urlParamKeys: ["id"], + }); + + public addProtocolMapper = this.makeUpdateRequest< + { id: string }, + ProtocolMapperRepresentation, + void + >({ + method: "POST", + path: "/client-scopes/{id}/protocol-mappers/models", + urlParamKeys: ["id"], + }); + + public listProtocolMappers = this.makeRequest< + { id: string }, + ProtocolMapperRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/protocol-mappers/models", + urlParamKeys: ["id"], + }); + + public findProtocolMapper = this.makeRequest< + { id: string; mapperId: string }, + ProtocolMapperRepresentation | undefined + >({ + method: "GET", + path: "/client-scopes/{id}/protocol-mappers/models/{mapperId}", + urlParamKeys: ["id", "mapperId"], + catchNotFound: true, + }); + + public findProtocolMappersByProtocol = this.makeRequest< + { id: string; protocol: string }, + ProtocolMapperRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/protocol-mappers/protocol/{protocol}", + urlParamKeys: ["id", "protocol"], + catchNotFound: true, + }); + + public updateProtocolMapper = this.makeUpdateRequest< + { id: string; mapperId: string }, + ProtocolMapperRepresentation, + void + >({ + method: "PUT", + path: "/client-scopes/{id}/protocol-mappers/models/{mapperId}", + urlParamKeys: ["id", "mapperId"], + }); + + public delProtocolMapper = this.makeRequest< + { id: string; mapperId: string }, + void + >({ + method: "DELETE", + path: "/client-scopes/{id}/protocol-mappers/models/{mapperId}", + urlParamKeys: ["id", "mapperId"], + }); + + /** + * Scope Mappings + */ + public listScopeMappings = this.makeRequest< + { id: string }, + MappingsRepresentation + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings", + urlParamKeys: ["id"], + }); + + public addClientScopeMappings = this.makeUpdateRequest< + { id: string; client: string }, + RoleRepresentation[], + void + >({ + method: "POST", + path: "/client-scopes/{id}/scope-mappings/clients/{client}", + urlParamKeys: ["id", "client"], + }); + + public listClientScopeMappings = this.makeRequest< + { id: string; client: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings/clients/{client}", + urlParamKeys: ["id", "client"], + }); + + public listAvailableClientScopeMappings = this.makeRequest< + { id: string; client: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings/clients/{client}/available", + urlParamKeys: ["id", "client"], + }); + + public listCompositeClientScopeMappings = this.makeRequest< + { id: string; client: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings/clients/{client}/composite", + urlParamKeys: ["id", "client"], + }); + + public delClientScopeMappings = this.makeUpdateRequest< + { id: string; client: string }, + RoleRepresentation[], + void + >({ + method: "DELETE", + path: "/client-scopes/{id}/scope-mappings/clients/{client}", + urlParamKeys: ["id", "client"], + }); + + public addRealmScopeMappings = this.makeUpdateRequest< + { id: string }, + RoleRepresentation[], + void + >({ + method: "POST", + path: "/client-scopes/{id}/scope-mappings/realm", + urlParamKeys: ["id"], + }); + + public listRealmScopeMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings/realm", + urlParamKeys: ["id"], + }); + + public listAvailableRealmScopeMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings/realm/available", + urlParamKeys: ["id"], + }); + + public listCompositeRealmScopeMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/client-scopes/{id}/scope-mappings/realm/composite", + urlParamKeys: ["id"], + }); + + public delRealmScopeMappings = this.makeUpdateRequest< + { id: string }, + RoleRepresentation[], + void + >({ + method: "DELETE", + path: "/client-scopes/{id}/scope-mappings/realm", + urlParamKeys: ["id"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } + + /** + * Find client scope by name. + */ + public async findOneByName(payload: { + realm?: string; + name: string; + }): Promise { + const allScopes = await this.find({ + ...(payload.realm ? { realm: payload.realm } : {}), + }); + return allScopes.find((item) => item.name === payload.name); + } + + /** + * Delete client scope by name. + */ + public async delByName(payload: { + realm?: string; + name: string; + }): Promise { + const scope = await this.findOneByName(payload); + + if (!scope) { + throw new Error("Scope not found."); + } + + await this.del({ + ...(payload.realm ? { realm: payload.realm } : {}), + id: scope.id!, + }); + } + + /** + * Find single protocol mapper by name. + */ + public async findProtocolMapperByName(payload: { + realm?: string; + id: string; + name: string; + }): Promise { + const allProtocolMappers = await this.listProtocolMappers({ + id: payload.id, + ...(payload.realm ? { realm: payload.realm } : {}), + }); + return allProtocolMappers.find((mapper) => mapper.name === payload.name); + } +} diff --git a/libs/keycloak-admin-client/src/resources/clients.ts b/libs/keycloak-admin-client/src/resources/clients.ts new file mode 100644 index 0000000000..8ea5f3c22d --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/clients.ts @@ -0,0 +1,1034 @@ +import type { KeycloakAdminClient } from "../client.js"; +import type CertificateRepresentation from "../defs/certificateRepresentation.js"; +import type ClientRepresentation from "../defs/clientRepresentation.js"; +import type ClientScopeRepresentation from "../defs/clientScopeRepresentation.js"; +import type CredentialRepresentation from "../defs/credentialRepresentation.js"; +import type GlobalRequestResult from "../defs/globalRequestResult.js"; +import type KeyStoreConfig from "../defs/keystoreConfig.js"; +import type { ManagementPermissionReference } from "../defs/managementPermissionReference.js"; +import type MappingsRepresentation from "../defs/mappingsRepresentation.js"; +import type PolicyEvaluationResponse from "../defs/policyEvaluationResponse.js"; +import type PolicyProviderRepresentation from "../defs/policyProviderRepresentation.js"; +import type PolicyRepresentation from "../defs/policyRepresentation.js"; +import type ProtocolMapperRepresentation from "../defs/protocolMapperRepresentation.js"; +import type ResourceEvaluation from "../defs/resourceEvaluation.js"; +import type ResourceRepresentation from "../defs/resourceRepresentation.js"; +import type ResourceServerRepresentation from "../defs/resourceServerRepresentation.js"; +import type RoleRepresentation from "../defs/roleRepresentation.js"; +import type ScopeRepresentation from "../defs/scopeRepresentation.js"; +import type UserRepresentation from "../defs/userRepresentation.js"; +import type UserSessionRepresentation from "../defs/userSessionRepresentation.js"; +import Resource from "./resource.js"; + +export interface PaginatedQuery { + first?: number; + max?: number; +} + +export interface ClientQuery extends PaginatedQuery { + clientId?: string; + viewableOnly?: boolean; + search?: boolean; +} + +export interface ResourceQuery extends PaginatedQuery { + id?: string; + name?: string; + type?: string; + owner?: string; + uri?: string; + deep?: boolean; +} + +export interface PolicyQuery extends PaginatedQuery { + id?: string; + name?: string; + type?: string; + resource?: string; + scope?: string; + permission?: string; + owner?: string; + fields?: string; +} + +export class Clients extends Resource<{ realm?: string }> { + public find = this.makeRequest({ + method: "GET", + }); + + public create = this.makeRequest({ + method: "POST", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + /** + * Single client + */ + + public findOne = this.makeRequest< + { id: string }, + ClientRepresentation | undefined + >({ + method: "GET", + path: "/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { id: string }, + ClientRepresentation, + void + >({ + method: "PUT", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public del = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/{id}", + urlParamKeys: ["id"], + }); + + /** + * Client roles + */ + + public createRole = this.makeRequest< + RoleRepresentation, + { roleName: string } + >({ + method: "POST", + path: "/{id}/roles", + urlParamKeys: ["id"], + returnResourceIdInLocationHeader: { field: "roleName" }, + }); + + public listRoles = this.makeRequest<{ id: string }, RoleRepresentation[]>({ + method: "GET", + path: "/{id}/roles", + urlParamKeys: ["id"], + }); + + public findRole = this.makeRequest< + { id: string; roleName: string }, + RoleRepresentation + >({ + method: "GET", + path: "/{id}/roles/{roleName}", + urlParamKeys: ["id", "roleName"], + catchNotFound: true, + }); + + public updateRole = this.makeUpdateRequest< + { id: string; roleName: string }, + RoleRepresentation, + void + >({ + method: "PUT", + path: "/{id}/roles/{roleName}", + urlParamKeys: ["id", "roleName"], + }); + + public delRole = this.makeRequest<{ id: string; roleName: string }, void>({ + method: "DELETE", + path: "/{id}/roles/{roleName}", + urlParamKeys: ["id", "roleName"], + }); + + public findUsersWithRole = this.makeRequest< + { id: string; roleName: string; first?: number; max?: number }, + UserRepresentation[] + >({ + method: "GET", + path: "/{id}/roles/{roleName}/users", + urlParamKeys: ["id", "roleName"], + }); + + /** + * Service account user + */ + + public getServiceAccountUser = this.makeRequest< + { id: string }, + UserRepresentation + >({ + method: "GET", + path: "/{id}/service-account-user", + urlParamKeys: ["id"], + }); + + /** + * Client secret + */ + + public generateNewClientSecret = this.makeRequest< + { id: string }, + CredentialRepresentation + >({ + method: "POST", + path: "/{id}/client-secret", + urlParamKeys: ["id"], + }); + + public invalidateSecret = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/{id}/client-secret/rotated", + urlParamKeys: ["id"], + }); + + public generateRegistrationAccessToken = this.makeRequest< + { id: string }, + { registrationAccessToken: string } + >({ + method: "POST", + path: "/{id}/registration-access-token", + urlParamKeys: ["id"], + }); + + public getClientSecret = this.makeRequest< + { id: string }, + CredentialRepresentation + >({ + method: "GET", + path: "/{id}/client-secret", + urlParamKeys: ["id"], + }); + + /** + * Client Scopes + */ + public listDefaultClientScopes = this.makeRequest< + { id: string }, + ClientScopeRepresentation[] + >({ + method: "GET", + path: "/{id}/default-client-scopes", + urlParamKeys: ["id"], + }); + + public addDefaultClientScope = this.makeRequest< + { id: string; clientScopeId: string }, + void + >({ + method: "PUT", + path: "/{id}/default-client-scopes/{clientScopeId}", + urlParamKeys: ["id", "clientScopeId"], + }); + + public delDefaultClientScope = this.makeRequest< + { id: string; clientScopeId: string }, + void + >({ + method: "DELETE", + path: "/{id}/default-client-scopes/{clientScopeId}", + urlParamKeys: ["id", "clientScopeId"], + }); + + public listOptionalClientScopes = this.makeRequest< + { id: string }, + ClientScopeRepresentation[] + >({ + method: "GET", + path: "/{id}/optional-client-scopes", + urlParamKeys: ["id"], + }); + + public addOptionalClientScope = this.makeRequest< + { id: string; clientScopeId: string }, + void + >({ + method: "PUT", + path: "/{id}/optional-client-scopes/{clientScopeId}", + urlParamKeys: ["id", "clientScopeId"], + }); + + public delOptionalClientScope = this.makeRequest< + { id: string; clientScopeId: string }, + void + >({ + method: "DELETE", + path: "/{id}/optional-client-scopes/{clientScopeId}", + urlParamKeys: ["id", "clientScopeId"], + }); + + /** + * Protocol Mappers + */ + + public addMultipleProtocolMappers = this.makeUpdateRequest< + { id: string }, + ProtocolMapperRepresentation[], + void + >({ + method: "POST", + path: "/{id}/protocol-mappers/add-models", + urlParamKeys: ["id"], + }); + + public addProtocolMapper = this.makeUpdateRequest< + { id: string }, + ProtocolMapperRepresentation, + void + >({ + method: "POST", + path: "/{id}/protocol-mappers/models", + urlParamKeys: ["id"], + }); + + public listProtocolMappers = this.makeRequest< + { id: string }, + ProtocolMapperRepresentation[] + >({ + method: "GET", + path: "/{id}/protocol-mappers/models", + urlParamKeys: ["id"], + }); + + public findProtocolMapperById = this.makeRequest< + { id: string; mapperId: string }, + ProtocolMapperRepresentation + >({ + method: "GET", + path: "/{id}/protocol-mappers/models/{mapperId}", + urlParamKeys: ["id", "mapperId"], + catchNotFound: true, + }); + + public findProtocolMappersByProtocol = this.makeRequest< + { id: string; protocol: string }, + ProtocolMapperRepresentation[] + >({ + method: "GET", + path: "/{id}/protocol-mappers/protocol/{protocol}", + urlParamKeys: ["id", "protocol"], + catchNotFound: true, + }); + + public updateProtocolMapper = this.makeUpdateRequest< + { id: string; mapperId: string }, + ProtocolMapperRepresentation, + void + >({ + method: "PUT", + path: "/{id}/protocol-mappers/models/{mapperId}", + urlParamKeys: ["id", "mapperId"], + }); + + public delProtocolMapper = this.makeRequest< + { id: string; mapperId: string }, + void + >({ + method: "DELETE", + path: "/{id}/protocol-mappers/models/{mapperId}", + urlParamKeys: ["id", "mapperId"], + }); + + /** + * Scope Mappings + */ + public listScopeMappings = this.makeRequest< + { id: string }, + MappingsRepresentation + >({ + method: "GET", + path: "/{id}/scope-mappings", + urlParamKeys: ["id"], + }); + + public addClientScopeMappings = this.makeUpdateRequest< + { id: string; client: string }, + RoleRepresentation[], + void + >({ + method: "POST", + path: "/{id}/scope-mappings/clients/{client}", + urlParamKeys: ["id", "client"], + }); + + public listClientScopeMappings = this.makeRequest< + { id: string; client: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/scope-mappings/clients/{client}", + urlParamKeys: ["id", "client"], + }); + + public listAvailableClientScopeMappings = this.makeRequest< + { id: string; client: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/scope-mappings/clients/{client}/available", + urlParamKeys: ["id", "client"], + }); + + public listCompositeClientScopeMappings = this.makeRequest< + { id: string; client: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/scope-mappings/clients/{client}/composite", + urlParamKeys: ["id", "client"], + }); + + public delClientScopeMappings = this.makeUpdateRequest< + { id: string; client: string }, + RoleRepresentation[], + void + >({ + method: "DELETE", + path: "/{id}/scope-mappings/clients/{client}", + urlParamKeys: ["id", "client"], + }); + + public evaluatePermission = this.makeRequest< + { + id: string; + roleContainer: string; + type: "granted" | "not-granted"; + scope: string; + }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/evaluate-scopes/scope-mappings/{roleContainer}/{type}", + urlParamKeys: ["id", "roleContainer", "type"], + queryParamKeys: ["scope"], + }); + + public evaluateListProtocolMapper = this.makeRequest< + { + id: string; + scope: string; + }, + ProtocolMapperRepresentation[] + >({ + method: "GET", + path: "/{id}/evaluate-scopes/protocol-mappers", + urlParamKeys: ["id"], + queryParamKeys: ["scope"], + }); + + public evaluateGenerateAccessToken = this.makeRequest< + { id: string; scope: string; userId: string }, + Record + >({ + method: "GET", + path: "/{id}/evaluate-scopes/generate-example-access-token", + urlParamKeys: ["id"], + queryParamKeys: ["scope", "userId"], + }); + + public evaluateGenerateUserInfo = this.makeRequest< + { id: string; scope: string; userId: string }, + Record + >({ + method: "GET", + path: "/{id}/evaluate-scopes/generate-example-userinfo", + urlParamKeys: ["id"], + queryParamKeys: ["scope", "userId"], + }); + + public evaluateGenerateIdToken = this.makeRequest< + { id: string; scope: string; userId: string }, + Record + >({ + method: "GET", + path: "/{id}/evaluate-scopes/generate-example-id-token", + urlParamKeys: ["id"], + queryParamKeys: ["scope", "userId"], + }); + + public addRealmScopeMappings = this.makeUpdateRequest< + { id: string }, + RoleRepresentation[], + void + >({ + method: "POST", + path: "/{id}/scope-mappings/realm", + urlParamKeys: ["id", "client"], + }); + + public listRealmScopeMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/scope-mappings/realm", + urlParamKeys: ["id"], + }); + + public listAvailableRealmScopeMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/scope-mappings/realm/available", + urlParamKeys: ["id"], + }); + + public listCompositeRealmScopeMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/scope-mappings/realm/composite", + urlParamKeys: ["id"], + }); + + public delRealmScopeMappings = this.makeUpdateRequest< + { id: string }, + RoleRepresentation[], + void + >({ + method: "DELETE", + path: "/{id}/scope-mappings/realm", + urlParamKeys: ["id"], + }); + + /** + * Sessions + */ + public listSessions = this.makeRequest< + { id: string; first?: number; max?: number }, + UserSessionRepresentation[] + >({ + method: "GET", + path: "/{id}/user-sessions", + urlParamKeys: ["id"], + }); + + public listOfflineSessions = this.makeRequest< + { id: string; first?: number; max?: number }, + UserSessionRepresentation[] + >({ + method: "GET", + path: "/{id}/offline-sessions", + urlParamKeys: ["id"], + }); + + public getSessionCount = this.makeRequest<{ id: string }, { count: number }>({ + method: "GET", + path: "/{id}/session-count", + urlParamKeys: ["id"], + }); + + /** + * Resource + */ + + public getResourceServer = this.makeRequest< + { id: string }, + ResourceServerRepresentation + >({ + method: "GET", + path: "{id}/authz/resource-server", + urlParamKeys: ["id"], + }); + + public updateResourceServer = this.makeUpdateRequest< + { id: string }, + ResourceServerRepresentation, + void + >({ + method: "PUT", + path: "{id}/authz/resource-server", + urlParamKeys: ["id"], + }); + + public listResources = this.makeRequest< + ResourceQuery, + ResourceRepresentation[] + >({ + method: "GET", + path: "{id}/authz/resource-server/resource", + urlParamKeys: ["id"], + }); + + public createResource = this.makeUpdateRequest< + { id: string }, + ResourceRepresentation, + ResourceRepresentation + >({ + method: "POST", + path: "{id}/authz/resource-server/resource", + urlParamKeys: ["id"], + }); + + public getResource = this.makeRequest< + { id: string; resourceId: string }, + ResourceRepresentation + >({ + method: "GET", + path: "{id}/authz/resource-server/resource/{resourceId}", + urlParamKeys: ["id", "resourceId"], + }); + + public updateResource = this.makeUpdateRequest< + { id: string; resourceId: string }, + ResourceRepresentation, + void + >({ + method: "PUT", + path: "/{id}/authz/resource-server/resource/{resourceId}", + urlParamKeys: ["id", "resourceId"], + }); + + public delResource = this.makeRequest< + { id: string; resourceId: string }, + void + >({ + method: "DELETE", + path: "/{id}/authz/resource-server/resource/{resourceId}", + urlParamKeys: ["id", "resourceId"], + }); + + public importResource = this.makeUpdateRequest< + { id: string }, + ResourceServerRepresentation + >({ + method: "POST", + path: "/{id}/authz/resource-server/import", + urlParamKeys: ["id"], + }); + + public exportResource = this.makeRequest< + { id: string }, + ResourceServerRepresentation + >({ + method: "GET", + path: "/{id}/authz/resource-server/settings", + urlParamKeys: ["id"], + }); + + public evaluateResource = this.makeUpdateRequest< + { id: string }, + ResourceEvaluation, + PolicyEvaluationResponse + >({ + method: "POST", + path: "{id}/authz/resource-server/policy/evaluate", + urlParamKeys: ["id"], + }); + + /** + * Policy + */ + public listPolicies = this.makeRequest< + PolicyQuery, + PolicyRepresentation[] | "" + >({ + method: "GET", + path: "{id}/authz/resource-server/policy", + urlParamKeys: ["id"], + }); + + public findPolicyByName = this.makeRequest< + { id: string; name: string }, + PolicyRepresentation + >({ + method: "GET", + path: "{id}/authz/resource-server/policy/search", + urlParamKeys: ["id"], + }); + + public updatePolicy = this.makeUpdateRequest< + { id: string; type: string; policyId: string }, + PolicyRepresentation, + void + >({ + method: "PUT", + path: "/{id}/authz/resource-server/policy/{type}/{policyId}", + urlParamKeys: ["id", "type", "policyId"], + }); + + public createPolicy = this.makeUpdateRequest< + { id: string; type: string }, + PolicyRepresentation, + PolicyRepresentation + >({ + method: "POST", + path: "/{id}/authz/resource-server/policy/{type}", + urlParamKeys: ["id", "type"], + }); + + public findOnePolicy = this.makeRequest< + { id: string; type: string; policyId: string }, + void + >({ + method: "GET", + path: "/{id}/authz/resource-server/policy/{type}/{policyId}", + urlParamKeys: ["id", "type", "policyId"], + catchNotFound: true, + }); + + public listDependentPolicies = this.makeRequest< + { id: string; policyId: string }, + PolicyRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/policy/{policyId}/dependentPolicies", + urlParamKeys: ["id", "policyId"], + }); + + public delPolicy = this.makeRequest<{ id: string; policyId: string }, void>({ + method: "DELETE", + path: "{id}/authz/resource-server/policy/{policyId}", + urlParamKeys: ["id", "policyId"], + }); + + public listPolicyProviders = this.makeRequest< + { id: string }, + PolicyProviderRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/policy/providers", + urlParamKeys: ["id"], + }); + + public async createOrUpdatePolicy(payload: { + id: string; + policyName: string; + policy: PolicyRepresentation; + }): Promise { + const policyFound = await this.findPolicyByName({ + id: payload.id, + name: payload.policyName, + }); + if (policyFound) { + await this.updatePolicy( + { + id: payload.id, + policyId: policyFound.id!, + type: payload.policy.type!, + }, + payload.policy + ); + return this.findPolicyByName({ + id: payload.id, + name: payload.policyName, + }); + } else { + return this.createPolicy( + { id: payload.id, type: payload.policy.type! }, + payload.policy + ); + } + } + + /** + * Scopes + */ + public listAllScopes = this.makeRequest< + { id: string; name?: string; deep?: boolean } & PaginatedQuery, + ScopeRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/scope", + urlParamKeys: ["id"], + }); + + public listAllResourcesByScope = this.makeRequest< + { id: string; scopeId: string }, + ResourceRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/scope/{scopeId}/resources", + urlParamKeys: ["id", "scopeId"], + }); + + public listAllPermissionsByScope = this.makeRequest< + { id: string; scopeId: string }, + PolicyRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/scope/{scopeId}/permissions", + urlParamKeys: ["id", "scopeId"], + }); + + public listPermissionsByResource = this.makeRequest< + { id: string; resourceId: string }, + ResourceServerRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/resource/{resourceId}/permissions", + urlParamKeys: ["id", "resourceId"], + }); + + public listScopesByResource = this.makeRequest< + { id: string; resourceName: string }, + { id: string; name: string }[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/resource/{resourceName}/scopes", + urlParamKeys: ["id", "resourceName"], + }); + + public createAuthorizationScope = this.makeUpdateRequest< + { id: string }, + ScopeRepresentation + >({ + method: "POST", + path: "{id}/authz/resource-server/scope", + urlParamKeys: ["id"], + }); + + public updateAuthorizationScope = this.makeUpdateRequest< + { id: string; scopeId: string }, + ScopeRepresentation + >({ + method: "PUT", + path: "/{id}/authz/resource-server/scope/{scopeId}", + urlParamKeys: ["id", "scopeId"], + }); + + public getAuthorizationScope = this.makeRequest< + { id: string; scopeId: string }, + ScopeRepresentation + >({ + method: "GET", + path: "/{id}/authz/resource-server/scope/{scopeId}", + urlParamKeys: ["id", "scopeId"], + }); + + public delAuthorizationScope = this.makeRequest< + { id: string; scopeId: string }, + void + >({ + method: "DELETE", + path: "/{id}/authz/resource-server/scope/{scopeId}", + urlParamKeys: ["id", "scopeId"], + }); + + /** + * Permissions + */ + public findPermissions = this.makeRequest< + { + id: string; + name?: string; + resource?: string; + scope?: string; + } & PaginatedQuery, + PolicyRepresentation[] + >({ + method: "GET", + path: "{id}/authz/resource-server/permission", + urlParamKeys: ["id"], + }); + + public createPermission = this.makeUpdateRequest< + { id: string; type: string }, + PolicyRepresentation, + PolicyRepresentation + >({ + method: "POST", + path: "/{id}/authz/resource-server/permission/{type}", + urlParamKeys: ["id", "type"], + }); + + public updatePermission = this.makeUpdateRequest< + { id: string; type: string; permissionId: string }, + PolicyRepresentation, + void + >({ + method: "PUT", + path: "/{id}/authz/resource-server/permission/{type}/{permissionId}", + urlParamKeys: ["id", "type", "permissionId"], + }); + + public delPermission = this.makeRequest< + { id: string; type: string; permissionId: string }, + void + >({ + method: "DELETE", + path: "/{id}/authz/resource-server/permission/{type}/{permissionId}", + urlParamKeys: ["id", "type", "permissionId"], + }); + + public findOnePermission = this.makeRequest< + { id: string; type: string; permissionId: string }, + PolicyRepresentation | undefined + >({ + method: "GET", + path: "/{id}/authz/resource-server/permission/{type}/{permissionId}", + urlParamKeys: ["id", "type", "permissionId"], + }); + + public getAssociatedScopes = this.makeRequest< + { id: string; permissionId: string }, + { id: string; name: string }[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/policy/{permissionId}/scopes", + urlParamKeys: ["id", "permissionId"], + }); + + public getAssociatedResources = this.makeRequest< + { id: string; permissionId: string }, + { _id: string; name: string }[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/policy/{permissionId}/resources", + urlParamKeys: ["id", "permissionId"], + }); + + public getAssociatedPolicies = this.makeRequest< + { id: string; permissionId: string }, + PolicyRepresentation[] + >({ + method: "GET", + path: "/{id}/authz/resource-server/policy/{permissionId}/associatedPolicies", + urlParamKeys: ["id", "permissionId"], + }); + + public getOfflineSessionCount = this.makeRequest< + { id: string }, + { count: number } + >({ + method: "GET", + path: "/{id}/offline-session-count", + urlParamKeys: ["id"], + }); + + public getInstallationProviders = this.makeRequest< + { id: string; providerId: string }, + string + >({ + method: "GET", + path: "/{id}/installation/providers/{providerId}", + urlParamKeys: ["id", "providerId"], + }); + + public pushRevocation = this.makeRequest<{ id: string }, GlobalRequestResult>( + { + method: "POST", + path: "/{id}/push-revocation", + urlParamKeys: ["id"], + } + ); + + public addClusterNode = this.makeRequest<{ id: string; node: string }, void>({ + method: "POST", + path: "/{id}/nodes", + urlParamKeys: ["id"], + }); + + public deleteClusterNode = this.makeRequest< + { id: string; node: string }, + void + >({ + method: "DELETE", + path: "/{id}/nodes/{node}", + urlParamKeys: ["id", "node"], + }); + + public testNodesAvailable = this.makeRequest< + { id: string }, + GlobalRequestResult + >({ + method: "GET", + path: "/{id}/test-nodes-available", + urlParamKeys: ["id"], + }); + + public getKeyInfo = this.makeRequest< + { id: string; attr: string }, + CertificateRepresentation + >({ + method: "GET", + path: "/{id}/certificates/{attr}", + urlParamKeys: ["id", "attr"], + }); + + public generateKey = this.makeRequest< + { id: string; attr: string }, + CertificateRepresentation + >({ + method: "POST", + path: "/{id}/certificates/{attr}/generate", + urlParamKeys: ["id", "attr"], + }); + + public downloadKey = this.makeUpdateRequest< + { id: string; attr: string }, + KeyStoreConfig, + string + >({ + method: "POST", + path: "/{id}/certificates/{attr}/download", + urlParamKeys: ["id", "attr"], + }); + + public generateAndDownloadKey = this.makeUpdateRequest< + { id: string; attr: string }, + KeyStoreConfig, + string + >({ + method: "POST", + path: "/{id}/certificates/{attr}/generate-and-download", + urlParamKeys: ["id", "attr"], + }); + + public uploadKey = this.makeUpdateRequest<{ id: string; attr: string }, any>({ + method: "POST", + path: "/{id}/certificates/{attr}/upload", + urlParamKeys: ["id", "attr"], + }); + + public uploadCertificate = this.makeUpdateRequest< + { id: string; attr: string }, + any + >({ + method: "POST", + path: "/{id}/certificates/{attr}/upload-certificate", + urlParamKeys: ["id", "attr"], + }); + + public updateFineGrainPermission = this.makeUpdateRequest< + { id: string }, + ManagementPermissionReference, + ManagementPermissionReference + >({ + method: "PUT", + path: "/{id}/management/permissions", + urlParamKeys: ["id"], + }); + + public listFineGrainPermissions = this.makeRequest< + { id: string }, + ManagementPermissionReference + >({ + method: "GET", + path: "/{id}/management/permissions", + urlParamKeys: ["id"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/clients", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } + + /** + * Find single protocol mapper by name. + */ + public async findProtocolMapperByName(payload: { + realm?: string; + id: string; + name: string; + }): Promise { + const allProtocolMappers = await this.listProtocolMappers({ + id: payload.id, + ...(payload.realm ? { realm: payload.realm } : {}), + }); + return allProtocolMappers.find((mapper) => mapper.name === payload.name); + } +} diff --git a/libs/keycloak-admin-client/src/resources/components.ts b/libs/keycloak-admin-client/src/resources/components.ts new file mode 100644 index 0000000000..524fe12ab1 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/components.ts @@ -0,0 +1,72 @@ +import Resource from "./resource.js"; +import type ComponentRepresentation from "../defs/componentRepresentation.js"; +import type ComponentTypeRepresentation from "../defs/componentTypeRepresentation.js"; +import type { KeycloakAdminClient } from "../client.js"; + +export interface ComponentQuery { + name?: string; + parent?: string; + type?: string; +} + +export class Components extends Resource<{ realm?: string }> { + /** + * components + * https://www.keycloak.org/docs-api/11.0/rest-api/#_component_resource + */ + + public find = this.makeRequest({ + method: "GET", + }); + + public create = this.makeRequest({ + method: "POST", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public findOne = this.makeRequest< + { id: string }, + ComponentRepresentation | undefined + >({ + method: "GET", + path: "/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { id: string }, + ComponentRepresentation, + void + >({ + method: "PUT", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public del = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public listSubComponents = this.makeRequest< + { id: string; type: string }, + ComponentTypeRepresentation[] + >({ + method: "GET", + path: "/{id}/sub-component-types", + urlParamKeys: ["id"], + queryParamKeys: ["type"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/components", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/groups.ts b/libs/keycloak-admin-client/src/resources/groups.ts new file mode 100644 index 0000000000..f2de81dece --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/groups.ts @@ -0,0 +1,242 @@ +import type { KeycloakAdminClient } from "../client.js"; +import type GroupRepresentation from "../defs/groupRepresentation.js"; +import type { ManagementPermissionReference } from "../defs/managementPermissionReference.js"; +import type MappingsRepresentation from "../defs/mappingsRepresentation.js"; +import type RoleRepresentation from "../defs/roleRepresentation.js"; +import type { RoleMappingPayload } from "../defs/roleRepresentation.js"; +import type UserRepresentation from "../defs/userRepresentation.js"; +import Resource from "./resource.js"; + +export interface GroupQuery { + first?: number; + max?: number; + search?: string; + briefRepresentation?: boolean; +} + +export interface GroupCountQuery { + search?: string; + top?: boolean; +} + +export class Groups extends Resource<{ realm?: string }> { + public find = this.makeRequest({ + method: "GET", + }); + + public create = this.makeRequest({ + method: "POST", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + /** + * Single user + */ + + public findOne = this.makeRequest< + { id: string }, + GroupRepresentation | undefined + >({ + method: "GET", + path: "/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { id: string }, + GroupRepresentation, + void + >({ + method: "PUT", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public del = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public count = this.makeRequest({ + method: "GET", + path: "/count", + }); + + /** + * Set or create child. + * This will just set the parent if it exists. Create it and set the parent if the group doesn’t exist. + */ + + public setOrCreateChild = this.makeUpdateRequest< + { id: string }, + GroupRepresentation, + { id: string } + >({ + method: "POST", + path: "/{id}/children", + urlParamKeys: ["id"], + returnResourceIdInLocationHeader: { field: "id" }, + }); + + /** + * Members + */ + + public listMembers = this.makeRequest< + { id: string; first?: number; max?: number }, + UserRepresentation[] + >({ + method: "GET", + path: "/{id}/members", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + /** + * Role mappings + * https://www.keycloak.org/docs-api/11.0/rest-api/#_role_mapper_resource + */ + + public listRoleMappings = this.makeRequest< + { id: string }, + MappingsRepresentation + >({ + method: "GET", + path: "/{id}/role-mappings", + urlParamKeys: ["id"], + }); + + public addRealmRoleMappings = this.makeRequest< + { id: string; roles: RoleMappingPayload[] }, + void + >({ + method: "POST", + path: "/{id}/role-mappings/realm", + urlParamKeys: ["id"], + payloadKey: "roles", + }); + + public listRealmRoleMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/realm", + urlParamKeys: ["id"], + }); + + public delRealmRoleMappings = this.makeRequest< + { id: string; roles: RoleMappingPayload[] }, + void + >({ + method: "DELETE", + path: "/{id}/role-mappings/realm", + urlParamKeys: ["id"], + payloadKey: "roles", + }); + + public listAvailableRealmRoleMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/realm/available", + urlParamKeys: ["id"], + }); + + // Get effective realm-level role mappings This will recurse all composite roles to get the result. + public listCompositeRealmRoleMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/realm/composite", + urlParamKeys: ["id"], + }); + + /** + * Client role mappings + * https://www.keycloak.org/docs-api/11.0/rest-api/#_client_role_mappings_resource + */ + + public listClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/clients/{clientUniqueId}", + urlParamKeys: ["id", "clientUniqueId"], + }); + + public addClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string; roles: RoleMappingPayload[] }, + void + >({ + method: "POST", + path: "/{id}/role-mappings/clients/{clientUniqueId}", + urlParamKeys: ["id", "clientUniqueId"], + payloadKey: "roles", + }); + + public delClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string; roles: RoleMappingPayload[] }, + void + >({ + method: "DELETE", + path: "/{id}/role-mappings/clients/{clientUniqueId}", + urlParamKeys: ["id", "clientUniqueId"], + payloadKey: "roles", + }); + + public listAvailableClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/clients/{clientUniqueId}/available", + urlParamKeys: ["id", "clientUniqueId"], + }); + + public listCompositeClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/clients/{clientUniqueId}/composite", + urlParamKeys: ["id", "clientUniqueId"], + }); + + /** + * Authorization permissions + */ + public updatePermission = this.makeUpdateRequest< + { id: string }, + ManagementPermissionReference, + ManagementPermissionReference + >({ + method: "PUT", + path: "/{id}/management/permissions", + urlParamKeys: ["id"], + }); + + public listPermissions = this.makeRequest< + { id: string }, + ManagementPermissionReference + >({ + method: "GET", + path: "/{id}/management/permissions", + urlParamKeys: ["id"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/groups", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/identityProviders.ts b/libs/keycloak-admin-client/src/resources/identityProviders.ts new file mode 100644 index 0000000000..49f56000d9 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/identityProviders.ts @@ -0,0 +1,157 @@ +import type { KeycloakAdminClient } from "../client.js"; +import type IdentityProviderMapperRepresentation from "../defs/identityProviderMapperRepresentation.js"; +import type { IdentityProviderMapperTypeRepresentation } from "../defs/identityProviderMapperTypeRepresentation.js"; +import type IdentityProviderRepresentation from "../defs/identityProviderRepresentation.js"; +import type { ManagementPermissionReference } from "../defs/managementPermissionReference.js"; +import Resource from "./resource.js"; + +export class IdentityProviders extends Resource<{ realm?: string }> { + /** + * Identity provider + * https://www.keycloak.org/docs-api/11.0/rest-api/#_identity_providers_resource + */ + + public find = this.makeRequest<{}, IdentityProviderRepresentation[]>({ + method: "GET", + path: "/instances", + }); + + public create = this.makeRequest< + IdentityProviderRepresentation, + { id: string } + >({ + method: "POST", + path: "/instances", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public findOne = this.makeRequest< + { alias: string }, + IdentityProviderRepresentation | undefined + >({ + method: "GET", + path: "/instances/{alias}", + urlParamKeys: ["alias"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { alias: string }, + IdentityProviderRepresentation, + void + >({ + method: "PUT", + path: "/instances/{alias}", + urlParamKeys: ["alias"], + }); + + public del = this.makeRequest<{ alias: string }, void>({ + method: "DELETE", + path: "/instances/{alias}", + urlParamKeys: ["alias"], + }); + + public findFactory = this.makeRequest<{ providerId: string }, any>({ + method: "GET", + path: "/providers/{providerId}", + urlParamKeys: ["providerId"], + }); + + public findMappers = this.makeRequest< + { alias: string }, + IdentityProviderMapperRepresentation[] + >({ + method: "GET", + path: "/instances/{alias}/mappers", + urlParamKeys: ["alias"], + }); + + public findOneMapper = this.makeRequest< + { alias: string; id: string }, + IdentityProviderMapperRepresentation | undefined + >({ + method: "GET", + path: "/instances/{alias}/mappers/{id}", + urlParamKeys: ["alias", "id"], + catchNotFound: true, + }); + + public createMapper = this.makeRequest< + { + alias: string; + identityProviderMapper: IdentityProviderMapperRepresentation; + }, + { id: string } + >({ + method: "POST", + path: "/instances/{alias}/mappers", + urlParamKeys: ["alias"], + payloadKey: "identityProviderMapper", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + public updateMapper = this.makeUpdateRequest< + { alias: string; id: string }, + IdentityProviderMapperRepresentation, + void + >({ + method: "PUT", + path: "/instances/{alias}/mappers/{id}", + urlParamKeys: ["alias", "id"], + }); + + public delMapper = this.makeRequest<{ alias: string; id: string }, void>({ + method: "DELETE", + path: "/instances/{alias}/mappers/{id}", + urlParamKeys: ["alias", "id"], + }); + + public findMapperTypes = this.makeRequest< + { alias: string }, + Record + >({ + method: "GET", + path: "/instances/{alias}/mapper-types", + urlParamKeys: ["alias"], + }); + + public importFromUrl = this.makeRequest< + { + fromUrl: string; + providerId: string; + }, + Record + >({ + method: "POST", + path: "/import-config", + }); + + public updatePermission = this.makeUpdateRequest< + { alias: string }, + ManagementPermissionReference, + ManagementPermissionReference + >({ + method: "PUT", + path: "/instances/{alias}/management/permissions", + urlParamKeys: ["alias"], + }); + + public listPermissions = this.makeRequest< + { alias: string }, + ManagementPermissionReference + >({ + method: "GET", + path: "/instances/{alias}/management/permissions", + urlParamKeys: ["alias"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/identity-provider", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/realms.ts b/libs/keycloak-admin-client/src/resources/realms.ts new file mode 100644 index 0000000000..d78d6c6fc0 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/realms.ts @@ -0,0 +1,402 @@ +import Resource from "./resource.js"; +import type AdminEventRepresentation from "../defs/adminEventRepresentation.js"; +import type RealmRepresentation from "../defs/realmRepresentation.js"; +import type { + PartialImportRealmRepresentation, + PartialImportResponse, +} from "../defs/realmRepresentation.js"; +import type EventRepresentation from "../defs/eventRepresentation.js"; +import type EventType from "../defs/eventTypes.js"; +import type KeysMetadataRepresentation from "../defs/keyMetadataRepresentation.js"; +import type ClientInitialAccessPresentation from "../defs/clientInitialAccessPresentation.js"; +import type TestLdapConnectionRepresentation from "../defs/testLdapConnection.js"; + +import type { KeycloakAdminClient } from "../client.js"; +import type { RealmEventsConfigRepresentation } from "../defs/realmEventsConfigRepresentation.js"; +import type GlobalRequestResult from "../defs/globalRequestResult.js"; +import type GroupRepresentation from "../defs/groupRepresentation.js"; +import type { ManagementPermissionReference } from "../defs/managementPermissionReference.js"; +import type ComponentTypeRepresentation from "../defs/componentTypeRepresentation.js"; + +export class Realms extends Resource { + /** + * Realm + * https://www.keycloak.org/docs-api/11.0/rest-api/#_realms_admin_resource + */ + + public find = this.makeRequest< + { briefRepresentation?: boolean }, + RealmRepresentation[] + >({ + method: "GET", + }); + + public create = this.makeRequest({ + method: "POST", + returnResourceIdInLocationHeader: { field: "realmName" }, + }); + + public findOne = this.makeRequest< + { realm: string }, + RealmRepresentation | undefined + >({ + method: "GET", + path: "/{realm}", + urlParamKeys: ["realm"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { realm: string }, + RealmRepresentation, + void + >({ + method: "PUT", + path: "/{realm}", + urlParamKeys: ["realm"], + }); + + public del = this.makeRequest<{ realm: string }, void>({ + method: "DELETE", + path: "/{realm}", + urlParamKeys: ["realm"], + }); + + public partialImport = this.makeRequest< + { + realm: string; + rep: PartialImportRealmRepresentation; + }, + PartialImportResponse + >({ + method: "POST", + path: "/{realm}/partialImport", + urlParamKeys: ["realm"], + payloadKey: "rep", + }); + + public export = this.makeRequest< + { + realm: string; + exportClients?: boolean; + exportGroupsAndRoles?: boolean; + }, + RealmRepresentation + >({ + method: "POST", + path: "/{realm}/partial-export", + urlParamKeys: ["realm"], + queryParamKeys: ["exportClients", "exportGroupsAndRoles"], + }); + + public getDefaultGroups = this.makeRequest< + { realm: string }, + GroupRepresentation[] + >({ + method: "GET", + path: "/{realm}/default-groups", + urlParamKeys: ["realm"], + }); + + public addDefaultGroup = this.makeRequest<{ realm: string; id: string }>({ + method: "PUT", + path: "/{realm}/default-groups/{id}", + urlParamKeys: ["realm", "id"], + }); + + public removeDefaultGroup = this.makeRequest<{ realm: string; id: string }>({ + method: "DELETE", + path: "/{realm}/default-groups/{id}", + urlParamKeys: ["realm", "id"], + }); + + public getGroupByPath = this.makeRequest< + { path: string; realm: string }, + GroupRepresentation + >({ + method: "GET", + path: "/{realm}/group-by-path/{path}", + urlParamKeys: ["realm", "path"], + }); + + /** + * Get events Returns all events, or filters them based on URL query parameters listed here + */ + public findEvents = this.makeRequest< + { + realm: string; + client?: string; + dateFrom?: Date; + dateTo?: Date; + first?: number; + ipAddress?: string; + max?: number; + type?: EventType | EventType[]; + user?: string; + }, + EventRepresentation[] + >({ + method: "GET", + path: "/{realm}/events", + urlParamKeys: ["realm"], + queryParamKeys: [ + "client", + "dateFrom", + "dateTo", + "first", + "ipAddress", + "max", + "type", + "user", + ], + }); + + public getConfigEvents = this.makeRequest< + { realm: string }, + RealmEventsConfigRepresentation + >({ + method: "GET", + path: "/{realm}/events/config", + urlParamKeys: ["realm"], + }); + + public updateConfigEvents = this.makeUpdateRequest< + { realm: string }, + RealmEventsConfigRepresentation, + void + >({ + method: "PUT", + path: "/{realm}/events/config", + urlParamKeys: ["realm"], + }); + + public clearEvents = this.makeRequest<{ realm: string }, void>({ + method: "DELETE", + path: "/{realm}/events", + urlParamKeys: ["realm"], + }); + + public clearAdminEvents = this.makeRequest<{ realm: string }, void>({ + method: "DELETE", + path: "/{realm}/admin-events", + urlParamKeys: ["realm"], + }); + + public getClientRegistrationPolicyProviders = this.makeRequest< + { realm: string }, + ComponentTypeRepresentation[] + >({ + method: "GET", + path: "/{realm}/client-registration-policy/providers", + urlParamKeys: ["realm"], + }); + + public getClientsInitialAccess = this.makeRequest< + { realm: string }, + ClientInitialAccessPresentation[] + >({ + method: "GET", + path: "/{realm}/clients-initial-access", + urlParamKeys: ["realm"], + }); + + public createClientsInitialAccess = this.makeUpdateRequest< + { realm: string }, + { count?: number; expiration?: number }, + ClientInitialAccessPresentation + >({ + method: "POST", + path: "/{realm}/clients-initial-access", + urlParamKeys: ["realm"], + }); + + public delClientsInitialAccess = this.makeRequest< + { realm: string; id: string }, + void + >({ + method: "DELETE", + path: "/{realm}/clients-initial-access/{id}", + urlParamKeys: ["realm", "id"], + }); + + /** + * Remove a specific user session. + */ + public removeSession = this.makeRequest< + { realm: string; sessionId: string }, + void + >({ + method: "DELETE", + path: "/{realm}/sessions/{session}", + urlParamKeys: ["realm", "session"], + catchNotFound: true, + }); + + /** + * Get admin events Returns all admin events, or filters events based on URL query parameters listed here + */ + public findAdminEvents = this.makeRequest< + { + realm: string; + authClient?: string; + authIpAddress?: string; + authRealm?: string; + authUser?: string; + dateFrom?: Date; + dateTo?: Date; + first?: number; + max?: number; + operationTypes?: string; + resourcePath?: string; + resourceTypes?: string; + }, + AdminEventRepresentation[] + >({ + method: "GET", + path: "/{realm}/admin-events", + urlParamKeys: ["realm"], + queryParamKeys: [ + "authClient", + "authIpAddress", + "authRealm", + "authUser", + "dateFrom", + "dateTo", + "max", + "first", + "operationTypes", + "resourcePath", + "resourceTypes", + ], + }); + + /** + * Users management permissions + */ + public getUsersManagementPermissions = this.makeRequest< + { realm: string }, + ManagementPermissionReference + >({ + method: "GET", + path: "/{realm}/users-management-permissions", + urlParamKeys: ["realm"], + }); + + public updateUsersManagementPermissions = this.makeRequest< + { realm: string; enabled: boolean }, + ManagementPermissionReference + >({ + method: "PUT", + path: "/{realm}/users-management-permissions", + urlParamKeys: ["realm"], + }); + + /** + * Sessions + */ + public logoutAll = this.makeRequest<{ realm: string }, void>({ + method: "POST", + path: "/{realm}/logout-all", + urlParamKeys: ["realm"], + }); + + public deleteSession = this.makeRequest< + { realm: string; session: string }, + void + >({ + method: "DELETE", + path: "/{realm}/sessions/{session}", + urlParamKeys: ["realm", "session"], + }); + + public pushRevocation = this.makeRequest< + { realm: string }, + GlobalRequestResult + >({ + method: "POST", + path: "/{realm}/push-revocation", + urlParamKeys: ["realm"], + ignoredKeys: ["realm"], + }); + + public getKeys = this.makeRequest< + { realm: string }, + KeysMetadataRepresentation + >({ + method: "GET", + path: "/{realm}/keys", + urlParamKeys: ["realm"], + }); + + public testLDAPConnection = this.makeUpdateRequest< + { realm: string }, + TestLdapConnectionRepresentation + >({ + method: "POST", + path: "/{realm}/testLDAPConnection", + urlParamKeys: ["realm"], + }); + + public testSMTPConnection = this.makeUpdateRequest< + { realm: string }, + Record + >({ + method: "POST", + path: "/{realm}/testSMTPConnection", + urlParamKeys: ["realm"], + }); + + public ldapServerCapabilities = this.makeUpdateRequest< + { realm: string }, + TestLdapConnectionRepresentation + >({ + method: "POST", + path: "/{realm}/ldap-server-capabilities", + urlParamKeys: ["realm"], + }); + + public getRealmSpecificLocales = this.makeRequest< + { realm: string }, + string[] + >({ + method: "GET", + path: "/{realm}/localization", + urlParamKeys: ["realm"], + }); + + public getRealmLocalizationTexts = this.makeRequest< + { realm: string; selectedLocale: string; first?: number; max?: number }, + Record + >({ + method: "GET", + path: "/{realm}/localization/{selectedLocale}", + urlParamKeys: ["realm", "selectedLocale"], + }); + + public addLocalization = this.makeUpdateRequest< + { realm: string; selectedLocale: string; key: string }, + string, + void + >({ + method: "PUT", + path: "/{realm}/localization/{selectedLocale}/{key}", + urlParamKeys: ["realm", "selectedLocale", "key"], + headers: { "content-type": "text/plain" }, + }); + + public deleteRealmLocalizationTexts = this.makeRequest< + { realm: string; selectedLocale: string; key?: string }, + void + >({ + method: "DELETE", + path: "/{realm}/localization/{selectedLocale}/{key}", + urlParamKeys: ["realm", "selectedLocale", "key"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms", + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/resource.ts b/libs/keycloak-admin-client/src/resources/resource.ts new file mode 100644 index 0000000000..d72fa4f3f1 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/resource.ts @@ -0,0 +1,42 @@ +import type { KeycloakAdminClient } from "../client.js"; +import { Agent, RequestArgs } from "./agent.js"; + +export default class Resource { + private agent: Agent; + constructor( + client: KeycloakAdminClient, + settings: { + path?: string; + getUrlParams?: () => Record; + getBaseUrl?: () => string; + } = {} + ) { + this.agent = new Agent({ + client, + ...settings, + }); + } + + public makeRequest = ( + args: RequestArgs + ): (( + payload?: PayloadType & ParamType, + options?: Pick + ) => Promise) => { + return this.agent.request(args); + }; + + // update request will take three types: query, payload and response + public makeUpdateRequest = < + QueryType = any, + PayloadType = any, + ResponseType = any + >( + args: RequestArgs + ): (( + query: QueryType & ParamType, + payload: PayloadType + ) => Promise) => { + return this.agent.updateRequest(args); + }; +} diff --git a/libs/keycloak-admin-client/src/resources/roles.ts b/libs/keycloak-admin-client/src/resources/roles.ts new file mode 100644 index 0000000000..9a97c2b303 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/roles.ts @@ -0,0 +1,178 @@ +import Resource from "./resource.js"; +import type RoleRepresentation from "../defs/roleRepresentation.js"; +import type UserRepresentation from "../defs/userRepresentation.js"; +import type { KeycloakAdminClient } from "../client.js"; +import type { ManagementPermissionReference } from "../defs/managementPermissionReference.js"; + +export interface RoleQuery { + first?: number; + max?: number; + search?: string; + briefRepresentation?: boolean; +} + +export class Roles extends Resource<{ realm?: string }> { + /** + * Realm roles + */ + + public find = this.makeRequest({ + method: "GET", + path: "/roles", + }); + + public create = this.makeRequest({ + method: "POST", + path: "/roles", + returnResourceIdInLocationHeader: { field: "roleName" }, + }); + + /** + * Roles by name + */ + + public findOneByName = this.makeRequest< + { name: string }, + RoleRepresentation | undefined + >({ + method: "GET", + path: "/roles/{name}", + urlParamKeys: ["name"], + catchNotFound: true, + }); + + public updateByName = this.makeUpdateRequest< + { name: string }, + RoleRepresentation, + void + >({ + method: "PUT", + path: "/roles/{name}", + urlParamKeys: ["name"], + }); + + public delByName = this.makeRequest<{ name: string }, void>({ + method: "DELETE", + path: "/roles/{name}", + urlParamKeys: ["name"], + }); + + public findUsersWithRole = this.makeRequest< + { name: string; first?: number; max?: number }, + UserRepresentation[] + >({ + method: "GET", + path: "/roles/{name}/users", + urlParamKeys: ["name"], + catchNotFound: true, + }); + + /** + * Roles by id + */ + + public findOneById = this.makeRequest< + { id: string }, + RoleRepresentation | undefined + >({ + method: "GET", + path: "/roles-by-id/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public createComposite = this.makeUpdateRequest< + { roleId: string }, + RoleRepresentation[], + void + >({ + method: "POST", + path: "/roles-by-id/{roleId}/composites", + urlParamKeys: ["roleId"], + }); + + public getCompositeRoles = this.makeRequest< + { id: string; search?: string; first?: number; max?: number }, + RoleRepresentation[] + >({ + method: "GET", + path: "/roles-by-id/{id}/composites", + urlParamKeys: ["id"], + }); + + public getCompositeRolesForRealm = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/roles-by-id/{id}/composites/realm", + urlParamKeys: ["id"], + }); + + public getCompositeRolesForClient = this.makeRequest< + { id: string; clientId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/roles-by-id/{id}/composites/clients/{clientId}", + urlParamKeys: ["id", "clientId"], + }); + + public delCompositeRoles = this.makeUpdateRequest< + { id: string }, + RoleRepresentation[], + void + >({ + method: "DELETE", + path: "/roles-by-id/{id}/composites", + urlParamKeys: ["id"], + }); + + public updateById = this.makeUpdateRequest< + { id: string }, + RoleRepresentation, + void + >({ + method: "PUT", + path: "/roles-by-id/{id}", + urlParamKeys: ["id"], + }); + + public delById = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/roles-by-id/{id}", + urlParamKeys: ["id"], + }); + + /** + * Authorization permissions + */ + public updatePermission = this.makeUpdateRequest< + { id: string }, + ManagementPermissionReference, + ManagementPermissionReference + >({ + method: "PUT", + path: "/roles-by-id/{id}/management/permissions", + urlParamKeys: ["id"], + }); + + public listPermissions = this.makeRequest< + { id: string }, + ManagementPermissionReference + >({ + method: "GET", + path: "/roles-by-id/{id}/management/permissions", + urlParamKeys: ["id"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/serverInfo.ts b/libs/keycloak-admin-client/src/resources/serverInfo.ts new file mode 100644 index 0000000000..b4b027d0ff --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/serverInfo.ts @@ -0,0 +1,17 @@ +import Resource from "./resource.js"; +import type { ServerInfoRepresentation } from "../defs/serverInfoRepesentation.js"; +import type KeycloakAdminClient from "../index.js"; + +export class ServerInfo extends Resource { + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/serverinfo", + getBaseUrl: () => client.baseUrl, + }); + } + + public find = this.makeRequest<{}, ServerInfoRepresentation>({ + method: "GET", + path: "/", + }); +} diff --git a/libs/keycloak-admin-client/src/resources/sessions.ts b/libs/keycloak-admin-client/src/resources/sessions.ts new file mode 100644 index 0000000000..b9105411b6 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/sessions.ts @@ -0,0 +1,18 @@ +import Resource from "./resource.js"; +import type KeycloakAdminClient from "../index.js"; + +export class Sessions extends Resource<{ realm?: string }> { + public find = this.makeRequest<{}, Record[]>({ + method: "GET", + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/client-session-stats", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/userStorageProvider.ts b/libs/keycloak-admin-client/src/resources/userStorageProvider.ts new file mode 100644 index 0000000000..da130b4c2a --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/userStorageProvider.ts @@ -0,0 +1,60 @@ +import type { KeycloakAdminClient } from "../client.js"; +import type SynchronizationResultRepresentation from "../defs/synchronizationResultRepresentation.js"; +import Resource from "./resource.js"; + +type ActionType = "triggerFullSync" | "triggerChangedUsersSync"; +type DirectionType = "fedToKeycloak" | "keycloakToFed"; +type NameResponse = { + id: string; + name: string; +}; + +export class UserStorageProvider extends Resource<{ realm?: string }> { + public name = this.makeRequest<{ id: string }, NameResponse>({ + method: "GET", + path: "/{id}/name", + urlParamKeys: ["id"], + }); + + public removeImportedUsers = this.makeRequest<{ id: string }, void>({ + method: "POST", + path: "/{id}/remove-imported-users", + urlParamKeys: ["id"], + }); + + public sync = this.makeRequest< + { id: string; action?: ActionType }, + SynchronizationResultRepresentation + >({ + method: "POST", + path: "/{id}/sync", + urlParamKeys: ["id"], + queryParamKeys: ["action"], + }); + + public unlinkUsers = this.makeRequest<{ id: string }, void>({ + method: "POST", + path: "/{id}/unlink-users", + urlParamKeys: ["id"], + }); + + public mappersSync = this.makeRequest< + { id: string; parentId: string; direction?: DirectionType }, + SynchronizationResultRepresentation + >({ + method: "POST", + path: "/{parentId}/mappers/{id}/sync", + urlParamKeys: ["id", "parentId"], + queryParamKeys: ["direction"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/user-storage", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/users.ts b/libs/keycloak-admin-client/src/resources/users.ts new file mode 100644 index 0000000000..abcf30b053 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/users.ts @@ -0,0 +1,488 @@ +import Resource from "./resource.js"; +import type UserRepresentation from "../defs/userRepresentation.js"; +import type UserConsentRepresentation from "../defs/userConsentRepresentation.js"; +import type UserSessionRepresentation from "../defs/userSessionRepresentation.js"; +import type { KeycloakAdminClient } from "../client.js"; +import type MappingsRepresentation from "../defs/mappingsRepresentation.js"; +import type RoleRepresentation from "../defs/roleRepresentation.js"; +import type { RoleMappingPayload } from "../defs/roleRepresentation.js"; +import type { RequiredActionAlias } from "../defs/requiredActionProviderRepresentation.js"; +import type FederatedIdentityRepresentation from "../defs/federatedIdentityRepresentation.js"; +import type GroupRepresentation from "../defs/groupRepresentation.js"; +import type CredentialRepresentation from "../defs/credentialRepresentation.js"; +import type UserProfileConfig from "../defs/userProfileConfig.js"; + +export interface UserQuery { + email?: string; + first?: number; + firstName?: string; + lastName?: string; + max?: number; + search?: string; + username?: string; + exact?: boolean; + [key: string]: string | number | undefined | boolean; +} + +export class Users extends Resource<{ realm?: string }> { + public find = this.makeRequest({ + method: "GET", + }); + + public create = this.makeRequest({ + method: "POST", + returnResourceIdInLocationHeader: { field: "id" }, + }); + + /** + * Single user + */ + + public findOne = this.makeRequest< + { id: string }, + UserRepresentation | undefined + >({ + method: "GET", + path: "/{id}", + urlParamKeys: ["id"], + catchNotFound: true, + }); + + public update = this.makeUpdateRequest< + { id: string }, + UserRepresentation, + void + >({ + method: "PUT", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public del = this.makeRequest<{ id: string }, void>({ + method: "DELETE", + path: "/{id}", + urlParamKeys: ["id"], + }); + + public count = this.makeRequest({ + method: "GET", + path: "/count", + }); + + public getProfile = this.makeRequest<{}, UserProfileConfig>({ + method: "GET", + path: "/profile", + }); + + public updateProfile = this.makeRequest( + { + method: "PUT", + path: "/profile", + } + ); + + /** + * role mappings + */ + + public listRoleMappings = this.makeRequest< + { id: string }, + MappingsRepresentation + >({ + method: "GET", + path: "/{id}/role-mappings", + urlParamKeys: ["id"], + }); + + public addRealmRoleMappings = this.makeRequest< + { id: string; roles: RoleMappingPayload[] }, + void + >({ + method: "POST", + path: "/{id}/role-mappings/realm", + urlParamKeys: ["id"], + payloadKey: "roles", + }); + + public listRealmRoleMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/realm", + urlParamKeys: ["id"], + }); + + public delRealmRoleMappings = this.makeRequest< + { id: string; roles: RoleMappingPayload[] }, + void + >({ + method: "DELETE", + path: "/{id}/role-mappings/realm", + urlParamKeys: ["id"], + payloadKey: "roles", + }); + + public listAvailableRealmRoleMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/realm/available", + urlParamKeys: ["id"], + }); + + // Get effective realm-level role mappings This will recurse all composite roles to get the result. + public listCompositeRealmRoleMappings = this.makeRequest< + { id: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/realm/composite", + urlParamKeys: ["id"], + }); + + /** + * Client role mappings + * https://www.keycloak.org/docs-api/11.0/rest-api/#_client_role_mappings_resource + */ + + public listClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/clients/{clientUniqueId}", + urlParamKeys: ["id", "clientUniqueId"], + }); + + public addClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string; roles: RoleMappingPayload[] }, + void + >({ + method: "POST", + path: "/{id}/role-mappings/clients/{clientUniqueId}", + urlParamKeys: ["id", "clientUniqueId"], + payloadKey: "roles", + }); + + public delClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string; roles: RoleMappingPayload[] }, + void + >({ + method: "DELETE", + path: "/{id}/role-mappings/clients/{clientUniqueId}", + urlParamKeys: ["id", "clientUniqueId"], + payloadKey: "roles", + }); + + public listAvailableClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/clients/{clientUniqueId}/available", + urlParamKeys: ["id", "clientUniqueId"], + }); + + public listCompositeClientRoleMappings = this.makeRequest< + { id: string; clientUniqueId: string }, + RoleRepresentation[] + >({ + method: "GET", + path: "/{id}/role-mappings/clients/{clientUniqueId}/composite", + urlParamKeys: ["id", "clientUniqueId"], + }); + + /** + * Send a update account email to the user + * an email contains a link the user can click to perform a set of required actions. + */ + + public executeActionsEmail = this.makeRequest< + { + id: string; + clientId?: string; + lifespan?: number; + redirectUri?: string; + actions?: (RequiredActionAlias | string)[]; + }, + void + >({ + method: "PUT", + path: "/{id}/execute-actions-email", + urlParamKeys: ["id"], + payloadKey: "actions", + queryParamKeys: ["lifespan", "redirectUri", "clientId"], + headers: { "content-type": "application/json" }, + keyTransform: { + clientId: "client_id", + redirectUri: "redirect_uri", + }, + }); + + /** + * Group + */ + + public listGroups = this.makeRequest< + { id: string; briefRepresentation?: boolean }, + GroupRepresentation[] + >({ + method: "GET", + path: "/{id}/groups", + urlParamKeys: ["id"], + }); + + public addToGroup = this.makeRequest<{ id: string; groupId: string }, string>( + { + method: "PUT", + path: "/{id}/groups/{groupId}", + urlParamKeys: ["id", "groupId"], + } + ); + + public delFromGroup = this.makeRequest< + { id: string; groupId: string }, + string + >({ + method: "DELETE", + path: "/{id}/groups/{groupId}", + urlParamKeys: ["id", "groupId"], + }); + + public countGroups = this.makeRequest< + { id: string; search?: string }, + { count: number } + >({ + method: "GET", + path: "/{id}/groups/count", + urlParamKeys: ["id"], + }); + + /** + * Federated Identity + */ + + public listFederatedIdentities = this.makeRequest< + { id: string }, + FederatedIdentityRepresentation[] + >({ + method: "GET", + path: "/{id}/federated-identity", + urlParamKeys: ["id"], + }); + + public addToFederatedIdentity = this.makeRequest< + { + id: string; + federatedIdentityId: string; + federatedIdentity: FederatedIdentityRepresentation; + }, + void + >({ + method: "POST", + path: "/{id}/federated-identity/{federatedIdentityId}", + urlParamKeys: ["id", "federatedIdentityId"], + payloadKey: "federatedIdentity", + }); + + public delFromFederatedIdentity = this.makeRequest< + { id: string; federatedIdentityId: string }, + void + >({ + method: "DELETE", + path: "/{id}/federated-identity/{federatedIdentityId}", + urlParamKeys: ["id", "federatedIdentityId"], + }); + + /** + * remove totp + */ + public removeTotp = this.makeRequest<{ id: string }, void>({ + method: "PUT", + path: "/{id}/remove-totp", + urlParamKeys: ["id"], + }); + + /** + * reset password + */ + public resetPassword = this.makeRequest< + { id: string; credential: CredentialRepresentation }, + void + >({ + method: "PUT", + path: "/{id}/reset-password", + urlParamKeys: ["id"], + payloadKey: "credential", + }); + + public getUserStorageCredentialTypes = this.makeRequest< + { id: string }, + string[] + >({ + method: "GET", + path: "/{id}/configured-user-storage-credential-types", + urlParamKeys: ["id"], + }); + + /** + * get user credentials + */ + public getCredentials = this.makeRequest< + { id: string }, + CredentialRepresentation[] + >({ + method: "GET", + path: "/{id}/credentials", + urlParamKeys: ["id"], + }); + + /** + * delete user credentials + */ + public deleteCredential = this.makeRequest< + { id: string; credentialId: string }, + void + >({ + method: "DELETE", + path: "/{id}/credentials/{credentialId}", + urlParamKeys: ["id", "credentialId"], + }); + + /** + * update a credential label for a user + */ + public updateCredentialLabel = this.makeUpdateRequest< + { id: string; credentialId: string }, + string, + void + >({ + method: "PUT", + path: "/{id}/credentials/{credentialId}/userLabel", + urlParamKeys: ["id", "credentialId"], + headers: { "content-type": "text/plain" }, + }); + + // Move a credential to a position behind another credential + public moveCredentialPositionDown = this.makeRequest< + { + id: string; + credentialId: string; + newPreviousCredentialId: string; + }, + void + >({ + method: "POST", + path: "/{id}/credentials/{credentialId}/moveAfter/{newPreviousCredentialId}", + urlParamKeys: ["id", "credentialId", "newPreviousCredentialId"], + }); + + // Move a credential to a first position in the credentials list of the user + public moveCredentialPositionUp = this.makeRequest< + { + id: string; + credentialId: string; + }, + void + >({ + method: "POST", + path: "/{id}/credentials/{credentialId}/moveToFirst", + urlParamKeys: ["id", "credentialId"], + }); + + /** + * send verify email + */ + public sendVerifyEmail = this.makeRequest< + { id: string; clientId?: string; redirectUri?: string }, + void + >({ + method: "PUT", + path: "/{id}/send-verify-email", + urlParamKeys: ["id"], + queryParamKeys: ["clientId", "redirectUri"], + keyTransform: { + clientId: "client_id", + redirectUri: "redirect_uri", + }, + }); + + /** + * list user sessions + */ + public listSessions = this.makeRequest< + { id: string }, + UserSessionRepresentation[] + >({ + method: "GET", + path: "/{id}/sessions", + urlParamKeys: ["id"], + }); + + /** + * list offline sessions associated with the user and client + */ + public listOfflineSessions = this.makeRequest< + { id: string; clientId: string }, + UserSessionRepresentation[] + >({ + method: "GET", + path: "/{id}/offline-sessions/{clientId}", + urlParamKeys: ["id", "clientId"], + }); + + /** + * logout user from all sessions + */ + public logout = this.makeRequest<{ id: string }, void>({ + method: "POST", + path: "/{id}/logout", + urlParamKeys: ["id"], + }); + + /** + * list consents granted by the user + */ + public listConsents = this.makeRequest< + { id: string }, + UserConsentRepresentation[] + >({ + method: "GET", + path: "/{id}/consents", + urlParamKeys: ["id"], + }); + + public impersonation = this.makeUpdateRequest< + { id: string }, + { user: string; realm: string }, + Record + >({ + method: "POST", + path: "/{id}/impersonation", + urlParamKeys: ["id"], + }); + + /** + * revoke consent and offline tokens for particular client from user + */ + public revokeConsent = this.makeRequest< + { id: string; clientId: string }, + void + >({ + method: "DELETE", + path: "/{id}/consents/{clientId}", + urlParamKeys: ["id", "clientId"], + }); + + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/realms/{realm}/users", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } +} diff --git a/libs/keycloak-admin-client/src/resources/whoAmI.ts b/libs/keycloak-admin-client/src/resources/whoAmI.ts new file mode 100644 index 0000000000..f6013d33d5 --- /dev/null +++ b/libs/keycloak-admin-client/src/resources/whoAmI.ts @@ -0,0 +1,20 @@ +import type WhoAmIRepresentation from "../defs/whoAmIRepresentation.js"; +import type KeycloakAdminClient from "../index.js"; +import Resource from "./resource.js"; + +export class WhoAmI extends Resource<{ realm?: string }> { + constructor(client: KeycloakAdminClient) { + super(client, { + path: "/admin/{realm}/console", + getUrlParams: () => ({ + realm: client.realmName, + }), + getBaseUrl: () => client.baseUrl, + }); + } + + public find = this.makeRequest<{}, WhoAmIRepresentation>({ + method: "GET", + path: "/whoami", + }); +} diff --git a/libs/keycloak-admin-client/src/utils/auth.ts b/libs/keycloak-admin-client/src/utils/auth.ts new file mode 100644 index 0000000000..0ef7cb3d32 --- /dev/null +++ b/libs/keycloak-admin-client/src/utils/auth.ts @@ -0,0 +1,88 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; +import camelize from "camelize-ts"; +import { defaultBaseUrl, defaultRealm } from "./constants.js"; +import { stringifyQueryParams } from "./stringifyQueryParams.js"; + +export type GrantTypes = "client_credentials" | "password" | "refresh_token"; + +export interface Credentials { + username?: string; + password?: string; + grantType: GrantTypes; + clientId: string; + clientSecret?: string; + totp?: string; + offlineToken?: boolean; + refreshToken?: string; +} + +export interface Settings { + realmName?: string; + baseUrl?: string; + credentials: Credentials; + requestConfig?: AxiosRequestConfig; +} + +export interface TokenResponseRaw { + access_token: string; + expires_in: string; + refresh_expires_in: number; + refresh_token: string; + token_type: string; + not_before_policy: number; + session_state: string; + scope: string; +} + +export interface TokenResponse { + accessToken: string; + expiresIn: string; + refreshExpiresIn: number; + refreshToken: string; + tokenType: string; + notBeforePolicy: number; + sessionState: string; + scope: string; +} + +export const getToken = async (settings: Settings): Promise => { + // Construct URL + const baseUrl = settings.baseUrl || defaultBaseUrl; + const realmName = settings.realmName || defaultRealm; + const url = `${baseUrl}/realms/${realmName}/protocol/openid-connect/token`; + + // Prepare credentials for openid-connect token request + // ref: http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint + const credentials = settings.credentials || ({} as any); + const payload = stringifyQueryParams({ + username: credentials.username, + password: credentials.password, + grant_type: credentials.grantType, + client_id: credentials.clientId, + totp: credentials.totp, + ...(credentials.offlineToken ? { scope: "offline_access" } : {}), + ...(credentials.refreshToken + ? { + refresh_token: credentials.refreshToken, + client_secret: credentials.clientSecret, + } + : {}), + }); + + const config: AxiosRequestConfig = { + ...settings.requestConfig, + }; + + if (credentials.clientSecret) { + config.auth = { + username: credentials.clientId, + password: credentials.clientSecret, + }; + } + + const { data } = await axios.default.post< + any, + AxiosResponse + >(url, payload, config); + return camelize(data); +}; diff --git a/libs/keycloak-admin-client/src/utils/constants.ts b/libs/keycloak-admin-client/src/utils/constants.ts new file mode 100644 index 0000000000..951654fc6f --- /dev/null +++ b/libs/keycloak-admin-client/src/utils/constants.ts @@ -0,0 +1,3 @@ +export const defaultBaseUrl = "http://127.0.0.1:8180"; + +export const defaultRealm = "master"; diff --git a/libs/keycloak-admin-client/src/utils/stringifyQueryParams.ts b/libs/keycloak-admin-client/src/utils/stringifyQueryParams.ts new file mode 100644 index 0000000000..cb085d0810 --- /dev/null +++ b/libs/keycloak-admin-client/src/utils/stringifyQueryParams.ts @@ -0,0 +1,21 @@ +export function stringifyQueryParams(params: Record) { + return new URLSearchParams( + Object.entries(params).filter((param): param is [string, string] => { + const [, value] = param; + + if (typeof value === "undefined" || value === null) { + return false; + } + + if (typeof value === "string" && value.length === 0) { + return false; + } + + if (Array.isArray(value) && value.length === 0) { + return false; + } + + return true; + }) + ).toString(); +} diff --git a/libs/keycloak-admin-client/test/attackDetection.spec.ts b/libs/keycloak-admin-client/test/attackDetection.spec.ts new file mode 100644 index 0000000000..91c9ed463d --- /dev/null +++ b/libs/keycloak-admin-client/test/attackDetection.spec.ts @@ -0,0 +1,47 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type UserRepresentation from "../src/defs/userRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Attack Detection", () => { + let kcAdminClient: KeycloakAdminClient; + let currentUser: UserRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const username = faker.internet.userName(); + currentUser = await kcAdminClient.users.create({ + username, + }); + }); + + after(async () => { + await kcAdminClient.users.del({ id: currentUser.id! }); + }); + + it("list attack detection for user", async () => { + const attackDetection = await kcAdminClient.attackDetection.findOne({ + id: currentUser.id!, + }); + expect(attackDetection).to.deep.equal({ + numFailures: 0, + disabled: false, + lastIPFailure: "n/a", + lastFailure: 0, + }); + }); + + it("clear any user login failures for all users", async () => { + await kcAdminClient.attackDetection.delAll(); + }); + + it("clear any user login failures for a user", async () => { + await kcAdminClient.attackDetection.del({ id: currentUser.id! }); + }); +}); diff --git a/libs/keycloak-admin-client/test/auth.spec.ts b/libs/keycloak-admin-client/test/auth.spec.ts new file mode 100644 index 0000000000..f2e420cec8 --- /dev/null +++ b/libs/keycloak-admin-client/test/auth.spec.ts @@ -0,0 +1,24 @@ +import * as chai from "chai"; +import { getToken } from "../src/utils/auth.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Authorization", () => { + it("should get token from local keycloak", async () => { + const data = await getToken({ + credentials, + }); + + expect(data).to.have.all.keys( + "accessToken", + "expiresIn", + "refreshExpiresIn", + "refreshToken", + "tokenType", + "notBeforePolicy", + "sessionState", + "scope" + ); + }); +}); diff --git a/libs/keycloak-admin-client/test/authenticationManagement.spec.ts b/libs/keycloak-admin-client/test/authenticationManagement.spec.ts new file mode 100644 index 0000000000..29104c6583 --- /dev/null +++ b/libs/keycloak-admin-client/test/authenticationManagement.spec.ts @@ -0,0 +1,415 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import { fail } from "assert"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { RequiredActionAlias } from "../src/defs/requiredActionProviderRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Authentication management", () => { + let kcAdminClient: KeycloakAdminClient; + let currentRealm: string; + let requiredActionProvider: Record; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + const realmName = faker.internet.userName().toLowerCase(); + await kcAdminClient.realms.create({ + id: realmName, + realm: realmName, + enabled: true, + }); + currentRealm = realmName; + kcAdminClient.setConfig({ + realmName, + }); + }); + + after(async () => { + // delete test realm + await kcAdminClient.realms.del({ realm: currentRealm }); + const realm = await kcAdminClient.realms.findOne({ + realm: currentRealm, + }); + expect(realm).to.be.null; + }); + + /** + * Required Actions + */ + describe("Required Actions", () => { + it("should delete required action by alias", async () => { + await kcAdminClient.authenticationManagement.deleteRequiredAction({ + alias: RequiredActionAlias.UPDATE_PROFILE, + }); + }); + + it("should get unregistered required actions", async () => { + const unregisteredReqActions = + await kcAdminClient.authenticationManagement.getUnregisteredRequiredActions(); + expect(unregisteredReqActions).to.be.an("array"); + expect(unregisteredReqActions.length).to.be.least(1); + requiredActionProvider = unregisteredReqActions[0]; + }); + + it("should register new required action", async () => { + const requiredAction = + await kcAdminClient.authenticationManagement.registerRequiredAction({ + providerId: requiredActionProvider.providerId, + name: requiredActionProvider.name, + }); + expect(requiredAction).to.be.empty; + }); + + it("should get required actions", async () => { + const requiredActions = + await kcAdminClient.authenticationManagement.getRequiredActions(); + expect(requiredActions).to.be.an("array"); + }); + + it("should get required action by alias", async () => { + const requiredAction = + await kcAdminClient.authenticationManagement.getRequiredActionForAlias({ + alias: requiredActionProvider.providerId, + }); + expect(requiredAction).to.be.ok; + }); + + it("should update required action by alias", async () => { + const requiredAction = + await kcAdminClient.authenticationManagement.getRequiredActionForAlias({ + alias: requiredActionProvider.providerId, + }); + const response = + await kcAdminClient.authenticationManagement.updateRequiredAction( + { alias: requiredActionProvider.providerId }, + { + ...requiredAction, + enabled: true, + priority: 10, + } + ); + expect(response).to.be.empty; + }); + + it("should lower required action priority", async () => { + const requiredAction = + await kcAdminClient.authenticationManagement.getRequiredActionForAlias({ + alias: requiredActionProvider.providerId, + }); + const response = + await kcAdminClient.authenticationManagement.lowerRequiredActionPriority( + { alias: requiredActionProvider.providerId } + ); + expect(response).to.be.empty; + const requiredActionUpdated = + await kcAdminClient.authenticationManagement.getRequiredActionForAlias({ + alias: requiredActionProvider.providerId, + }); + expect(requiredActionUpdated.priority).to.be.greaterThan( + requiredAction.priority + ); + }); + + it("should raise required action priority", async () => { + const requiredAction = + await kcAdminClient.authenticationManagement.getRequiredActionForAlias({ + alias: requiredActionProvider.providerId, + }); + const response = + await kcAdminClient.authenticationManagement.raiseRequiredActionPriority( + { alias: requiredActionProvider.providerId } + ); + expect(response).to.be.empty; + const requiredActionUpdated = + await kcAdminClient.authenticationManagement.getRequiredActionForAlias({ + alias: requiredActionProvider.providerId, + }); + expect(requiredActionUpdated.priority).to.be.lessThan( + requiredAction.priority + ); + }); + + it("should get client authenticator providers", async () => { + const authenticationProviders = + await kcAdminClient.authenticationManagement.getClientAuthenticatorProviders(); + + expect(authenticationProviders).is.ok; + expect(authenticationProviders.length).to.be.equal(4); + }); + + it("should fetch form providers", async () => { + const formProviders = + await kcAdminClient.authenticationManagement.getFormActionProviders(); + expect(formProviders).is.ok; + expect(formProviders.length).to.be.eq(4); + }); + + it("should fetch authenticator providers", async () => { + const providers = + await kcAdminClient.authenticationManagement.getAuthenticatorProviders(); + expect(providers).is.ok; + expect(providers.length).to.be.greaterThan(1); + }); + }); + describe("Flows", () => { + it("should get the registered form providers", async () => { + const formProviders = + await kcAdminClient.authenticationManagement.getFormProviders(); + + expect(formProviders).to.be.ok; + expect(formProviders.length).to.be.eq(1); + expect(formProviders[0].displayName).to.be.eq("Registration Page"); + }); + + it("should get authentication flows", async () => { + const flows = await kcAdminClient.authenticationManagement.getFlows(); + + expect(flows.map((flow) => flow.alias)).to.be.deep.eq([ + "browser", + "direct grant", + "registration", + "reset credentials", + "clients", + "first broker login", + "docker auth", + "http challenge", + ]); + }); + + it("should get authentication flow", async () => { + const flows = await kcAdminClient.authenticationManagement.getFlows(); + const flow = await kcAdminClient.authenticationManagement.getFlow({ + flowId: flows[0].id!, + }); + + expect(flow.alias).to.be.eq("browser"); + }); + + it("should create new authentication flow", async () => { + const flow = "test"; + await kcAdminClient.authenticationManagement.createFlow({ + alias: flow, + providerId: "basic-flow", + description: "", + topLevel: true, + builtIn: false, + }); + + const flows = await kcAdminClient.authenticationManagement.getFlows(); + expect(flows.find((f) => f.alias === flow)).to.be.ok; + }); + + const flowName = "copy of browser"; + it("should copy existing authentication flow", async () => { + await kcAdminClient.authenticationManagement.copyFlow({ + flow: "browser", + newName: flowName, + }); + + const flows = await kcAdminClient.authenticationManagement.getFlows(); + const flow = flows.find((f) => f.alias === flowName); + expect(flow).to.be.ok; + }); + + it("should update authentication flow", async () => { + const flows = await kcAdminClient.authenticationManagement.getFlows(); + const flow = flows.find((f) => f.alias === flowName)!; + const description = "Updated description"; + flow.description = description; + const updatedFlow = + await kcAdminClient.authenticationManagement.updateFlow( + { flowId: flow.id! }, + flow + ); + + expect(updatedFlow.description).to.be.eq(description); + }); + + it("should delete authentication flow", async () => { + let flows = await kcAdminClient.authenticationManagement.getFlows(); + const flow = flows.find((f) => f.alias === flowName)!; + await kcAdminClient.authenticationManagement.deleteFlow({ + flowId: flow.id!, + }); + + flows = await kcAdminClient.authenticationManagement.getFlows(); + expect(flows.find((f) => f.alias === flowName)).to.be.undefined; + }); + }); + describe("Flow executions", () => { + it("should fetch all executions for a flow", async () => { + const executions = + await kcAdminClient.authenticationManagement.getExecutions({ + flow: "browser", + }); + expect(executions.length).to.be.gt(5); + }); + + const flowName = "executionTest"; + it("should add execution to a flow", async () => { + await kcAdminClient.authenticationManagement.copyFlow({ + flow: "browser", + newName: flowName, + }); + const execution = + await kcAdminClient.authenticationManagement.addExecutionToFlow({ + flow: flowName, + provider: "auth-otp-form", + }); + + expect(execution.id).to.be.ok; + }); + + it("should add flow to a flow", async () => { + const flow = await kcAdminClient.authenticationManagement.addFlowToFlow({ + flow: flowName, + alias: "subFlow", + description: "", + provider: "registration-page-form", + type: "basic-flow", + }); + const executions = + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + expect(flow.id).to.be.ok; + + expect(executions.map((execution) => execution.displayName)).includes( + "subFlow" + ); + }); + + it("should update execution to a flow", async () => { + let executions = + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + let execution = executions[executions.length - 1]; + const choice = execution.requirementChoices![1]; + execution.requirement = choice; + await kcAdminClient.authenticationManagement.updateExecution( + { flow: flowName }, + execution + ); + + executions = await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + execution = executions[executions.length - 1]; + + expect(execution.requirement).to.be.eq(choice); + }); + + it("should delete execution", async () => { + let executions = + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + const id = executions[0].id!; + await kcAdminClient.authenticationManagement.delExecution({ id }); + executions = await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + expect(executions.find((ex) => ex.id === id)).to.be.undefined; + }); + + it("should raise priority of execution", async () => { + let executions = + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + let execution = executions[executions.length - 1]; + const priority = execution.index!; + await kcAdminClient.authenticationManagement.raisePriorityExecution({ + id: execution.id!, + }); + + executions = await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + execution = executions.find((ex) => ex.id === execution.id)!; + + expect(execution.index).to.be.eq(priority - 1); + }); + + it("should lower priority of execution", async () => { + let executions = + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + let execution = executions[0]; + const priority = execution.index!; + await kcAdminClient.authenticationManagement.lowerPriorityExecution({ + id: execution.id!, + }); + + executions = await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }); + execution = executions.find((ex) => ex.id === execution.id)!; + + expect(execution.index).to.be.eq(priority + 1); + }); + + it("should create, update and delete config for execution", async () => { + const execution = ( + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }) + )[0]; + const alias = "test"; + let config = await kcAdminClient.authenticationManagement.createConfig({ + id: execution.id, + alias, + }); + config = await kcAdminClient.authenticationManagement.getConfig({ + id: config.id!, + }); + expect(config.alias).to.be.eq(alias); + + const extraConfig = { defaultProvider: "sdf" }; + await kcAdminClient.authenticationManagement.updateConfig({ + ...config, + config: extraConfig, + }); + config = await kcAdminClient.authenticationManagement.getConfig({ + id: config.id!, + }); + + expect(config.config!.defaultProvider).to.be.eq( + extraConfig.defaultProvider + ); + + await kcAdminClient.authenticationManagement.delConfig({ + id: config.id!, + }); + try { + await kcAdminClient.authenticationManagement.getConfig({ + id: config.id!, + }); + fail("should not find deleted config"); + } catch (error) { + // ignore + } + }); + + it("should fetch config description for execution", async () => { + const execution = ( + await kcAdminClient.authenticationManagement.getExecutions({ + flow: flowName, + }) + )[0]; + + const configDescription = + await kcAdminClient.authenticationManagement.getConfigDescription({ + providerId: execution.providerId!, + }); + expect(configDescription).is.ok; + expect(configDescription.providerId).to.be.eq(execution.providerId); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/clientPolicies.spec.ts b/libs/keycloak-admin-client/test/clientPolicies.spec.ts new file mode 100644 index 0000000000..149868299f --- /dev/null +++ b/libs/keycloak-admin-client/test/clientPolicies.spec.ts @@ -0,0 +1,58 @@ +// tslint:disable:no-unused-expression +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Client Policies", () => { + let kcAdminClient: KeycloakAdminClient; + const newPolicy = { + name: "new_test_policy", + }; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + }); + + it("creates/updates client policy", async () => { + const createdPolicy = await kcAdminClient.clientPolicies.updatePolicy({ + policies: [newPolicy], + }); + expect(createdPolicy).to.be.deep.eq(""); + }); + + it("lists client policy profiles", async () => { + const profiles = await kcAdminClient.clientPolicies.listProfiles({ + includeGlobalProfiles: true, + }); + expect(profiles).to.be.ok; + }); + + it("create client policy profiles", async () => { + const profiles = await kcAdminClient.clientPolicies.listProfiles({ + includeGlobalProfiles: true, + }); + const globalProfiles = profiles.globalProfiles; + const newClientProfiles = { + profiles: [ + { + name: "test", + executors: [], + }, + ], + globalProfiles, + }; + + const createdClientProfile = + await kcAdminClient.clientPolicies.createProfiles(newClientProfiles); + + expect(createdClientProfile).to.be.deep.eq(""); + }); + + it("lists client policy policies", async () => { + const policies = await kcAdminClient.clientPolicies.listPolicies(); + expect(policies).to.be.ok; + }); +}); diff --git a/libs/keycloak-admin-client/test/clientRegistrationPolicies.ts b/libs/keycloak-admin-client/test/clientRegistrationPolicies.ts new file mode 100644 index 0000000000..4663568ada --- /dev/null +++ b/libs/keycloak-admin-client/test/clientRegistrationPolicies.ts @@ -0,0 +1,20 @@ +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Client Registration Policies", () => { + let client: KeycloakAdminClient; + + before(async () => { + client = new KeycloakAdminClient(); + await client.auth(credentials); + }); + + it("list client registration policies", async () => { + const clientRegistrationPolicies = + await client.clientRegistrationPolicies.find(); + expect(clientRegistrationPolicies).to.be.ok; + }); +}); diff --git a/libs/keycloak-admin-client/test/clientScopes.spec.ts b/libs/keycloak-admin-client/test/clientScopes.spec.ts new file mode 100644 index 0000000000..3f3175a313 --- /dev/null +++ b/libs/keycloak-admin-client/test/clientScopes.spec.ts @@ -0,0 +1,663 @@ +// tslint:disable:no-unused-expression +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ClientRepresentation from "../src/defs/clientRepresentation.js"; +import type ClientScopeRepresentation from "../src/defs/clientScopeRepresentation.js"; +import type ProtocolMapperRepresentation from "../src/defs/protocolMapperRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Client Scopes", () => { + let kcAdminClient: KeycloakAdminClient; + let currentClientScope: ClientScopeRepresentation; + let currentClientScopeName: string; + let currentClient: ClientRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + }); + + beforeEach(async () => { + currentClientScopeName = "best-of-the-bests-scope"; + await kcAdminClient.clientScopes.create({ + name: currentClientScopeName, + }); + currentClientScope = (await kcAdminClient.clientScopes.findOneByName({ + name: currentClientScopeName, + }))!; + }); + + afterEach(async () => { + // cleanup default client scopes + try { + await kcAdminClient.clientScopes.delDefaultClientScope({ + id: currentClientScope.id!, + }); + } catch (e) { + // ignore + } + + // cleanup optional client scopes + try { + await kcAdminClient.clientScopes.delDefaultOptionalClientScope({ + id: currentClientScope.id!, + }); + } catch (e) { + // ignore + } + + // cleanup client scopes + try { + await kcAdminClient.clientScopes.delByName({ + name: currentClientScopeName, + }); + } catch (e) { + // ignore + } + }); + + it("list client scopes", async () => { + const scopes = await kcAdminClient.clientScopes.find(); + expect(scopes).to.be.ok; + }); + + it("create client scope and get by name", async () => { + // ensure that the scope does not exist + try { + await kcAdminClient.clientScopes.delByName({ + name: currentClientScopeName, + }); + } catch (e) { + // ignore + } + + await kcAdminClient.clientScopes.create({ + name: currentClientScopeName, + }); + + const scope = (await kcAdminClient.clientScopes.findOneByName({ + name: currentClientScopeName, + }))!; + expect(scope).to.be.ok; + expect(scope.name).to.equal(currentClientScopeName); + }); + + it("create client scope and return id", async () => { + // ensure that the scope does not exist + try { + await kcAdminClient.clientScopes.delByName({ + name: currentClientScopeName, + }); + } catch (e) { + // ignore + } + + const { id } = await kcAdminClient.clientScopes.create({ + name: currentClientScopeName, + }); + + const scope = (await kcAdminClient.clientScopes.findOne({ + id, + }))!; + expect(scope).to.be.ok; + expect(scope.name).to.equal(currentClientScopeName); + }); + + it("find scope by id", async () => { + const scope = await kcAdminClient.clientScopes.findOne({ + id: currentClientScope.id!, + }); + expect(scope).to.be.ok; + expect(scope).to.eql(currentClientScope); + }); + + it("find scope by name", async () => { + const scope = (await kcAdminClient.clientScopes.findOneByName({ + name: currentClientScopeName, + }))!; + expect(scope).to.be.ok; + expect(scope.name).to.eql(currentClientScopeName); + }); + + it("return null if scope not found by id", async () => { + const scope = await kcAdminClient.clientScopes.findOne({ + id: "I do not exist", + }); + expect(scope).to.be.null; + }); + + it("return null if scope not found by name", async () => { + const scope = await kcAdminClient.clientScopes.findOneByName({ + name: "I do not exist", + }); + expect(scope).to.be.undefined; + }); + + it.skip("update client scope", async () => { + const { id, description: oldDescription } = currentClientScope; + const description = "This scope is totally awesome."; + + await kcAdminClient.clientScopes.update({ id: id! }, { description }); + const updatedScope = (await kcAdminClient.clientScopes.findOne({ + id: id!, + }))!; + expect(updatedScope).to.be.ok; + expect(updatedScope).not.to.eql(currentClientScope); + expect(updatedScope.description).to.eq(description); + expect(updatedScope.description).not.to.eq(oldDescription); + }); + + it("delete single client scope by id", async () => { + await kcAdminClient.clientScopes.del({ + id: currentClientScope.id!, + }); + const scope = await kcAdminClient.clientScopes.findOne({ + id: currentClientScope.id!, + }); + expect(scope).not.to.be.ok; + }); + + it("delete single client scope by name", async () => { + await kcAdminClient.clientScopes.delByName({ + name: currentClientScopeName, + }); + const scope = await kcAdminClient.clientScopes.findOneByName({ + name: currentClientScopeName, + }); + expect(scope).not.to.be.ok; + }); + + describe("default client scope", () => { + it("list default client scopes", async () => { + const defaultClientScopes = + await kcAdminClient.clientScopes.listDefaultClientScopes(); + expect(defaultClientScopes).to.be.ok; + }); + + it("add default client scope", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addDefaultClientScope({ id: id! }); + + const defaultClientScopeList = + await kcAdminClient.clientScopes.listDefaultClientScopes(); + const defaultClientScope = defaultClientScopeList.find( + (scope) => scope.id === id + )!; + + expect(defaultClientScope).to.be.ok; + expect(defaultClientScope.id).to.equal(currentClientScope.id); + expect(defaultClientScope.name).to.equal(currentClientScope.name); + }); + + it("delete default client scope", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addDefaultClientScope({ id: id! }); + + await kcAdminClient.clientScopes.delDefaultClientScope({ id: id! }); + + const defaultClientScopeList = + await kcAdminClient.clientScopes.listDefaultClientScopes(); + const defaultClientScope = defaultClientScopeList.find( + (scope) => scope.id === id + ); + + expect(defaultClientScope).not.to.be.ok; + }); + }); + + describe("default optional client scopes", () => { + it("list default optional client scopes", async () => { + const defaultOptionalClientScopes = + await kcAdminClient.clientScopes.listDefaultOptionalClientScopes(); + expect(defaultOptionalClientScopes).to.be.ok; + }); + + it("add default optional client scope", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addDefaultOptionalClientScope({ + id: id!, + }); + + const defaultOptionalClientScopeList = + await kcAdminClient.clientScopes.listDefaultOptionalClientScopes(); + const defaultOptionalClientScope = defaultOptionalClientScopeList.find( + (scope) => scope.id === id + )!; + + expect(defaultOptionalClientScope).to.be.ok; + expect(defaultOptionalClientScope.id).to.eq(currentClientScope.id); + expect(defaultOptionalClientScope.name).to.eq(currentClientScope.name); + }); + + it("delete default optional client scope", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addDefaultOptionalClientScope({ + id: id!, + }); + await kcAdminClient.clientScopes.delDefaultOptionalClientScope({ + id: id!, + }); + + const defaultOptionalClientScopeList = + await kcAdminClient.clientScopes.listDefaultOptionalClientScopes(); + const defaultOptionalClientScope = defaultOptionalClientScopeList.find( + (scope) => scope.id === id + ); + + expect(defaultOptionalClientScope).not.to.be.ok; + }); + }); + + describe("protocol mappers", () => { + let dummyMapper: ProtocolMapperRepresentation; + + beforeEach(() => { + dummyMapper = { + name: "mapping-maps-mapper", + protocol: "openid-connect", + protocolMapper: "oidc-audience-mapper", + }; + }); + + afterEach(async () => { + try { + const { id } = currentClientScope; + const { id: mapperId } = + (await kcAdminClient.clientScopes.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + await kcAdminClient.clientScopes.delProtocolMapper({ + id: id!, + mapperId: mapperId!, + }); + } catch (e) { + // ignore + } + }); + + it("list protocol mappers", async () => { + const { id } = currentClientScope; + const mapperList = await kcAdminClient.clientScopes.listProtocolMappers({ + id: id!, + }); + expect(mapperList).to.be.ok; + }); + + it("add multiple protocol mappers", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addMultipleProtocolMappers({ id: id! }, [ + dummyMapper, + ]); + + const mapper = (await kcAdminClient.clientScopes.findProtocolMapperByName( + { + id: id!, + name: dummyMapper.name!, + } + ))!; + expect(mapper).to.be.ok; + expect(mapper.protocol).to.eq(dummyMapper.protocol); + expect(mapper.protocolMapper).to.eq(dummyMapper.protocolMapper); + }); + + it("add single protocol mapper", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addProtocolMapper( + { id: id! }, + dummyMapper + ); + + const mapper = (await kcAdminClient.clientScopes.findProtocolMapperByName( + { + id: id!, + name: dummyMapper.name!, + } + ))!; + expect(mapper).to.be.ok; + expect(mapper.protocol).to.eq(dummyMapper.protocol); + expect(mapper.protocolMapper).to.eq(dummyMapper.protocolMapper); + }); + + it("find protocol mapper by id", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addProtocolMapper( + { id: id! }, + dummyMapper + ); + + const { id: mapperId } = + (await kcAdminClient.clientScopes.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + const mapper = await kcAdminClient.clientScopes.findProtocolMapper({ + id: id!, + mapperId: mapperId!, + }); + + expect(mapper).to.be.ok; + expect(mapper?.id).to.eql(mapperId); + }); + + it("find protocol mapper by name", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addProtocolMapper( + { id: id! }, + dummyMapper + ); + + const mapper = (await kcAdminClient.clientScopes.findProtocolMapperByName( + { + id: id!, + name: dummyMapper.name!, + } + ))!; + + expect(mapper).to.be.ok; + expect(mapper.name).to.eql(dummyMapper.name); + }); + + it("find protocol mappers by protocol", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addProtocolMapper( + { id: id! }, + dummyMapper + ); + + const mapperList = + await kcAdminClient.clientScopes.findProtocolMappersByProtocol({ + id: id!, + protocol: dummyMapper.protocol!, + }); + + expect(mapperList).to.be.ok; + expect(mapperList.length).to.be.gte(1); + + const mapper = mapperList.find((item) => item.name === dummyMapper.name); + expect(mapper).to.be.ok; + }); + + it("update protocol mapper", async () => { + const { id } = currentClientScope; + + dummyMapper.config = { "access.token.claim": "true" }; + await kcAdminClient.clientScopes.addProtocolMapper( + { id: id! }, + dummyMapper + ); + const mapper = (await kcAdminClient.clientScopes.findProtocolMapperByName( + { + id: id!, + name: dummyMapper.name!, + } + ))!; + + expect(mapper.config!["access.token.claim"]).to.eq("true"); + + mapper.config = { "access.token.claim": "false" }; + + await kcAdminClient.clientScopes.updateProtocolMapper( + { id: id!, mapperId: mapper.id! }, + mapper + ); + + const updatedMapper = + (await kcAdminClient.clientScopes.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + expect(updatedMapper.config!["access.token.claim"]).to.eq("false"); + }); + + it("delete protocol mapper", async () => { + const { id } = currentClientScope; + await kcAdminClient.clientScopes.addProtocolMapper( + { id: id! }, + dummyMapper + ); + + const { id: mapperId } = + (await kcAdminClient.clientScopes.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + await kcAdminClient.clientScopes.delProtocolMapper({ + id: id!, + mapperId: mapperId!, + }); + + const mapper = await kcAdminClient.clientScopes.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }); + + expect(mapper).not.to.be.ok; + }); + }); + + describe("scope mappings", () => { + it("list client and realm scope mappings", async () => { + const { id } = currentClientScope; + const scopes = await kcAdminClient.clientScopes.listScopeMappings({ + id: id!, + }); + expect(scopes).to.be.ok; + }); + + describe("client", () => { + const dummyClientId = "scopeMappings-dummy"; + const dummyRoleName = "scopeMappingsRole-dummy"; + + beforeEach(async () => { + const { id } = await kcAdminClient.clients.create({ + clientId: dummyClientId, + }); + currentClient = (await kcAdminClient.clients.findOne({ + id, + }))!; + + await kcAdminClient.clients.createRole({ + id, + name: dummyRoleName, + }); + }); + + afterEach(async () => { + const { id } = currentClient; + await kcAdminClient.clients.delRole({ + id: id!, + roleName: dummyRoleName, + }); + await kcAdminClient.clients.del({ id: id! }); + }); + + it("add scope mappings", async () => { + const { id } = currentClientScope; + const { id: clientUniqueId } = currentClient; + + const availableRoles = + await kcAdminClient.clientScopes.listAvailableClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + + const filteredRoles = availableRoles.filter((role) => !role.composite); + + await kcAdminClient.clientScopes.addClientScopeMappings( + { + id: id!, + client: clientUniqueId!, + }, + filteredRoles + ); + + const roles = await kcAdminClient.clientScopes.listClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + + expect(roles).to.be.ok; + expect(roles).to.be.eql(filteredRoles); + }); + + it("list scope mappings", async () => { + const { id } = currentClientScope; + const { id: clientUniqueId } = currentClient; + const roles = await kcAdminClient.clientScopes.listClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + expect(roles).to.be.ok; + }); + + it("list available scope mappings", async () => { + const { id } = currentClientScope; + const { id: clientUniqueId } = currentClient; + const roles = + await kcAdminClient.clientScopes.listAvailableClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + expect(roles).to.be.ok; + }); + + it("list composite scope mappings", async () => { + const { id } = currentClientScope; + const { id: clientUniqueId } = currentClient; + const roles = + await kcAdminClient.clientScopes.listCompositeClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + expect(roles).to.be.ok; + }); + + it("delete scope mappings", async () => { + const { id } = currentClientScope; + const { id: clientUniqueId } = currentClient; + + const rolesBefore = + await kcAdminClient.clientScopes.listClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + + await kcAdminClient.clientScopes.delClientScopeMappings( + { + id: id!, + client: clientUniqueId!, + }, + rolesBefore + ); + + const rolesAfter = + await kcAdminClient.clientScopes.listClientScopeMappings({ + id: id!, + client: clientUniqueId!, + }); + + expect(rolesAfter).to.be.ok; + expect(rolesAfter).to.eql([]); + }); + }); + + describe("realm", () => { + const dummyRoleName = "realmScopeMappingsRole-dummy"; + + beforeEach(async () => { + await kcAdminClient.roles.create({ + name: dummyRoleName, + }); + }); + + afterEach(async () => { + try { + await kcAdminClient.roles.delByName({ + name: dummyRoleName, + }); + } catch (e) { + // ignore + } + }); + + it("add scope mappings", async () => { + const { id } = currentClientScope; + + const availableRoles = + await kcAdminClient.clientScopes.listAvailableRealmScopeMappings({ + id: id!, + }); + + const filteredRoles = availableRoles.filter((role) => !role.composite); + + await kcAdminClient.clientScopes.addRealmScopeMappings( + { id: id! }, + filteredRoles + ); + + const roles = await kcAdminClient.clientScopes.listRealmScopeMappings({ + id: id!, + }); + + expect(roles).to.be.ok; + expect(roles).to.include.deep.members(filteredRoles); + }); + + it("list scope mappings", async () => { + const { id } = currentClientScope; + const roles = await kcAdminClient.clientScopes.listRealmScopeMappings({ + id: id!, + }); + expect(roles).to.be.ok; + }); + + it("list available scope mappings", async () => { + const { id } = currentClientScope; + const roles = + await kcAdminClient.clientScopes.listAvailableRealmScopeMappings({ + id: id!, + }); + expect(roles).to.be.ok; + }); + + it("list composite scope mappings", async () => { + const { id } = currentClientScope; + const roles = + await kcAdminClient.clientScopes.listCompositeRealmScopeMappings({ + id: id!, + }); + expect(roles).to.be.ok; + }); + + it("delete scope mappings", async () => { + const { id } = currentClientScope; + + const rolesBefore = + await kcAdminClient.clientScopes.listRealmScopeMappings({ + id: id!, + }); + + await kcAdminClient.clientScopes.delRealmScopeMappings( + { + id: id!, + }, + rolesBefore + ); + + const rolesAfter = + await kcAdminClient.clientScopes.listRealmScopeMappings({ + id: id!, + }); + + expect(rolesAfter).to.be.ok; + expect(rolesAfter).to.eql([]); + }); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/clients.spec.ts b/libs/keycloak-admin-client/test/clients.spec.ts new file mode 100644 index 0000000000..8d8490e944 --- /dev/null +++ b/libs/keycloak-admin-client/test/clients.spec.ts @@ -0,0 +1,1308 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ClientRepresentation from "../src/defs/clientRepresentation.js"; +import type ClientScopeRepresentation from "../src/defs/clientScopeRepresentation.js"; +import type PolicyRepresentation from "../src/defs/policyRepresentation.js"; +import { Logic } from "../src/defs/policyRepresentation.js"; +import type ProtocolMapperRepresentation from "../src/defs/protocolMapperRepresentation.js"; +import type ResourceRepresentation from "../src/defs/resourceRepresentation.js"; +import type ScopeRepresentation from "../src/defs/scopeRepresentation.js"; +import type UserRepresentation from "../src/defs/userRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Clients", () => { + let kcAdminClient: KeycloakAdminClient; + let currentClient: ClientRepresentation; + let currentClientScope: ClientScopeRepresentation; + let currentRoleName: string; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + // create client and also test it + // NOTICE: to be clear, clientId stands for the property `clientId` of client + // clientUniqueId stands for property `id` of client + const clientId = faker.internet.userName(); + const createdClient = await kcAdminClient.clients.create({ + clientId, + }); + expect(createdClient.id).to.be.ok; + + const client = await kcAdminClient.clients.findOne({ + id: createdClient.id, + }); + expect(client).to.be.ok; + currentClient = client!; + }); + + after(async () => { + // delete the current one + await kcAdminClient.clients.del({ + id: currentClient.id!, + }); + }); + + it("list clients", async () => { + const clients = await kcAdminClient.clients.find(); + expect(clients).to.be.ok; + }); + + it("get single client", async () => { + const clientUniqueId = currentClient.id!; + const client = await kcAdminClient.clients.findOne({ + id: clientUniqueId, + }); + // not sure why entity from list api will not have property: authorizationServicesEnabled + expect(client).to.deep.include(currentClient); + }); + + it("update single client", async () => { + const { clientId, id: clientUniqueId } = currentClient; + await kcAdminClient.clients.update( + { id: clientUniqueId! }, + { + // clientId is required in client update. no idea why... + clientId, + description: "test", + } + ); + + const client = await kcAdminClient.clients.findOne({ + id: clientUniqueId!, + }); + expect(client).to.include({ + description: "test", + }); + }); + + it("delete single client", async () => { + // create another one for delete test + const clientId = faker.internet.userName(); + const { id } = await kcAdminClient.clients.create({ + clientId, + }); + + // delete it + await kcAdminClient.clients.del({ + id, + }); + + const delClient = await kcAdminClient.clients.findOne({ + id, + }); + expect(delClient).to.be.null; + }); + + /** + * client roles + */ + describe("client roles", () => { + before(async () => { + const roleName = faker.internet.userName(); + // create a client role + const { roleName: createdRoleName } = + await kcAdminClient.clients.createRole({ + id: currentClient.id, + name: roleName, + }); + + expect(createdRoleName).to.be.equal(roleName); + + // assign currentClientRole + currentRoleName = roleName; + }); + + after(async () => { + // delete client role + await kcAdminClient.clients.delRole({ + id: currentClient.id!, + roleName: currentRoleName, + }); + }); + + it("list the client roles", async () => { + const roles = await kcAdminClient.clients.listRoles({ + id: currentClient.id!, + }); + + expect(roles[0]).to.include({ + name: currentRoleName, + }); + }); + + it("find the client role", async () => { + const role = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName: currentRoleName, + }); + + expect(role).to.include({ + name: currentRoleName, + clientRole: true, + containerId: currentClient.id, + }); + }); + + it("update the client role", async () => { + // NOTICE: roleName MUST be in the payload, no idea why... + const delta = { + name: currentRoleName, + description: "test", + }; + await kcAdminClient.clients.updateRole( + { + id: currentClient.id!, + roleName: currentRoleName, + }, + delta + ); + + // check the change + const role = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName: currentRoleName, + }); + + expect(role).to.include(delta); + }); + + it("delete a client role", async () => { + const roleName = faker.internet.userName(); + // create a client role + await kcAdminClient.clients.createRole({ + id: currentClient.id, + name: roleName, + }); + + // delete + await kcAdminClient.clients.delRole({ + id: currentClient.id!, + roleName, + }); + + // check it's null + const role = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName, + }); + + expect(role).to.be.null; + }); + }); + + describe("client secret", () => { + before(async () => { + const { clientId, id: clientUniqueId } = currentClient; + // update with serviceAccountsEnabled: true + await kcAdminClient.clients.update( + { + id: clientUniqueId!, + }, + { + clientId, + serviceAccountsEnabled: true, + } + ); + }); + + it("get client secret", async () => { + const credential = await kcAdminClient.clients.getClientSecret({ + id: currentClient.id!, + }); + + expect(credential).to.have.all.keys("type", "value"); + }); + + it("generate new client secret", async () => { + const newCredential = await kcAdminClient.clients.generateNewClientSecret( + { + id: currentClient.id!, + } + ); + + const credential = await kcAdminClient.clients.getClientSecret({ + id: currentClient.id!, + }); + + expect(newCredential).to.be.eql(credential); + }); + + it("generate new registration access token", async () => { + const newRegistrationAccessToken = + await kcAdminClient.clients.generateRegistrationAccessToken({ + id: currentClient.id!, + }); + + expect(newRegistrationAccessToken).to.be.ok; + }); + + it("invalidate rotation token", async () => { + await kcAdminClient.clients.invalidateSecret({ + id: currentClient.id!, + }); + }); + + it("get installation providers", async () => { + const installationProvider = + await kcAdminClient.clients.getInstallationProviders({ + id: currentClient.id!, + providerId: "keycloak-oidc-jboss-subsystem", + }); + expect(installationProvider).to.be.ok; + expect(typeof installationProvider).to.be.equal("string"); + }); + + it("get service account user", async () => { + const serviceAccountUser = + await kcAdminClient.clients.getServiceAccountUser({ + id: currentClient.id!, + }); + + expect(serviceAccountUser).to.be.ok; + }); + }); + + describe("default client scopes", () => { + let dummyClientScope: ClientScopeRepresentation; + + beforeEach(async () => { + dummyClientScope = { + name: "does-anyone-read-this", + description: "Oh - seems like you are reading Hey there!", + protocol: "openid-connect", + }; + + // setup dummy client scope + await kcAdminClient.clientScopes.create(dummyClientScope); + currentClientScope = (await kcAdminClient.clientScopes.findOneByName({ + name: dummyClientScope.name!, + }))!; + }); + + afterEach(async () => { + // cleanup default scopes + try { + const { id } = currentClient; + const { id: clientScopeId } = currentClientScope; + await kcAdminClient.clients.delDefaultClientScope({ + clientScopeId: clientScopeId!, + id: id!, + }); + } catch (e) { + // ignore + } + + // cleanup client scopes + try { + await kcAdminClient.clientScopes.delByName({ + name: dummyClientScope.name!, + }); + } catch (e) { + // ignore + } + }); + + it("list default client scopes", async () => { + const defaultClientScopes = + await kcAdminClient.clients.listDefaultClientScopes({ + id: currentClient.id!, + }); + + expect(defaultClientScopes).to.be.ok; + }); + + it("add default client scope", async () => { + const { id } = currentClient; + const { id: clientScopeId } = currentClientScope; + + await kcAdminClient.clients.addDefaultClientScope({ + id: id!, + clientScopeId: clientScopeId!, + }); + + const defaultScopes = await kcAdminClient.clients.listDefaultClientScopes( + { id: id! } + ); + + expect(defaultScopes).to.be.ok; + + const clientScope = defaultScopes.find( + (scope) => scope.id === clientScopeId + ); + expect(clientScope).to.be.ok; + }); + + it("delete default client scope", async () => { + const { id } = currentClient; + const { id: clientScopeId } = currentClientScope; + + await kcAdminClient.clients.addDefaultClientScope({ + id: id!, + clientScopeId: clientScopeId!, + }); + + await kcAdminClient.clients.delDefaultClientScope({ + id: id!, + clientScopeId: clientScopeId!, + }); + const defaultScopes = await kcAdminClient.clients.listDefaultClientScopes( + { id: id! } + ); + + const clientScope = defaultScopes.find( + (scope) => scope.id === clientScopeId + ); + expect(clientScope).not.to.be.ok; + }); + }); + + describe("optional client scopes", () => { + let dummyClientScope: ClientScopeRepresentation; + + beforeEach(async () => { + dummyClientScope = { + name: "i-hope-your-well", + description: "Everyone has that one friend.", + protocol: "openid-connect", + }; + + // setup dummy client scope + await kcAdminClient.clientScopes.create(dummyClientScope); + currentClientScope = (await kcAdminClient.clientScopes.findOneByName({ + name: dummyClientScope.name!, + }))!; + }); + + afterEach(async () => { + // cleanup optional scopes + try { + const { id } = currentClient; + const { id: clientScopeId } = currentClientScope; + await kcAdminClient.clients.delOptionalClientScope({ + clientScopeId: clientScopeId!, + id: id!, + }); + } catch (e) { + // ignore + } + + // cleanup client scopes + try { + await kcAdminClient.clientScopes.delByName({ + name: dummyClientScope.name!, + }); + } catch (e) { + // ignore + } + }); + + it("list optional client scopes", async () => { + const optionalClientScopes = + await kcAdminClient.clients.listOptionalClientScopes({ + id: currentClient.id!, + }); + + expect(optionalClientScopes).to.be.ok; + }); + + it("add optional client scope", async () => { + const { id } = currentClient; + const { id: clientScopeId } = currentClientScope; + + await kcAdminClient.clients.addOptionalClientScope({ + id: id!, + clientScopeId: clientScopeId!, + }); + + const optionalScopes = + await kcAdminClient.clients.listOptionalClientScopes({ id: id! }); + + expect(optionalScopes).to.be.ok; + + const clientScope = optionalScopes.find( + (scope) => scope.id === clientScopeId + ); + expect(clientScope).to.be.ok; + }); + + it("delete optional client scope", async () => { + const { id } = currentClient; + const { id: clientScopeId } = currentClientScope; + + await kcAdminClient.clients.addOptionalClientScope({ + id: id!, + clientScopeId: clientScopeId!, + }); + + await kcAdminClient.clients.delOptionalClientScope({ + id: id!, + clientScopeId: clientScopeId!, + }); + const optionalScopes = + await kcAdminClient.clients.listOptionalClientScopes({ id: id! }); + + const clientScope = optionalScopes.find( + (scope) => scope.id === clientScopeId + ); + expect(clientScope).not.to.be.ok; + }); + }); + + describe("protocol mappers", () => { + let dummyMapper: ProtocolMapperRepresentation; + + beforeEach(() => { + dummyMapper = { + name: "become-a-farmer", + protocol: "openid-connect", + protocolMapper: "oidc-role-name-mapper", + config: { + role: "admin", + "new.role.name": "farmer", + }, + }; + }); + + afterEach(async () => { + try { + const { id: clientUniqueId } = currentClient; + const { id: mapperId } = + (await kcAdminClient.clients.findProtocolMapperByName({ + id: clientUniqueId!, + name: dummyMapper.name!, + }))!; + await kcAdminClient.clients.delProtocolMapper({ + id: clientUniqueId!, + mapperId: mapperId!, + }); + } catch (e) { + // ignore + } + }); + + it("list protocol mappers", async () => { + const { id } = currentClient; + const mapperList = await kcAdminClient.clients.listProtocolMappers({ + id: id!, + }); + expect(mapperList).to.be.ok; + }); + + it("add multiple protocol mappers", async () => { + const { id } = currentClient; + await kcAdminClient.clients.addMultipleProtocolMappers({ id: id! }, [ + dummyMapper, + ]); + + const mapper = (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + expect(mapper).to.be.ok; + expect(mapper.protocol).to.eq(dummyMapper.protocol); + expect(mapper.protocolMapper).to.eq(dummyMapper.protocolMapper); + }); + + it("add single protocol mapper", async () => { + const { id } = currentClient; + await kcAdminClient.clients.addProtocolMapper({ id: id! }, dummyMapper); + + const mapper = (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + expect(mapper).to.be.ok; + expect(mapper.protocol).to.eq(dummyMapper.protocol); + expect(mapper.protocolMapper).to.eq(dummyMapper.protocolMapper); + }); + + it("find protocol mapper by id", async () => { + const { id } = currentClient; + await kcAdminClient.clients.addProtocolMapper({ id: id! }, dummyMapper); + + const { id: mapperId } = + (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + const mapper = await kcAdminClient.clients.findProtocolMapperById({ + mapperId: mapperId!, + id: id!, + }); + + expect(mapper).to.be.ok; + expect(mapper.id).to.eql(mapperId); + }); + + it("find protocol mapper by name", async () => { + const { id } = currentClient; + await kcAdminClient.clients.addProtocolMapper({ id: id! }, dummyMapper); + + const mapper = (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + expect(mapper).to.be.ok; + expect(mapper.name).to.eql(dummyMapper.name); + }); + + it("find protocol mappers by protocol", async () => { + const { id } = currentClient; + await kcAdminClient.clients.addProtocolMapper({ id: id! }, dummyMapper); + + const mapperList = + await kcAdminClient.clients.findProtocolMappersByProtocol({ + id: id!, + protocol: dummyMapper.protocol!, + }); + + expect(mapperList).to.be.ok; + expect(mapperList.length).to.be.gte(1); + + const mapper = mapperList.find((item) => item.name === dummyMapper.name); + expect(mapper).to.be.ok; + }); + + it("update protocol mapper", async () => { + const { id } = currentClient; + + dummyMapper.config = { "access.token.claim": "true" }; + await kcAdminClient.clients.addProtocolMapper({ id: id! }, dummyMapper); + const mapper = (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + expect(mapper.config!["access.token.claim"]).to.eq("true"); + + mapper.config = { "access.token.claim": "false" }; + + await kcAdminClient.clients.updateProtocolMapper( + { id: id!, mapperId: mapper.id! }, + mapper + ); + + const updatedMapper = + (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + expect(updatedMapper.config!["access.token.claim"]).to.eq("false"); + }); + + it("delete protocol mapper", async () => { + const { id } = currentClient; + await kcAdminClient.clients.addProtocolMapper({ id: id! }, dummyMapper); + + const { id: mapperId } = + (await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }))!; + + await kcAdminClient.clients.delProtocolMapper({ + id: id!, + mapperId: mapperId!, + }); + + const mapper = await kcAdminClient.clients.findProtocolMapperByName({ + id: id!, + name: dummyMapper.name!, + }); + + expect(mapper).not.to.be.ok; + }); + }); + + describe("scope mappings", () => { + it("list client and realm scope mappings", async () => { + const { id } = currentClient; + const scopes = await kcAdminClient.clients.listScopeMappings({ + id: id!, + }); + expect(scopes).to.be.ok; + }); + + describe("client", () => { + const dummyRoleName = "clientScopeMappingsRole-dummy"; + + beforeEach(async () => { + const { id } = currentClient; + await kcAdminClient.clients.createRole({ + id, + name: dummyRoleName, + }); + }); + + afterEach(async () => { + try { + const { id } = currentClient; + await kcAdminClient.clients.delRole({ + id: id!, + roleName: dummyRoleName, + }); + } catch (e) { + // ignore + } + }); + + it("add scope mappings", async () => { + const { id: clientUniqueId } = currentClient; + + const availableRoles = + await kcAdminClient.clients.listAvailableClientScopeMappings({ + id: clientUniqueId!, + client: clientUniqueId!, + }); + + await kcAdminClient.clients.addClientScopeMappings( + { + id: clientUniqueId!, + client: clientUniqueId!, + }, + availableRoles + ); + + const roles = await kcAdminClient.clients.listClientScopeMappings({ + id: clientUniqueId!, + client: clientUniqueId!, + }); + + expect(roles).to.be.ok; + expect(roles).to.be.eql(availableRoles); + }); + + it("list scope mappings", async () => { + const { id: clientUniqueId } = currentClient; + const roles = await kcAdminClient.clients.listClientScopeMappings({ + id: clientUniqueId!, + client: clientUniqueId!, + }); + expect(roles).to.be.ok; + }); + + it("list available scope mappings", async () => { + const { id: clientUniqueId } = currentClient; + const roles = + await kcAdminClient.clients.listAvailableClientScopeMappings({ + id: clientUniqueId!, + client: clientUniqueId!, + }); + expect(roles).to.be.ok; + }); + + it("list composite scope mappings", async () => { + const { id: clientUniqueId } = currentClient; + const roles = + await kcAdminClient.clients.listCompositeClientScopeMappings({ + id: clientUniqueId!, + client: clientUniqueId!, + }); + expect(roles).to.be.ok; + }); + + it("delete scope mappings", async () => { + const { id: clientUniqueId } = currentClient; + + const rolesBefore = await kcAdminClient.clients.listClientScopeMappings( + { + id: clientUniqueId!, + client: clientUniqueId!, + } + ); + + await kcAdminClient.clients.delClientScopeMappings( + { + id: clientUniqueId!, + client: clientUniqueId!, + }, + rolesBefore + ); + + const rolesAfter = await kcAdminClient.clients.listClientScopeMappings({ + id: clientUniqueId!, + client: clientUniqueId!, + }); + + expect(rolesAfter).to.be.ok; + expect(rolesAfter).to.eql([]); + }); + + it("get effective scope mapping of all roles for a specific container", async () => { + const { id: clientUniqueId } = currentClient; + const roles = await kcAdminClient.clients.evaluatePermission({ + id: clientUniqueId!, + roleContainer: "master", + type: "granted", + scope: "openid", + }); + + expect(roles).to.be.ok; + expect(roles.length).to.be.eq(5); + }); + + it("get list of all protocol mappers", async () => { + const { id: clientUniqueId } = currentClient; + const protocolMappers = + await kcAdminClient.clients.evaluateListProtocolMapper({ + id: clientUniqueId!, + scope: "openid", + }); + expect(protocolMappers).to.be.ok; + expect(protocolMappers.length).to.be.gt(10); + }); + + it("get JSON with payload of examples", async () => { + const { id: clientUniqueId } = currentClient; + const username = faker.internet.userName(); + const user = await kcAdminClient.users.create({ + username, + }); + const accessToken = + await kcAdminClient.clients.evaluateGenerateAccessToken({ + id: clientUniqueId!, + userId: user.id, + scope: "openid", + }); + const idToken = await kcAdminClient.clients.evaluateGenerateIdToken({ + id: clientUniqueId!, + userId: user.id, + scope: "openid", + }); + const userInfo = await kcAdminClient.clients.evaluateGenerateUserInfo({ + id: clientUniqueId!, + userId: user.id, + scope: "openid", + }); + + expect(accessToken).to.be.ok; + expect(idToken).to.be.ok; + expect(userInfo).to.be.ok; + await kcAdminClient.users.del({ id: user.id }); + }); + }); + + describe("realm", () => { + const dummyRoleName = "realmScopeMappingsRole-dummy"; + + beforeEach(async () => { + await kcAdminClient.roles.create({ + name: dummyRoleName, + }); + }); + + afterEach(async () => { + try { + await kcAdminClient.roles.delByName({ + name: dummyRoleName, + }); + } catch (e) { + // ignore + } + }); + + it("add scope mappings", async () => { + const { id } = currentClient; + + const availableRoles = + await kcAdminClient.clients.listAvailableRealmScopeMappings({ + id: id!, + }); + + await kcAdminClient.clients.addRealmScopeMappings( + { id: id! }, + availableRoles + ); + + const roles = await kcAdminClient.clients.listRealmScopeMappings({ + id: id!, + }); + + expect(roles).to.be.ok; + expect(roles).to.deep.members(availableRoles); + }); + + it("list scope mappings", async () => { + const { id } = currentClient; + const roles = await kcAdminClient.clients.listRealmScopeMappings({ + id: id!, + }); + expect(roles).to.be.ok; + }); + + it("list available scope mappings", async () => { + const { id } = currentClient; + const roles = + await kcAdminClient.clients.listAvailableRealmScopeMappings({ + id: id!, + }); + expect(roles).to.be.ok; + }); + + it("list composite scope mappings", async () => { + const { id } = currentClient; + const roles = + await kcAdminClient.clients.listCompositeRealmScopeMappings({ + id: id!, + }); + expect(roles).to.be.ok; + }); + + it("delete scope mappings", async () => { + const { id } = currentClient; + + const rolesBefore = await kcAdminClient.clients.listRealmScopeMappings({ + id: id!, + }); + + await kcAdminClient.clients.delRealmScopeMappings( + { id: id! }, + rolesBefore + ); + + const rolesAfter = await kcAdminClient.clients.listRealmScopeMappings({ + id: id!, + }); + + expect(rolesAfter).to.be.ok; + expect(rolesAfter).to.eql([]); + }); + }); + }); + + describe("sessions", () => { + it("list clients user sessions", async () => { + const clientUniqueId = currentClient.id; + const userSessions = await kcAdminClient.clients.listSessions({ + id: clientUniqueId!, + }); + expect(userSessions).to.be.ok; + }); + + it("list clients offline user sessions", async () => { + const clientUniqueId = currentClient.id; + const userSessions = await kcAdminClient.clients.listOfflineSessions({ + id: clientUniqueId!, + }); + expect(userSessions).to.be.ok; + }); + + it("list clients user session count", async () => { + const clientUniqueId = currentClient.id; + const userSessions = await kcAdminClient.clients.getSessionCount({ + id: clientUniqueId!, + }); + expect(userSessions).to.be.ok; + }); + + it("list clients offline user session count", async () => { + const clientUniqueId = currentClient.id; + const userSessions = await kcAdminClient.clients.getOfflineSessionCount({ + id: clientUniqueId!, + }); + expect(userSessions).to.be.ok; + }); + }); + + describe("nodes", () => { + const host = "127.0.0.1"; + it("register a node manually", async () => { + await kcAdminClient.clients.addClusterNode({ + id: currentClient.id!, + node: host, + }); + const client = (await kcAdminClient.clients.findOne({ + id: currentClient.id!, + }))!; + + expect(Object.keys(client.registeredNodes!)).to.be.eql([host]); + }); + + it("remove registered host", async () => { + await kcAdminClient.clients.deleteClusterNode({ + id: currentClient.id!, + node: host, + }); + const client = (await kcAdminClient.clients.findOne({ + id: currentClient.id!, + }))!; + + expect(client.registeredNodes).to.be.undefined; + }); + }); + + describe("client attribute certificate", () => { + const keystoreConfig = { + format: "JKS", + keyAlias: "new", + keyPassword: "password", + realmAlias: "master", + realmCertificate: false, + storePassword: "password", + }; + const attr = "jwt.credential"; + + it("generate and download keys", async () => { + const result = await kcAdminClient.clients.generateAndDownloadKey( + { id: currentClient.id!, attr }, + keystoreConfig + ); + + expect(result).to.be.ok; + }); + + it("generate key and updated info", async () => { + const certificate = await kcAdminClient.clients.generateKey({ + id: currentClient.id!, + attr, + }); + + expect(certificate).to.be.ok; + expect(certificate.certificate).to.be.ok; + + const info = await kcAdminClient.clients.getKeyInfo({ + id: currentClient.id!, + attr, + }); + expect(info).to.be.eql(certificate); + }); + + it("download key", async () => { + const result = await kcAdminClient.clients.downloadKey( + { id: currentClient.id!, attr }, + keystoreConfig + ); + + expect(result).to.be.ok; + }); + }); + + describe("authorization", async () => { + const resourceConfig = { + name: "testResourceName", + type: "testResourceType", + scopeNames: ["testScopeA", "testScopeB", "testScopeC"], + }; + const policyConfig = { + name: "testPolicyName", + type: "user", + logic: Logic.POSITIVE, + }; + const permissionConfig = { + name: "testPermissionName", + type: "scope", + logic: Logic.POSITIVE, + }; + let scopes: ScopeRepresentation[]; + let resource: ResourceRepresentation; + let policy: PolicyRepresentation; + let permission: PolicyRepresentation; + let user: UserRepresentation; + + before("enable authorization services", async () => { + await kcAdminClient.clients.update( + { id: currentClient.id! }, + { + clientId: currentClient.clientId, + authorizationServicesEnabled: true, + serviceAccountsEnabled: true, + } + ); + }); + + before("create test user", async () => { + const username = faker.internet.userName(); + user = await kcAdminClient.users.create({ + username, + }); + }); + + after("delete test user", async () => { + await kcAdminClient.users.del({ + id: user.id!, + }); + }); + + after("disable authorization services", async () => { + await kcAdminClient.clients.update( + { id: currentClient.id! }, + { + clientId: currentClient.clientId, + authorizationServicesEnabled: false, + serviceAccountsEnabled: false, + } + ); + }); + + it("create authorization scopes", async () => { + scopes = ( + await Promise.all( + resourceConfig.scopeNames.map(async (name) => { + const result = await kcAdminClient.clients.createAuthorizationScope( + { id: currentClient.id! }, + { + name, + } + ); + expect(result).to.be.ok; + return result; + }) + ) + ).sort((a, b) => (a.name < b.name ? -1 : 1)); + }); + + it("list all authorization scopes", async () => { + const result = await kcAdminClient.clients.listAllScopes({ + id: currentClient.id!, + }); + expect(result.sort((a, b) => (a.name! < b.name! ? -1 : 1))).to.deep.equal( + scopes + ); + }); + + it("update authorization scope", async () => { + const updatedScope = { ...scopes[0], displayName: "Hello" }; + await kcAdminClient.clients.updateAuthorizationScope( + { id: currentClient.id!, scopeId: scopes[0].id! }, + updatedScope + ); + + const fetchedScope = await kcAdminClient.clients.getAuthorizationScope({ + id: currentClient.id!, + scopeId: scopes[0].id!, + }); + + expect(fetchedScope).to.deep.equal(updatedScope); + }); + + it("list all resources by scope", async () => { + const result = await kcAdminClient.clients.listAllResourcesByScope({ + id: currentClient.id!, + scopeId: scopes[0].id!, + }); + expect(result).to.deep.equal([]); + }); + + it("list all permissions by scope", async () => { + const result = await kcAdminClient.clients.listAllPermissionsByScope({ + id: currentClient.id!, + scopeId: scopes[0].id!, + }); + expect(result).to.deep.equal([]); + }); + + it("import resource", async () => { + await kcAdminClient.clients.importResource( + { id: currentClient.id! }, + { + allowRemoteResourceManagement: true, + policyEnforcementMode: "ENFORCING", + resources: [], + policies: [], + scopes: [], + decisionStrategy: "UNANIMOUS", + } + ); + }); + + it("export resource", async () => { + const result = await kcAdminClient.clients.exportResource({ + id: currentClient.id!, + }); + + expect(result.allowRemoteResourceManagement).to.be.equal(true); + expect(result.resources?.length).to.be.equal(1); + }); + + it("create resource", async () => { + resource = await kcAdminClient.clients.createResource( + { id: currentClient.id! }, + { + name: resourceConfig.name, + type: resourceConfig.type, + scopes, + } + ); + expect(resource).to.be.ok; + }); + + it("get resource", async () => { + const r = await kcAdminClient.clients.getResource({ + id: currentClient.id!, + resourceId: resource._id!, + }); + expect(r).to.deep.equal(resource); + }); + + it("get resource server", async () => { + const resourceServer = await kcAdminClient.clients.getResourceServer({ + id: currentClient.id!, + }); + expect(resourceServer).to.be.ok; + expect(resourceServer.clientId).to.be.equal(currentClient.id); + + resourceServer.decisionStrategy = "UNANIMOUS"; + await kcAdminClient.clients.updateResourceServer( + { id: currentClient.id! }, + resourceServer + ); + }); + + it("list permission by resource", async () => { + const result = await kcAdminClient.clients.listPermissionsByResource({ + id: currentClient.id!, + resourceId: resource._id!, + }); + + expect(result).to.be.ok; + }); + + it("list scopes by resource", async () => { + const result = await kcAdminClient.clients.listScopesByResource({ + id: currentClient.id!, + resourceName: resource._id!, + }); + expect(result.sort((a, b) => (a.name < b.name ? -1 : 1))).to.deep.equal( + scopes + ); + }); + + it("list resources", async () => { + const result = await kcAdminClient.clients.listResources({ + id: currentClient.id!, + }); + expect(result).to.deep.include(resource); + }); + + it("update resource", async () => { + resource.name = "foo"; + await kcAdminClient.clients.updateResource( + { + id: currentClient.id!, + resourceId: resource._id!, + }, + resource + ); + const result = await kcAdminClient.clients.getResource({ + id: currentClient.id!, + resourceId: resource._id!, + }); + + expect(result.name).to.equal("foo"); + }); + + it("create policy", async () => { + policy = await kcAdminClient.clients.createPolicy( + { + id: currentClient.id!, + type: policyConfig.type, + }, + { + name: policyConfig.name, + logic: policyConfig.logic, + users: [user.id!], + } + ); + expect(policy).to.be.ok; + }); + + it("policy list dependencies", async () => { + const dependencies = await kcAdminClient.clients.listDependentPolicies({ + id: currentClient.id!, + policyId: policy.id!, + }); + expect(dependencies).to.be.ok; + }); + + it("create permission", async () => { + permission = await kcAdminClient.clients.createPermission( + { + id: currentClient.id!, + type: "scope", + }, + { + name: permissionConfig.name, + logic: permissionConfig.logic, + // @ts-ignore + resources: [resource._id], + policies: [policy.id!], + scopes: scopes.map((scope) => scope.id!), + } + ); + + const p = await kcAdminClient.clients.findPermissions({ + id: currentClient.id!, + name: permissionConfig.name, + }); + + expect(p.length).to.be.eq(1); + expect(p[0].logic).to.be.eq(permissionConfig.logic); + }); + + it("get associated scopes for permission", async () => { + const result = await kcAdminClient.clients.getAssociatedScopes({ + id: currentClient.id!, + permissionId: permission.id!, + }); + expect(result.sort((a, b) => (a.name < b.name ? -1 : 1))).to.deep.equal( + scopes + ); + }); + + it("get associated policies for permission", async () => { + const result = await kcAdminClient.clients.getAssociatedPolicies({ + id: currentClient.id!, + permissionId: permission.id!, + }); + + expect(result.length).to.be.eq(1); + expect(result[0].id).to.be.eq(policy.id); + }); + + it("get associated resources for permission", async () => { + const result = await kcAdminClient.clients.getAssociatedResources({ + id: currentClient.id!, + permissionId: permission.id!, + }); + expect(result).to.deep.equal([ + { + _id: resource._id, + name: resource.name, + }, + ]); + }); + + it("list policy providers", async () => { + const result = await kcAdminClient.clients.listPolicyProviders({ + id: currentClient.id!, + }); + expect(result).to.be.ok; + }); + + it.skip("Enable fine grained permissions", async () => { + const permission = await kcAdminClient.clients.updateFineGrainPermission( + { id: currentClient.id! }, + { enabled: true } + ); + expect(permission).to.include({ + enabled: true, + }); + }); + + it.skip("List fine grained permissions for this client", async () => { + const permissions = (await kcAdminClient.clients.listFineGrainPermissions( + { id: currentClient.id! } + ))!; + + expect(permissions.scopePermissions).to.be.an("object"); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/components.spec.ts b/libs/keycloak-admin-client/test/components.spec.ts new file mode 100644 index 0000000000..fdbc64d1c4 --- /dev/null +++ b/libs/keycloak-admin-client/test/components.spec.ts @@ -0,0 +1,96 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ComponentRepresentation from "../src/defs/componentRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("User federation using component api", () => { + let kcAdminClient: KeycloakAdminClient; + let currentUserFed: ComponentRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + // create user fed + const name = faker.internet.userName(); + const component = await kcAdminClient.components.create({ + name, + parentId: "master", + providerId: "ldap", + providerType: "org.keycloak.storage.UserStorageProvider", + config: { + editMode: ["READ_ONLY"], + }, + }); + expect(component.id).to.be.ok; + + // assign current user fed + const fed = (await kcAdminClient.components.findOne({ + id: component.id, + }))!; + currentUserFed = fed; + }); + + after(async () => { + await kcAdminClient.components.del({ + id: currentUserFed.id!, + }); + + // check deleted + const idp = await kcAdminClient.components.findOne({ + id: currentUserFed.id!, + }); + expect(idp).to.be.null; + }); + + it("list user federations", async () => { + const feds = await kcAdminClient.components.find({ + parent: "master", + type: "org.keycloak.storage.UserStorageProvider", + }); + expect(feds.length).to.be.least(1); + }); + + it("get a user federation", async () => { + const fed = await kcAdminClient.components.findOne({ + id: currentUserFed.id!, + }); + expect(fed).to.include({ + id: currentUserFed.id, + }); + }); + + it("get a sub components", async () => { + const list = await kcAdminClient.components.listSubComponents({ + id: currentUserFed.id!, + type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + }); + + expect(list).to.be.ok; + }); + + it("update a user federation", async () => { + await kcAdminClient.components.update( + { id: currentUserFed.id! }, + { + // parentId, providerId, providerType required for update + parentId: "master", + providerId: "ldap", + providerType: "org.keycloak.storage.UserStorageProvider", + name: "cool-name", + } + ); + const updated = await kcAdminClient.components.findOne({ + id: currentUserFed.id!, + }); + + expect(updated).to.include({ + id: currentUserFed.id, + name: "cool-name", + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/constants.ts b/libs/keycloak-admin-client/test/constants.ts new file mode 100644 index 0000000000..6e58d47470 --- /dev/null +++ b/libs/keycloak-admin-client/test/constants.ts @@ -0,0 +1,8 @@ +import type { Credentials } from "../src/utils/auth.js"; + +export const credentials: Credentials = { + username: "admin", + password: "admin", + grantType: "password", + clientId: "admin-cli", +}; diff --git a/libs/keycloak-admin-client/test/crossRealm.spec.ts b/libs/keycloak-admin-client/test/crossRealm.spec.ts new file mode 100644 index 0000000000..b36a4eb784 --- /dev/null +++ b/libs/keycloak-admin-client/test/crossRealm.spec.ts @@ -0,0 +1,46 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Realms", () => { + let kcAdminClient: KeycloakAdminClient; + let currentRealmId: string; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const realmId = faker.internet.userName(); + const realm = await kcAdminClient.realms.create({ + id: realmId, + realm: realmId, + }); + expect(realm.realmName).to.be.ok; + currentRealmId = realmId; + }); + + after(async () => { + await kcAdminClient.realms.del({ realm: currentRealmId }); + }); + + it("add a user to another realm", async () => { + const username = faker.internet.userName().toLowerCase(); + const user = await kcAdminClient.users.create({ + realm: currentRealmId, + username, + email: "test@keycloak.org", + // enabled required to be true in order to send actions email + emailVerified: true, + enabled: true, + }); + const foundUser = (await kcAdminClient.users.findOne({ + realm: currentRealmId, + id: user.id, + }))!; + expect(foundUser.username).to.be.eql(username); + }); +}); diff --git a/libs/keycloak-admin-client/test/groupUser.spec.ts b/libs/keycloak-admin-client/test/groupUser.spec.ts new file mode 100644 index 0000000000..8610f1bc42 --- /dev/null +++ b/libs/keycloak-admin-client/test/groupUser.spec.ts @@ -0,0 +1,206 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { omit, pick } from "lodash-es"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ClientRepresentation from "../src/defs/clientRepresentation.js"; +import type GroupRepresentation from "../src/defs/groupRepresentation.js"; +import type PolicyRepresentation from "../src/defs/policyRepresentation.js"; +import { DecisionStrategy, Logic } from "../src/defs/policyRepresentation.js"; +import type UserRepresentation from "../src/defs/userRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Group user integration", () => { + let kcAdminClient: KeycloakAdminClient; + let currentGroup: GroupRepresentation; + let currentUser: UserRepresentation; + let managementClient: ClientRepresentation; + let currentUserPolicy: PolicyRepresentation; + let currentPolicy: PolicyRepresentation; + + before(async () => { + const groupName = faker.internet.userName(); + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + // create group + const group = await kcAdminClient.groups.create({ + name: groupName, + }); + currentGroup = (await kcAdminClient.groups.findOne({ id: group.id }))!; + + // create user + const username = faker.internet.userName(); + const user = await kcAdminClient.users.create({ + username, + email: "test@keycloak.org", + enabled: true, + }); + currentUser = (await kcAdminClient.users.findOne({ id: user.id }))!; + }); + + after(async () => { + await kcAdminClient.groups.del({ + id: currentGroup.id!, + }); + await kcAdminClient.users.del({ + id: currentUser.id!, + }); + }); + + it("should list user's group and expect empty", async () => { + const groups = await kcAdminClient.users.listGroups({ + id: currentUser.id!, + }); + expect(groups).to.be.eql([]); + }); + + it("should add user to group", async () => { + await kcAdminClient.users.addToGroup({ + id: currentUser.id!, + groupId: currentGroup.id!, + }); + + const groups = await kcAdminClient.users.listGroups({ + id: currentUser.id!, + }); + // expect id,name,path to be the same + expect(groups[0]).to.be.eql(pick(currentGroup, ["id", "name", "path"])); + }); + + it("should list members using group api", async () => { + const members = await kcAdminClient.groups.listMembers({ + id: currentGroup.id!, + }); + // access will not returned from member api + expect(members[0]).to.be.eql(omit(currentUser, ["access"])); + }); + + it("should remove user from group", async () => { + await kcAdminClient.users.delFromGroup({ + id: currentUser.id!, + groupId: currentGroup.id!, + }); + + const groups = await kcAdminClient.users.listGroups({ + id: currentUser.id!, + }); + expect(groups).to.be.eql([]); + }); + + /** + * Authorization permissions + */ + describe.skip("authorization permissions", () => { + before(async () => { + const clients = await kcAdminClient.clients.find(); + managementClient = clients.find( + (client) => client.clientId === "master-realm" + )!; + }); + after(async () => { + await kcAdminClient.clients.delPolicy({ + id: managementClient.id!, + policyId: currentUserPolicy.id!, + }); + }); + + it("Enable permissions", async () => { + const permission = await kcAdminClient.groups.updatePermission( + { id: currentGroup.id! }, + { enabled: true } + ); + expect(permission).to.include({ + enabled: true, + }); + }); + + it("list of users in policy management", async () => { + const userPolicyData: PolicyRepresentation = { + type: "user", + logic: Logic.POSITIVE, + decisionStrategy: DecisionStrategy.UNANIMOUS, + name: `policy.manager.${currentGroup.id}`, + users: [currentUser.id!], + }; + currentUserPolicy = await kcAdminClient.clients.createPolicy( + { id: managementClient.id!, type: userPolicyData.type! }, + userPolicyData + ); + + expect(currentUserPolicy).to.include({ + type: "user", + logic: Logic.POSITIVE, + decisionStrategy: DecisionStrategy.UNANIMOUS, + name: `policy.manager.${currentGroup.id}`, + }); + }); + + it("list the roles available for this group", async () => { + const permissions = (await kcAdminClient.groups.listPermissions({ + id: currentGroup.id!, + }))!; + + expect(permissions.scopePermissions).to.be.an("object"); + + const scopes = (await kcAdminClient.clients.listScopesByResource({ + id: managementClient.id!, + resourceName: permissions.resource!, + }))!; + + const policies = await kcAdminClient.clients.listPolicies({ + id: managementClient.id, + resource: permissions.resource, + max: 2, + }); + expect(policies).to.have.length(2); + + expect(scopes).to.have.length(5); + + // Search for the id of the management role + const roleId = scopes.find((scope) => scope.name === "manage")!.id; + + const userPolicy = await kcAdminClient.clients.findPolicyByName({ + id: managementClient.id!, + name: `policy.manager.${currentGroup.id}`, + }); + + expect(userPolicy).to.deep.include({ + name: `policy.manager.${currentGroup.id}`, + }); + + // Update of the role with the above modifications + const policyData: PolicyRepresentation = { + id: permissions.scopePermissions!.manage!, + name: `manage.permission.group.${currentGroup.id}`, + type: "scope", + logic: Logic.POSITIVE, + decisionStrategy: DecisionStrategy.UNANIMOUS, + resources: [permissions.resource!], + scopes: [roleId], + policies: [userPolicy.id!], + }; + await kcAdminClient.clients.updatePermission( + { + id: managementClient.id!, + permissionId: permissions.scopePermissions!.manage, + type: "scope", + }, + policyData + ); + currentPolicy = (await kcAdminClient.clients.findOnePermission({ + id: managementClient.id!, + permissionId: permissions.scopePermissions!.manage, + type: "scope", + }))!; + expect(currentPolicy).to.deep.include({ + id: permissions.scopePermissions!.manage, + name: `manage.permission.group.${currentGroup.id}`, + type: "scope", + logic: Logic.POSITIVE, + decisionStrategy: DecisionStrategy.UNANIMOUS, + }); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/groups.spec.ts b/libs/keycloak-admin-client/test/groups.spec.ts new file mode 100644 index 0000000000..87bf93042e --- /dev/null +++ b/libs/keycloak-admin-client/test/groups.spec.ts @@ -0,0 +1,306 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ClientRepresentation from "../src/defs/clientRepresentation.js"; +import type GroupRepresentation from "../src/defs/groupRepresentation.js"; +import type RoleRepresentation from "../src/defs/roleRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Groups", () => { + let kcAdminClient: KeycloakAdminClient; + let currentClient: ClientRepresentation; + let currentGroup: GroupRepresentation; + let currentRole: RoleRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + // initialize group + const group = await kcAdminClient.groups.create({ + name: "cool-group", + }); + expect(group.id).to.be.ok; + currentGroup = (await kcAdminClient.groups.findOne({ id: group.id }))!; + }); + + after(async () => { + const groupId = currentGroup.id; + await kcAdminClient.groups.del({ + id: groupId!, + }); + + const group = await kcAdminClient.groups.findOne({ + id: groupId!, + }); + expect(group).to.be.null; + }); + + it("list groups", async () => { + const groups = await kcAdminClient.groups.find(); + expect(groups).to.be.ok; + }); + + it("count groups", async () => { + const result = await kcAdminClient.groups.count(); + expect(result.count).to.eq(1); + }); + + it("count groups with filter", async () => { + let result = await kcAdminClient.groups.count({ search: "fake-group" }); + expect(result.count).to.eq(0); + + result = await kcAdminClient.groups.count({ search: "cool-group" }); + expect(result.count).to.eq(1); + }); + + it("get single groups", async () => { + const groupId = currentGroup.id; + const group = await kcAdminClient.groups.findOne({ + id: groupId!, + }); + // get group from id will contains more fields than listing api + expect(group).to.deep.include(currentGroup); + }); + + it("update single groups", async () => { + const groupId = currentGroup.id; + await kcAdminClient.groups.update( + { id: groupId! }, + { name: "another-group-name" } + ); + + const group = await kcAdminClient.groups.findOne({ + id: groupId!, + }); + expect(group).to.include({ + name: "another-group-name", + }); + }); + + it("set or create child", async () => { + const groupName = "child-group"; + const groupId = currentGroup.id; + const childGroup = await kcAdminClient.groups.setOrCreateChild( + { id: groupId! }, + { name: groupName } + ); + + expect(childGroup.id).to.be.ok; + + const group = (await kcAdminClient.groups.findOne({ + id: groupId!, + }))!; + expect(group.subGroups![0]).to.deep.include({ + id: childGroup.id, + name: groupName, + path: `/${group.name}/${groupName}`, + }); + }); + + /** + * Role mappings + */ + describe("role-mappings", () => { + before(async () => { + // create new role + const roleName = faker.internet.userName(); + const { roleName: createdRoleName } = await kcAdminClient.roles.create({ + name: roleName, + }); + expect(createdRoleName).to.be.equal(roleName); + const role = await kcAdminClient.roles.findOneByName({ + name: roleName, + }); + currentRole = role!; + }); + + after(async () => { + await kcAdminClient.roles.delByName({ name: currentRole.name! }); + }); + + it("add a role to group", async () => { + // add role-mappings with role id + await kcAdminClient.groups.addRealmRoleMappings({ + id: currentGroup.id!, + + // at least id and name should appear + roles: [ + { + id: currentRole.id!, + name: currentRole.name!, + }, + ], + }); + }); + + it("list available role-mappings", async () => { + const roles = await kcAdminClient.groups.listAvailableRealmRoleMappings({ + id: currentGroup.id!, + }); + + // admin, create-realm, offline_access, uma_authorization + expect(roles.length).to.be.least(4); + }); + + it("list role-mappings", async () => { + const { realmMappings } = await kcAdminClient.groups.listRoleMappings({ + id: currentGroup.id!, + }); + + expect(realmMappings).to.be.ok; + // currentRole will have an empty `attributes`, but role-mappings do not + expect(currentRole).to.deep.include(realmMappings![0]); + }); + + it("list realm role-mappings of group", async () => { + const roles = await kcAdminClient.groups.listRealmRoleMappings({ + id: currentGroup.id!, + }); + // currentRole will have an empty `attributes`, but role-mappings do not + expect(currentRole).to.deep.include(roles[0]); + }); + + it("list realm composite role-mappings of group", async () => { + const roles = await kcAdminClient.groups.listCompositeRealmRoleMappings({ + id: currentGroup.id!, + }); + // todo: add data integrity check later + expect(roles).to.be.ok; + }); + + it("del realm role-mappings from group", async () => { + await kcAdminClient.groups.delRealmRoleMappings({ + id: currentGroup.id!, + roles: [ + { + id: currentRole.id!, + name: currentRole.name!, + }, + ], + }); + + const roles = await kcAdminClient.groups.listRealmRoleMappings({ + id: currentGroup.id!, + }); + expect(roles).to.be.empty; + }); + }); + + /** + * client Role mappings + */ + describe("client role-mappings", () => { + before(async () => { + // create new client + const clientId = faker.internet.userName(); + await kcAdminClient.clients.create({ + clientId, + }); + + const clients = await kcAdminClient.clients.find({ clientId }); + expect(clients[0]).to.be.ok; + currentClient = clients[0]; + + // create new client role + const roleName = faker.internet.userName(); + await kcAdminClient.clients.createRole({ + id: currentClient.id, + name: roleName, + }); + + // assign to currentRole + currentRole = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName, + }); + }); + + after(async () => { + await kcAdminClient.clients.delRole({ + id: currentClient.id!, + roleName: currentRole.name!, + }); + await kcAdminClient.clients.del({ id: currentClient.id! }); + }); + + it("add a client role to group", async () => { + // add role-mappings with role id + await kcAdminClient.groups.addClientRoleMappings({ + id: currentGroup.id!, + clientUniqueId: currentClient.id!, + + // at least id and name should appear + roles: [ + { + id: currentRole.id!, + name: currentRole.name!, + }, + ], + }); + }); + + it("list available client role-mappings for group", async () => { + const roles = await kcAdminClient.groups.listAvailableClientRoleMappings({ + id: currentGroup.id!, + clientUniqueId: currentClient.id!, + }); + + expect(roles).to.be.empty; + }); + + it("list client role-mappings of group", async () => { + const roles = await kcAdminClient.groups.listClientRoleMappings({ + id: currentGroup.id!, + clientUniqueId: currentClient.id!, + }); + + // currentRole will have an empty `attributes`, but role-mappings do not + expect(currentRole).to.deep.include(roles[0]); + }); + + it("list composite client role-mappings for group", async () => { + const roles = await kcAdminClient.groups.listCompositeClientRoleMappings({ + id: currentGroup.id!, + clientUniqueId: currentClient.id!, + }); + + expect(roles).to.be.ok; + }); + + it("del client role-mappings from group", async () => { + const roleName = faker.internet.userName(); + await kcAdminClient.clients.createRole({ + id: currentClient.id, + name: roleName, + }); + const role = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName, + }); + + // delete the created role + await kcAdminClient.groups.delClientRoleMappings({ + id: currentGroup.id!, + clientUniqueId: currentClient.id!, + roles: [ + { + id: role.id!, + name: role.name!, + }, + ], + }); + + // check if mapping is successfully deleted + const roles = await kcAdminClient.groups.listClientRoleMappings({ + id: currentGroup.id!, + clientUniqueId: currentClient.id!, + }); + + // should only left the one we added in the previous test + expect(roles.length).to.be.eql(1); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/idp.spec.ts b/libs/keycloak-admin-client/test/idp.spec.ts new file mode 100644 index 0000000000..a104677987 --- /dev/null +++ b/libs/keycloak-admin-client/test/idp.spec.ts @@ -0,0 +1,184 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Identity providers", () => { + let kcAdminClient: KeycloakAdminClient; + let currentIdpAlias: string; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + // create idp + const alias = faker.internet.userName(); + const idp = await kcAdminClient.identityProviders.create({ + alias, + providerId: "saml", + }); + expect(idp.id).to.be.ok; + currentIdpAlias = alias; + + // create idp mapper + const mapper = { + name: "First Name", + identityProviderAlias: currentIdpAlias, + identityProviderMapper: "saml-user-attribute-idp-mapper", + config: {}, + }; + const idpMapper = await kcAdminClient.identityProviders.createMapper({ + alias: currentIdpAlias, + identityProviderMapper: mapper, + }); + expect(idpMapper.id).to.be.ok; + }); + + after(async () => { + const idpMapper = await kcAdminClient.identityProviders.findMappers({ + alias: currentIdpAlias, + }); + + const idpMapperId = idpMapper[0].id; + await kcAdminClient.identityProviders.delMapper({ + alias: currentIdpAlias, + id: idpMapperId!, + }); + + const idpMapperUpdated = + await kcAdminClient.identityProviders.findOneMapper({ + alias: currentIdpAlias, + id: idpMapperId!, + }); + + // check idp mapper deleted + expect(idpMapperUpdated).to.be.null; + + await kcAdminClient.identityProviders.del({ + alias: currentIdpAlias, + }); + + const idp = await kcAdminClient.identityProviders.findOne({ + alias: currentIdpAlias, + }); + + // check idp deleted + expect(idp).to.be.null; + }); + + it("list idp", async () => { + const idps = await kcAdminClient.identityProviders.find(); + expect(idps.length).to.be.least(1); + }); + + it("get an idp", async () => { + const idp = await kcAdminClient.identityProviders.findOne({ + alias: currentIdpAlias, + }); + expect(idp).to.include({ + alias: currentIdpAlias, + }); + }); + + it("update an idp", async () => { + const idp = (await kcAdminClient.identityProviders.findOne({ + alias: currentIdpAlias, + }))!; + await kcAdminClient.identityProviders.update( + { alias: currentIdpAlias }, + { + // alias and providerId are required to update + alias: idp.alias!, + providerId: idp.providerId!, + displayName: "test", + } + ); + const updatedIdp = await kcAdminClient.identityProviders.findOne({ + alias: currentIdpAlias, + }); + + expect(updatedIdp).to.include({ + alias: currentIdpAlias, + displayName: "test", + }); + }); + + it("list idp factory", async () => { + const idpFactory = await kcAdminClient.identityProviders.findFactory({ + providerId: "saml", + }); + + expect(idpFactory).to.include({ + id: "saml", + }); + }); + + it("get an idp mapper", async () => { + const mappers = await kcAdminClient.identityProviders.findMappers({ + alias: currentIdpAlias, + }); + expect(mappers.length).to.be.least(1); + }); + + it("update an idp mapper", async () => { + const idpMapper = await kcAdminClient.identityProviders.findMappers({ + alias: currentIdpAlias, + }); + const idpMapperId = idpMapper[0].id; + + await kcAdminClient.identityProviders.updateMapper( + { alias: currentIdpAlias, id: idpMapperId! }, + { + id: idpMapperId, + identityProviderAlias: currentIdpAlias, + identityProviderMapper: "saml-user-attribute-idp-mapper", + config: { + "user.attribute": "firstName", + }, + } + ); + + const updatedIdpMappers = + (await kcAdminClient.identityProviders.findOneMapper({ + alias: currentIdpAlias, + id: idpMapperId!, + }))!; + + const userAttribute = updatedIdpMappers.config["user.attribute"]; + expect(userAttribute).to.equal("firstName"); + }); + + it("Import from url", async () => { + const result = await kcAdminClient.identityProviders.importFromUrl({ + providerId: "oidc", + fromUrl: + "http://localhost:8180/realms/master/.well-known/openid-configuration", + }); + + expect(result).to.be.ok; + expect(result.authorizationUrl).to.equal( + "http://localhost:8180/realms/master/protocol/openid-connect/auth" + ); + }); + + it.skip("Enable fine grained permissions", async () => { + const permission = await kcAdminClient.identityProviders.updatePermission( + { alias: currentIdpAlias }, + { enabled: true } + ); + expect(permission).to.include({ + enabled: true, + }); + }); + + it.skip("list permissions", async () => { + const permissions = await kcAdminClient.identityProviders.listPermissions({ + alias: currentIdpAlias, + }); + + expect(permissions.scopePermissions).to.be.an("object"); + }); +}); diff --git a/libs/keycloak-admin-client/test/realms.spec.ts b/libs/keycloak-admin-client/test/realms.spec.ts new file mode 100644 index 0000000000..3a772d9b82 --- /dev/null +++ b/libs/keycloak-admin-client/test/realms.spec.ts @@ -0,0 +1,525 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import { fail } from "assert"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type GroupRepresentation from "../src/defs/groupRepresentation.js"; +import type { PartialImportRealmRepresentation } from "../src/defs/realmRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +const createRealm = async (kcAdminClient: KeycloakAdminClient) => { + const realmId = faker.internet.userName().toLowerCase(); + const realmName = faker.internet.userName().toLowerCase(); + const realm = await kcAdminClient.realms.create({ + id: realmId, + realm: realmName, + }); + expect(realm.realmName).to.be.equal(realmName); + + return { realmId, realmName }; +}; + +const deleteRealm = async ( + kcAdminClient: KeycloakAdminClient, + currentRealmName: string +) => { + await kcAdminClient.realms.del({ realm: currentRealmName }); + const realm = await kcAdminClient.realms.findOne({ + realm: currentRealmName, + }); + expect(realm).to.be.null; +}; + +describe("Realms", () => { + let kcAdminClient: KeycloakAdminClient; + let currentRealmId: string; + let currentRealmName: string; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + }); + + it("list realms", async () => { + const realms = await kcAdminClient.realms.find(); + expect(realms.length).to.be.least(1); + }); + + it("create realm", async () => { + const realmId = faker.internet.userName().toLowerCase(); + const realmName = faker.internet.userName().toLowerCase(); + const realm = await kcAdminClient.realms.create({ + id: realmId, + realm: realmName, + }); + expect(realm.realmName).to.be.equal(realmName); + currentRealmId = realmId; + currentRealmName = realmName; + }); + + it("get a realm", async () => { + const realm = await kcAdminClient.realms.findOne({ + realm: currentRealmName, + }); + expect(realm).to.include({ + id: currentRealmId, + realm: currentRealmName, + }); + }); + + const roleToImport: PartialImportRealmRepresentation = { + ifResourceExists: "FAIL", + roles: { + realm: [ + { + id: "9d2638c8-4c62-4c42-90ea-5f3c836d0cc8", + name: "myRole", + scopeParamRequired: false, + composite: false, + }, + ], + }, + }; + + it("does partial import", async () => { + const result = await kcAdminClient.realms.partialImport({ + realm: currentRealmName, + rep: roleToImport, + }); + expect(result.added).to.be.eq(1); + expect(result.overwritten).to.be.eq(0); + expect(result.skipped).to.be.eq(0); + expect(result.results.length).to.be.eq(1); + expect(result.results[0].action).to.be.eq("ADDED"); + expect(result.results[0].resourceName).to.be.eq("myRole"); + expect(result.results[0].id).to.exist; + }); + + it("export a realm", async () => { + const realm = await kcAdminClient.realms.export({ + realm: currentRealmName, + exportClients: true, + exportGroupsAndRoles: true, + }); + expect(realm).to.include({ + id: currentRealmId, + realm: currentRealmName, + }); + }); + + it("update a realm", async () => { + await kcAdminClient.realms.update( + { realm: currentRealmName }, + { + displayName: "test", + } + ); + const realm = await kcAdminClient.realms.findOne({ + realm: currentRealmName, + }); + expect(realm).to.include({ + id: currentRealmId, + realm: currentRealmName, + displayName: "test", + }); + }); + + it("client registration policy providers", async () => { + const list = + await kcAdminClient.realms.getClientRegistrationPolicyProviders({ + realm: currentRealmName, + }); + + expect(list).to.be.ok; + }); + + it("delete a realm", async () => { + await kcAdminClient.realms.del({ realm: currentRealmName }); + const realm = await kcAdminClient.realms.findOne({ + realm: currentRealmName, + }); + expect(realm).to.be.null; + }); + + describe("Realm Client Initial Access", () => { + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const created = await createRealm(kcAdminClient); + currentRealmName = created.realmName; + + await kcAdminClient.realms.createClientsInitialAccess( + { realm: currentRealmName }, + { count: 1, expiration: 0 } + ); + }); + + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); + }); + + it("list client initial access", async () => { + const initialAccess = await kcAdminClient.realms.getClientsInitialAccess({ + realm: currentRealmName, + }); + expect(initialAccess).to.be.ok; + expect(initialAccess[0].count).to.be.eq(1); + }); + + it("del client initial access", async () => { + const access = await kcAdminClient.realms.createClientsInitialAccess( + { realm: currentRealmName }, + { count: 1, expiration: 0 } + ); + expect( + ( + await kcAdminClient.realms.getClientsInitialAccess({ + realm: currentRealmName, + }) + ).length + ).to.be.eq(2); + + await kcAdminClient.realms.delClientsInitialAccess({ + realm: currentRealmName, + id: access.id!, + }); + + const initialAccess = await kcAdminClient.realms.getClientsInitialAccess({ + realm: currentRealmName, + }); + expect(initialAccess).to.be.ok; + expect(initialAccess[0].count).to.be.eq(1); + }); + }); + + describe("Realm default groups", () => { + const groupName = "my-group"; + let currentGroup: GroupRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + currentRealmName = (await createRealm(kcAdminClient)).realmName; + currentGroup = await kcAdminClient.groups.create({ + name: groupName, + realm: currentRealmName, + }); + }); + + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); + }); + + it("add group to default groups", async () => { + await kcAdminClient.realms.addDefaultGroup({ + id: currentGroup.id!, + realm: currentRealmName, + }); + + const defaultGroups = await kcAdminClient.realms.getDefaultGroups({ + realm: currentRealmName, + }); + + expect(defaultGroups).to.be.ok; + expect(defaultGroups.length).to.be.eq(1); + expect(defaultGroups[0].id).to.be.eq(currentGroup.id); + }); + + it("get a group by its path name", async () => { + const queriedGroup = await kcAdminClient.realms.getGroupByPath({ + realm: currentRealmName, + path: groupName, + }); + + expect(queriedGroup).to.be.ok; + expect(queriedGroup.id).to.be.eq(currentGroup.id); + }); + + it("remove group from default groups", async () => { + await kcAdminClient.realms.removeDefaultGroup({ + id: currentGroup.id!, + realm: currentRealmName, + }); + + const defaultGroups = await kcAdminClient.realms.getDefaultGroups({ + realm: currentRealmName, + }); + + expect(defaultGroups).to.be.ok; + expect(defaultGroups.length).to.be.eq(0); + }); + }); + + describe("Realm Events", () => { + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const created = await createRealm(kcAdminClient); + currentRealmId = created.realmId; + currentRealmName = created.realmName; + }); + + it("get events config for a realm", async () => { + const config = await kcAdminClient.realms.getConfigEvents({ + realm: currentRealmName, + }); + + expect(config).to.be.ok; + expect(config.adminEventsEnabled).to.be.eq(false); + }); + + it("enable events", async () => { + const config = await kcAdminClient.realms.getConfigEvents({ + realm: currentRealmName, + }); + config.eventsEnabled = true; + await kcAdminClient.realms.updateConfigEvents( + { realm: currentRealmName }, + config + ); + + const newConfig = await kcAdminClient.realms.getConfigEvents({ + realm: currentRealmName, + }); + + expect(newConfig).to.be.ok; + expect(newConfig.eventsEnabled).to.be.eq(true); + }); + + it("list events of a realm", async () => { + // @TODO: In order to test it, there have to be events + const events = await kcAdminClient.realms.findEvents({ + realm: currentRealmName, + }); + + expect(events).to.be.ok; + }); + + it("list admin events of a realm", async () => { + // @TODO: In order to test it, there have to be events + const events = await kcAdminClient.realms.findAdminEvents({ + realm: currentRealmName, + }); + + expect(events).to.be.ok; + }); + + it("clear events", async () => { + await kcAdminClient.realms.clearEvents({ realm: currentRealmName }); + await kcAdminClient.realms.clearAdminEvents({ realm: currentRealmName }); + + const events = await kcAdminClient.realms.findAdminEvents({ + realm: currentRealmName, + }); + + expect(events).to.deep.eq([]); + }); + + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); + }); + }); + + describe("Realm Users Management Permissions", () => { + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const created = await createRealm(kcAdminClient); + currentRealmId = created.realmId; + currentRealmName = created.realmName; + }); + + it("get users management permissions", async () => { + const managementPermissions = + await kcAdminClient.realms.getUsersManagementPermissions({ + realm: currentRealmName, + }); + expect(managementPermissions).to.be.ok; + }); + + it.skip("enable users management permissions", async () => { + const managementPermissions = + await kcAdminClient.realms.updateUsersManagementPermissions({ + realm: currentRealmName, + enabled: true, + }); + expect(managementPermissions).to.include({ enabled: true }); + }); + + it("get realm keys", async () => { + const keys = await kcAdminClient.realms.getKeys({ + realm: currentRealmName, + }); + expect(keys.active).to.be.ok; + }); + + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); + }); + }); + + describe("Realm Session Management", () => { + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const created = await createRealm(kcAdminClient); + currentRealmId = created.realmId; + currentRealmName = created.realmName; + }); + + it("push revocation", async () => { + const push = await kcAdminClient.realms.pushRevocation({ + realm: currentRealmName, + }); + expect(push).to.be.ok; + }); + + it("logs out all sessions", async () => { + const logout = await kcAdminClient.realms.logoutAll({ + realm: currentRealmName, + }); + expect(logout).to.be.ok; + }); + + after(async () => { + deleteRealm(kcAdminClient, currentRealmName); + }); + }); + + describe("Realm connection settings", () => { + it("should fail with invalid ldap settings", async () => { + try { + await kcAdminClient.realms.testLDAPConnection( + { realm: "master" }, + { + action: "testConnection", + authType: "simple", + bindCredential: "1", + bindDn: "1", + connectionTimeout: "", + connectionUrl: "1", + startTls: "", + useTruststoreSpi: "ldapsOnly", + } + ); + fail("exception should have been thrown"); + } catch (error) { + expect(error).to.be.ok; + } + }); + + it("should fail with invalid smtp settings", async () => { + try { + const user = ( + await kcAdminClient.users.find({ username: credentials.username }) + )[0]; + user.email = "test@test.com"; + await kcAdminClient.users.update({ id: user.id! }, user); + await kcAdminClient.realms.testSMTPConnection( + { realm: "master" }, + { + from: "cdd1641ff4-1781a4@inbox.mailtrap.io", + host: "localhost", + port: 3025, + } + ); + fail("exception should have been thrown"); + } catch (error) { + expect(error).to.be.ok; + } + }); + + it("should fail with invalid ldap server capabilities", async () => { + try { + await kcAdminClient.realms.ldapServerCapabilities( + { realm: "master" }, + { + action: "testConnection", + authType: "simple", + bindCredential: "1", + bindDn: "1", + connectionTimeout: "", + connectionUrl: "1", + startTls: "", + useTruststoreSpi: "ldapsOnly", + } + ); + fail("exception should have been thrown"); + } catch (error) { + expect(error).to.be.ok; + } + }); + }); + + describe("Realm localization", () => { + currentRealmName = "master"; + + it.skip("enable localization", async () => { + await kcAdminClient.realms.getRealmLocalizationTexts({ + realm: currentRealmName, + selectedLocale: "nl", + }); + }); + + it.skip("should add localization", async () => { + await kcAdminClient.realms.addLocalization( + { realm: currentRealmName, selectedLocale: "nl", key: "theKey" }, + "value" + ); + }); + + it.skip("should get realm specific locales", async () => { + const locales = await kcAdminClient.realms.getRealmSpecificLocales({ + realm: currentRealmName, + }); + + expect(locales).to.be.ok; + expect(locales).to.be.deep.eq(["nl"]); + }); + + it.skip("should get localization for specified locale", async () => { + const texts = await kcAdminClient.realms.getRealmLocalizationTexts({ + realm: currentRealmName, + selectedLocale: "nl", + }); + + expect(texts).to.be.ok; + expect(texts.theKey).to.be.eq("value"); + }); + + it.skip("should delete localization for specified locale key", async () => { + await kcAdminClient.realms.deleteRealmLocalizationTexts({ + realm: currentRealmName, + selectedLocale: "nl", + key: "theKey", + }); + + const texts = await kcAdminClient.realms.getRealmLocalizationTexts({ + realm: currentRealmName, + selectedLocale: "nl", + }); + expect(texts).to.be.ok; + expect(texts).to.be.deep.eq({}); + }); + + it.skip("should delete localization for specified locale", async () => { + await kcAdminClient.realms.deleteRealmLocalizationTexts({ + realm: currentRealmName, + selectedLocale: "nl", + }); + + const locales = await kcAdminClient.realms.getRealmSpecificLocales({ + realm: currentRealmName, + }); + expect(locales).to.be.ok; + expect(locales).to.be.deep.eq([]); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/roles.spec.ts b/libs/keycloak-admin-client/test/roles.spec.ts new file mode 100644 index 0000000000..dc15837f26 --- /dev/null +++ b/libs/keycloak-admin-client/test/roles.spec.ts @@ -0,0 +1,227 @@ +// tslint:disable:no-unused-expression +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ClientRepresentation from "../src/defs/clientRepresentation.js"; +import type RoleRepresentation from "../src/defs/roleRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Roles", () => { + let client: KeycloakAdminClient; + let currentRole: RoleRepresentation; + + before(async () => { + client = new KeycloakAdminClient(); + await client.auth(credentials); + }); + + after(async () => { + // delete the currentRole with id + await client.roles.delById({ + id: currentRole.id!, + }); + }); + + it("list roles", async () => { + const roles = await client.roles.find(); + expect(roles).to.be.ok; + }); + + it("create roles and get by name", async () => { + const roleName = "cool-role"; + const createdRole = await client.roles.create({ + name: roleName, + }); + + expect(createdRole.roleName).to.be.equal(roleName); + const role = await client.roles.findOneByName({ name: roleName }); + expect(role).to.be.ok; + currentRole = role!; + }); + + it("get single roles by id", async () => { + const roleId = currentRole.id; + const role = await client.roles.findOneById({ + id: roleId!, + }); + expect(role).to.deep.include(currentRole); + }); + + it("update single role by name & by id", async () => { + await client.roles.updateByName( + { name: currentRole.name! }, + { + // dont know why if role name not exist in payload, role name will be overriden with empty string + // todo: open an issue on keycloak + name: "cool-role", + description: "cool", + } + ); + + const role = await client.roles.findOneByName({ + name: currentRole.name!, + }); + expect(role).to.include({ + description: "cool", + }); + + await client.roles.updateById( + { id: currentRole.id! }, + { + description: "another description", + } + ); + + const roleById = await client.roles.findOneById({ + id: currentRole.id!, + }); + expect(roleById).to.include({ + description: "another description", + }); + }); + + it("delete single roles by id", async () => { + await client.roles.create({ + name: "for-delete", + }); + + await client.roles.delByName({ + name: "for-delete", + }); + + const roleDelByName = await client.roles.findOneByName({ + name: "for-delete", + }); + expect(roleDelByName).to.be.null; + }); + + it("get users with role by name in realm", async () => { + const users = await client.roles.findUsersWithRole({ + name: "admin", + }); + expect(users).to.be.ok; + expect(users).to.be.an("array"); + }); + + it.skip("Enable fine grained permissions", async () => { + const permission = await client.roles.updatePermission( + { id: currentRole.id! }, + { enabled: true } + ); + expect(permission).to.include({ + enabled: true, + }); + }); + + it.skip("List fine grained permissions for this role", async () => { + const permissions = (await client.roles.listPermissions({ + id: currentRole.id!, + }))!; + + expect(permissions.scopePermissions).to.be.an("object"); + }); + + describe("Composite roles", () => { + const compositeRoleName = "compositeRole"; + let compositeRole: RoleRepresentation; + + beforeEach(async () => { + await client.roles.create({ + name: compositeRoleName, + }); + compositeRole = (await client.roles.findOneByName({ + name: compositeRoleName, + }))!; + await client.roles.createComposite({ roleId: currentRole.id! }, [ + compositeRole, + ]); + }); + + afterEach(async () => { + await client.roles.delByName({ + name: compositeRoleName, + }); + }); + + it("make the role a composite role by associating some child roles", async () => { + const children = await client.roles.getCompositeRoles({ + id: currentRole.id!, + }); + + // attributes on the composite role are empty and when fetched not there. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { attributes, ...rest } = compositeRole; + expect(children).to.be.eql([rest]); + }); + + it("search for composite roles", async () => { + const children = await client.roles.getCompositeRoles({ + id: currentRole.id!, + search: "not", + }); + + expect(children).to.be.an("array").that.is.length(0); + }); + + it("delete composite roles", async () => { + await client.roles.delCompositeRoles({ id: currentRole.id! }, [ + compositeRole, + ]); + const children = await client.roles.getCompositeRoles({ + id: currentRole.id!, + }); + + expect(children).to.be.an("array").that.is.empty; + }); + + describe("Get composite roles for client and realm", () => { + let createdClient: ClientRepresentation; + let clientRole: RoleRepresentation; + before(async () => { + createdClient = await client.clients.create({ clientId: "test" }); + const clientRoleName = "clientRole"; + await client.clients.createRole({ + id: createdClient.id, + name: clientRoleName, + }); + clientRole = await client.clients.findRole({ + id: createdClient.id!, + roleName: clientRoleName, + }); + + await client.roles.createComposite({ roleId: currentRole.id! }, [ + clientRole, + ]); + }); + + after(async () => { + await client.clients.del({ id: createdClient.id! }); + }); + + it("get composite role for the realm", async () => { + const realmChildren = await client.roles.getCompositeRolesForRealm({ + id: currentRole.id!, + }); + const children = await client.roles.getCompositeRoles({ + id: currentRole.id!, + }); + + delete compositeRole.attributes; + expect(realmChildren).to.be.eql([compositeRole]); + + expect(children).to.be.an("array").that.is.length(2); + }); + + it("get composite for the client", async () => { + const clientChildren = await client.roles.getCompositeRolesForClient({ + id: currentRole.id!, + clientId: createdClient.id!, + }); + + delete clientRole.attributes; + expect(clientChildren).to.be.eql([clientRole]); + }); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/serverInfo.spec.ts b/libs/keycloak-admin-client/test/serverInfo.spec.ts new file mode 100644 index 0000000000..2037c311be --- /dev/null +++ b/libs/keycloak-admin-client/test/serverInfo.spec.ts @@ -0,0 +1,20 @@ +// tslint:disable:no-unused-expression +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Server Info", () => { + let client: KeycloakAdminClient; + + before(async () => { + client = new KeycloakAdminClient(); + await client.auth(credentials); + }); + + it("list server info", async () => { + const serverInfo = await client.serverInfo.find(); + expect(serverInfo).to.be.ok; + }); +}); diff --git a/libs/keycloak-admin-client/test/sessions.spec.ts b/libs/keycloak-admin-client/test/sessions.spec.ts new file mode 100644 index 0000000000..762ca35e6a --- /dev/null +++ b/libs/keycloak-admin-client/test/sessions.spec.ts @@ -0,0 +1,22 @@ +// tslint:disable:no-unused-expression +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Sessions", () => { + let client: KeycloakAdminClient; + + before(async () => { + client = new KeycloakAdminClient(); + await client.auth(credentials); + }); + + it("list sessions", async () => { + const sessions = await client.sessions.find(); + expect(sessions).to.be.ok; + expect(sessions.length).to.be.eq(1); + expect(sessions[0].clientId).to.be.eq("admin-cli"); + }); +}); diff --git a/libs/keycloak-admin-client/test/stringifyQueryParams.spec.ts b/libs/keycloak-admin-client/test/stringifyQueryParams.spec.ts new file mode 100644 index 0000000000..dbe3d4a4b5 --- /dev/null +++ b/libs/keycloak-admin-client/test/stringifyQueryParams.spec.ts @@ -0,0 +1,31 @@ +import { expect } from "chai"; +import { stringifyQueryParams } from "../src/utils/stringifyQueryParams.js"; + +describe("stringifyQueryParams", () => { + it("ignores undefined and null", () => { + expect(stringifyQueryParams({ foo: undefined, bar: null })).to.equal(""); + }); + + it("ignores empty strings", () => { + expect(stringifyQueryParams({ foo: "" })).to.equal(""); + }); + + it("ignores empty arrays", () => { + expect(stringifyQueryParams({ foo: [] })).to.equal(""); + }); + + it("accepts all other values", () => { + expect( + stringifyQueryParams({ + boolTrue: true, + boolFalse: false, + numPositive: 1, + numZero: 0, + numNegative: -1, + str: "Hello World!", + }) + ).to.equal( + "boolTrue=true&boolFalse=false&numPositive=1&numZero=0&numNegative=-1&str=Hello+World%21" + ); + }); +}); diff --git a/libs/keycloak-admin-client/test/userStorageProvider.spec.ts b/libs/keycloak-admin-client/test/userStorageProvider.spec.ts new file mode 100644 index 0000000000..42a64b8f4b --- /dev/null +++ b/libs/keycloak-admin-client/test/userStorageProvider.spec.ts @@ -0,0 +1,54 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ComponentRepresentation from "../src/defs/componentRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Users federation provider", () => { + let kcAdminClient: KeycloakAdminClient; + let currentUserFed: ComponentRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + const name = faker.internet.userName(); + currentUserFed = await kcAdminClient.components.create({ + name, + parentId: "master", + providerId: "ldap", + providerType: "org.keycloak.storage.UserStorageProvider", + config: { + editMode: ["READ_ONLY"], + }, + }); + }); + + after(async () => { + await kcAdminClient.components.del({ + id: currentUserFed.id!, + }); + }); + + it("list storage provider", async () => { + const name = await kcAdminClient.userStorageProvider.name({ + id: currentUserFed.id!, + }); + expect(name).to.be.ok; + }); + + it("remove imported users", async () => { + await kcAdminClient.userStorageProvider.removeImportedUsers({ + id: currentUserFed.id!, + }); + }); + + it("unlink users", async () => { + await kcAdminClient.userStorageProvider.unlinkUsers({ + id: currentUserFed.id!, + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/users.spec.ts b/libs/keycloak-admin-client/test/users.spec.ts new file mode 100644 index 0000000000..176a25f720 --- /dev/null +++ b/libs/keycloak-admin-client/test/users.spec.ts @@ -0,0 +1,682 @@ +// tslint:disable:no-unused-expression +import { faker } from "@faker-js/faker"; +import { fail } from "assert"; +import * as chai from "chai"; +import { omit } from "lodash-es"; +import { KeycloakAdminClient } from "../src/client.js"; +import type ClientRepresentation from "../src/defs/clientRepresentation.js"; +import type FederatedIdentityRepresentation from "../src/defs/federatedIdentityRepresentation.js"; +import type GroupRepresentation from "../src/defs/groupRepresentation.js"; +import { RequiredActionAlias } from "../src/defs/requiredActionProviderRepresentation.js"; +import type RoleRepresentation from "../src/defs/roleRepresentation.js"; +import type UserRepresentation from "../src/defs/userRepresentation.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Users", () => { + let kcAdminClient: KeycloakAdminClient; + let currentClient: ClientRepresentation; + let currentUser: UserRepresentation; + let currentRole: RoleRepresentation; + let federatedIdentity: FederatedIdentityRepresentation; + + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + // initialize user + const username = faker.internet.userName(); + const user = await kcAdminClient.users.create({ + username, + email: "test@keycloak.org", + // enabled required to be true in order to send actions email + emailVerified: true, + enabled: true, + attributes: { + key: "value", + }, + }); + + expect(user.id).to.be.ok; + currentUser = (await kcAdminClient.users.findOne({ id: user.id }))!; + + // add smtp to realm + await kcAdminClient.realms.update( + { realm: "master" }, + { + smtpServer: { + auth: true, + from: "0830021730-07fb21@inbox.mailtrap.io", + host: "smtp.mailtrap.io", + user: process.env.SMTP_USER, + password: process.env.SMTP_PWD, + }, + } + ); + }); + + after(async () => { + const userId = currentUser.id; + await kcAdminClient.users.del({ + id: userId!, + }); + + const user = await kcAdminClient.users.findOne({ + id: userId!, + }); + expect(user).to.be.null; + }); + + it("list users", async () => { + const users = await kcAdminClient.users.find(); + expect(users).to.be.ok; + }); + + it("count users", async () => { + const numUsers = await kcAdminClient.users.count(); + // admin user + created user in before hook + expect(numUsers).to.equal(2); + }); + + it("count users with filter", async () => { + const numUsers = await kcAdminClient.users.count({ + email: "test@keycloak.org", + }); + expect(numUsers).to.equal(1); + }); + + it.skip("gets the profile", async () => { + const profile = await kcAdminClient.users.getProfile(); + expect(profile).to.be.ok; + }); + + it.skip("updates the profile", async () => { + const profile = await kcAdminClient.users.updateProfile({}); + expect(profile).to.be.ok; + }); + + it.skip("find users by custom attributes", async () => { + // Searching by attributes is only available from Keycloak > 15 + const users = await kcAdminClient.users.find({ key: "value" }); + expect(users.length).to.be.equal(2); + expect(users[0]).to.be.deep.include(currentUser); + }); + + it("get single users", async () => { + const userId = currentUser.id; + const user = await kcAdminClient.users.findOne({ + id: userId!, + }); + expect(user).to.be.deep.include(currentUser); + }); + + it("update single users", async () => { + const userId = currentUser.id; + await kcAdminClient.users.update( + { id: userId! }, + { + firstName: "william", + lastName: "chang", + requiredActions: [RequiredActionAlias.UPDATE_PASSWORD], + emailVerified: true, + } + ); + + const user = await kcAdminClient.users.findOne({ + id: userId!, + }); + expect(user).to.deep.include({ + firstName: "william", + lastName: "chang", + requiredActions: [RequiredActionAlias.UPDATE_PASSWORD], + emailVerified: true, + }); + }); + + /** + * reset password + */ + + it("should reset user password", async () => { + const userId = currentUser.id; + await kcAdminClient.users.resetPassword({ + id: userId!, + credential: { + temporary: false, + type: "password", + value: "test", + }, + }); + }); + + /** + * get user credentials + */ + it("get user credentials", async () => { + const userId = currentUser.id; + const result = await kcAdminClient.users.getCredentials({ + id: userId!, + }); + + expect(result.map((c) => c.type)).to.include("password"); + }); + + it("get configured user storage credential types", async () => { + const userId = currentUser.id; + const result = await kcAdminClient.users.getUserStorageCredentialTypes({ + id: userId!, + }); + + expect(result).to.be.deep.eq([]); + }); + + /** + * delete user credentials + */ + it("delete user credentials", async () => { + const userId = currentUser.id; + const result = await kcAdminClient.users.getCredentials({ + id: userId!, + }); + + expect(result.map((c) => c.type)).to.include("password"); + + const credential = result[0]; + await kcAdminClient.users.deleteCredential({ + id: userId!, + credentialId: credential.id!, + }); + + const credentialsAfterDelete = await kcAdminClient.users.getCredentials({ + id: userId!, + }); + + expect(credentialsAfterDelete).to.not.deep.include(credential); + + // Add deleted password back + await kcAdminClient.users.resetPassword({ + id: userId!, + credential: { + temporary: false, + type: "password", + value: "test", + }, + }); + }); + + /** + * update a credential label for a user + */ + it("update a credential label for a user", async () => { + const userId = currentUser.id; + const result = await kcAdminClient.users.getCredentials({ + id: userId!, + }); + + expect(result.map((c) => c.type)).to.include("password"); + + const credential = result[0]; + + await kcAdminClient.users.updateCredentialLabel( + { id: userId!, credentialId: credential.id! }, + "New user label" + ); + + const credentialsAfterLabelUpdate = + await kcAdminClient.users.getCredentials({ + id: userId!, + }); + + expect(credentialsAfterLabelUpdate.map((c) => c.userLabel)).to.include( + "New user label" + ); + }); + + /** + * Groups + */ + describe("user groups", () => { + let currentGroup: GroupRepresentation; + before(async () => { + const group = await kcAdminClient.groups.create({ + name: "cool-group", + }); + expect(group.id).to.be.ok; + currentGroup = (await kcAdminClient.groups.findOne({ id: group.id }))!; + }); + + after(async () => { + const groupId = currentGroup.id; + const groups = await kcAdminClient.groups.find({ max: 100 }); + await Promise.all( + groups.map((_group: GroupRepresentation) => { + return kcAdminClient.groups.del({ id: _group.id! }); + }) + ); + + const group = await kcAdminClient.groups.findOne({ + id: groupId!, + }); + expect(group).to.be.null; + }); + + it("add group", async () => { + let count = ( + await kcAdminClient.users.countGroups({ id: currentUser.id! }) + ).count; + expect(count).to.eq(0); + await kcAdminClient.users.addToGroup({ + groupId: currentGroup.id!, + id: currentUser.id!, + }); + count = (await kcAdminClient.users.countGroups({ id: currentUser.id! })) + .count; + expect(count).to.eq(1); + }); + + it("count groups", async () => { + let { count } = await kcAdminClient.users.countGroups({ + id: currentUser.id!, + }); + expect(count).to.eq(1); + + count = ( + await kcAdminClient.users.countGroups({ + id: currentUser.id!, + search: "cool-group", + }) + ).count; + expect(count).to.eq(1); + + count = ( + await kcAdminClient.users.countGroups({ + id: currentUser.id!, + search: "fake-group", + }) + ).count; + expect(count).to.eq(0); + }); + + it("list groups", async () => { + const groups = await kcAdminClient.users.listGroups({ + id: currentUser.id!, + }); + expect(groups).to.be.ok; + expect(groups.length).to.be.eq(1); + expect(groups[0].name).to.eq("cool-group"); + }); + + it("remove group", async () => { + const newGroup = await kcAdminClient.groups.create({ + name: "test-group", + }); + await kcAdminClient.users.addToGroup({ + id: currentUser.id!, + groupId: newGroup.id, + }); + let count = ( + await kcAdminClient.users.countGroups({ id: currentUser.id! }) + ).count; + expect(count).to.eq(2); + + try { + await kcAdminClient.users.delFromGroup({ + id: currentUser.id!, + groupId: newGroup.id, + }); + } catch (e) { + fail("Didn't expect an error when deleting a valid group id"); + } + + count = (await kcAdminClient.users.countGroups({ id: currentUser.id! })) + .count; + expect(count).to.equal(1); + + await kcAdminClient.groups.del({ id: newGroup.id }); + + // delete a non-existing group should throw an error + try { + await kcAdminClient.users.delFromGroup({ + id: currentUser.id!, + groupId: "fake-group-id", + }); + fail( + "Expected an error when deleting a fake id not assigned to the user" + ); + } catch (e) { + expect(e).to.be.ok; + } + }); + }); + + /** + * Role mappings + */ + describe("role-mappings", () => { + before(async () => { + // create new role + const roleName = faker.internet.userName(); + await kcAdminClient.roles.create({ + name: roleName, + }); + const role = await kcAdminClient.roles.findOneByName({ + name: roleName, + }); + currentRole = role!; + }); + + after(async () => { + await kcAdminClient.roles.delByName({ name: currentRole.name! }); + }); + + it("add a role to user", async () => { + // add role-mappings with role id + await kcAdminClient.users.addRealmRoleMappings({ + id: currentUser.id!, + + // at least id and name should appear + roles: [ + { + id: currentRole.id!, + name: currentRole.name!, + }, + ], + }); + }); + + it("list available role-mappings for user", async () => { + const roles = await kcAdminClient.users.listAvailableRealmRoleMappings({ + id: currentUser.id!, + }); + + // admin, create-realm + // not sure why others like offline_access, uma_authorization not included + expect(roles.length).to.be.least(2); + }); + + it("list role-mappings of user", async () => { + const res = await kcAdminClient.users.listRoleMappings({ + id: currentUser.id!, + }); + + expect(res).have.all.keys("realmMappings"); + }); + + it("list realm role-mappings of user", async () => { + const roles = await kcAdminClient.users.listRealmRoleMappings({ + id: currentUser.id!, + }); + // currentRole will have an empty `attributes`, but role-mappings do not + expect(roles).to.deep.include(omit(currentRole, "attributes")); + }); + + it("list realm composite role-mappings of user", async () => { + const roles = await kcAdminClient.users.listCompositeRealmRoleMappings({ + id: currentUser.id!, + }); + // todo: add data integrity check later + expect(roles).to.be.ok; + }); + + it("del realm role-mappings from user", async () => { + await kcAdminClient.users.delRealmRoleMappings({ + id: currentUser.id!, + roles: [ + { + id: currentRole.id!, + name: currentRole.name!, + }, + ], + }); + + const roles = await kcAdminClient.users.listRealmRoleMappings({ + id: currentUser.id!, + }); + expect(roles).to.not.deep.include(currentRole); + }); + }); + + /** + * client Role mappings + */ + describe("client role-mappings", () => { + before(async () => { + // create new client + const clientId = faker.internet.userName(); + await kcAdminClient.clients.create({ + clientId, + }); + + const clients = await kcAdminClient.clients.find({ clientId }); + expect(clients[0]).to.be.ok; + currentClient = clients[0]; + + // create new client role + const roleName = faker.internet.userName(); + await kcAdminClient.clients.createRole({ + id: currentClient.id, + name: roleName, + }); + + // assign to currentRole + currentRole = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName, + }); + }); + + after(async () => { + await kcAdminClient.clients.delRole({ + id: currentClient.id!, + roleName: currentRole.name!, + }); + await kcAdminClient.clients.del({ id: currentClient.id! }); + }); + + it("add a client role to user", async () => { + // add role-mappings with role id + await kcAdminClient.users.addClientRoleMappings({ + id: currentUser.id!, + clientUniqueId: currentClient.id!, + + // at least id and name should appear + roles: [ + { + id: currentRole.id!, + name: currentRole.name!, + }, + ], + }); + }); + + it("list available client role-mappings for user", async () => { + const roles = await kcAdminClient.users.listAvailableClientRoleMappings({ + id: currentUser.id!, + clientUniqueId: currentClient.id!, + }); + + expect(roles).to.be.empty; + }); + + it("list composite client role-mappings for user", async () => { + const roles = await kcAdminClient.users.listCompositeClientRoleMappings({ + id: currentUser.id!, + clientUniqueId: currentClient.id!, + }); + + expect(roles).to.be.ok; + }); + + it("list client role-mappings of user", async () => { + const roles = await kcAdminClient.users.listClientRoleMappings({ + id: currentUser.id!, + clientUniqueId: currentClient.id!, + }); + + // currentRole will have an empty `attributes`, but role-mappings do not + expect(currentRole).to.deep.include(roles[0]); + }); + + it("del client role-mappings from user", async () => { + const roleName = faker.internet.userName(); + await kcAdminClient.clients.createRole({ + id: currentClient.id, + name: roleName, + }); + const role = await kcAdminClient.clients.findRole({ + id: currentClient.id!, + roleName, + }); + + // delete the created role + await kcAdminClient.users.delClientRoleMappings({ + id: currentUser.id!, + clientUniqueId: currentClient.id!, + roles: [ + { + id: role.id!, + name: role.name!, + }, + ], + }); + + // check if mapping is successfully deleted + const roles = await kcAdminClient.users.listClientRoleMappings({ + id: currentUser.id!, + clientUniqueId: currentClient.id!, + }); + + // should only left the one we added in the previous test + expect(roles.length).to.be.eql(1); + }); + }); + + describe("User sessions", () => { + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + // create client + const clientId = faker.internet.userName(); + await kcAdminClient.clients.create({ + clientId, + consentRequired: true, + }); + + const clients = await kcAdminClient.clients.find({ clientId }); + expect(clients[0]).to.be.ok; + currentClient = clients[0]; + }); + + after(async () => { + await kcAdminClient.clients.del({ + id: currentClient.id!, + }); + }); + + it("list user sessions", async () => { + // @TODO: In order to test it, currentUser has to be logged in + const userSessions = await kcAdminClient.users.listSessions({ + id: currentUser.id!, + }); + + expect(userSessions).to.be.ok; + }); + + it("list users off-line sessions", async () => { + // @TODO: In order to test it, currentUser has to be logged in + const userOfflineSessions = await kcAdminClient.users.listOfflineSessions( + { id: currentUser.id!, clientId: currentClient.id! } + ); + + expect(userOfflineSessions).to.be.ok; + }); + + it("logout user from all sessions", async () => { + // @TODO: In order to test it, currentUser has to be logged in + await kcAdminClient.users.logout({ id: currentUser.id! }); + }); + + it("list consents granted by the user", async () => { + const consents = await kcAdminClient.users.listConsents({ + id: currentUser.id!, + }); + + expect(consents).to.be.ok; + }); + + it("revoke consent and offline tokens for particular client", async () => { + // @TODO: In order to test it, currentUser has to granted consent to client + const consents = await kcAdminClient.users.listConsents({ + id: currentUser.id!, + }); + + if (consents.length) { + const consent = consents[0]; + + await kcAdminClient.users.revokeConsent({ + id: currentUser.id!, + clientId: consent.clientId!, + }); + } + }); + + it("impersonate user", async () => { + const result = await kcAdminClient.users.impersonation( + { id: currentUser.id! }, + { user: currentUser.id!, realm: kcAdminClient.realmName } + ); + expect(result).to.be.ok; + await kcAdminClient.auth(credentials); + }); + }); + + describe("Federated Identity user integration", () => { + before(async () => { + kcAdminClient = new KeycloakAdminClient(); + await kcAdminClient.auth(credentials); + + federatedIdentity = { + identityProvider: "foobar", + userId: "userid1", + userName: "username1", + }; + }); + + it("should list user federated identities and expect empty", async () => { + const federatedIdentities = + await kcAdminClient.users.listFederatedIdentities({ + id: currentUser.id!, + }); + expect(federatedIdentities).to.be.eql([]); + }); + + it("should add federated identity to user", async () => { + await kcAdminClient.users.addToFederatedIdentity({ + id: currentUser.id!, + federatedIdentityId: "foobar", + federatedIdentity, + }); + + // @TODO: In order to test the integration with federated identities, the User Federation + // would need to be created first, this is not implemented yet. + // const federatedIdentities = await kcAdminClient.users.listFederatedIdentities({ + // id: currentUser.id, + // }); + // expect(federatedIdentities[0]).to.be.eql(federatedIdentity); + }); + + it("should remove federated identity from user", async () => { + await kcAdminClient.users.delFromFederatedIdentity({ + id: currentUser.id!, + federatedIdentityId: "foobar", + }); + + const federatedIdentities = + await kcAdminClient.users.listFederatedIdentities({ + id: currentUser.id!, + }); + expect(federatedIdentities).to.be.eql([]); + }); + }); +}); diff --git a/libs/keycloak-admin-client/test/whoAmI.spec.ts b/libs/keycloak-admin-client/test/whoAmI.spec.ts new file mode 100644 index 0000000000..ce0393a7ad --- /dev/null +++ b/libs/keycloak-admin-client/test/whoAmI.spec.ts @@ -0,0 +1,21 @@ +// tslint:disable:no-unused-expression +import * as chai from "chai"; +import { KeycloakAdminClient } from "../src/client.js"; +import { credentials } from "./constants.js"; + +const expect = chai.expect; + +describe("Who am I", () => { + let client: KeycloakAdminClient; + + before(async () => { + client = new KeycloakAdminClient(); + await client.auth(credentials); + }); + + it("list who I am", async () => { + const whoAmI = await client.whoAmI.find(); + expect(whoAmI).to.be.ok; + expect(whoAmI.displayName).to.be.equal("admin"); + }); +}); diff --git a/libs/keycloak-admin-client/tsconfig.json b/libs/keycloak-admin-client/tsconfig.json new file mode 100644 index 0000000000..27933244be --- /dev/null +++ b/libs/keycloak-admin-client/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "lib", + "noEmit": false, + "declaration": true + } +} \ No newline at end of file diff --git a/libs/keycloak-admin-client/tsconfig.test.json b/libs/keycloak-admin-client/tsconfig.test.json new file mode 100644 index 0000000000..1bd1e8a246 --- /dev/null +++ b/libs/keycloak-admin-client/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "include": ["test"] +} diff --git a/package-lock.json b/package-lock.json index cff740b7be..dec5ea6c41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "keycloak-ui", "workspaces": [ + "libs/keycloak-admin-client", "libs/keycloak-js", "libs/keycloak-masthead", "apps/account-ui", @@ -140,7 +141,7 @@ }, "apps/admin-ui": { "dependencies": { - "@keycloak/keycloak-admin-client": "^21.0.0-dev.3", + "@keycloak/keycloak-admin-client": "999.0.0-dev", "@patternfly/patternfly": "^4.219.2", "@patternfly/react-code-editor": "^4.82.55", "@patternfly/react-core": "^4.258.3", @@ -245,6 +246,31 @@ } } }, + "libs/keycloak-admin-client": { + "name": "@keycloak/keycloak-admin-client", + "version": "999.0.0-dev", + "license": "Apache-2.0", + "dependencies": { + "axios": "^0.27.2", + "camelize-ts": "^2.1.1", + "lodash-es": "^4.17.21", + "url-join": "^5.0.0", + "url-template": "^3.0.0" + }, + "devDependencies": { + "@faker-js/faker": "^7.1.0", + "@types/chai": "^4.2.14", + "@types/lodash-es": "^4.17.5", + "@types/mocha": "^10.0.0", + "@types/node": "^18.0.3", + "chai": "^4.1.2", + "mocha": "^10.0.0", + "ts-node": "^10.2.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "libs/keycloak-js": { "version": "999.0.0-dev", "license": "Apache-2.0", @@ -2820,6 +2846,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.6", "dev": true, @@ -2953,19 +2989,8 @@ } }, "node_modules/@keycloak/keycloak-admin-client": { - "version": "21.0.0-dev.3", - "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-21.0.0-dev.3.tgz", - "integrity": "sha512-wFvxZXrVGiHgd3OCab4YWAwcinykPYhuNlO8BokyP6XClKKL5qCQgNm+iWxgDB/njUdY3ovqDBTnY9KUKI3qWA==", - "dependencies": { - "axios": "^0.27.2", - "camelize-ts": "^2.1.1", - "lodash-es": "^4.17.21", - "url-join": "^5.0.0", - "url-template": "^3.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } + "resolved": "libs/keycloak-admin-client", + "link": true }, "node_modules/@microsoft/api-extractor": { "version": "7.33.5", @@ -4131,6 +4156,12 @@ "@types/lodash": "*" } }, + "node_modules/@types/mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -5393,6 +5424,12 @@ "dev": true, "license": "MIT" }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/browserify-aes": { "version": "1.2.0", "dev": true, @@ -6003,6 +6040,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clsx": { "version": "1.2.1", "license": "MIT", @@ -7353,21 +7430,22 @@ } }, "node_modules/es-abstract": { - "version": "1.20.2", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.2", + "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", + "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", @@ -7377,6 +7455,7 @@ "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.5", "string.prototype.trimstart": "^1.0.5", "unbox-primitive": "^1.0.2" @@ -8590,6 +8669,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.0", "dev": true, @@ -8599,9 +8687,10 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.2", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -8952,6 +9041,15 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/history": { "version": "4.10.1", "license": "MIT", @@ -9268,9 +9366,10 @@ } }, "node_modules/is-callable": { - "version": "1.2.5", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -9491,6 +9590,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "dev": true, @@ -10660,6 +10768,159 @@ "dev": true, "license": "MIT" }, + "node_modules/mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", + "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/monaco-editor": { "version": "0.34.0", "license": "MIT", @@ -12190,6 +12451,15 @@ "throttleit": "^1.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "dev": true, @@ -12389,6 +12659,20 @@ "ret": "~0.1.10" } }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "dev": true, @@ -14963,6 +15247,12 @@ "errno": "~0.1.7" } }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "dev": true, @@ -15069,6 +15359,110 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yauzl": { "version": "2.10.0", "dev": true, @@ -16771,6 +17165,12 @@ "strip-json-comments": "^3.1.1" } }, + "@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true + }, "@humanwhocodes/config-array": { "version": "0.11.6", "dev": true, @@ -16862,13 +17262,19 @@ } }, "@keycloak/keycloak-admin-client": { - "version": "21.0.0-dev.3", - "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-21.0.0-dev.3.tgz", - "integrity": "sha512-wFvxZXrVGiHgd3OCab4YWAwcinykPYhuNlO8BokyP6XClKKL5qCQgNm+iWxgDB/njUdY3ovqDBTnY9KUKI3qWA==", + "version": "file:libs/keycloak-admin-client", "requires": { + "@faker-js/faker": "^7.1.0", + "@types/chai": "^4.2.14", + "@types/lodash-es": "^4.17.5", + "@types/mocha": "^10.0.0", + "@types/node": "^18.0.3", "axios": "^0.27.2", "camelize-ts": "^2.1.1", + "chai": "^4.1.2", "lodash-es": "^4.17.21", + "mocha": "^10.0.0", + "ts-node": "^10.2.1", "url-join": "^5.0.0", "url-template": "^3.0.0" } @@ -17718,6 +18124,12 @@ "@types/lodash": "*" } }, + "@types/mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==", + "dev": true + }, "@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -18276,7 +18688,7 @@ "@babel/preset-env": "^7.19.4", "@cypress/webpack-batteries-included-preprocessor": "^2.2.3", "@cypress/webpack-preprocessor": "^5.15.3", - "@keycloak/keycloak-admin-client": "^21.0.0-dev.3", + "@keycloak/keycloak-admin-client": "999.0.0-dev", "@octokit/rest": "^19.0.5", "@patternfly/patternfly": "^4.219.2", "@patternfly/react-code-editor": "^4.82.55", @@ -18735,6 +19147,12 @@ "version": "1.1.0", "dev": true }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "browserify-aes": { "version": "1.2.0", "dev": true, @@ -19137,6 +19555,42 @@ "string-width": "^5.0.0" } }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, "clsx": { "version": "1.2.1" }, @@ -20044,20 +20498,22 @@ } }, "es-abstract": { - "version": "1.20.2", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", "dev": true, "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.2", + "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", + "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", @@ -20067,6 +20523,7 @@ "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.5", "string.prototype.trimstart": "^1.0.5", "unbox-primitive": "^1.0.2" @@ -20852,12 +21309,20 @@ "version": "1.0.0-beta.2", "dev": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-func-name": { "version": "2.0.0", "dev": true }, "get-intrinsic": { - "version": "1.1.2", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dev": true, "requires": { "function-bind": "^1.1.1", @@ -21079,6 +21544,12 @@ "minimalistic-assert": "^1.0.1" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "history": { "version": "4.10.1", "requires": { @@ -21279,7 +21750,9 @@ } }, "is-callable": { - "version": "1.2.5", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, "is-ci": { @@ -21403,6 +21876,12 @@ "version": "4.0.0", "dev": true }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, "is-plain-object": { "version": "5.0.0", "dev": true @@ -22210,6 +22689,124 @@ "version": "0.5.3", "dev": true }, + "mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", + "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "monaco-editor": { "version": "0.34.0", "peer": true @@ -23211,6 +23808,12 @@ "throttleit": "^1.0.0" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, "requires-port": { "version": "1.0.0", "dev": true @@ -23333,6 +23936,17 @@ "ret": "~0.1.10" } }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, "safer-buffer": { "version": "2.1.2", "dev": true @@ -25047,6 +25661,12 @@ "errno": "~0.1.7" } }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, "wrap-ansi": { "version": "7.0.0", "dev": true, @@ -25108,6 +25728,84 @@ "version": "2.1.1", "dev": true }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } + } + }, "yauzl": { "version": "2.10.0", "dev": true, diff --git a/package.json b/package.json index 6cc0f04d6b..131389875a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "keycloak-ui", "workspaces": [ + "libs/keycloak-admin-client", "libs/keycloak-js", "libs/keycloak-masthead", "apps/account-ui",