Move UI repository to main repo

This commit is contained in:
Jon Koops 2023-03-07 12:10:08 +01:00
commit 1ec7a5a198
No known key found for this signature in database
1155 changed files with 123237 additions and 88 deletions

View File

@ -4,6 +4,8 @@ module.exports = {
ignorePatterns: [
"node_modules",
"dist",
"keycloak-theme",
"server",
// Keycloak JS follows a completely different and outdated style, so we'll exclude it for now.
// TODO: Eventually align the code-style for Keycloak JS.
"libs/keycloak-js",
@ -51,6 +53,9 @@ module.exports = {
"react/prop-types": "off",
// Prevent fragments from being added that have only a single child.
"react/jsx-no-useless-fragment": "error",
// Ban nesting components, as this will cause unintended re-mounting of components.
// TODO: All issues should be fixed and this rule should be set to "error".
"react/no-unstable-nested-components": ["warn", { allowAsProps: true }],
"prefer-arrow-callback": "error",
"prettier/prettier": [
"error",

3
js/.gitignore vendored
View File

@ -29,6 +29,9 @@ assets
# Optional eslint cache
.eslintcache
# Keycloak server
server
# Wireit
.wireit

3
js/.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"singleQuote": false
}

View File

@ -1,14 +1,21 @@
# Keycloak JavaScript
This directory contains an [NPM workspace](https://docs.npmjs.com/cli/v9/using-npm/workspaces) with JavaScript (and TypeScript) code related to the UIs and libraries of the Keycloak project.
This directory contains the UIs and related libraries of the Keycloak project written in JavaScript (and TypeScript).
## Repository structure
## Directory structure
├── apps
│ ├── account-ui # Account UI for account management i.e controlling password and account access, tracking and managing permissions
│ └── admin-ui # Admin UI for handling login, registration, administration, and account management
│ ├── admin-ui # Admin UI for handling login, registration, administration, and account management
│ └── keycloak-server # Keycloak server for local development of UIs
├── keycloak-theme # Maven build for the Keycloak theme
├── libs
│ ├── keycloak-admin-client # Keycloak Admin Client library for Keycloak REST API
│ ├── keycloak-js # Keycloak JS library for securing HTML5/JavaScript applications
│ └── keycloak-masthead # Keycloak Masthead library for an easy way to bring applications into the Keycloak ecosystem, allow users to access
│ # and manage security for those applications and manage authorization of resources
├── ...
## Data processing
Red Hat may process information including business contact information and code contributions as part of its participation in the project, data is processed in accordance with [Red Hat Privacy Statement](https://www.redhat.com/en/about/privacy-policy).

View File

@ -0,0 +1,3 @@
# Keycloak Account UI
This project is the next generation of the Keycloak Account UI. It is written with React and [PatternFly 4](https://www.patternfly.org/v4/) and uses [Vite](https://vitejs.dev/guide/).

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<base href="./" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Web site to manage keycloak" />
<title>Keycloak account console</title>
<style>
body {
margin: 0;
}
body, #app {
height: 100%;
}
.container {
padding: 0;
margin: 0;
width: 100%;
}
.keycloak__loading-container {
height: 100vh;
width: 100%;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin: 0;
}
#loading-text {
z-index: 1000;
font-size: 20px;
font-weight: 600;
padding-top: 32px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="keycloak__loading-container">
<span class="pf-c-spinner pf-m-xl" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<div>
<p id="loading-text">Loading the account console</p>
</div>
</div>
</div>
</div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,67 @@
{
"name": "account-ui",
"scripts": {
"dev": "wireit",
"build": "wireit",
"preview": "wireit",
"lint": "wireit"
},
"dependencies": {
"@patternfly/patternfly": "^4.224.2",
"@patternfly/react-core": "^4.276.6",
"@patternfly/react-icons": "^4.93.6",
"i18next": "^22.4.11",
"i18next-http-backend": "^2.1.1",
"keycloak-js": "999.0.0-SNAPSHOT",
"keycloak-masthead": "999.0.0-SNAPSHOT",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.5",
"react-i18next": "^12.2.0",
"react-router-dom": "6.8.2",
"ui-shared": "999.0.0-SNAPSHOT"
},
"devDependencies": {
"@types/lodash-es": "^4.17.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react-swc": "^3.2.0",
"vite": "^4.1.4",
"vite-plugin-checker": "^0.5.6"
},
"wireit": {
"dev": {
"command": "vite --host",
"dependencies": [
"../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build"
]
},
"preview": {
"command": "vite preview",
"dependencies": [
"../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build"
]
},
"build": {
"command": "vite build",
"dependencies": [
"../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build"
]
},
"lint": {
"command": "eslint . --ext js,jsx,mjs,ts,tsx",
"dependencies": [
"../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build"
]
}
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 36 36" version="1.1" viewBox="0 0 36 36" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<style type="text/css">
/*stylelint-disable*/
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st1{filter:url(#b);}
.st2{mask:url(#a);}
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#BBBBBB;}
.st4{opacity:0.1;fill-rule:evenodd;clip-rule:evenodd;enable-background:new ;}
.st5{opacity:8.000000e-02;fill-rule:evenodd;clip-rule:evenodd;fill:#231F20;enable-background:new ;}
/*stylelint-enable*/
</style>
<circle class="st0" cx="18" cy="18.5" r="18"/>
<defs>
<filter id="b" x="5.2" y="7.2" width="25.6" height="53.6" filterUnits="userSpaceOnUse">
<feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask id="a" x="5.2" y="7.2" width="25.6" height="53.6" maskUnits="userSpaceOnUse">
<g class="st1">
<circle class="st0" cx="18" cy="18.5" r="18"/>
</g>
</mask>
<g class="st2">
<g transform="translate(5.04 6.88)">
<path class="st3" d="m22.6 18.1c-1.1-1.4-2.3-2.2-3.5-2.6s-1.8-0.6-6.3-0.6-6.1 0.7-6.1 0.7 0 0 0 0c-1.2 0.4-2.4 1.2-3.4 2.6-2.3 2.8-3.2 12.3-3.2 14.8 0 3.2 0.4 12.3 0.6 15.4 0 0-0.4 5.5 4 5.5l-0.3-6.3-0.4-3.5 0.2-0.9c0.9 0.4 3.6 1.2 8.6 1.2 5.3 0 8-0.9 8.8-1.3l0.2 1-0.2 3.6-0.3 6.3c3 0.1 3.7-3 3.8-4.4s0.6-12.6 0.6-16.5c0.1-2.6-0.8-12.1-3.1-15z"/>
<path class="st4" d="m22.5 26c-0.1-2.1-1.5-2.8-4.8-2.8l2.2 9.6s1.8-1.7 3-1.8c0 0-0.4-4.6-0.4-5z"/>
<path class="st3" d="m12.7 13.2c-3.5 0-6.4-2.9-6.4-6.4s2.9-6.4 6.4-6.4 6.4 2.9 6.4 6.4-2.8 6.4-6.4 6.4z"/>
<path class="st5" d="m9.4 6.8c0-3 2.1-5.5 4.9-6.3-0.5-0.1-1-0.2-1.6-0.2-3.5 0-6.4 2.9-6.4 6.4s2.9 6.4 6.4 6.4c0.6 0 1.1-0.1 1.6-0.2-2.8-0.6-4.9-3.1-4.9-6.1z"/>
<path class="st4" d="m8.3 22.4c-2 0.4-2.9 1.4-3.1 3.5l-0.6 18.6s1.7 0.7 3.6 0.9l0.1-23z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,63 @@
<svg id="Layer_1" data-name="Layer 1" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs id="defs10">
<clipPath id="clip-path">
<path class="cls-1" id="rect4" d="M-1018.62 565.7H862.62v1175.78h-1881.24z" />
</clipPath>
<clipPath id="clip-path-2">
<path class="cls-1" id="rect7" d="M0 0h512v512H0z" />
</clipPath>
<style id="style2">.cls-1{fill:none}.cls-24{fill:#d0d0d0}.cls-26{fill:#d9d9d9}.cls-28{fill:#d8d8d8}.cls-29{fill:#e2e2e2}.cls-31{fill:#dedede}.cls-36{fill:#00b8e3}.cls-37{fill:#33c6e9}.cls-38{fill:#008aaa}</style>
</defs>
<g clip-path="url(#clip-path)" id="g36" stroke-width="1.51">
<path d="M-42.82 358l245 24.8 199.4 2z" id="path14" fill="#b17c81" stroke="#b17c81" />
<path d="M-42.82 358l444.44 26.79 227.18-2z" id="path16" fill="#a2747c" stroke="#a2747c" />
<path d="M401.62 384.74l163.69 138.89 63.49-140.87z" id="path18" fill="#996976" stroke="#996976" />
<path d="M202.22 382.76l54.56 14.88 144.84-12.9z" id="path20" fill="#aa787e" stroke="#aa787e" />
<path d="M401.62 384.74L356 537.52l209.32-13.89z" id="path22" fill="#b2777e" stroke="#b2777e" />
<path d="M256.78 397.64L356 537.52l45.63-152.78z" id="path24" fill="#b27a7f" stroke="#b27a7f" />
<path d="M256.78 397.64l-92.26 135.91 191.47 4z" id="path26" fill="#c78485" stroke="#c78485" />
<path d="M202.22 382.76l-37.7 150.79 92.26-135.91z" id="path28" fill="#c08184" stroke="#c08184" />
<path d="M-42.82 358l207.34 175.55 37.7-150.79z" id="path30" fill="#c48485" stroke="#c48485" />
<path d="M-42.82 358l-51.59 137.9 258.93 37.7z" id="path32" fill="#d58b88" stroke="#d58b88" />
<path d="M-94.41 495.85L-33.89 598l198.41-64.48z" id="path34" fill="#e09790" stroke="#e09790" />
</g>
<g clip-path="url(#clip-path-2)" id="g110">
<path d="M438.48 152a3.79 3.79 0 01-3.32-1.89L377.39 49.94a3.91 3.91 0 00-3.39-1.89H138.33a3.79 3.79 0 00-3.33 1.89L75 153.89l-55.83 100.2a3.88 3.88 0 000 3.82L75 358l60 104a3.79 3.79 0 003.32 1.89H374a3.91 3.91 0 003.36-1.89l57.84-100.1a3.79 3.79 0 013.32-1.89h71.93a4.32 4.32 0 004.32-4.32V156.32a4.32 4.32 0 00-4.32-4.32h-72z" id="path38" fill="#4d4d4d" />
<path d="M72.85 157.64l-55.191 98.369 5.871 12.931 54.845 89.592L114.19 360l287.27-.02H461l38.264-9.68 15.496-42.81.01-49.36v-45.72L510.46 152h-71.98l-22.11.01H147.94l-75.09 5.63" id="path27674" fill="#e2e2e2" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-opacity="1" fill-opacity="1" />
<path class="cls-1" d="M510.46 152H78.34a3.91 3.91 0 00-3.34 1.89v.07l-2.14 3.69-26.45 45.83-29.23 50.63a3.8 3.8 0 000 3.83l6.35 11L75 358.06a3.84 3.84 0 003.34 1.94h432.18a4.27 4.27 0 004.24-4.28V156.34a4.32 4.32 0 00-4.3-4.34z" id="path40" />
<path d="M88.1 245.5l-64.57 23.44-6.35-11a3.8 3.8 0 010-3.83l29.23-50.63z" id="path42" fill="#e1e1e1" />
<path id="polygon44" fill="#c8c8c8" d="M472.21 264.21l42.56-6.08v49.36l-42.56-43.28z" />
<path d="M472.21 264.21l42.55 43.28v48.21a4.27 4.27 0 01-4.24 4.28H461z" id="path46" fill="#c2c2c2" />
<path id="polygon48" fill="#c7c7c7" d="M472.21 264.21L461 359.98h-59.54l-18.04-43.45 88.79-52.32z" />
<path id="polygon50" fill="#cecece" d="M472.21 264.21l42.56-51.8v45.72l-42.56 6.08z" />
<path d="M514.77 156.33v56.08l-42.55 51.8L440.12 152h70.33a4.32 4.32 0 014.32 4.33z" id="path52" fill="#d3d3d3" />
<path id="polygon54" fill="#c6c6c6" d="M401.46 359.98h-31.4l-8.14-11.67 21.5-31.78 18.04 43.45z" />
<path id="polygon56" fill="#d5d5d5" d="M472.21 264.21l-117.79-49.79 61.95-62.41h23.75l32.09 112.2z" />
<path class="cls-24" d="M354.42 214.42l29 102.11 88.8-52.32z" id="path58" />
<path id="polygon60" fill="#bfbfbf" d="M370.06 359.98h-8.52l.38-11.67 8.14 11.67z" />
<path class="cls-26" id="polygon62" d="M416.37 152.01l-61.95 62.41-11.18-55.82 23.92-6.59h49.21z" />
<path d="M354.42 214.42l-143 33 150.5 100.89z" id="path64" fill="#d4d4d4" />
<path class="cls-24" d="M354.42 214.42l7.49 133.9 21.5-31.78z" id="path66" />
<path class="cls-26" d="M343.24 158.6l-131.77 88.79 143-33z" id="path68" />
<path class="cls-28" id="polygon70" d="M211.47 247.39L149.5 359.98h-35.31L88.1 245.5l123.37 1.89z" />
<path class="cls-29" d="M147.94 152L88.1 245.5l-15.25-87.86 2.15-3.7v-.07a3.91 3.91 0 013.33-1.87h69.61z" id="path72" />
<path class="cls-28" d="M114.19 360H78.33a3.84 3.84 0 01-3.33-2l-51.47-89.06L88.1 245.5z" id="path74" />
<path id="polygon76" fill="#e4e4e4" d="M46.41 203.47l26.44-45.83L88.1 245.5z" />
<path class="cls-31" id="polygon78" d="M276.77 152.01H172.39l39.08 95.38 131.77-88.79-39.72-6.59h-26.75z" />
<path class="cls-31" id="polygon80" d="M156.09 152.01h-8.15L88.1 245.5l123.37 1.89-39.08-95.38h-16.3z" />
<path id="polygon82" fill="#c5c5c5" d="M333.23 359.98h28.31l.38-11.67-28.69 11.67z" />
<path class="cls-24" id="polygon84" d="M361.92 348.31L211.47 247.39l27.1 112.59h94.66l28.69-11.67z" />
<path id="polygon86" fill="#d1d1d1" d="M149.5 359.98H238.57l-27.1-112.59-61.97 112.59z" />
<path id="polygon88" fill="#ddd" d="M343.65 152.01l-.41 6.59 23.92-6.59H343.65z" />
<path id="polygon90" fill="#e3e3e3" d="M303.52 152.01l39.72 6.59-3.66-6.59h-36.06z" />
<path class="cls-29" id="polygon92" d="M339.58 152.01l3.66 6.59.41-6.59h-4.07z" />
<path class="cls-36" d="M235.15 153.81L177 254.46a3.38 3.38 0 00-.42 1.64h-40.51l79.74-138.18a3.14 3.14 0 011.19 1.15l.11.11 18.08 31.41a3.49 3.49 0 01-.04 3.22z" id="path94" />
<path class="cls-37" d="M235.08 361.89l-18 31.27a3.51 3.51 0 01-1.22 1.15L136 256.14h40.6a3.09 3.09 0 00.38 1.57.37.37 0 00.07.17l58 100.58a3.41 3.41 0 01.03 3.43z" id="path96" />
<path class="cls-38" d="M215.81 117.92L136.07 256.1l-20 34.66-19.1-33.12a3.09 3.09 0 01-.38-1.57 3.38 3.38 0 01.42-1.64l19.3-33.43 58.75-101.74a3.4 3.4 0 013-1.75h36.04a3.58 3.58 0 011.71.41z" id="path98" />
<path class="cls-36" d="M215.81 394.31a3.58 3.58 0 01-1.71.45H178a3.4 3.4 0 01-3-1.75l-53.72-93-5.28-9.22 20-34.66z" id="path100" />
<path class="cls-38" d="M376.19 256.1l-79.8 138.21a3.73 3.73 0 01-1.19-1.15l-.07-.1L277 361.72a3.49 3.49 0 010-3.22l58.06-100.65a3.38 3.38 0 00.49-1.75h40.57z" id="path102" />
<path class="cls-36" d="M415.68 256.1a3.38 3.38 0 01-.49 1.75l-78.13 135.31a3.42 3.42 0 01-2.9 1.61h-36a3.72 3.72 0 01-1.75-.45l79.78-138.22 20-34.62 19 32.91a3.35 3.35 0 01.49 1.71z" id="path104" />
<path class="cls-36" d="M376.19 256.1h-40.56a3.35 3.35 0 00-.49-1.71l-58-100.55a3.41 3.41 0 010-3.46l18.08-31.3a3.73 3.73 0 011.19-1.15z" id="path106" />
<path class="cls-37" d="M396.2 221.44l-20 34.62-79.81-138.14a3.72 3.72 0 011.75-.45h36a3.42 3.42 0 012.9 1.61z" id="path108" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,161 @@
{
"accept": "Accept",
"accessGrantedOn": "Access granted on",
"accountSecurity": "Account security",
"accountUpdatedError": "Could not update account due to validation errors",
"accountUpdatedMessage": "Your account has been updated.",
"add": "Add",
"application": "Application",
"applicationDetails": "Application details",
"applications": "Applications",
"applicationsIntroMessage": "Track and manage your app permission to access your account",
"applicationType": "Application type",
"avatar": "Avatar",
"basic-authentication": "Basic authentication",
"cancel": "Cancel",
"choose": "Choose...",
"client": "Client",
"clients": "Clients",
"close": "Close",
"credentialCreatedAt": "<0>Created</0> {{date}}.",
"currentSession": "Current session",
"description": "Description",
"device-activity": "Device activity",
"deviceActivity": "Device activity",
"directMembership": "Direct membership",
"doCancel": "Cancel",
"doDeny": "Deny",
"done": "Done",
"doSave": "Save",
"doSignOut": "Sign out",
"edit": "Edit",
"editTheResource": "Share the resource - {{0}}",
"email": "Email",
"error-empty": "Please specify value of '{{0}}'.",
"error-invalid-blank": "Please specify value of '{{0}}'.",
"error-invalid-date": "'{{0}}' is invalid date.",
"error-invalid-email": "Invalid email address.",
"error-invalid-length-too-long": "'{{0}}' must have maximal length of {{2}}.",
"error-invalid-length-too-short": "'{{0}}' must have minimal length of {{1}}.",
"error-invalid-length": "'{{0}}' must have a length between {{1}} and {{2}}.",
"error-invalid-number": "'{{0}}' is invalid number.",
"error-invalid-uri-fragment": "'{{0}}' is invalid URL fragment.",
"error-invalid-uri-scheme": "'{{0}}' has invalid URL scheme.",
"error-invalid-uri": "'{{0}}' is invalid URL.",
"error-invalid-value": "'{{0}}' has invalid value.",
"error-number-out-of-range-too-big": "'{{0}}' must have maximal value of {{2}}.",
"error-number-out-of-range-too-small": "'{{0}}' must have minimal value of {{1}}.",
"error-number-out-of-range": "'{{0}}' must be a number between {{1}} and {{2}}.",
"error-pattern-no-match": "'{{0}}' doesn't match required format.",
"error-person-name-invalid-character": "'{{0}}' contains invalid character.",
"error-user-attribute-required": "Please specify '{{0}}'.",
"error-username-invalid-character": "'{{0}}' contains invalid character.",
"errorRemovedMessage": "Could not remove {{userLabel}} due to: {{error}}",
"errorSignOutMessage": "Could not be signed out: {{error}}",
"expires": "Expires",
"filterByName": "Filter By Name ...",
"firstName": "First name",
"fullName": "{{givenName}} {{familyName}}",
"groupDescriptionLabel": "View groups that you are associated with",
"groupLabel": "Groups",
"groups": "Groups",
"infoMessage": "By clicking Remove Access, you will remove granted permissions of this application. This application will no longer use your information.",
"internalApp": "Internal",
"inUse": "In use",
"ipAddress": "IP address",
"lastAccessedOn": "Last accessed",
"lastName": "Last name",
"link": "Link account",
"linkedAccounts": "Linked accounts",
"linkedAccountsIntroMessage": "Manage logins through third-party accounts.",
"linkedAccountsTitle": "Linked accounts",
"linkedEmpty": "No linked providers",
"linkedLoginProviders": "Linked login providers",
"linkError": "Could not link due to: {{error}}",
"logo": "Logo",
"manageAccount": "Manage account",
"myResources": "My Resources",
"name": "Name",
"noGroups": "No groups",
"noGroupsText": "You are not joined in any group",
"notInUse": "Not in use",
"notSetUp": "{{0}} is not set up.",
"offlineAccess": "Offline access",
"otp-display-name": "Authenticator application",
"otp-help-text": "Enter a verification code from authenticator application.",
"password-display-name": "Password",
"password-help-text": "Sign in by entering your password.",
"password": "My password",
"path": "Path",
"permissionRequest": "Permission requests - {{0}}",
"permissionRequests": "Permission requests",
"permissions": "Permissions",
"personalInfo": "Personal info",
"personalInfoDescription": "Manage your basic information",
"privacyPolicy": "Privacy policy",
"refreshPage": "Refresh the page",
"removeButton": "Remove access",
"removeConsentError": "Could not remove consent due to: {{error}}",
"removeConsentSuccess": "Successfully removed consent",
"removeCred": "Remove {{0}}",
"removeCredAriaLabel": "Remove credential",
"removeModalMessage": "This will remove the currently granted access permission for {{0}}. You will need to grant access again if you want to use this app.",
"removeModalTitle": "Remove access",
"requestor": "Requestor",
"required": "Required",
"resourceAlreadyShared": "Resource is already shared with this user.",
"resourceIntroMessage": "Share your resources among team members",
"resourceName": "Resource name",
"resources": "Resources",
"resourceSharedWith_one": "Resource is shared with <0>{{username}}</0>",
"resourceSharedWith_other": "Resource is shared with <0>{{username}}</0> and <1>{{other}}</1> other users",
"resourceSharedWith_zero": "This resource is not shared.",
"selectOne": "Select an option",
"setUpNew": "Set up {{0}}",
"share": "Share",
"sharedWithMe": "Shared with Me",
"shareError": "Could not share the resource due to: {{error}}",
"shareSuccess": "Resource successfully shared.",
"shareTheResource": "Share the resource - {{0}}",
"shareUser": "Add users to share your resource with",
"shareWith": "Share with ",
"signedInDevices": "Signed in devices",
"signedInDevicesExplanation": "Sign out of any unfamiliar devices.",
"signedOutSession": "Signed out {{0}}/{{1}}",
"signingIn": "Signing in",
"signingInDescription": "Configure ways to sign in.",
"signOut": "Sign out",
"signOutAllDevices": "Sign out all devices",
"signOutAllDevicesWarning": "This action will sign out all the devices that have signed in to your account, including the current device you are using.",
"signOutWarning": "Sign out the session?",
"socialLogin": "Social login",
"somethingWentWrong": "Something went wrong",
"somethingWentWrongDescription": "Sorry, an unexpected error has occurred.",
"started": "Started",
"status": "Status",
"stopUsingCred": "Stop using {{0}}?",
"successRemovedMessage": "{{userLabel}} was removed.",
"systemDefined": "System defined",
"termsOfService": "Terms of service",
"thirdPartyApp": "Third-party",
"tryAgain": "Try again",
"two-factor": "Two-factor authentication",
"unknownOperatingSystem": "Unknown operating system",
"unknownUser": "Anonymous",
"unLink": "Unlink account",
"unlinkedEmpty": "No unlinked providers",
"unlinkedLoginProviders": "Unlinked login providers",
"unLinkError": "Could not unlink due to: {{error}}",
"unLinkSuccess": "Successfully unlinked account",
"unShare": "Unshare all",
"unShareError": "Could not un-share the resource due to: {{error}}",
"unShareSuccess": "Resource successfully un-shared.",
"update": "Update",
"updateCredAriaLabel": "Update credential",
"updateError": "Could not update the resource due to: {{error}}",
"updateSuccess": "Resource successfully updated.",
"user": "User",
"username": "Username",
"usernamePlaceholder": "Username or email",
"welcomeMessage": "Welcome to Keycloak Account Management."
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,116 @@
import {
Button,
DataListAction,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
Label,
Split,
SplitItem,
} from "@patternfly/react-core";
import { LinkIcon, UnlinkIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
import { IconMapper, useAlerts } from "ui-shared";
import { linkAccount, unLinkAccount } from "../api/methods";
import { LinkedAccountRepresentation } from "../api/representations";
type AccountRowProps = {
account: LinkedAccountRepresentation;
isLinked?: boolean;
refresh: () => void;
};
export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const unLink = async (account: LinkedAccountRepresentation) => {
try {
await unLinkAccount(account);
addAlert(t("unLinkSuccess"));
} catch (error) {
addError(t("unLinkError", { error }).toString());
}
};
const link = async (account: LinkedAccountRepresentation) => {
try {
const { accountLinkUri } = await linkAccount(account);
location.href = accountLinkUri;
} catch (error) {
addError(t("linkError", { error }).toString());
}
};
return (
<DataListItem
id={`${account.providerAlias}-idp`}
key={account.providerName}
aria-labelledby={t("linkedAccountsTitle")}
>
<DataListItemRow key={account.providerName}>
<DataListItemCells
dataListCells={[
<DataListCell key="idp">
<Split>
<SplitItem className="pf-u-mr-sm">
<IconMapper icon={account.providerName} />
</SplitItem>
<SplitItem className="pf-u-my-xs" isFilled>
<span id={`${account.providerAlias}-idp-name`}>
{account.displayName}
</span>
</SplitItem>
</Split>
</DataListCell>,
<DataListCell key="label">
<Split>
<SplitItem className="pf-u-my-xs" isFilled>
<span id={`${account.providerAlias}-idp-label`}>
<Label color={account.social ? "blue" : "green"}>
{t(account.social ? "socialLogin" : "systemDefined")}
</Label>
</span>
</SplitItem>
</Split>
</DataListCell>,
<DataListCell key="username" width={5}>
<Split>
<SplitItem className="pf-u-my-xs" isFilled>
<span id={`${account.providerAlias}-idp-username`}>
{account.linkedUsername}
</span>
</SplitItem>
</Split>
</DataListCell>,
]}
/>
<DataListAction
aria-labelledby={t("link")}
aria-label={t("unLink")}
id="setPasswordAction"
>
{isLinked && (
<Button
id={`${account.providerAlias}-idp-unlink`}
variant="link"
onClick={() => unLink(account)}
>
<UnlinkIcon size="sm" /> {t("unLink")}
</Button>
)}
{!isLinked && (
<Button
id={`${account.providerAlias}-idp-link`}
variant="link"
onClick={() => link(account)}
>
<LinkIcon size="sm" /> {t("link")}
</Button>
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
);
};

View File

@ -0,0 +1,244 @@
import {
Button,
DataList,
DataListContent,
DataListItem,
DataListItemRow,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
Grid,
GridItem,
Label,
Spinner,
Split,
SplitItem,
Title,
Tooltip,
} from "@patternfly/react-core";
import {
SyncAltIcon,
MobileAltIcon,
DesktopIcon,
} from "@patternfly/react-icons";
import { TFuncKey } from "i18next";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { deleteSession, getDevices } from "../api/methods";
import {
DeviceRepresentation,
SessionRepresentation,
ClientRepresentation,
} from "../api/representations";
import { useAlerts, ContinueCancelModal } from "ui-shared";
import useFormatter from "../components/formatter/format-date";
import { Page } from "../components/page/Page";
import { keycloak } from "../keycloak";
import { usePromise } from "../utils/usePromise";
const DeviceActivity = () => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const { formatTime } = useFormatter();
const [devices, setDevices] = useState<DeviceRepresentation[]>();
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
const moveCurrentToTop = (devices: DeviceRepresentation[]) => {
let currentDevice = devices[0];
const index = devices.findIndex((d) => d.current);
currentDevice = devices.splice(index, 1)[0];
devices.unshift(currentDevice);
const sessionIndex = currentDevice.sessions.findIndex((s) => s.current);
const currentSession = currentDevice.sessions.splice(sessionIndex, 1)[0];
currentDevice.sessions.unshift(currentSession);
setDevices(devices);
};
usePromise((signal) => getDevices({ signal }), moveCurrentToTop, [key]);
const signOutAll = async () => {
await deleteSession();
keycloak.logout();
};
const signOutSession = async (
session: SessionRepresentation,
device: DeviceRepresentation
) => {
try {
await deleteSession(session.id);
addAlert(t("signedOutSession", [session.browser, device.os]));
refresh();
} catch (error) {
addError(t("errorSignOutMessage", { error }).toString());
}
};
const makeClientsString = (clients: ClientRepresentation[]): string => {
let clientsString = "";
clients.forEach((client, index) => {
let clientName: string;
if (client.clientName !== "") {
clientName = t(client.clientName as TFuncKey);
} else {
clientName = client.clientId;
}
clientsString += clientName;
if (clients.length > index + 1) clientsString += ", ";
});
return clientsString;
};
if (!devices) {
return <Spinner />;
}
return (
<Page
title={t("device-activity")}
description={t("signedInDevicesExplanation")}
>
<Split hasGutter className="pf-u-mb-lg">
<SplitItem isFilled>
<Title headingLevel="h2" size="xl">
{t("signedInDevices")}
</Title>
</SplitItem>
<SplitItem>
<Tooltip content={t("refreshPage")}>
<Button
aria-describedby="refresh page"
id="refresh-page"
variant="link"
onClick={() => refresh()}
icon={<SyncAltIcon />}
>
Refresh
</Button>
</Tooltip>
{(devices.length > 1 || devices[0].sessions.length > 1) && (
<ContinueCancelModal
buttonTitle={t("signOutAllDevices")}
modalTitle={t("signOutAllDevices")}
modalMessage={t("signOutAllDevicesWarning")}
onContinue={() => signOutAll()}
/>
)}
</SplitItem>
</Split>
<DataList
className="signed-in-device-list"
aria-label={t("signedInDevices")}
>
<DataListItem aria-labelledby="sessions">
{devices.map((device) =>
device.sessions.map((session) => (
<DataListItemRow key={device.id}>
<DataListContent
aria-label="device-sessions-content"
className="pf-u-flex-grow-1"
>
<Grid hasGutter>
<GridItem span={1} rowSpan={2}>
{device.mobile ? <MobileAltIcon /> : <DesktopIcon />}
</GridItem>
<GridItem sm={8} md={9} span={10}>
<span className="pf-u-mr-md session-title">
{device.os.toLowerCase().includes("unknown")
? t("unknownOperatingSystem")
: device.os}{" "}
{!device.osVersion.toLowerCase().includes("unknown") &&
device.osVersion}{" "}
/ {session.browser}
</span>
{session.current && (
<Label color="green">{t("currentSession")}</Label>
)}
</GridItem>
<GridItem
className="pf-u-text-align-right"
sm={3}
md={2}
span={1}
>
{!session.current && (
<ContinueCancelModal
buttonTitle={t("doSignOut")}
modalTitle={t("doSignOut")}
buttonVariant="secondary"
modalMessage={t("signOutWarning")}
onContinue={() => signOutSession(session, device)}
/>
)}
</GridItem>
<GridItem span={11}>
<DescriptionList
className="signed-in-device-grid"
columnModifier={{ sm: "2Col", lg: "3Col" }}
cols={5}
rows={1}
>
<DescriptionListGroup>
<DescriptionListTerm>
{t("ipAddress")}
</DescriptionListTerm>
<DescriptionListDescription>
{session.ipAddress}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
{t("lastAccessedOn")}
</DescriptionListTerm>
<DescriptionListDescription>
{formatTime(session.lastAccess)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
{t("clients")}
</DescriptionListTerm>
<DescriptionListDescription>
{makeClientsString(session.clients)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
{t("started")}
</DescriptionListTerm>
<DescriptionListDescription>
{formatTime(session.started)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
{t("expires")}
</DescriptionListTerm>
<DescriptionListDescription>
{formatTime(session.expires)}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</GridItem>
</Grid>
</DataListContent>
</DataListItemRow>
))
)}
</DataListItem>
</DataList>
</Page>
);
};
export default DeviceActivity;

View File

@ -0,0 +1,78 @@
import { DataList, Stack, StackItem, Title } from "@patternfly/react-core";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { getLinkedAccounts } from "../api/methods";
import { LinkedAccountRepresentation } from "../api/representations";
import { EmptyRow } from "../components/datalist/EmptyRow";
import { Page } from "../components/page/Page";
import { usePromise } from "../utils/usePromise";
import { AccountRow } from "./AccountRow";
const LinkedAccounts = () => {
const { t } = useTranslation();
const [accounts, setAccounts] = useState<LinkedAccountRepresentation[]>([]);
const [key, setKey] = useState(1);
const refresh = () => setKey(key + 1);
usePromise((signal) => getLinkedAccounts({ signal }), setAccounts, [key]);
const linkedAccounts = useMemo(
() => accounts.filter((account) => account.connected),
[accounts]
);
const unLinkedAccounts = useMemo(
() => accounts.filter((account) => !account.connected),
[accounts]
);
return (
<Page
title={t("linkedAccountsTitle")}
description={t("linkedAccountsIntroMessage")}
>
<Stack hasGutter>
<StackItem>
<Title headingLevel="h2" className="pf-u-mb-lg" size="xl">
{t("linkedLoginProviders")}
</Title>
<DataList id="linked-idps" aria-label={t("linkedLoginProviders")}>
{linkedAccounts.length > 0 ? (
linkedAccounts.map((account) => (
<AccountRow
key={account.providerName}
account={account}
isLinked
refresh={refresh}
/>
))
) : (
<EmptyRow message={t("linkedEmpty")} />
)}
</DataList>
</StackItem>
<StackItem>
<Title headingLevel="h2" className="pf-u-mt-xl pf-u-mb-lg" size="xl">
{t("unlinkedLoginProviders")}
</Title>
<DataList id="unlinked-idps" aria-label={t("unlinkedLoginProviders")}>
{unLinkedAccounts.length > 0 ? (
unLinkedAccounts.map((account) => (
<AccountRow
key={account.providerName}
account={account}
refresh={refresh}
/>
))
) : (
<EmptyRow message={t("unlinkedEmpty")} />
)}
</DataList>
</StackItem>
</Stack>
</Page>
);
};
export default LinkedAccounts;

View File

@ -0,0 +1,229 @@
import {
Button,
DataList,
DataListAction,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
Dropdown,
DropdownItem,
KebabToggle,
PageSection,
Spinner,
Split,
SplitItem,
Title,
} from "@patternfly/react-core";
import { TFuncKey } from "i18next";
import { CSSProperties, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { ContinueCancelModal, useAlerts } from "ui-shared";
import { deleteCredentials, getCredentials } from "../api/methods";
import {
CredentialContainer,
CredentialMetadataRepresentation,
CredentialRepresentation,
} from "../api/representations";
import { EmptyRow } from "../components/datalist/EmptyRow";
import useFormatter from "../components/format/format-date";
import { Page } from "../components/page/Page";
import { keycloak } from "../keycloak";
import { usePromise } from "../utils/usePromise";
type MobileLinkProps = {
title: string;
onClick: () => void;
};
const MobileLink = ({ title, onClick }: MobileLinkProps) => {
const [open, setOpen] = useState(false);
return (
<>
<Dropdown
isPlain
position="right"
toggle={<KebabToggle onToggle={setOpen} />}
className="pf-u-display-none-on-lg"
isOpen={open}
dropdownItems={[
<DropdownItem key="1" onClick={onClick}>
{title}
</DropdownItem>,
]}
/>
<Button
variant="link"
onClick={onClick}
className="pf-u-display-none pf-u-display-inline-flex-on-lg"
>
{title}
</Button>
</>
);
};
const SigningIn = () => {
const { t } = useTranslation();
const { formatDate } = useFormatter();
const { addAlert, addError } = useAlerts();
const { login } = keycloak;
const [credentials, setCredentials] = useState<CredentialContainer[]>();
const [key, setKey] = useState(1);
const refresh = () => setKey(key + 1);
usePromise((signal) => getCredentials({ signal }), setCredentials, [key]);
const credentialRowCells = (
credMetadata: CredentialMetadataRepresentation
) => {
const credential = credMetadata.credential;
const maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as CSSProperties;
const items = [
<DataListCell
id={`cred-${credMetadata.credential.id}`}
key="title"
className="pf-u-max-width"
style={maxWidth}
>
{credential.userLabel || t(credential.type as TFuncKey)}
</DataListCell>,
];
if (credential.createdDate) {
items.push(
<DataListCell key={"created" + credential.id}>
<Trans i18nKey="credentialCreatedAt">
<strong className="pf-u-mr-md"></strong>
{{ date: formatDate(new Date(credential.createdDate)) }}
</Trans>
</DataListCell>
);
}
return items;
};
const label = (credential: CredentialRepresentation) =>
credential.userLabel || t(credential.type as TFuncKey);
if (!credentials) {
return <Spinner />;
}
return (
<Page title={t("signingIn")} description={t("signingInDescription")}>
<DataList aria-label="user credential" className="pf-u-mb-xl">
{credentials.map((container) => (
<PageSection
key={container.category}
variant="light"
className="pf-u-px-0"
>
<Title headingLevel="h2" size="xl">
{t(container.category as TFuncKey)}
</Title>
<Split className="pf-u-mt-lg pf-u-mb-lg">
<SplitItem>
<Title headingLevel="h3" size="md" className="pf-u-mb-md">
<span className="cred-title pf-u-display-block">
{t(container.displayName as TFuncKey)}
</span>
</Title>
{t(container.helptext as TFuncKey)}
</SplitItem>
{container.createAction && (
<SplitItem isFilled>
<div className="pf-u-float-right">
<MobileLink
onClick={() =>
login({
action: container.createAction,
})
}
title={t("setUpNew", [
t(container.displayName as TFuncKey),
])}
/>
</div>
</SplitItem>
)}
</Split>
<DataList aria-label="credential list" className="pf-u-mb-xl">
{container.userCredentialMetadatas.length === 0 && (
<EmptyRow
message={t("notSetUp", [
t(container.displayName as TFuncKey),
])}
/>
)}
{container.userCredentialMetadatas.map((meta) => (
<DataListItem key={meta.credential.id}>
<DataListItemRow>
<DataListItemCells
className="pf-u-py-0"
dataListCells={[
...credentialRowCells(meta),
<DataListAction
key="action"
id={`action-${meta.credential.id}`}
aria-label={t("updateCredAriaLabel")}
aria-labelledby={`cred-${meta.credential.id}`}
>
{container.removeable ? (
<ContinueCancelModal
buttonTitle="remove"
buttonVariant="danger"
modalTitle={t("removeCred", [
label(meta.credential),
])}
modalMessage={t("stopUsingCred", [
label(meta.credential),
])}
onContinue={async () => {
try {
await deleteCredentials(meta.credential);
addAlert(
t("successRemovedMessage", {
userLabel: label(meta.credential),
})
);
refresh();
} catch (error) {
addError(
t("errorRemovedMessage", {
userLabel: label(meta.credential),
error,
}).toString()
);
}
}}
/>
) : (
<Button
variant="secondary"
onClick={() => {
if (container.updateAction)
login({ action: container.updateAction });
}}
>
{t("update")}
</Button>
)}
</DataListAction>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
</PageSection>
))}
</DataList>
</Page>
);
};
export default SigningIn;

View File

@ -0,0 +1,107 @@
import { Links, parseLinks } from "./api/parse-links";
import { Permission, Resource, Scope } from "./api/representations";
import { environment } from "./environment";
import { keycloak } from "./keycloak";
import { joinPath } from "./utils/joinPath";
export const fetchResources = async (
params: RequestInit,
requestParams: Record<string, string>,
shared: boolean | undefined = false
): Promise<{ data: Resource[]; links: Links }> => {
const response = await get(
`/resources${shared ? "/shared-with-me?" : "?"}${new URLSearchParams(
requestParams
)}`,
params
);
let links: Links;
try {
links = parseLinks(response);
} catch (error) {
links = {};
}
return {
data: checkResponse(await response.json()),
links,
};
};
export const fetchPermission = async (
params: RequestInit,
resourceId: string
): Promise<Permission[]> => {
const response = await request<Permission[]>(
`/resources/${resourceId}/permissions`,
params
);
return checkResponse(response);
};
export const updateRequest = (
resourceId: string,
username: string,
scopes: Scope[] | string[]
) =>
request(`/resources/${resourceId}/permissions`, {
method: "put",
body: JSON.stringify([{ username, scopes }]),
});
export const updatePermissions = (
resourceId: string,
permissions: Permission[]
) =>
request(`/resources/${resourceId}/permissions`, {
method: "put",
body: JSON.stringify(permissions),
});
function checkResponse<T>(response: T) {
if (!response) throw new Error("Could not fetch");
return response;
}
async function get(path: string, params: RequestInit): Promise<Response> {
const url = joinPath(
environment.authServerUrl,
"realms",
environment.loginRealm,
"account",
path
);
const response = await fetch(url, {
...params,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${await getAccessToken()}`,
},
});
if (!response.ok) {
throw new Error(response.statusText);
}
return response;
}
async function request<T>(
path: string,
params: RequestInit
): Promise<T | undefined> {
const response = await get(path, params);
if (response.status !== 204) return response.json();
}
async function getAccessToken() {
try {
await keycloak.updateToken(5);
} catch (error) {
keycloak.login();
}
return keycloak.token;
}

View File

@ -0,0 +1,2 @@
export const CONTENT_TYPE_HEADER = "content-type";
export const CONTENT_TYPE_JSON = "application/json";

View File

@ -0,0 +1,124 @@
import { environment } from "../environment";
import { joinPath } from "../utils/joinPath";
import { parseResponse } from "./parse-response";
import {
ClientRepresentation,
CredentialContainer,
CredentialRepresentation,
DeviceRepresentation,
Group,
LinkedAccountRepresentation,
Permission,
UserRepresentation,
} from "./representations";
import { request } from "./request";
export type CallOptions = {
signal?: AbortSignal;
};
export type PaginationParams = {
first: number;
max: number;
};
export async function getPersonalInfo({
signal,
}: CallOptions = {}): Promise<UserRepresentation> {
const response = await request("/?userProfileMetadata=true", { signal });
return parseResponse<UserRepresentation>(response);
}
export async function savePersonalInfo(
info: UserRepresentation
): Promise<void> {
const response = await request("/", { body: info, method: "POST" });
if (!response.ok) {
const { errors } = await response.json();
throw errors;
}
return undefined;
}
export async function getPermissionRequests(
resourceId: string,
{ signal }: CallOptions = {}
): Promise<Permission[]> {
const response = await request(
`/resources/${resourceId}/permissions/requests`,
{ signal }
);
return parseResponse<Permission[]>(response);
}
export async function getDevices({
signal,
}: CallOptions): Promise<DeviceRepresentation[]> {
const response = await request("/sessions/devices", { signal });
return parseResponse<DeviceRepresentation[]>(response);
}
export async function getApplications({ signal }: CallOptions = {}): Promise<
ClientRepresentation[]
> {
const response = await request("/applications", { signal });
return parseResponse<ClientRepresentation[]>(response);
}
export async function deleteConsent(id: string) {
return request(`/applications/${id}/consent`, { method: "DELETE" });
}
export async function deleteSession(id?: string) {
return request(`"/sessions${id ? `/${id}` : ""}`, {
method: "DELETE",
});
}
export async function getCredentials({ signal }: CallOptions) {
const response = await request("/credentials", {
signal,
});
return parseResponse<CredentialContainer[]>(response);
}
export async function deleteCredentials(credential: CredentialRepresentation) {
return request("/credentials/" + credential.id, {
method: "DELETE",
});
}
export async function getLinkedAccounts({ signal }: CallOptions) {
const response = await request("/linked-accounts", { signal });
return parseResponse<LinkedAccountRepresentation[]>(response);
}
export async function unLinkAccount(account: LinkedAccountRepresentation) {
const response = await request("/linked-accounts/" + account.providerName, {
method: "DELETE",
});
return parseResponse(response);
}
export async function linkAccount(account: LinkedAccountRepresentation) {
const redirectUri = encodeURIComponent(
joinPath(
environment.authServerUrl,
"realms",
environment.loginRealm,
"account"
)
);
const response = await request("/linked-accounts/" + account.providerName, {
searchParams: { providerId: account.providerName, redirectUri },
});
return parseResponse<{ accountLinkUri: string }>(response);
}
export async function getGroups({ signal }: CallOptions) {
const response = await request("/groups", {
signal,
});
return parseResponse<Group[]>(response);
}

View File

@ -0,0 +1,28 @@
export type Links = {
prev?: Record<string, string>;
next?: Record<string, string>;
};
export function parseLinks(response: Response): Links {
const linkHeader = response.headers.get("link");
if (!linkHeader) {
throw new Error("Attempted to parse links, but no header was found.");
}
const links = linkHeader.split(/,\s*</);
return links.reduce<Links>((acc: Links, link: string) => {
const matcher = link.match(/<?([^>]*)>(.*)/);
if (!matcher) return {};
const linkUrl = matcher[1];
const rel = matcher[2].match(/\s*(.+)\s*=\s*"?([^"]+)"?/);
if (rel) {
const link: Record<string, string> = {};
for (const [key, value] of new URL(linkUrl).searchParams.entries()) {
link[key] = value;
}
acc[rel[2] as keyof Links] = link;
}
return acc;
}, {});
}

View File

@ -0,0 +1,53 @@
import { isRecord } from "../utils/isRecord";
import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants";
export class ApiError extends Error {}
export async function parseResponse<T>(response: Response): Promise<T> {
const contentType = response.headers.get(CONTENT_TYPE_HEADER);
const isJSON = contentType ? contentType.includes(CONTENT_TYPE_JSON) : false;
if (!isJSON) {
throw new Error(
`Expected response to have a JSON content type, got '${contentType}' instead.`
);
}
const data = await parseJSON(response);
if (!response.ok) {
throw new ApiError(getErrorMessage(data));
}
return data as T;
}
async function parseJSON(response: Response): Promise<unknown> {
try {
return await response.json();
} catch (error) {
throw new Error("Unable to parse response as valid JSON.", {
cause: error,
});
}
}
function getErrorMessage(data: unknown): string {
if (!isRecord(data)) {
throw new Error("Unable to retrieve error message from response.");
}
const errorKeys = ["error_description", "errorMessage", "error"];
for (const key of errorKeys) {
const value = data[key];
if (typeof value === "string") {
return value;
}
}
throw new Error(
"Unable to retrieve error message from response, no matching key found."
);
}

View File

@ -0,0 +1,210 @@
// Generated using typescript-generator version 2.37.1128 on 2022-09-16 15:57:05.
export interface AccountLinkUriRepresentation {
accountLinkUri: string;
nonce: string;
hash: string;
}
export interface ClientRepresentation {
clientId: string;
clientName: string;
description: string;
userConsentRequired: boolean;
inUse: boolean;
offlineAccess: boolean;
rootUrl: string;
baseUrl: string;
effectiveUrl: string;
consent?: ConsentRepresentation;
logoUri: string;
policyUri: string;
tosUri: string;
}
export interface ConsentRepresentation {
grantedScopes: ConsentScopeRepresentation[];
createdDate: number;
lastUpdatedDate: number;
}
export interface ConsentScopeRepresentation {
id: string;
name: string;
displayTest: string;
}
export interface CredentialMetadataRepresentation {
infoMessage: string;
warningMessageTitle: string;
warningMessageDescription: string;
credential: CredentialRepresentation;
}
export interface DeviceRepresentation {
id: string;
ipAddress: string;
os: string;
osVersion: string;
browser: string;
device: string;
lastAccess: number;
current: boolean;
sessions: SessionRepresentation[];
mobile: boolean;
}
export interface LinkedAccountRepresentation {
connected: boolean;
providerAlias: string;
providerName: string;
displayName: string;
linkedUsername: string;
social: boolean;
}
export interface SessionRepresentation {
id: string;
ipAddress: string;
started: number;
lastAccess: number;
expires: number;
clients: ClientRepresentation[];
browser: string;
current: boolean;
}
export interface UserProfileAttributeMetadata {
name: string;
displayName: string;
required: boolean;
readOnly: boolean;
annotations: { [index: string]: any };
validators: { [index: string]: { [index: string]: any } };
}
export interface UserProfileMetadata {
attributes: UserProfileAttributeMetadata[];
}
export interface UserRepresentation {
id: string;
username: string;
firstName: string;
lastName: string;
email: string;
emailVerified: boolean;
userProfileMetadata: UserProfileMetadata;
attributes: { [index: string]: string[] };
}
export interface CredentialRepresentation {
id: string;
type: string;
userLabel: string;
createdDate: number;
secretData: string;
credentialData: string;
priority: number;
value: string;
temporary: boolean;
/**
* @deprecated
*/
device: string;
/**
* @deprecated
*/
hashedSaltedValue: string;
/**
* @deprecated
*/
salt: string;
/**
* @deprecated
*/
hashIterations: number;
/**
* @deprecated
*/
counter: number;
/**
* @deprecated
*/
algorithm: string;
/**
* @deprecated
*/
digits: number;
/**
* @deprecated
*/
period: number;
/**
* @deprecated
*/
config: { [index: string]: string[] };
}
export interface CredentialTypeMetadata {
type: string;
displayName: string;
helpText: string;
iconCssClass: string;
createAction: string;
updateAction: string;
removeable: boolean;
category: "basic-authentication" | "two-factor" | "passwordless";
}
export interface CredentialContainer {
type: string;
category: string;
displayName: string;
helptext: string;
iconCssClass: string;
createAction: string;
updateAction: string;
removeable: boolean;
userCredentialMetadatas: CredentialMetadataRepresentation[];
metadata: CredentialTypeMetadata;
}
export interface Client {
baseUrl: string;
clientId: string;
name?: string;
}
export interface Scope {
name: string;
displayName?: string;
}
export interface Resource {
_id: string;
name: string;
client: Client;
scopes: Scope[];
uris: string[];
shareRequests: Permission[];
}
export interface Permission {
email?: string;
firstName?: string;
lastName?: string;
scopes: Scope[] | string[]; // this should be Scope[] - fix API
username: string;
}
export interface Permissions {
permissions: Permission[];
row?: number;
}
export interface Group {
id?: string;
name: string;
path: string;
}

View File

@ -0,0 +1,52 @@
import { environment } from "../environment";
import { keycloak } from "../keycloak";
import { joinPath } from "../utils/joinPath";
import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants";
export type RequestOptions = {
signal?: AbortSignal;
method?: "POST" | "PUT" | "DELETE";
searchParams?: Record<string, string>;
body?: unknown;
};
export async function request(
path: string,
{ signal, method, searchParams, body }: RequestOptions = {}
): Promise<Response> {
const url = new URL(
joinPath(
environment.authServerUrl,
"realms",
environment.loginRealm,
"account",
path
)
);
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) =>
url.searchParams.set(key, value)
);
}
return fetch(url, {
signal,
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON,
authorization: `Bearer ${await getAccessToken()}`,
},
});
}
async function getAccessToken() {
try {
await keycloak.updateToken(5);
} catch (error) {
await keycloak.login();
}
return keycloak.token;
}

View File

@ -0,0 +1,269 @@
import {
Button,
DataList,
DataListCell,
DataListContent,
DataListItem,
DataListItemCells,
DataListItemRow,
DataListToggle,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
Grid,
GridItem,
Spinner,
} from "@patternfly/react-core";
import {
CheckIcon,
ExternalLinkAltIcon,
InfoAltIcon,
} from "@patternfly/react-icons";
import { TFuncKey } from "i18next";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { deleteConsent, getApplications } from "../api/methods";
import { ClientRepresentation } from "../api/representations";
import { useAlerts, ContinueCancelModal } from "ui-shared";
import { Page } from "../components/page/Page";
import { usePromise } from "../utils/usePromise";
type Application = ClientRepresentation & {
open: boolean;
};
const Applications = () => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const [applications, setApplications] = useState<Application[]>();
const [key, setKey] = useState(1);
const refresh = () => setKey(key + 1);
usePromise(
(signal) => getApplications({ signal }),
(clients) => setApplications(clients.map((c) => ({ ...c, open: false }))),
[key]
);
const toggleOpen = (clientId: string) => {
setApplications([
...applications!.map((a) =>
a.clientId === clientId ? { ...a, open: !a.open } : a
),
]);
};
const removeConsent = async (id: string) => {
try {
await deleteConsent(id);
refresh();
addAlert(t("removeConsentSuccess"));
} catch (error) {
addError(t("removeConsentError", { error }).toString());
}
};
if (!applications) {
return <Spinner />;
}
return (
<Page title={t("application")} description={t("applicationsIntroMessage")}>
<DataList id="applications-list" aria-label={t("application")}>
<DataListItem
id="applications-list-header"
aria-labelledby="Columns names"
>
<DataListItemRow>
<span style={{ visibility: "hidden", height: 55 }}>
<DataListToggle
id="applications-list-header-invisible-toggle"
aria-controls="hidden"
/>
</span>
<DataListItemCells
dataListCells={[
<DataListCell
key="applications-list-client-id-header"
width={2}
className="pf-u-pt-md"
>
<strong>{t("name")}</strong>
</DataListCell>,
<DataListCell
key="applications-list-app-type-header"
width={2}
className="pf-u-pt-md"
>
<strong>{t("applicationType")}</strong>
</DataListCell>,
<DataListCell
key="applications-list-status"
width={2}
className="pf-u-pt-md"
>
<strong>{t("status")}</strong>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
{applications.map((application) => (
<DataListItem
key={application.clientId}
aria-labelledby="applications-list"
isExpanded={application.open}
>
<DataListItemRow className="pf-u-align-items-center">
<DataListToggle
onClick={() => toggleOpen(application.clientId)}
isExpanded={application.open}
id={`toggle-${application.clientId}`}
/>
<DataListItemCells
className="pf-u-align-items-center"
dataListCells={[
<DataListCell width={2} key={`client${application.clientId}`}>
<Button
className="pf-u-pl-0 title-case"
component="a"
variant="link"
onClick={() => window.open(application.effectiveUrl)}
>
{application.clientName || application.clientId}{" "}
<ExternalLinkAltIcon />
</Button>
</DataListCell>,
<DataListCell
width={2}
key={`internal${application.clientId}`}
>
{application.userConsentRequired
? t("thirdPartyApp")
: t("internalApp")}
{application.offlineAccess ? ", " + t("offlineAccess") : ""}
</DataListCell>,
<DataListCell width={2} key={`status${application.clientId}`}>
{application.inUse ? t("inUse") : t("notInUse")}
</DataListCell>,
]}
/>
</DataListItemRow>
<DataListContent
className="pf-u-pl-4xl"
aria-label={t("applicationDetails")}
isHidden={!application.open}
>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>{t("client")}</DescriptionListTerm>
<DescriptionListDescription>
{application.clientId}
</DescriptionListDescription>
</DescriptionListGroup>
{application.description && (
<DescriptionListGroup>
<DescriptionListTerm>
{t("description")}
</DescriptionListTerm>
<DescriptionListDescription>
{application.description}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{application.effectiveUrl && (
<DescriptionListGroup>
<DescriptionListTerm>URL</DescriptionListTerm>
<DescriptionListDescription>
{application.effectiveUrl.split('"')}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{application.consent && (
<>
<DescriptionListGroup>
<DescriptionListTerm>Has access to</DescriptionListTerm>
{application.consent.grantedScopes.map((scope) => (
<DescriptionListDescription key={`scope${scope.id}`}>
<CheckIcon /> {t(scope.name as TFuncKey)}
</DescriptionListDescription>
))}
</DescriptionListGroup>
{application.tosUri && (
<DescriptionListGroup>
<DescriptionListTerm>
{t("termsOfService")}
</DescriptionListTerm>
<DescriptionListDescription>
{application.tosUri}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{application.policyUri && (
<DescriptionListGroup>
<DescriptionListTerm>
{t("privacyPolicy")}
</DescriptionListTerm>
<DescriptionListDescription>
{application.policyUri}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{application.logoUri && (
<DescriptionListGroup>
<DescriptionListTerm>{t("logo")}</DescriptionListTerm>
<DescriptionListDescription>
<img src={application.logoUri} />
</DescriptionListDescription>
</DescriptionListGroup>
)}
<DescriptionListGroup>
<DescriptionListTerm>
{t("accessGrantedOn") + ": "}
</DescriptionListTerm>
<DescriptionListDescription>
{new Intl.DateTimeFormat("en", {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).format(application.consent.createdDate)}
</DescriptionListDescription>
</DescriptionListGroup>
</>
)}
</DescriptionList>
{(application.consent || application.offlineAccess) && (
<Grid hasGutter>
<hr />
<GridItem>
<ContinueCancelModal
buttonTitle="removeButton"
buttonVariant="secondary"
modalTitle="removeModalTitle"
modalMessage={t("removeModalMessage", [
application.clientId,
])}
continueLabel="confirmButton"
onContinue={() => removeConsent(application.clientId)} // required
/>
</GridItem>
<GridItem>
<InfoAltIcon /> {t("infoMessage")}
</GridItem>
</Grid>
)}
</DataListContent>
</DataListItem>
))}
</DataList>
</Page>
);
};
export default Applications;

View File

@ -0,0 +1,22 @@
import {
DataListItem,
DataListItemRow,
DataListItemCells,
DataListCell,
} from "@patternfly/react-core";
type EmptyRowProps = {
message: string;
};
export const EmptyRow = ({ message }: EmptyRowProps) => {
return (
<DataListItem className="pf-u-align-items-center pf-p-b-0">
<DataListItemRow>
<DataListItemCells
dataListCells={[<DataListCell key="0">{message}</DataListCell>]}
/>
</DataListItemRow>
</DataListItem>
);
};

View File

@ -0,0 +1,30 @@
const DATE_AND_TIME_FORMAT: Intl.DateTimeFormatOptions = {
dateStyle: "long",
timeStyle: "short",
};
const TIME_FORMAT: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
//todo use user local
export default function useFormatter() {
return {
formatDate: function (
date: Date,
options: Intl.DateTimeFormatOptions | undefined = DATE_AND_TIME_FORMAT
) {
return date.toLocaleString("en", options);
},
formatTime: function (
time: number,
options: Intl.DateTimeFormatOptions | undefined = TIME_FORMAT
) {
return new Intl.DateTimeFormat("en", options).format(time);
},
};
}

View File

@ -0,0 +1,29 @@
const DATE_AND_TIME_FORMAT: Intl.DateTimeFormatOptions = {
dateStyle: "long",
timeStyle: "short",
};
const TIME_FORMAT: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
export default function useFormatter() {
return {
formatDate: function (
date: Date,
options: Intl.DateTimeFormatOptions | undefined = DATE_AND_TIME_FORMAT
) {
return date.toLocaleString("en", options);
},
formatTime: function (
time: number,
options: Intl.DateTimeFormatOptions | undefined = TIME_FORMAT
) {
return new Intl.DateTimeFormat("en", options).format(time);
},
};
}

View File

@ -0,0 +1,25 @@
import { PageSection, Text, TextContent, Title } from "@patternfly/react-core";
import { PropsWithChildren } from "react";
type PageProps = {
title: string;
description: string;
};
export const Page = ({
title,
description,
children,
}: PropsWithChildren<PageProps>) => {
return (
<>
<PageSection variant="light">
<TextContent>
<Title headingLevel="h1">{title}</Title>
<Text component="p">{description}</Text>
</TextContent>
</PageSection>
<PageSection variant="light">{children}</PageSection>
</>
);
};

View File

@ -0,0 +1,20 @@
export type Environment = {
/** The realm which should be used when signing into the application. */
loginRealm: string;
/** The URL to the root of the auth server. */
authServerUrl: string;
/** The URL to resources such as the files in the `public` directory. */
resourceUrl: string;
/** Indicates if the application is running as a Keycloak theme. */
isRunningAsTheme: boolean;
};
// The default environment, used during development.
const defaultEnvironment: Environment = {
loginRealm: "master",
authServerUrl: "http://localhost:8180",
resourceUrl: "http://localhost:8080",
isRunningAsTheme: false,
};
export { defaultEnvironment as environment };

View File

@ -0,0 +1,133 @@
import {
Checkbox,
DataList,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
} from "@patternfly/react-core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { getGroups } from "../api/methods";
import { Group } from "../api/representations";
import { Page } from "../components/page/Page";
import { usePromise } from "../utils/usePromise";
const Groups = () => {
const { t } = useTranslation();
const [groups, setGroups] = useState<Group[]>([]);
const [directMembership, setDirectMembership] = useState(false);
usePromise(
(signal) => getGroups({ signal }),
(groups) => {
if (directMembership) {
groups.forEach((el) =>
getParents(
el,
groups,
groups.map(({ path }) => path)
)
);
}
setGroups(groups);
},
[directMembership]
);
const getParents = (el: Group, groups: Group[], groupsPaths: string[]) => {
const parentPath = el.path.slice(0, el.path.lastIndexOf("/"));
if (parentPath && !groupsPaths.includes(parentPath)) {
el = {
name: parentPath.slice(parentPath.lastIndexOf("/") + 1),
path: parentPath,
};
groups.push(el);
groupsPaths.push(parentPath);
getParents(el, groups, groupsPaths);
}
};
return (
<Page title={t("groupLabel")} description={t("groupDescriptionLabel")}>
<DataList id="groups-list" aria-label={t("groupLabel")} isCompact>
<DataListItem id="groups-list-header" aria-labelledby="Columns names">
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="directMembership-header">
<Checkbox
label={t("directMembership")}
id="directMembership-checkbox"
isChecked={directMembership}
onChange={(checked) => setDirectMembership(checked)}
/>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
<DataListItem id="groups-list-header" aria-labelledby="Columns names">
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="group-name-header" width={2}>
<strong>{t("name")}</strong>
</DataListCell>,
<DataListCell key="group-path-header" width={2}>
<strong>{t("path")}</strong>
</DataListCell>,
<DataListCell key="group-direct-membership-header" width={2}>
<strong>{t("directMembership")}</strong>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
{groups.map((group, appIndex) => (
<DataListItem
id={`${appIndex}-group`}
key={"group-" + appIndex}
aria-labelledby="groups-list"
>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell
id={`${appIndex}-group-name`}
width={2}
key={"name-" + appIndex}
>
{group.name}
</DataListCell>,
<DataListCell
id={`${appIndex}-group-path`}
width={2}
key={"path-" + appIndex}
>
{group.path}
</DataListCell>,
<DataListCell
id={`${appIndex}-group-directMembership`}
width={2}
key={"directMembership-" + appIndex}
>
<Checkbox
id={`${appIndex}-checkbox-directMembership`}
isChecked={group.id != null}
isDisabled={true}
/>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
</Page>
);
};
export default Groups;

View File

@ -0,0 +1,24 @@
import { createInstance } from "i18next";
import HttpBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import { environment } from "./environment";
import { joinPath } from "./utils/joinPath";
const DEFAULT_LOCALE = "en";
const DEFAULT_NAMESPACE = "translation";
export const i18n = createInstance({
defaultNS: DEFAULT_NAMESPACE,
fallbackLng: DEFAULT_LOCALE,
ns: [DEFAULT_NAMESPACE],
interpolation: {
escapeValue: false,
},
backend: {
loadPath: joinPath(environment.resourceUrl, "locales/{{lng}}/{{ns}}.json"),
},
});
i18n.use(HttpBackend);
i18n.use(initReactI18next);

18
js/apps/account-ui/src/i18next.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
// https://www.i18next.com/overview/typescript
import "i18next";
import translation from "../public/locales/en/translation.json";
export type TranslationKeys = keyof translation;
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "translation";
resources: {
translation: typeof translation;
};
// TODO: This flag should be removed and code that errors out should be made functional.
// This will have to be done incrementally as the amount of errors the default produces is just too much.
allowObjectInHTMLChildren: true;
}
}

View File

@ -0,0 +1,10 @@
import Keycloak from "keycloak-js";
import { environment } from "./environment";
export const keycloak = new Keycloak({
url: environment.authServerUrl,
realm: environment.loginRealm,
clientId: environment.isRunningAsTheme
? "account-console"
: "security-admin-console-v2",
});

View File

@ -0,0 +1,29 @@
import "@patternfly/react-core/dist/styles/base.css";
import "@patternfly/patternfly/patternfly-addons.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { i18n } from "./i18n";
import { keycloak } from "./keycloak";
import { routes } from "./routes";
// Initialize required components before rendering app.
await Promise.all([
keycloak.init({
onLoad: "check-sso",
pkceMethod: "S256",
}),
i18n.init(),
]);
const router = createBrowserRouter(routes);
const container = document.getElementById("app");
const root = createRoot(container!);
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);

View File

@ -0,0 +1,88 @@
import { FormGroup, Select, SelectOption } from "@patternfly/react-core";
import { TFuncKey } from "i18next";
import { get } from "lodash-es";
import { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { KeycloakTextInput } from "ui-shared";
import { UserProfileAttributeMetadata } from "../api/representations";
import { fieldName, isBundleKey, unWrap } from "./PersonalInfo";
type FormFieldProps = {
attribute: UserProfileAttributeMetadata;
};
export const FormField = ({ attribute }: FormFieldProps) => {
const { t } = useTranslation();
const {
formState: { errors },
register,
control,
} = useFormContext();
const [open, setOpen] = useState(false);
const toggle = () => setOpen(!open);
const isSelect = (attribute: UserProfileAttributeMetadata) =>
Object.hasOwn(attribute.validators, "options");
return (
<FormGroup
key={attribute.name}
label={
(isBundleKey(attribute.displayName)
? t(unWrap(attribute.displayName) as TFuncKey)
: attribute.displayName) || attribute.name
}
fieldId={attribute.name}
isRequired={attribute.required}
validated={get(errors, fieldName(attribute.name)) ? "error" : "default"}
helperTextInvalid={
get(errors, fieldName(attribute.name))?.message as string
}
>
{isSelect(attribute) ? (
<Controller
name={fieldName(attribute.name)}
defaultValue=""
control={control}
render={({ field }) => (
<Select
toggleId={attribute.name}
onToggle={toggle}
onSelect={(_, value) => {
field.onChange(value.toString());
toggle();
}}
selections={field.value}
variant="single"
aria-label={t("selectOne")}
isOpen={open}
>
{[
<SelectOption key="empty" value="">
{t("choose")}
</SelectOption>,
...(
attribute.validators.options as { options: string[] }
).options.map((option) => (
<SelectOption
selected={field.value === option}
key={option}
value={option}
>
{option}
</SelectOption>
)),
]}
</Select>
)}
/>
) : (
<KeycloakTextInput
id={attribute.name}
{...register(fieldName(attribute.name))}
/>
)}
</FormGroup>
);
};

View File

@ -0,0 +1,87 @@
import { ActionGroup, Button, Form } from "@patternfly/react-core";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAlerts } from "ui-shared";
import { getPersonalInfo, savePersonalInfo } from "../api/methods";
import {
UserProfileMetadata,
UserRepresentation,
} from "../api/representations";
import { Page } from "../components/page/Page";
import { usePromise } from "../utils/usePromise";
import { FormField } from "./FormField";
type FieldError = {
field: string;
errorMessage: string;
params: string[];
};
const ROOT_ATTRIBUTES = ["username", "firstName", "lastName", "email"];
export const isBundleKey = (key?: string) => key?.includes("${");
export const unWrap = (key: string) => key.substring(2, key.length - 1);
export const isRootAttribute = (attr?: string) =>
attr && ROOT_ATTRIBUTES.includes(attr);
export const fieldName = (name: string) =>
`${isRootAttribute(name) ? "" : "attributes."}${name}`;
const PersonalInfo = () => {
const { t } = useTranslation();
const [userProfileMetadata, setUserProfileMetadata] =
useState<UserProfileMetadata>();
const form = useForm<UserRepresentation>({ mode: "onChange" });
const { handleSubmit, reset, setError } = form;
const { addAlert, addError } = useAlerts();
usePromise(
(signal) => getPersonalInfo({ signal }),
(personalInfo) => {
setUserProfileMetadata(personalInfo.userProfileMetadata);
reset(personalInfo);
}
);
const onSubmit = async (user: UserRepresentation) => {
try {
await savePersonalInfo(user);
addAlert(t("accountUpdatedMessage"));
} catch (error) {
addError(t("accountUpdatedError").toString());
(error as FieldError[]).forEach((e) => {
const params = Object.assign(
{},
e.params.map((p) => (isBundleKey(p) ? unWrap(p) : p))
);
setError(fieldName(e.field) as keyof UserRepresentation, {
message: t(e.errorMessage, { ...params, defaultValue: e.field }),
type: "server",
});
});
}
};
return (
<Page title={t("personalInfo")} description={t("personalInfoDescription")}>
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...form}>
{(userProfileMetadata?.attributes || []).map((attribute) => (
<FormField key={attribute.name} attribute={attribute} />
))}
</FormProvider>
<ActionGroup>
<Button type="submit" id="save-btn" variant="primary">
{t("doSave")}
</Button>
<Button id="cancel-btn" variant="link" onClick={() => reset()}>
{t("doCancel")}
</Button>
</ActionGroup>
</Form>
</Page>
);
};
export default PersonalInfo;

View File

@ -0,0 +1,100 @@
import { Button, Form, FormGroup, Modal } from "@patternfly/react-core";
import { Fragment, useEffect } from "react";
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { KeycloakTextInput, SelectControl, useAlerts } from "ui-shared";
import { updatePermissions } from "../api";
import type { Permission, Resource } from "../api/representations";
type EditTheResourceProps = {
resource: Resource;
permissions?: Permission[];
onClose: () => void;
};
type FormValues = {
permissions: Permission[];
};
export const EditTheResource = ({
resource,
permissions,
onClose,
}: EditTheResourceProps) => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const form = useForm<FormValues>();
const { control, register, reset, handleSubmit } = form;
const { fields } = useFieldArray<FormValues>({
control,
name: "permissions",
});
useEffect(() => reset({ permissions }), []);
const editShares = async ({ permissions }: FormValues) => {
try {
await Promise.all(
permissions.map((permission) =>
updatePermissions(resource._id, [permission])
)
);
addAlert(t("updateSuccess"));
onClose();
} catch (error) {
addError(t("updateError", { error }).toString());
}
};
return (
<Modal
title={t("editTheResource", [resource.name])}
variant="medium"
isOpen
onClose={onClose}
actions={[
<Button
key="confirm"
variant="primary"
id="done"
type="submit"
form="edit-form"
>
{t("done")}
</Button>,
]}
>
<Form id="edit-form" onSubmit={handleSubmit(editShares)}>
<FormProvider {...form}>
{fields.map((p, index) => (
<Fragment key={p.id}>
<FormGroup label={t("user")} fieldId={`user-${p.id}`}>
<KeycloakTextInput
id={`user-${p.id}`}
type="text"
{...register(`permissions.${index}.username`)}
isDisabled
/>
</FormGroup>
<SelectControl
id={`permissions-${p.id}`}
name={`permissions.${index}.scopes`}
label="permissions"
variant="typeaheadmulti"
controller={{ defaultValue: [] }}
options={resource.scopes.map(({ name, displayName }) => ({
key: name,
value: displayName || name,
}))}
menuAppendTo="parent"
/>
</Fragment>
))}
</FormProvider>
</Form>
</Modal>
);
};

View File

@ -0,0 +1,132 @@
import {
Badge,
Button,
Chip,
Modal,
ModalVariant,
Text,
} from "@patternfly/react-core";
import { UserCheckIcon } from "@patternfly/react-icons";
import {
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAlerts } from "ui-shared";
import { fetchPermission, updateRequest } from "../api";
import { Permission, Resource } from "../api/representations";
type PermissionRequestProps = {
resource: Resource;
refresh: () => void;
};
export const PermissionRequest = ({
resource,
refresh,
}: PermissionRequestProps) => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const [open, setOpen] = useState(false);
const toggle = () => setOpen(!open);
const approveDeny = async (
shareRequest: Permission,
approve: boolean = false
) => {
try {
const permissions = await fetchPermission({}, resource._id);
const { scopes, username } = permissions.find(
(p) => p.username === shareRequest.username
)!;
await updateRequest(
resource._id,
username,
approve
? [...(scopes as string[]), ...(shareRequest.scopes as string[])]
: scopes
);
addAlert(t("shareSuccess"));
toggle();
refresh();
} catch (error) {
addError(t("shareError", { error }).toString());
}
};
return (
<>
<Button variant="link" onClick={toggle}>
<UserCheckIcon size="lg" />
<Badge>{resource.shareRequests.length}</Badge>
</Button>
<Modal
title={t("permissionRequest", [resource.name])}
variant={ModalVariant.large}
isOpen={open}
onClose={toggle}
actions={[
<Button key="close" variant="link" onClick={toggle}>
{t("close")}
</Button>,
]}
>
<TableComposable aria-label={t("resources")}>
<Thead>
<Tr>
<Th>{t("requestor")}</Th>
<Th>{t("permissionRequests")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{resource.shareRequests.map((shareRequest) => (
<Tr key={shareRequest.username}>
<Td>
{shareRequest.firstName} {shareRequest.lastName}{" "}
{shareRequest.lastName ? "" : shareRequest.username}
<br />
<Text component="small">{shareRequest.email}</Text>
</Td>
<Td>
{shareRequest.scopes.map((scope) => (
<Chip key={scope.toString()} isReadOnly>
{scope as string}
</Chip>
))}
</Td>
<Td>
<Button
onClick={() => {
approveDeny(shareRequest, true);
}}
>
{t("accept")}
</Button>
<Button
onClick={() => {
approveDeny(shareRequest);
}}
className="pf-u-ml-sm"
variant="danger"
>
{t("doDeny")}
</Button>
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
</Modal>
</>
);
};

View File

@ -0,0 +1,87 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Pagination,
SearchInput,
ToggleTemplateProps,
Toolbar,
ToolbarContent,
ToolbarItem,
} from "@patternfly/react-core";
type ResourceToolbarProps = {
onFilter: (nameFilter: string) => void;
count: number;
first: number;
max: number;
onNextClick: (page: number) => void;
onPreviousClick: (page: number) => void;
onPerPageSelect: (max: number, first: number) => void;
hasNext: boolean;
};
export const ResourceToolbar = ({
count,
first,
max,
onNextClick,
onPreviousClick,
onPerPageSelect,
onFilter,
hasNext,
}: ResourceToolbarProps) => {
const { t } = useTranslation();
const [nameFilter, setNameFilter] = useState("");
const page = Math.round(first / max) + 1;
return (
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<SearchInput
placeholder={t("filterByName")}
aria-label={t("filterByName")}
value={nameFilter}
onChange={(_, value) => {
setNameFilter(value);
}}
onSearch={() => onFilter(nameFilter)}
onKeyDown={(e) => {
if (e.key === "Enter") {
onFilter(nameFilter);
}
}}
onClear={() => {
setNameFilter("");
onFilter("");
}}
/>
</ToolbarItem>
<ToolbarItem variant="pagination">
<Pagination
isCompact
perPageOptions={[
{ title: "5", value: 5 },
{ title: "10", value: 10 },
{ title: "20", value: 20 },
]}
toggleTemplate={({
firstIndex,
lastIndex,
}: ToggleTemplateProps) => (
<b>
{firstIndex} - {lastIndex}
</b>
)}
itemCount={count + (page - 1) * max + (hasNext ? 1 : 0)}
page={page}
perPage={max}
onNextClick={(_, p) => onNextClick((p - 1) * max)}
onPreviousClick={(_, p) => onPreviousClick((p - 1) * max)}
onPerPageSelect={(_, m, f) => onPerPageSelect(f - 1, m)}
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
);
};

View File

@ -0,0 +1,37 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import { ResourcesTab } from "./ResourcesTab";
import { Page } from "../components/page/Page";
const Resources = () => {
const { t } = useTranslation();
const [activeTabKey, setActiveTabKey] = useState(0);
return (
<Page title={t("resources")} description={t("resourceIntroMessage")}>
<Tabs
activeKey={activeTabKey}
onSelect={(_, key) => setActiveTabKey(key as number)}
mountOnEnter
unmountOnExit
>
<Tab
eventKey={0}
title={<TabTitleText>{t("myResources")}</TabTitleText>}
>
<ResourcesTab />
</Tab>
<Tab
eventKey={1}
title={<TabTitleText>{t("sharedWithMe")}</TabTitleText>}
>
<h1>Share</h1>
</Tab>
</Tabs>
</Page>
);
};
export default Resources;

View File

@ -0,0 +1,323 @@
import {
Button,
Dropdown,
DropdownItem,
KebabToggle,
OverflowMenu,
OverflowMenuContent,
OverflowMenuControl,
OverflowMenuDropdownItem,
OverflowMenuGroup,
OverflowMenuItem,
Spinner,
} from "@patternfly/react-core";
import {
EditAltIcon,
ExternalLinkAltIcon,
Remove2Icon,
ShareAltIcon,
} from "@patternfly/react-icons";
import {
ExpandableRowContent,
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { fetchPermission, fetchResources, updatePermissions } from "../api";
import { getPermissionRequests } from "../api/methods";
import { Links } from "../api/parse-links";
import { Permission, Resource } from "../api/representations";
import { ContinueCancelModal, useAlerts } from "ui-shared";
import { usePromise } from "../utils/usePromise";
import { EditTheResource } from "./EditTheResource";
import { PermissionRequest } from "./PermissionRequest";
import { ResourceToolbar } from "./ResourceToolbar";
import { SharedWith } from "./SharedWith";
import { ShareTheResource } from "./ShareTheResource";
type PermissionDetail = {
contextOpen?: boolean;
rowOpen?: boolean;
shareDialogOpen?: boolean;
editDialogOpen?: boolean;
permissions?: Permission[];
};
export const ResourcesTab = () => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const [params, setParams] = useState<Record<string, string>>({
first: "0",
max: "5",
});
const [links, setLinks] = useState<Links | undefined>();
const [resources, setResources] = useState<Resource[]>();
const [details, setDetails] = useState<
Record<string, PermissionDetail | undefined>
>({});
const [key, setKey] = useState(1);
const refresh = () => setKey(key + 1);
usePromise(
async (signal) => {
const result = await fetchResources({ signal }, params);
await Promise.all(
result.data.map(
async (r) =>
(r.shareRequests = await getPermissionRequests(r._id, { signal }))
)
);
return result;
},
({ data, links }) => {
setResources(data);
setLinks(links);
},
[params, key]
);
if (!resources) {
return <Spinner />;
}
const fetchPermissions = async (id: string) => {
let permissions = details[id]?.permissions || [];
if (!details[id]) {
permissions = await fetchPermission({}, id);
}
return permissions;
};
const removeShare = async (resource: Resource) => {
try {
const permissions = (await fetchPermissions(resource._id)).map(
({ username }) =>
({
username,
scopes: [],
} as Permission)
)!;
await updatePermissions(resource._id, permissions);
setDetails({});
addAlert(t("unShareSuccess"));
} catch (error) {
addError(t("unShareError", { error }).toString());
}
};
const toggleOpen = async (
id: string,
field: keyof PermissionDetail,
open: boolean
) => {
const permissions = await fetchPermissions(id);
setDetails({
...details,
[id]: { ...details[id], [field]: open, permissions },
});
};
return (
<>
<ResourceToolbar
onFilter={(name) => setParams({ ...params, name })}
count={resources.length}
first={parseInt(params["first"])}
max={parseInt(params["max"])}
onNextClick={() => setParams(links?.next || {})}
onPreviousClick={() => setParams(links?.prev || {})}
onPerPageSelect={(first, max) =>
setParams({ first: `${first}`, max: `${max}` })
}
hasNext={!!links?.next}
/>
<TableComposable aria-label={t("resources")}>
<Thead>
<Tr>
<Th />
<Th>{t("resourceName")}</Th>
<Th>{t("application")}</Th>
<Th>{t("permissionRequests")}</Th>
</Tr>
</Thead>
{resources.map((resource, index) => (
<Tbody
key={resource.name}
isExpanded={details[resource._id]?.rowOpen}
>
<Tr>
<Td
expand={{
isExpanded: details[resource._id]?.rowOpen || false,
rowIndex: index,
onToggle: () =>
toggleOpen(
resource._id,
"rowOpen",
!details[resource._id]?.rowOpen
),
}}
/>
<Td dataLabel={t("resourceName")}>{resource.name}</Td>
<Td dataLabel={t("application")}>
<a href={resource.client.baseUrl}>
{resource.client.name || resource.client.clientId}{" "}
<ExternalLinkAltIcon />
</a>
</Td>
<Td dataLabel={t("permissionRequests")}>
{resource.shareRequests.length > 0 && (
<PermissionRequest
resource={resource}
refresh={() => refresh()}
/>
)}
<ShareTheResource
resource={resource}
permissions={details[resource._id]?.permissions}
open={details[resource._id]?.shareDialogOpen || false}
onClose={() => setDetails({})}
/>
{details[resource._id]?.editDialogOpen && (
<EditTheResource
resource={resource}
permissions={details[resource._id]?.permissions}
onClose={() => setDetails({})}
/>
)}
</Td>
<Td isActionCell>
<OverflowMenu breakpoint="lg">
<OverflowMenuContent>
<OverflowMenuGroup groupType="button">
<OverflowMenuItem>
<Button
variant="link"
onClick={() =>
toggleOpen(resource._id, "shareDialogOpen", true)
}
>
<ShareAltIcon /> {t("share")}
</Button>
</OverflowMenuItem>
<OverflowMenuItem>
<Dropdown
position="right"
toggle={
<KebabToggle
onToggle={(open) =>
toggleOpen(resource._id, "contextOpen", open)
}
/>
}
isOpen={details[resource._id]?.contextOpen}
isPlain
dropdownItems={[
<DropdownItem
key="edit"
isDisabled={
details[resource._id]?.permissions?.length === 0
}
onClick={() =>
toggleOpen(resource._id, "editDialogOpen", true)
}
>
<EditAltIcon /> {t("edit")}
</DropdownItem>,
<ContinueCancelModal
key="unShare"
isDisabled={
details[resource._id]?.permissions?.length === 0
}
buttonTitle={
<>
<Remove2Icon /> {t("unShare")}
</>
}
component={DropdownItem}
modalTitle="unShare"
modalMessage="unShareAllConfirm"
continueLabel="confirmButton"
onContinue={() => removeShare(resource)}
/>,
]}
/>
</OverflowMenuItem>
</OverflowMenuGroup>
</OverflowMenuContent>
<OverflowMenuControl>
<Dropdown
position="right"
toggle={
<KebabToggle
onToggle={(open) =>
toggleOpen(resource._id, "contextOpen", open)
}
/>
}
isOpen={details[resource._id]?.contextOpen}
isPlain
dropdownItems={[
<OverflowMenuDropdownItem
key="share"
isShared
onClick={() =>
toggleOpen(resource._id, "shareDialogOpen", true)
}
>
<ShareAltIcon /> {t("share")}
</OverflowMenuDropdownItem>,
<OverflowMenuDropdownItem
key="edit"
isShared
onClick={() =>
toggleOpen(resource._id, "editDialogOpen", true)
}
>
<EditAltIcon /> {t("edit")}
</OverflowMenuDropdownItem>,
<ContinueCancelModal
key="unShare"
isDisabled={
details[resource._id]?.permissions?.length === 0
}
buttonTitle={
<>
<Remove2Icon /> {t("unShare")}
</>
}
component={OverflowMenuDropdownItem}
modalTitle="unShare"
modalMessage="unShareAllConfirm"
continueLabel="confirmButton"
onContinue={() => removeShare(resource)}
/>,
]}
/>
</OverflowMenuControl>
</OverflowMenu>
</Td>
</Tr>
<Tr isExpanded={details[resource._id]?.rowOpen || false}>
<Td colSpan={4} textCenter>
<ExpandableRowContent>
<SharedWith
permissions={details[resource._id]?.permissions}
/>
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
))}
</TableComposable>
</>
);
};

View File

@ -0,0 +1,203 @@
import {
Button,
Chip,
ChipGroup,
Form,
FormGroup,
InputGroup,
Modal,
ValidatedOptions,
} from "@patternfly/react-core";
import { useEffect } from "react";
import {
FormProvider,
useFieldArray,
useForm,
useWatch,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { updateRequest } from "../api";
import { Permission, Resource } from "../api/representations";
import { useAlerts, SelectControl, KeycloakTextInput } from "ui-shared";
import { SharedWith } from "./SharedWith";
type ShareTheResourceProps = {
resource: Resource;
permissions?: Permission[];
open: boolean;
onClose: () => void;
};
type FormValues = {
permissions: string[];
usernames: { value: string }[];
};
export const ShareTheResource = ({
resource,
permissions,
open,
onClose,
}: ShareTheResourceProps) => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const form = useForm<FormValues>();
const {
control,
register,
reset,
formState: { errors, isValid },
setError,
clearErrors,
handleSubmit,
} = form;
const { fields, append, remove } = useFieldArray<FormValues>({
control,
name: "usernames",
});
useEffect(() => {
if (fields.length === 0) {
append({ value: "" });
}
}, [fields]);
const watchFields = useWatch({
control,
name: "usernames",
defaultValue: [],
});
const isDisabled = watchFields.every(
({ value }) => value.trim().length === 0
);
const addShare = async ({ usernames, permissions }: FormValues) => {
try {
await Promise.all(
usernames
.filter(({ value }) => value !== "")
.map(({ value: username }) =>
updateRequest(resource._id, username, permissions)
)
);
addAlert(t("shareSuccess"));
onClose();
} catch (error) {
addError(t("shareError", { error }).toString());
}
reset({});
};
const validateUser = async () => {
const userOrEmails = fields.map((f) => f.value).filter((f) => f !== "");
const userPermission = permissions
?.map((p) => [p.username, p.email])
.flat();
const hasUsers = userOrEmails.length > 0;
const alreadyShared =
userOrEmails.filter((u) => userPermission?.includes(u)).length !== 0;
if (!hasUsers || alreadyShared) {
setError("usernames", {
message: !hasUsers ? t("required") : t("resourceAlreadyShared"),
});
} else {
clearErrors();
}
return hasUsers && !alreadyShared;
};
return (
<Modal
title={t("shareTheResource", [resource.name])}
variant="medium"
isOpen={open}
onClose={onClose}
actions={[
<Button
key="confirm"
variant="primary"
id="done"
isDisabled={!isValid}
type="submit"
form="share-form"
>
{t("done")}
</Button>,
<Button key="cancel" variant="link" onClick={onClose}>
{t("cancel")}
</Button>,
]}
>
<Form id="share-form" onSubmit={handleSubmit(addShare)}>
<FormGroup
label={t("shareUser")}
type="string"
helperTextInvalid={errors.usernames?.message}
fieldId="users"
isRequired
validated={
errors.usernames ? ValidatedOptions.error : ValidatedOptions.default
}
>
<InputGroup>
<KeycloakTextInput
id="users"
placeholder={t("usernamePlaceholder")}
validated={
errors.usernames
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register(`usernames.${fields.length - 1}.value`, {
validate: validateUser,
})}
/>
<Button
key="add-user"
variant="primary"
id="add"
onClick={() => append({ value: "" })}
isDisabled={isDisabled}
>
{t("add")}
</Button>
</InputGroup>
{fields.length > 1 && (
<ChipGroup categoryName={t("shareWith")}>
{fields.map(
(field, index) =>
index !== fields.length - 1 && (
<Chip key={field.id} onClick={() => remove(index)}>
{field.value}
</Chip>
)
)}
</ChipGroup>
)}
</FormGroup>
<FormProvider {...form}>
<FormGroup label="" fieldId="permissions-selected">
<SelectControl
name="permissions"
variant="typeaheadmulti"
controller={{ defaultValue: [] }}
options={resource.scopes.map(({ name, displayName }) => ({
key: name,
value: displayName || name,
}))}
menuAppendTo="parent"
/>
</FormGroup>
</FormProvider>
<FormGroup>
<SharedWith permissions={permissions} />
</FormGroup>
</Form>
</Modal>
);
};

View File

@ -0,0 +1,24 @@
import { Trans } from "react-i18next";
import { Permission } from "../api/representations";
type SharedWithProps = {
permissions?: Permission[];
};
export const SharedWith = ({ permissions: p = [] }: SharedWithProps) => {
return (
<Trans i18nKey="resourceSharedWith" count={p.length}>
<strong>
{{
username: p[0] ? p[0].username : undefined,
}}
</strong>
<strong>
{{
other: p.length - 1,
}}
</strong>
</Trans>
);
};

View File

@ -0,0 +1,57 @@
import {
Button,
Modal,
ModalVariant,
Page,
Text,
TextContent,
TextVariants,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useRouteError } from "react-router-dom";
export const ErrorPage = () => {
const { t } = useTranslation();
const error = useRouteError();
const errorMessage = getErrorMessage(error);
function onRetry() {
location.href = location.origin + location.pathname;
}
return (
<Page>
<Modal
variant={ModalVariant.small}
title={t("somethingWentWrong")}
titleIconVariant="danger"
showClose={false}
isOpen
actions={[
<Button key="tryAgain" variant="primary" onClick={onRetry}>
{t("tryAgain")}
</Button>,
]}
>
<TextContent>
<Text>{t("somethingWentWrongDescription")}</Text>
{errorMessage && (
<Text component={TextVariants.small}>{errorMessage}</Text>
)}
</TextContent>
</Modal>
</Page>
);
};
function getErrorMessage(error: unknown) {
if (typeof error === "string") {
return error;
}
if (error instanceof Error) {
return error.message;
}
return null;
}

View File

@ -0,0 +1,151 @@
import {
Nav,
NavExpandable,
NavItem,
NavList,
PageSidebar,
} from "@patternfly/react-core";
import { TFuncKey } from "i18next";
import {
MouseEvent as ReactMouseEvent,
PropsWithChildren,
useMemo,
} from "react";
import { useTranslation } from "react-i18next";
import {
matchPath,
To,
useHref,
useLinkClickHandler,
useLocation,
} from "react-router-dom";
type RootMenuItem = {
label: TFuncKey;
path: string;
};
type MenuItemWithChildren = {
label: TFuncKey;
children: MenuItem[];
};
type MenuItem = RootMenuItem | MenuItemWithChildren;
const menuItems: MenuItem[] = [
{
label: "personalInfo",
path: "personal-info",
},
{
label: "accountSecurity",
children: [
{
label: "signingIn",
path: "account-security/signing-in",
},
{
label: "deviceActivity",
path: "account-security/device-activity",
},
{
label: "linkedAccounts",
path: "account-security/linked-accounts",
},
],
},
{
label: "applications",
path: "applications",
},
{
label: "groups",
path: "groups",
},
{
label: "resources",
path: "resources",
},
];
export const PageNav = () => (
<PageSidebar
nav={
<Nav>
<NavList>
{menuItems.map((menuItem) => (
<NavMenuItem key={menuItem.label} menuItem={menuItem} />
))}
</NavList>
</Nav>
}
/>
);
type NavMenuItemProps = {
menuItem: MenuItem;
};
function NavMenuItem({ menuItem }: NavMenuItemProps) {
const { t } = useTranslation();
const { pathname } = useLocation();
const isActive = useMemo(
() => matchMenuItem(pathname, menuItem),
[pathname, menuItem]
);
if ("path" in menuItem) {
return (
<NavLink to={menuItem.path} isActive={isActive}>
{t(menuItem.label)}
</NavLink>
);
}
return (
<NavExpandable
title={t(menuItem.label)}
isActive={isActive}
isExpanded={isActive}
>
{menuItem.children.map((child) => (
<NavMenuItem key={child.label} menuItem={child} />
))}
</NavExpandable>
);
}
function matchMenuItem(currentPath: string, menuItem: MenuItem): boolean {
if ("path" in menuItem) {
return !!matchPath(menuItem.path, currentPath);
}
return menuItem.children.some((child) => matchMenuItem(currentPath, child));
}
type NavLinkProps = {
to: To;
isActive: boolean;
};
const NavLink = ({
to,
isActive,
children,
}: PropsWithChildren<NavLinkProps>) => {
const href = useHref(to);
const handleClick = useLinkClickHandler(to);
return (
<NavItem
to={href}
isActive={isActive}
onClick={(event) =>
// PatternFly does not have the correct type for this event, so we need to cast it.
handleClick(event as unknown as ReactMouseEvent<HTMLAnchorElement>)
}
>
{children}
</NavItem>
);
};

View File

@ -0,0 +1,3 @@
.brand {
height: 35px;
}

View File

@ -0,0 +1,59 @@
import { Page, Spinner } from "@patternfly/react-core";
import {
KeycloakMasthead,
Translations,
TranslationsProvider,
} from "keycloak-masthead";
import { Suspense, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Outlet } from "react-router-dom";
import { AlertProvider } from "ui-shared";
import { environment } from "../environment";
import { keycloak } from "../keycloak";
import { joinPath } from "../utils/joinPath";
import { PageNav } from "./PageNav";
import style from "./Root.module.css";
export const Root = () => {
const { t } = useTranslation();
const translations = useMemo<Translations>(
() => ({
avatar: t("avatar"),
fullName: t("fullName"),
manageAccount: t("manageAccount"),
signOut: t("signOut"),
unknownUser: t("unknownUser"),
}),
[t]
);
return (
<Page
header={
<TranslationsProvider translations={translations}>
<KeycloakMasthead
features={{ hasManageAccount: false }}
showNavToggle
brand={{
src: joinPath(environment.resourceUrl, "logo.svg"),
alt: t("logo"),
className: style.brand,
}}
dropdownItems={[]}
keycloak={keycloak}
/>
</TranslationsProvider>
}
sidebar={<PageNav />}
isManagedSidebar
>
<AlertProvider>
<Suspense fallback={<Spinner />}>
<Outlet />
</Suspense>
</AlertProvider>
</Page>
);
};

View File

@ -0,0 +1,7 @@
import { PageSection } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
export const RootIndex = () => {
const { t } = useTranslation();
return <PageSection>{t("welcomeMessage")}</PageSection>;
};

View File

@ -0,0 +1,72 @@
import { lazy } from "react";
import type { IndexRouteObject, RouteObject } from "react-router-dom";
import { ErrorPage } from "./root/ErrorPage";
import { Root } from "./root/Root";
import { RootIndex } from "./root/RootIndex";
const DeviceActivity = lazy(() => import("./account-security/DeviceActivity"));
const LinkedAccounts = lazy(() => import("./account-security/LinkedAccounts"));
const SigningIn = lazy(() => import("./account-security/SigningIn"));
const Applications = lazy(() => import("./applications/Applications"));
const Groups = lazy(() => import("./groups/Groups"));
const PersonalInfo = lazy(() => import("./personal-info/PersonalInfo"));
const Resources = lazy(() => import("./resources/Resources"));
export const DeviceActivityRoute: RouteObject = {
path: "account-security/device-activity",
element: <DeviceActivity />,
};
export const LinkedAccountsRoute: RouteObject = {
path: "account-security/linked-accounts",
element: <LinkedAccounts />,
};
export const SigningInRoute: RouteObject = {
path: "account-security/signing-in",
element: <SigningIn />,
};
export const ApplicationsRoute: RouteObject = {
path: "applications",
element: <Applications />,
};
export const GroupsRoute: RouteObject = {
path: "groups",
element: <Groups />,
};
export const PersonalInfoRoute: RouteObject = {
path: "personal-info",
element: <PersonalInfo />,
};
export const ResourcesRoute: RouteObject = {
path: "resources",
element: <Resources />,
};
export const RootIndexRoute: IndexRouteObject = {
index: true,
element: <RootIndex />,
};
export const RootRoute: RouteObject = {
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
children: [
RootIndexRoute,
DeviceActivityRoute,
LinkedAccountsRoute,
SigningInRoute,
ApplicationsRoute,
GroupsRoute,
PersonalInfoRoute,
ResourcesRoute,
],
};
export const routes: RouteObject[] = [RootRoute];

View File

@ -0,0 +1,2 @@
export const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;

View File

@ -0,0 +1,22 @@
const PATH_SEPARATOR = "/";
export function joinPath(...paths: string[]) {
const normalizedPaths = paths.map((path, index) => {
const isFirst = index === 0;
const isLast = index === paths.length - 1;
// Strip out any leading slashes from the path.
if (!isFirst && path.startsWith(PATH_SEPARATOR)) {
path = path.slice(1);
}
// Strip out any trailing slashes from the path.
if (!isLast && path.endsWith(PATH_SEPARATOR)) {
path = path.slice(0, -1);
}
return path;
}, []);
return normalizedPaths.join(PATH_SEPARATOR);
}

View File

@ -0,0 +1,66 @@
import type { DependencyList } from "react";
import { useEffect } from "react";
/**
* Function that creates a Promise. Receives an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
* which is aborted when the component unmounts, or the dependencies of the hook have changed.
*/
export type PromiseFactoryFn<T> = (signal: AbortSignal) => Promise<T>;
/**
* Function which is called with the value of the Promise when it resolves.
*/
export type PromiseResolvedFn<T> = (value: T) => void;
/**
* Takes a function that creates a Promise and returns its resolved result through a callback.
*
* ```ts
* const [products, setProducts] = useState();
*
* function getProducts() {
* return fetch('/api/products').then((res) => res.json());
* }
*
* usePromise(() => getProducts(), setProducts);
* ```
*
* Also takes a list of dependencies, when the dependencies change the Promise is recreated.
*
* ```ts
* usePromise(() => getProduct(id), setProduct, [id]);
* ```
*
* Can abort a fetch request, an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) is provided from the factory function to do so.
* This signal will be aborted if the component unmounts, or if the dependencies of the hook have changed.
*
* ```ts
* usePromise((signal) => fetch(`/api/products/${id}`, { signal }).then((res) => res.json()), setProduct, [id]);
* ```
*
* @param factory Function that creates the Promise.
* @param callback Function that gets called with the value of the Promise when it resolves.
* @param deps If present, Promise will be recreated if the values in the list change.
*/
export function usePromise<T>(
factory: PromiseFactoryFn<T>,
callback: PromiseResolvedFn<T>,
deps: DependencyList = []
) {
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
async function handlePromise() {
const value = await factory(signal);
if (!signal.aborted) {
callback(value);
}
}
handlePromise();
return () => controller.abort();
}, deps);
}

1
js/apps/account-ui/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"exclude": [
"**/*.test.ts",
"**/*.test.tsx"
]
}

View File

@ -0,0 +1,19 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { checker } from "vite-plugin-checker";
// https://vitejs.dev/config/
export default defineConfig({
base: "",
server: {
port: 8080,
},
build: {
target: "ES2022",
},
resolve: {
// Resolve the 'module' entrypoint at all times (not the default due to Node.js compatibility issues).
mainFields: ["module"],
},
plugins: [react(), checker({ typescript: true })],
});

View File

@ -0,0 +1,218 @@
# Coding Guidelines
## Package managers
The default package manager for the Keycloak UI projects is NPM. There are several reasons why NPM is used over other package managers (such as Yarn and PNPM):
- It comes included with NodeJS by default, meaning it does not have to be installed manually.
- Most contributors are familiar with the NPM ecosystem and tooling.
- We do not use any of the 'advanced' features of other package managers (such as [Yarn's PNP](https://yarnpkg.com/features/pnp)).
If you submit a pull request that changes the dependencies, make sure that you also update the `package-lock.json` as well.
## Typescript
The Keycloak UI projects uses best practices based off the official [React TypeScript Cheat sheet](https://react-typescript-cheatsheet.netlify.app/), with modifications for this project. The React TypeScript Cheat sheet is maintained and used by developers through out the world, and is a place where developers can bring together lessons learned using TypeScript and React.
### Non-null assertion operator
In the project you will sometimes see the [non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator) (`!`) used to tell the TypeScript compiler that you guarantee that a value is not `null` or `undefined`. Because this might possibly introduce errors at run-time if you have not checked this value yourself it should be used sparingly.
The only place where it is valid to use the non-null assertion operator is on the types that are provided by the [Admin API client](https://github.com/keycloak/keycloak-nodejs-admin-client). The reason for this is that the types are generated from Java code, which does not explicitly provide information about the nullability of fields (more on that [here](https://github.com/keycloak/keycloak-nodejs-admin-client/issues/187)).
### State management
We have made a conscious decision to stay away from state management technologies such as Redux. These overarching state management schemes tend to be overly complex and encourage dumping everything into the global state.
Instead, we are following a simple philosophy that state should remain close to where it is used and moved to a wider scope only as truly needed. This encourages encapsulation and makes management of the state much simpler.
The way this plays out in our application is that we first prefer state to remain in the scope of the component that uses it. If the state is required by more than one component, we move to a more complex strategy for management of that state. In other words, in order of preference, state should be managed by:
1. Storing in the component that uses it.
2. If #1 is not sufficient, [lift state up](https://reactjs.org/docs/lifting-state-up.html).
3. If #2 is not sufficient, try [component composition](https://reactjs.org/docs/context.html#before-you-use-context).
4. If #3, is not sufficient, use a [global context](https://reactjs.org/docs/context.html).
A good tutorial on this approach is found in [Kent Dodds blog](https://kentcdodds.com/blog/application-state-management-with-react).
### Hooks
When using hooks with Typescript there are few recommendations that we follow below. Additional recommendations besides the ones mentioned in this document can be found [here](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks).
### Inference vs Types for useState
Currently we recommend using inference for the primitive types booleans, numbers, and strings when using useState. Anything other then these 3 types should use a declarative syntax to specify what is expected. For example the following is an example of how to use inference:
```javascript
const [isEnabled, setIsEnabled] = useState(false);
```
Here is an example how to use a declarative syntax. When using a declarative syntax, if the value can be null, that will also need to be specified:
```javascript
const [user, setUser] = useState<IUser | null>(null);
...
setUser(newUser);
```
#### useReducers
When using reducers make sure you specify the [return type and not use inference](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks#usereducer).
#### useEffect
For useEffect only [return the function or undefined](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks#useeffect).
### Additional Typescript Pointers
Besides the details outlined above a list of recommendations for Typescript is maintained by several Typescript React developers [here](https://react-typescript-cheatsheet.netlify.app/). This is a great reference to use for any additional questions that are not outlined within the coding standards.
## CSS
We use custom CSS in rare cases where PatternFly styling does not meet our design guidelines. If styling needs to be added, we should first check that the PatternFly component is being properly built and whether a variant is already provided to meet the use case. Next, PatternFly layouts should be used for most positioning of components. For one-off tweaks (e.g. spacing an icon slightly away from the text next to it), a PatternFly utility class should be used. In all cases, PatternFly variables should be used for colors, spacing, etc. rather than hard coding color or pixel values.
We will use one global CSS file to surface customization variables. Styles particular to a component should be located in a .CSS file within the components folder. A modified BEM naming convention should be used as detailed below.
### Location of files, location of classes
* Global styling should be located…? *./public/index.css*.
* The CSS relating to a single component should be located in a file within each components folder.
### Naming CSS classes
PatternFly reference https://pf4.patternfly.org/guidelines#variables
For the Admin UI, we modify the PatternFly convention to namespace the classes and variables to the Keycloak packages.
**Class name**
```css
.keycloak-admin--block[__element][--modifier][--state][--breakpoint][--pseudo-element]
```
**Examples of custom CSS classes**
```css
// Modification to all data tables throughout Keycloak admin
.keycloak-admin--data-table {
...
}
// Data tables throughout keycloak that are marked as compact
.keycloak-admin--data-table--compact {
...
}
// Child elements of a compact data-table
// Dont increase specificity with a selector like this:
// .keycloak-admin--data-table--compact .data-table-item
// Instead, follow the pattern for a single class on the child
.keycloak-admin--data-table__data-table-item--compact {
...
}
// Compact data table just in the management UI at the lg or higher breakpoint
.keycloak-admin--data-table--compact--lg {
...
}
```
### Naming CSS custom properties and using PatternFlys custom properties
Usually, PatternFly components will properly style components. Sometimes problems with the spacing or other styling indicate that a wrapper component is missing or that components havent been put together quite as intended. Often there is a variant of the component available that will accomplish the design.
However, there are other times when modifications must be made to the styling provided by PatternFly, or when styling a custom component. In these cases, PatternFly custom properties (CSS variables) should be used as attribute values. PatternFly defines custom properties for colors, spacing, border width, box shadow, and more. Besides a full color palette, colors are defined specifically for borders, statuses (success, warning, danger, info), backgrounds, etc.
These values can be seen in the [PatternFly design guidelines](https://www.patternfly.org/v4/design-guidelines/styles/colors) and a [full listing of variables](https://www.patternfly.org/v4/documentation/overview/global-css-variables) can be found in the documentation section.
For the Admin UI, we modify the PatternFly convention to namespace the classes and variables to the Keycloak packages.
**Custom property**
```css
--keycloak-admin--block[__element][--modifier][--state][--breakpoint][--pseudo-element]--PropertyCamelCase
```
**Example of a CSS custom property**
```css
// Modify the height of the brand image
--keycloak-admin--brand--Height: var(--pf-global--spacer--xl);
```
**Example**
```css
// Dont increase specificity
// Dont use pixel values
.keycloak-admin--manage-columns__modal .pf-c-dropdown {
margin-bottom: 24px
}
// Do use a new class
// Do use a PatternFly global spacer variable
.keycloak-admin--manage-columns__dropdown {
margin-bottom: var(--pf-global--spacer--xl);
}
```
### Using utility classes
Utility classes can be used to add specific styling to a component, such as margin-bottom or padding. However, their use should be limited to one-off styling needs.
For example, instead of using the utility class for margin-right multiple times, we should define a new Admin UI class that adds this *margin-right: var(--pf-global--spacer--sm);* and in this example, the new class can set the color appropriately as well.
**Using a utility class **
```css
switch (titleStatus) {
case "success":
return (
<>
<InfoCircleIcon
className="pf-u-mr-sm" // utility class
color="var(--pf-global--info-color--100)"
/>{" "}
{titleText}{" "}
</>
);
case "failure":
return (
<>
<InfoCircleIcon
className="pf-u-mr-sm"
color="var(--pf-global--danger-color--100)"
/>{" "}
{titleText}{" "}
</>
);
}
```
**Better way with a custom class**
```css
switch (titleStatus) {
case "success":
return (
<>
<InfoCircleIcon
className="keycloak-admin--icon--info" // use a new keycloak class
/>{" "}
{titleText}{" "}
</>
);
case "failure":
return (
<>
<InfoCircleIcon
className="keycloak-admin--icon--info"
/>{" "}
{titleText}{" "}
</>
);
}
```
## Resources
* [PatternFly Docs](https://www.patternfly.org/v4/)
* [Katacoda PatternFly tutorials](https://www.patternfly.org/v4/documentation/react/overview/react-training)
* [PatternFly global CSS variables](https://www.patternfly.org/v4/documentation/overview/global-css-variables)
* [PatternFly CSS utility classes](https://www.patternfly.org/v4/documentation/core/utilities/accessibility)
* [React Typescript Cheat sheet](https://react-typescript-cheatsheet.netlify.app/)

View File

@ -0,0 +1,76 @@
# Keycloak Admin UI
This project is the next generation of the Keycloak Administration UI. It is written with React and [PatternFly 4](https://www.patternfly.org/v4/) and uses [Vite](https://vitejs.dev/guide/) and [Cypress](https://docs.cypress.io/guides/overview/why-cypress).
## Development
### Prerequisites
Make sure that you have Node.js version 18 (or later) installed on your system. If you do not have Node.js installed we recommend using [Node Version Manager](https://github.com/nvm-sh/nvm) to install it.
You can find out which version of Node.js you are using by running the following command:
```bash
node --version
```
In order to run the Keycloak server you will also have to install the Java Development Kit (JDK). We recommend that you use the same version of the JDK as [required by the Keycloak server](https://github.com/keycloak/keycloak/blob/main/docs/building.md#building-from-source).
### Running the Keycloak server
See the instructions in the [Keycloak server app](../keycloak-server/README.md).
### Running the development server
Now that the Keycloak sever is running it's time to run the development server for the Admin UI. This server is used to build the Admin UI in a manner that it can be iterated on quickly in a browser, using features such as [Hot Module Replacement (HMR)](https://vitejs.dev/guide/features.html#hot-module-replacement) and [Fast Refresh](https://www.npmjs.com/package/react-refresh).
To start the development server run the following command:
```bash
npm run dev
```
Once the process of optimization is done your browser will automatically open your local host on port `8080`. From here you will be redirected to the Keycloak server to authenticate, which you can do with the default username and password (`admin`).
You can now start making changes to the source code, and they will be reflected in your browser.
## Building as a Keycloak theme
If you want to build the application using Maven and produce a JAR that can be installed directly into Keycloak, check out the [Keycloak theme documentation](../../keycloak-theme/README.md).
## Linting
Every time you create a commit it should be automatically linted and formatted for you. It is also possible to trigger the linting manually:
```bash
npm run lint
```
## Integration testing with Cypress
This repository contains integration tests developed with the [Cypress framework](https://www.cypress.io/).
### Prerequisites
Ensure the Keycloak and development server are running as [outlined previously](#running-the-keycloak-server) in this document.
### Running the tests
You can run the tests using the interactive graphical user interface using the following command:
```bash
npm run cy:open
```
Alternatively the tests can also run headless as follows:
```
npm run cy:run
```
For more information about the Cypress command-line interface consult [the documentation](https://docs.cypress.io/guides/guides/command-line).
### Project Structure
You can find information about the project structure in the [official Cypress documentation](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Folder-structure).
Read more about [how to write tests](./cypress/WRITING_TESTS.md)

View File

@ -0,0 +1,32 @@
import { defineConfig } from "cypress";
export default defineConfig({
projectId: "j4yhox",
screenshotsFolder: "assets/screenshots",
videosFolder: "assets/videos",
chromeWebSecurity: false,
viewportWidth: 1360,
viewportHeight: 768,
defaultCommandTimeout: 30000,
videoCompression: false,
numTestsKeptInMemory: 30,
videoUploadOnPasses: false,
experimentalMemoryManagement: true,
retries: {
runMode: 3,
},
e2e: {
baseUrl: "http://localhost:8080",
slowTestThreshold: 30000,
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
},
component: {
devServer: {
framework: "react",
bundler: "vite",
},
},
});

View File

@ -0,0 +1,87 @@
## How to write tests
### Create PageObjects for the page screens to reuse the methods
Don't do this:
```typescript
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
cy.findAllByTestId("provider-name-link")
.contains("test_hmac-generated")
.click();
```
Instead use a page object as it is a lot easier to get the intended functionality of the test:
```typescript
realmSettings.goToProvidersTab();
realmSettings.fillOutForm({ name: "test" }).save();
```
#### Clean code
Write locators in the PageObject layer and in object variables when possible to avoid hardcoding.
Set type of element at the end of the variable name (e.g. saveBtn, nameInput, userDrpDwn…)
Avoid adding “click” to name methods, methods should be named as an action “goToX”, “openX”, “createX”, etc.
Consistent naming (e.g goToTab methods, somewhere named goToTab, other goToXTab)
### Test structure
We have `keycloakBefore` that will navigate to the main page, control request errors and wait for the load to finish.
You can then have multiple test or create a new `describe` block to setup some data for your test.
```typescript
describe("Realm roles test", () => {
before(() => {
keycloakBefore();
loginPage.logIn();
});
beforeEach(() => {
sidebarPage.goToRealmRoles();
});
```
Example of `describe` to setup data for that test:
```typescript
import adminClient from "../support/util/AdminClient";
describe("Edit role details", () => {
before(() => {
adminClient.createRealmRole({
name: editRoleName,
description,
});
});
after(() => {
adminClient.deleteRealmRole(editRoleName);
});
it("Should edit realm role details", () => {
// ...
```
### Waiting for results
Sometimes you will get the following error:
> Cypress failed because the element has been detached from the DOM
This is because the DOM has been updated in between selectors. You can remedy this by waiting on REST calls.
You can see the rest calls in the cypress IDE.
```typescript
cy.intercept("/admin/realms/master/").as("search");
// ... pressing a button that will perform the search
cy.wait(["@search"]); // wait for the call named search
```
If there were no calls and you still get this error, try using `{force: true}`, but try not to use it everywhere. For example, there could be an unexpected open modal blocking the element, so even the user wouldnt be able to use that element.
### Some more reading:
* [Moises document](https://docs.google.com/document/d/11sm1IpEvVLHO59JEVmwgNOUD0zoP4YMvInIU4v5iVNk/edit)
* [Cypress blog do not get too detached](https://www.cypress.io/blog/2020/07/22/do-not-get-too-detached/)
* [See the clients_test.spec as an example](./cypress/e2e/clients_test.spec.ts)

View File

@ -0,0 +1,30 @@
import { mount } from "cypress/react";
import { ConfirmDialogModal } from "../../src/components/confirm-dialog/ConfirmDialog";
describe("ConfirmDialogModal", () => {
const bodySelector = "#pf-modal-part-2";
it("should mount", () => {
const toggle = cy.spy().as("toggleDialogSpy");
const confirm = cy.spy().as("onConfirmSpy");
mount(
<ConfirmDialogModal
continueButtonLabel="Yes"
cancelButtonLabel="No"
titleKey="Hello"
open
toggleDialog={toggle}
onConfirm={confirm}
>
Some text
</ConfirmDialogModal>
);
cy.get(bodySelector).should("have.text", "Some text");
cy.findByTestId("confirm").click();
cy.get("@onConfirmSpy").should("have.been.called");
cy.findAllByTestId("cancel").click();
cy.get("@toggleDialogSpy").should("have.been.called");
});
});

View File

@ -0,0 +1,125 @@
import {
ActionGroup,
Button,
Form,
Page,
PageSection,
} from "@patternfly/react-core";
import { mount } from "cypress/react";
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { KeyValueType } from "../../src/components/key-value-form/key-value-convert";
import { KeyValueInput } from "../../src/components/key-value-form/KeyValueInput";
type KeyValueInputTestProps = {
submit: (values: any) => void;
defaultValues?: KeyValueType[];
};
const KeyValueInputTest = ({
submit,
defaultValues,
}: KeyValueInputTestProps) => {
const form = useForm();
useEffect(() => {
form.setValue("name", defaultValues || "");
}, [form.setValue]);
return (
<Page>
<PageSection variant="light">
<FormProvider {...form}>
<Form isHorizontal onSubmit={form.handleSubmit(submit)}>
<KeyValueInput name="name" />
<ActionGroup>
<Button data-testid="save" type="submit">
Save
</Button>
</ActionGroup>
</Form>
</FormProvider>
</PageSection>
</Page>
);
};
describe("KeyValueInput", () => {
it("basic interaction", () => {
const submit = cy.spy().as("onSubmit");
mount(<KeyValueInputTest submit={submit} />);
cy.get("input").should("exist");
cy.findAllByTestId("name-add-row").should("exist").should("be.disabled");
cy.findAllByTestId("name[0].key").type("key");
cy.findAllByTestId("name[0].value").type("value");
cy.findAllByTestId("name-add-row").should("be.enabled");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: [{ key: "key", value: "value" }],
});
});
it("from existing values", () => {
const submit = cy.spy().as("onSubmit");
mount(
<KeyValueInputTest
submit={submit}
defaultValues={[{ key: "key1", value: "value1" }]}
/>
);
cy.findAllByTestId("name[0].key").should("have.value", "key1");
cy.findAllByTestId("name[0].value").should("have.value", "value1");
cy.findAllByTestId("name-add-row").should("be.enabled").click();
cy.findAllByTestId("name[1].key").type("key2");
cy.findAllByTestId("name[1].value").type("value2");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: [
{ key: "key1", value: "value1" },
{ key: "key2", value: "value2" },
],
});
});
it("leaving it empty", () => {
const submit = cy.spy().as("onSubmit");
mount(<KeyValueInputTest submit={submit} />);
cy.get("input").should("exist");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: [{ key: "", value: "" }],
});
});
it("deleting values", () => {
const submit = cy.spy().as("onSubmit");
mount(
<KeyValueInputTest
submit={submit}
defaultValues={[
{ key: "key1", value: "value1" },
{ key: "key2", value: "value2" },
{ key: "key3", value: "value3" },
{ key: "key4", value: "value4" },
]}
/>
);
cy.findAllByTestId("name[2].remove").click();
cy.findAllByTestId("name[1].remove").click();
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: [
{ key: "key1", value: "value1" },
{ key: "key4", value: "value4" },
],
});
});
});

View File

@ -0,0 +1,155 @@
import {
ActionGroup,
Button,
Form,
FormGroup,
Page,
PageSection,
} from "@patternfly/react-core";
import { mount } from "cypress/react";
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
MultiLineInput,
MultiLineInputProps,
} from "../../src/components/multi-line-input/MultiLineInput";
type MultiLineInputTestProps = Omit<MultiLineInputProps, "name"> & {
submit: (values: any) => void;
defaultValues?: string | string[];
};
describe("MultiLineInput", () => {
const MultiLineInputTest = ({
submit,
defaultValues,
...rest
}: MultiLineInputTestProps) => {
const form = useForm();
useEffect(() => {
form.setValue("name", defaultValues || "");
}, [form.setValue]);
return (
<Page>
<PageSection variant="light">
<FormProvider {...form}>
<Form isHorizontal onSubmit={form.handleSubmit(submit)}>
<FormGroup label="Test field" fieldId="name">
<MultiLineInput
id="name"
name="name"
aria-label="test"
addButtonLabel="Add"
{...rest}
/>
</FormGroup>
<ActionGroup>
<Button data-testid="save" type="submit">
Save
</Button>
</ActionGroup>
</Form>
</FormProvider>
</PageSection>
</Page>
);
};
it("basic interaction", () => {
const submit = cy.spy().as("onSubmit");
mount(<MultiLineInputTest submit={submit} />);
cy.get("input").should("exist");
cy.findByTestId("name0").type("value");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", { name: ["value"] });
});
it("add values", () => {
const submit = cy.spy().as("onSubmit");
mount(<MultiLineInputTest submit={submit} />);
cy.findByTestId("name0").type("value");
cy.findByTestId("addValue").click();
cy.findByTestId("name1").type("value1");
cy.findByTestId("addValue").click();
cy.findByTestId("name2").type("value2");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: ["value", "value1", "value2"],
});
});
it("from existing values as string", () => {
const submit = cy.spy().as("onSubmit");
mount(
<MultiLineInputTest
submit={submit}
defaultValues="one##two##three"
stringify
/>
);
cy.findByTestId("name0").should("have.value", "one");
cy.findByTestId("name1").should("have.value", "two");
cy.findByTestId("name2").should("have.value", "three");
cy.findByTestId("addValue").click();
cy.findByTestId("name3").type("four");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: "one##two##three##four",
});
});
it("from existing values as string[]", () => {
const submit = cy.spy().as("onSubmit");
mount(
<MultiLineInputTest
submit={submit}
defaultValues={["one", "two", "three"]}
/>
);
cy.findByTestId("name0").should("have.value", "one");
cy.findByTestId("name1").should("have.value", "two");
cy.findByTestId("name2").should("have.value", "three");
cy.findByTestId("remove0").click();
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: ["two", "three"],
});
});
it("remove test", () => {
const submit = cy.spy().as("onSubmit");
mount(
<MultiLineInputTest
submit={submit}
defaultValues={["one", "two", "three", "four", "five", "six"]}
/>
);
cy.findByTestId("remove0").click();
cy.findByTestId("remove2").click();
cy.findByTestId("remove2").click();
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: ["two", "three", "six"],
});
});
it("add / update test", () => {
const submit = cy.spy().as("onSubmit");
mount(<MultiLineInputTest submit={submit} defaultValues={["one"]} />);
cy.findByTestId("name0").type("-one");
cy.findByTestId("addValue").click();
cy.findByTestId("name1").type("twos");
cy.findAllByTestId("save").click();
cy.get("@onSubmit").should("have.been.calledWith", {
name: ["one-one", "twos"],
});
});
});

View File

@ -0,0 +1,72 @@
import { keycloakBefore } from "../support/util/keycloak_hooks";
import Masthead from "../support/pages/admin-ui/Masthead";
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import OTPPolicies from "../support/pages/admin-ui/manage/authentication/OTPPolicies";
import WebAuthnPolicies from "../support/pages/admin-ui/manage/authentication/WebAuthnPolicies";
describe("Policies", () => {
const masthead = new Masthead();
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
describe("OTP policies tab", () => {
const otpPoliciesPage = new OTPPolicies();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToAuthentication();
otpPoliciesPage.goToTab();
});
it("should change to hotp", () => {
otpPoliciesPage.checkSupportedApplications(
"FreeOTP",
"Google Authenticator",
"Microsoft Authenticator"
);
otpPoliciesPage.setPolicyType("hotp").increaseInitialCounter().save();
masthead.checkNotificationMessage("OTP policy successfully updated");
otpPoliciesPage.checkSupportedApplications("FreeOTP");
});
});
describe("Webauthn policies tabs", () => {
const webauthnPage = new WebAuthnPolicies();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToAuthentication();
});
it("should fill webauthn settings", () => {
webauthnPage.goToTab();
webauthnPage.fillSelects({
webAuthnPolicyAttestationConveyancePreference: "Indirect",
webAuthnPolicyRequireResidentKey: "Yes",
webAuthnPolicyUserVerificationRequirement: "Preferred",
});
webauthnPage.webAuthnPolicyCreateTimeout(30).save();
masthead.checkNotificationMessage(
"Updated webauthn policies successfully"
);
});
it("should fill webauthn passwordless settings", () => {
webauthnPage.goToPasswordlessTab();
webauthnPage
.fillSelects(
{
webAuthnPolicyAttestationConveyancePreference: "Indirect",
webAuthnPolicyRequireResidentKey: "Yes",
webAuthnPolicyUserVerificationRequirement: "Preferred",
},
true
)
.save();
masthead.checkNotificationMessage(
"Updated webauthn policies successfully"
);
});
});
});

View File

@ -0,0 +1,84 @@
import Form from "../support/forms/Form";
import FormValidation from "../support/forms/FormValidation";
import Select from "../support/forms/Select";
import CIBAPolicyPage from "../support/pages/admin-ui/manage/authentication/CIBAPolicyPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import adminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_hooks";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
describe("Authentication - Policies - CIBA", () => {
const realmName = crypto.randomUUID();
before(() => adminClient.createRealm(realmName));
after(() => adminClient.deleteRealm(realmName));
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
sidebarPage.goToAuthentication();
CIBAPolicyPage.goToTab();
});
it("displays the initial state", () => {
Select.assertSelectedItem(
CIBAPolicyPage.getBackchannelTokenDeliveryModeSelect(),
"Poll"
);
CIBAPolicyPage.getExpiresInput().should("have.value", "120");
CIBAPolicyPage.getIntervalInput().should("have.value", "5");
Form.assertSaveButtonDisabled();
});
it("validates the fields", () => {
// Required fields.
CIBAPolicyPage.getExpiresInput().clear();
CIBAPolicyPage.getIntervalInput().clear();
FormValidation.assertRequired(CIBAPolicyPage.getExpiresInput());
FormValidation.assertRequired(CIBAPolicyPage.getIntervalInput());
Form.assertSaveButtonDisabled();
// Fields with minimum value.
CIBAPolicyPage.getExpiresInput().type("9");
CIBAPolicyPage.getIntervalInput().type("-1");
FormValidation.assertMinValue(CIBAPolicyPage.getExpiresInput(), 10);
FormValidation.assertMinValue(CIBAPolicyPage.getIntervalInput(), 0);
Form.assertSaveButtonDisabled();
// Fields with maximum value.
CIBAPolicyPage.getExpiresInput().clear().type("601");
CIBAPolicyPage.getIntervalInput().clear().type("601");
FormValidation.assertMaxValue(CIBAPolicyPage.getExpiresInput(), 600);
FormValidation.assertMaxValue(CIBAPolicyPage.getIntervalInput(), 600);
Form.assertSaveButtonDisabled();
});
it("saves the form", () => {
// Select new values for fields.
Select.selectItem(
CIBAPolicyPage.getBackchannelTokenDeliveryModeSelect(),
"Ping"
);
CIBAPolicyPage.getExpiresInput().clear().type("140");
CIBAPolicyPage.getIntervalInput().clear().type("20");
// Save form.
Form.clickSaveButton();
CIBAPolicyPage.assertSaveSuccess();
// Assert values are saved.
Select.assertSelectedItem(
CIBAPolicyPage.getBackchannelTokenDeliveryModeSelect(),
"Ping"
);
CIBAPolicyPage.getExpiresInput().should("have.value", "140");
CIBAPolicyPage.getIntervalInput().should("have.value", "20");
});
});

View File

@ -0,0 +1,255 @@
import { keycloakBefore } from "../support/util/keycloak_hooks";
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import DuplicateFlowModal from "../support/pages/admin-ui/manage/authentication/DuplicateFlowModal";
import FlowDetails from "../support/pages/admin-ui/manage/authentication/FlowDetail";
import RequiredActions from "../support/pages/admin-ui/manage/authentication/RequiredActions";
import adminClient from "../support/util/AdminClient";
import PasswordPolicies from "../support/pages/admin-ui/manage/authentication/PasswordPolicies";
import ModalUtils from "../support/util/ModalUtils";
import CommonPage from "../support/pages/CommonPage";
import BindFlowModal from "../support/pages/admin-ui/manage/authentication/BindFlowModal";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const commonPage = new CommonPage();
const listingPage = new ListingPage();
const realmName = "test" + crypto.randomUUID();
describe("Authentication test", () => {
const detailPage = new FlowDetails();
const duplicateFlowModal = new DuplicateFlowModal();
const modalUtil = new ModalUtils();
before(() => adminClient.createRealm(realmName));
after(() => adminClient.deleteRealm(realmName));
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
sidebarPage.goToAuthentication();
});
it("authentication empty search test", () => {
commonPage.tableToolbarUtils().searchItem("", false);
commonPage.tableUtils().checkIfExists(true);
});
it("authentication search flow", () => {
const itemId = "Browser";
commonPage.tableToolbarUtils().searchItem(itemId, false);
commonPage.tableUtils().checkRowItemExists(itemId);
});
it("should create duplicate of existing flow", () => {
listingPage.clickRowDetails("Browser").clickDetailMenu("Duplicate");
duplicateFlowModal.fill("Copy of browser");
masthead.checkNotificationMessage("Flow successfully duplicated");
detailPage.flowExists("Copy of browser");
});
it("Should fail duplicate with empty flow name", () => {
listingPage.clickRowDetails("Browser").clickDetailMenu("Duplicate");
duplicateFlowModal.fill().shouldShowError("Required field");
modalUtil.cancelModal();
});
it("Should fail duplicate with duplicated name", () => {
listingPage.clickRowDetails("Browser").clickDetailMenu("Duplicate");
duplicateFlowModal.fill("browser");
masthead.checkNotificationMessage(
"Could not duplicate flow: New flow alias name already exists"
);
});
it("Should show the details of a flow as a table", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.executionExists("Cookie");
});
it("Should move kerberos down", () => {
listingPage.goToItemDetails("Copy of browser");
const fromRow = "Kerberos";
detailPage.expectPriorityChange(fromRow, () => {
detailPage.moveRowTo(
fromRow,
`[data-testid="Identity Provider Redirector"]`
);
});
});
it("Should edit flow details", () => {
const name = "Copy of browser";
listingPage.goToItemDetails(name);
const commonPage = new CommonPage();
commonPage
.actionToolbarUtils()
.clickActionToggleButton()
.clickDropdownItem("Edit info");
duplicateFlowModal.fill(name, "Other description");
masthead.checkNotificationMessage("Flow successfully updated");
});
it("Should change requirement of cookie", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.changeRequirement("Cookie", "Required");
masthead.checkNotificationMessage("Flow successfully updated");
});
it("Should switch to diagram mode", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.goToDiagram();
cy.get(".react-flow").should("exist");
});
it("Should add a execution", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.addExecution(
"Copy of browser forms",
"reset-credentials-choose-user"
);
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.executionExists("Choose User");
});
it("should add a condition", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.addCondition(
"Copy of browser Browser - Conditional OTP",
"conditional-user-role"
);
masthead.checkNotificationMessage("Flow successfully updated");
});
it("Should add a sub-flow", () => {
const flowName = "SubFlow";
listingPage.goToItemDetails("Copy of browser");
detailPage.addSubFlow(
"Copy of browser Browser - Conditional OTP",
flowName
);
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.flowExists(flowName);
});
it("Should remove an execution", () => {
listingPage.goToItemDetails("Copy of browser");
detailPage.executionExists("Cookie").clickRowDelete("Cookie");
modalUtil.confirmModal();
detailPage.executionExists("Cookie", false);
});
it("Should set as default in action menu", () => {
const bindFlow = new BindFlowModal();
listingPage.clickRowDetails("Copy of browser").clickDetailMenu("Bind flow");
bindFlow.fill("Direct grant flow").save();
masthead.checkNotificationMessage("Flow successfully updated");
});
const flowName = "Empty Flow";
it("should create flow from scratch", () => {
listingPage.goToCreateItem();
detailPage.fillCreateForm(
flowName,
"Some nice description about what this flow does so that we can use it later",
"Client flow"
);
masthead.checkNotificationMessage("Flow created");
detailPage.addSubFlowToEmpty(flowName, "EmptySubFlow");
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.flowExists(flowName);
});
it("Should delete a flow from action menu", () => {
listingPage.clickRowDetails(flowName).clickDetailMenu("Delete");
modalUtil.confirmModal();
masthead.checkNotificationMessage("Flow successfully deleted");
});
});
describe("Required actions", () => {
const requiredActionsPage = new RequiredActions();
before(() => adminClient.createRealm(realmName));
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
sidebarPage.goToAuthentication();
requiredActionsPage.goToTab();
});
after(() => adminClient.deleteRealm(realmName));
it("should enable delete account", () => {
const action = "Delete Account";
requiredActionsPage.enableAction(action);
masthead.checkNotificationMessage("Updated required action successfully");
requiredActionsPage.isChecked(action);
});
it("should register an unregistered action", () => {
const action = "Verify Profile";
requiredActionsPage.enableAction(action);
masthead.checkNotificationMessage("Updated required action successfully");
requiredActionsPage.isChecked(action).isDefaultEnabled(action);
});
it("should set action as default", () => {
const action = "Configure OTP";
requiredActionsPage.setAsDefault(action);
masthead.checkNotificationMessage("Updated required action successfully");
requiredActionsPage.isDefaultChecked(action);
});
it("should reorder required actions", () => {
const action = "Terms and Conditions";
requiredActionsPage.moveRowTo(action, "Update Profile");
masthead.checkNotificationMessage("Updated required action successfully");
});
});
describe("Password policies tab", () => {
const passwordPoliciesPage = new PasswordPolicies();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToAuthentication();
passwordPoliciesPage.goToTab();
});
it("should add password policies", () => {
passwordPoliciesPage
.shouldShowEmptyState()
.addPolicy("Not Recently Used")
.save();
masthead.checkNotificationMessage("Password policies successfully updated");
});
it("should remove password policies", () => {
passwordPoliciesPage.removePolicy("remove-passwordHistory").save();
masthead.checkNotificationMessage("Password policies successfully updated");
passwordPoliciesPage.shouldShowEmptyState();
});
});

View File

@ -0,0 +1,196 @@
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
import LoginPage from "../support/pages/LoginPage";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import AuthorizationTab from "../support/pages/admin-ui/manage/clients/client_details/tabs/AuthorizationTab";
import ModalUtils from "../support/util/ModalUtils";
import ClientDetailsPage from "../support/pages/admin-ui/manage/clients/client_details/ClientDetailsPage";
import PoliciesTab from "../support/pages/admin-ui/manage/clients/client_details/tabs/authorization_subtabs/PoliciesTab";
import PermissionsTab from "../support/pages/admin-ui/manage/clients/client_details/tabs/authorization_subtabs/PermissionsTab";
import CreateResourcePage from "../support/pages/admin-ui/manage/clients/client_details/CreateResourcePage";
describe("Client authentication subtab", () => {
const loginPage = new LoginPage();
const listingPage = new ListingPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const authenticationTab = new AuthorizationTab();
const clientDetailsPage = new ClientDetailsPage();
const policiesSubTab = new PoliciesTab();
const permissionsSubTab = new PermissionsTab();
const clientId = "client-authentication-" + crypto.randomUUID();
before(() =>
adminClient.createClient({
protocol: "openid-connect",
clientId,
publicClient: false,
authorizationServicesEnabled: true,
serviceAccountsEnabled: true,
standardFlowEnabled: true,
})
);
after(() => {
adminClient.deleteClient(clientId);
});
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClients();
listingPage.searchItem(clientId).goToItemDetails(clientId);
clientDetailsPage.goToAuthorizationTab();
});
it("Should update the resource server settings", () => {
policiesSubTab.setPolicy("DISABLED").formUtils().save();
masthead.checkNotificationMessage("Resource successfully updated", true);
});
it("Should create a resource", () => {
const resourcesSubtab = authenticationTab.goToResourcesSubTab();
listingPage.assertDefaultResource();
resourcesSubtab
.createResource()
.fillResourceForm({
name: "Resource",
displayName: "The display name",
type: "type",
uris: ["one", "two"],
})
.formUtils()
.save();
masthead.checkNotificationMessage("Resource created successfully", true);
sidebarPage.waitForPageLoad();
authenticationTab.formUtils().cancel();
});
it("Edit a resource", () => {
authenticationTab.goToResourcesSubTab();
listingPage.goToItemDetails("Resource");
new CreateResourcePage()
.fillResourceForm({
displayName: "updated",
})
.formUtils()
.save();
masthead.checkNotificationMessage("Resource successfully updated");
sidebarPage.waitForPageLoad();
authenticationTab.formUtils().cancel();
});
it("Should create a scope", () => {
authenticationTab
.goToScopesSubTab()
.createAuthorizationScope()
.fillScopeForm({
name: "The scope",
displayName: "Display something",
iconUri: "res://something",
})
.formUtils()
.save();
masthead.checkNotificationMessage(
"Authorization scope created successfully",
true
);
authenticationTab.goToScopesSubTab();
listingPage.itemExist("The scope");
});
it("Should create a policy", () => {
authenticationTab.goToPoliciesSubTab();
cy.intercept(
"GET",
"/admin/realms/master/clients/*/authz/resource-server/policy/regex/*"
).as("get");
policiesSubTab
.createPolicy("regex")
.fillBasePolicyForm({
name: "Regex policy",
description: "Policy for regex",
targetClaim: "I don't know",
regexPattern: ".*?",
})
.formUtils()
.save();
cy.wait(["@get"]);
masthead.checkNotificationMessage("Successfully created the policy", true);
sidebarPage.waitForPageLoad();
authenticationTab.formUtils().cancel();
});
it("Should delete a policy", () => {
authenticationTab.goToPoliciesSubTab();
listingPage.deleteItem("Regex policy");
new ModalUtils().confirmModal();
masthead.checkNotificationMessage("The Policy successfully deleted", true);
});
it("Should create a client policy", () => {
authenticationTab.goToPoliciesSubTab();
cy.intercept(
"GET",
"/admin/realms/master/clients/*/authz/resource-server/policy/client/*"
).as("get");
policiesSubTab
.createPolicy("client")
.fillBasePolicyForm({
name: "Client policy",
description: "Extra client field",
})
.inputClient("master-realm")
.formUtils()
.save();
cy.wait(["@get"]);
masthead.checkNotificationMessage("Successfully created the policy", true);
sidebarPage.waitForPageLoad();
authenticationTab.formUtils().cancel();
});
it("Should create a permission", () => {
authenticationTab.goToPermissionsSubTab();
permissionsSubTab.createPermission("resource").fillPermissionForm({
name: "Permission name",
description: "Something describing this permission",
});
permissionsSubTab.selectResource("Default Resource").formUtils().save();
cy.intercept(
"/admin/realms/master/clients/*/authz/resource-server/resource?first=0&max=10&permission=false"
).as("load");
masthead.checkNotificationMessage(
"Successfully created the permission",
true
);
authenticationTab.formUtils().cancel();
});
it.skip("Should copy auth details", () => {
const exportTab = authenticationTab.goToExportSubTab();
sidebarPage.waitForPageLoad();
exportTab.copy();
masthead.checkNotificationMessage("Authorization details copied.", true);
});
it("Should export auth details", () => {
const exportTab = authenticationTab.goToExportSubTab();
sidebarPage.waitForPageLoad();
exportTab.export();
masthead.checkNotificationMessage(
"Successfully exported authorization details.",
true
);
});
});

View File

@ -0,0 +1,61 @@
import ListingPage from "../support/pages/admin-ui/ListingPage";
import { ClientRegistrationPage } from "../support/pages/admin-ui/manage/clients/ClientRegistrationPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
describe("Client registration policies subtab", () => {
const loginPage = new LoginPage();
const listingPage = new ListingPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const clientRegistrationPage = new ClientRegistrationPage();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClients();
clientRegistrationPage.goToClientRegistrationTab();
sidebarPage.waitForPageLoad();
});
it("add anonymous client registration policy", () => {
clientRegistrationPage
.createPolicy()
.selectRow("max-clients")
.fillPolicyForm({
name: "new policy",
})
.formUtils()
.save();
masthead.checkNotificationMessage("New client policy created successfully");
clientRegistrationPage.formUtils().cancel();
listingPage.itemExist("new policy");
});
it("edit anonymous client registration policy", () => {
listingPage.goToItemDetails("new policy");
clientRegistrationPage
.fillPolicyForm({
name: "policy 2",
})
.formUtils()
.save();
masthead.checkNotificationMessage("Client policy updated successfully");
clientRegistrationPage.formUtils().cancel();
listingPage.itemExist("policy 2");
});
it("delete anonymous client registration policy", () => {
listingPage.clickRowDetails("policy 2").clickDetailMenu("Delete");
clientRegistrationPage.modalUtils().confirmModal();
masthead.checkNotificationMessage(
"Client registration policy deleted successfully"
);
listingPage.itemExist("policy 2", false);
});
});

View File

@ -0,0 +1,407 @@
import LoginPage from "../support/pages/LoginPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ListingPage, {
Filter,
FilterAssignedType,
FilterProtocol,
} from "../support/pages/admin-ui/ListingPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import CreateClientScopePage from "../support/pages/admin-ui/manage/client_scopes/CreateClientScopePage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import RoleMappingTab from "../support/pages/admin-ui/manage/RoleMappingTab";
import ModalUtils from "../support/util/ModalUtils";
import adminClient from "../support/util/AdminClient";
import ClientScopeDetailsPage from "../support/pages/admin-ui/manage/client_scopes/client_scope_details/ClientScopeDetailsPage";
import CommonPage from "../support/pages/CommonPage";
import MappersTab from "../support/pages/admin-ui/manage/client_scopes/client_scope_details/tabs/MappersTab";
import MapperDetailsPage, {
ClaimJsonType,
} from "../support/pages/admin-ui/manage/client_scopes/client_scope_details/tabs/mappers/MapperDetailsPage";
let itemId = "client_scope_crud";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const commonPage = new CommonPage();
const listingPage = new ListingPage();
const createClientScopePage = new CreateClientScopePage();
const modalUtils = new ModalUtils();
describe("Client Scopes test", () => {
const modalMessageDeleteConfirmation =
"Are you sure you want to delete this client scope";
const notificationMessageDeletionConfirmation =
"The client scope has been deleted";
const clientScopeName = "client-scope-test";
const openIDConnectItemText = "OpenID Connect";
const clientScope = {
name: clientScopeName,
description: "",
protocol: "openid-connect",
attributes: {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"gui.order": "1",
"consent.screen.text": "",
},
};
before(async () => {
for (let i = 0; i < 5; i++) {
clientScope.name = clientScopeName + i;
await adminClient.createClientScope(clientScope);
}
});
after(async () => {
for (let i = 0; i < 5; i++) {
if (await adminClient.existsClientScope(clientScopeName + i)) {
await adminClient.deleteClientScope(clientScopeName + i);
}
}
});
describe("Client Scope filter list items", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClientScopes();
});
it("should filter item by name", () => {
const itemName = clientScopeName + 0;
listingPage
.searchItem(itemName, false)
.itemsEqualTo(1)
.itemExist(itemName, true);
});
it("should filter items by Assigned type All types", () => {
listingPage
.selectFilter(Filter.AssignedType)
.selectSecondaryFilterAssignedType(FilterAssignedType.AllTypes)
.itemExist(FilterAssignedType.Default, true)
.itemExist(FilterAssignedType.Optional, true)
.itemExist(FilterAssignedType.None, true);
});
it("should filter items by Assigned type Default", () => {
listingPage
.selectFilter(Filter.AssignedType)
.selectSecondaryFilterAssignedType(FilterAssignedType.Default)
.itemExist(FilterAssignedType.Default, true)
.itemExist(FilterAssignedType.Optional, false)
.itemExist(FilterAssignedType.None, false);
});
it("should filter items by Assigned type Optional", () => {
listingPage
.selectFilter(Filter.AssignedType)
.selectSecondaryFilterAssignedType(FilterAssignedType.Optional)
.itemExist(FilterAssignedType.Default, false)
.itemExist(FilterAssignedType.Optional, true)
.itemExist(FilterAssignedType.None, false);
});
it("should filter items by Protocol All", () => {
listingPage
.selectFilter(Filter.Protocol)
.selectSecondaryFilterProtocol(FilterProtocol.All);
sidebarPage.waitForPageLoad();
listingPage
.showNextPageTableItems()
.itemExist(FilterProtocol.SAML, true)
.itemExist(openIDConnectItemText, true); //using FilterProtocol.OpenID will fail, text does not match.
});
it("should filter items by Protocol SAML", () => {
listingPage
.selectFilter(Filter.Protocol)
.selectSecondaryFilterProtocol(FilterProtocol.SAML)
.itemExist(FilterProtocol.SAML, true)
.itemExist(openIDConnectItemText, false); //using FilterProtocol.OpenID will fail, text does not match.
});
it("should filter items by Protocol OpenID", () => {
listingPage
.selectFilter(Filter.Protocol)
.selectSecondaryFilterProtocol(FilterProtocol.OpenID)
.itemExist(FilterProtocol.SAML, false)
.itemExist(openIDConnectItemText, true); //using FilterProtocol.OpenID will fail, text does not match.
});
it("should show items on next page are more than 11", () => {
listingPage.showNextPageTableItems();
listingPage.itemsGreaterThan(1);
});
});
describe("Client Scope modify list items", () => {
const itemName = clientScopeName + 0;
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClientScopes();
});
it("should modify selected item type to Default from search bar", () => {
listingPage
.clickItemCheckbox(itemName)
.changeTypeToOfSelectedItems(FilterAssignedType.Default);
listingPage.itemContainValue(itemName, 2, FilterAssignedType.Default);
});
it("should modify selected item type to Optional from search bar", () => {
listingPage
.clickItemCheckbox(itemName)
.changeTypeToOfSelectedItems(FilterAssignedType.Optional);
listingPage.itemContainValue(itemName, 2, FilterAssignedType.Optional);
});
const expectedItemAssignedTypes = [
FilterAssignedType.Default,
FilterAssignedType.Optional,
FilterAssignedType.None,
];
expectedItemAssignedTypes.forEach(($assignedType) => {
const itemName = clientScopeName + 0;
it(`should modify item ${itemName} AssignedType to ${$assignedType} from item bar`, () => {
listingPage
.searchItem(clientScopeName, false)
.clickRowSelectItem(itemName, $assignedType);
cy.wait(2000);
listingPage.searchItem(itemName, false).itemExist($assignedType);
});
});
it("should not allow to modify item AssignedType from search bar when no item selected", () => {
const itemName = clientScopeName + 0;
listingPage
.searchItem(itemName, false)
.checkInSearchBarChangeTypeToButtonIsDisabled()
.clickSearchBarActionButton()
.checkDropdownItemIsDisabled("Delete")
.clickItemCheckbox(itemName)
.checkInSearchBarChangeTypeToButtonIsDisabled(false)
.clickSearchBarActionButton()
.checkDropdownItemIsDisabled("Delete", false)
.clickItemCheckbox(itemName)
.checkInSearchBarChangeTypeToButtonIsDisabled()
.clickSearchBarActionButton()
.checkDropdownItemIsDisabled("Delete");
});
//TODO: blocked by https://github.com/keycloak/keycloak-admin-ui/issues/1952
//it("should export item from item bar", () => {
//});
});
describe("Client Scope delete list items ", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClientScopes();
});
it("should delete item from item bar", () => {
listingPage
.checkInSearchBarChangeTypeToButtonIsDisabled()
.clickItemCheckbox(clientScopeName + 0)
.deleteItem(clientScopeName + 0);
modalUtils
.checkModalMessage(modalMessageDeleteConfirmation)
.confirmModal();
masthead.checkNotificationMessage(
notificationMessageDeletionConfirmation
);
listingPage.checkInSearchBarChangeTypeToButtonIsDisabled();
});
it("should delete selected item from search bar", () => {
listingPage
.checkInSearchBarChangeTypeToButtonIsDisabled()
.clickItemCheckbox(clientScopeName + 1)
.clickSearchBarActionButton()
.clickSearchBarActionItem("Delete");
modalUtils
.checkModalMessage(modalMessageDeleteConfirmation)
.confirmModal();
masthead.checkNotificationMessage(
notificationMessageDeletionConfirmation
);
listingPage.checkInSearchBarChangeTypeToButtonIsDisabled();
});
it("should delete multiple selected items from search bar", () => {
listingPage
.checkInSearchBarChangeTypeToButtonIsDisabled()
.clickItemCheckbox(clientScopeName + 2)
.clickItemCheckbox(clientScopeName + 3)
.clickItemCheckbox(clientScopeName + 4)
.clickSearchBarActionButton()
.clickSearchBarActionItem("Delete");
modalUtils
.checkModalMessage(modalMessageDeleteConfirmation)
.confirmModal();
masthead.checkNotificationMessage(
notificationMessageDeletionConfirmation
);
listingPage.checkInSearchBarChangeTypeToButtonIsDisabled();
});
});
describe("Client Scope creation", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClientScopes();
});
it("should fail creating client scope", () => {
sidebarPage.waitForPageLoad();
listingPage.goToCreateItem();
createClientScopePage.fillClientScopeData("address").save();
masthead.checkNotificationMessage(
"Could not create client scope: 'Client Scope address already exists'"
);
});
it("hides 'consent text' field when 'display consent' switch is disabled", () => {
sidebarPage.waitForPageLoad();
listingPage.goToCreateItem();
createClientScopePage
.getSwitchDisplayOnConsentScreenInput()
.should("be.checked");
createClientScopePage.getConsentScreenTextInput().should("exist");
createClientScopePage.switchDisplayOnConsentScreen();
createClientScopePage
.getSwitchDisplayOnConsentScreenInput()
.should("not.be.checked");
createClientScopePage.getConsentScreenTextInput().should("not.exist");
});
it("Client scope CRUD test", () => {
itemId += "_" + crypto.randomUUID();
// Create
listingPage.itemExist(itemId, false).goToCreateItem();
createClientScopePage.fillClientScopeData(itemId).save();
masthead.checkNotificationMessage("Client scope created");
sidebarPage.goToClientScopes();
sidebarPage.waitForPageLoad();
// Delete
listingPage
.searchItem(itemId, false)
.itemExist(itemId)
.deleteItem(itemId);
modalUtils
.checkModalMessage(modalMessageDeleteConfirmation)
.confirmModal();
masthead.checkNotificationMessage("The client scope has been deleted");
listingPage.itemExist(itemId, false);
});
});
describe("Scope tab test", () => {
const scopeTab = new RoleMappingTab("client-scope");
const scopeName = "address";
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClientScopes();
});
it("Assign and unassign role", () => {
const role = "admin";
listingPage.searchItem(scopeName, false).goToItemDetails(scopeName);
scopeTab.goToScopeTab().assignRole().selectRow(role).assign();
masthead.checkNotificationMessage("Role mapping updated");
scopeTab.checkRoles([role]);
scopeTab.hideInheritedRoles().selectRow(role).unAssign();
modalUtils.checkModalTitle("Remove role?").confirmModal();
scopeTab.checkRoles([]);
});
});
describe("Mappers tab test", () => {
const clientScopeDetailsPage = new ClientScopeDetailsPage();
const mappersTab = new MappersTab();
const mapperDetailsTab = new MapperDetailsPage();
const scopeName = "address";
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClientScopes();
});
it("CRUD mappers", () => {
const predefinedMapperName = "Predefined Mapper test";
const predefinedMapper = "Allowed Web Origins";
const mappers1 = ["birthdate"];
const mappers2 = ["email verified", "email", "family name"];
listingPage.searchItem(scopeName, false).goToItemDetails(scopeName);
clientScopeDetailsPage
.goToMappersTab()
.addPredefinedMappers(mappers1)
.addPredefinedMappers(mappers2);
listingPage.searchItem(mappers1[0], false).goToItemDetails(mappers1[0]);
mapperDetailsTab
.fillUserAttribute(mappers1[0] + "1")
.fillTokenClaimName(mappers1[0] + "2")
.changeClaimJsonType(ClaimJsonType.Long);
commonPage.formUtils().save();
commonPage
.masthead()
.checkNotificationMessage("Mapping successfully updated");
sidebarPage.goToClientScopes();
listingPage.searchItem(scopeName, false).goToItemDetails(scopeName);
clientScopeDetailsPage.goToMappersTab();
listingPage.searchItem(mappers1[0], false).goToItemDetails(mappers1[0]);
mapperDetailsTab
.checkUserAttribute(mappers1[0] + "1")
.checkTokenClaimName(mappers1[0] + "2")
.checkClaimJsonType(ClaimJsonType.Long);
commonPage.formUtils().cancel();
mappersTab
.removeMappers(mappers1.concat(mappers2))
.addMappersByConfiguration(predefinedMapper, predefinedMapperName);
sidebarPage.goToClientScopes();
listingPage.searchItem(scopeName, false).goToItemDetails(scopeName);
clientScopeDetailsPage.goToMappersTab();
commonPage.tableUtils().checkRowItemExists(predefinedMapperName, true);
mappersTab.removeMappers([predefinedMapperName]);
});
});
});

View File

@ -0,0 +1,195 @@
import LoginPage from "../support/pages/LoginPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import ModalUtils from "../support/util/ModalUtils";
import adminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import SettingsTab from "../support/pages/admin-ui/manage/clients/client_details/tabs/SettingsTab";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const listingPage = new ListingPage();
const modalUtils = new ModalUtils();
describe("Clients SAML tests", () => {
describe("SAML test", () => {
const samlClientName = "saml";
before(() => {
adminClient.createClient({
protocol: "saml",
clientId: samlClientName,
publicClient: false,
});
});
after(() => {
adminClient.deleteClient(samlClientName);
});
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClients();
listingPage.searchItem(samlClientName).goToItemDetails(samlClientName);
});
it("should display the saml sections on details screen", () => {
cy.get(".pf-c-jump-links__list").should(($ul) => {
expect($ul)
.to.contain("SAML capabilities")
.to.contain("Signature and Encryption");
});
});
it("should save force name id format", () => {
cy.get(".pf-c-jump-links__list").contains("SAML capabilities").click();
cy.findByTestId("forceNameIdFormat").click({
force: true,
});
cy.findByTestId("settingsSave").click();
masthead.checkNotificationMessage("Client successfully updated");
});
});
describe("SAML keys tab", () => {
const clientId = "saml-keys";
before(() => {
adminClient.createClient({
clientId,
protocol: "saml",
});
});
after(() => {
adminClient.deleteClient(clientId);
});
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClients();
listingPage.searchItem(clientId).goToItemDetails(clientId);
cy.findByTestId("keysTab").click();
});
it("should doesn't disable signature when cancel", () => {
cy.findByTestId("clientSignature").click({ force: true });
modalUtils
.checkModalTitle('Disable "Client signature required"')
.cancelModal();
cy.findAllByTestId("certificate").should("have.length", 1);
});
it("should disable client signature", () => {
cy.intercept(
"admin/realms/master/clients/*/certificates/saml.signing"
).as("load");
cy.findByTestId("clientSignature").click({ force: true });
modalUtils
.checkModalTitle('Disable "Client signature required"')
.confirmModal();
masthead.checkNotificationMessage("Client successfully updated");
cy.findAllByTestId("certificate").should("have.length", 0);
});
it("should enable Encryption keys config", () => {
cy.findByTestId("encryptAssertions").click({ force: true });
cy.findByTestId("generate").click();
masthead.checkNotificationMessage(
"New key pair and certificate generated successfully"
);
modalUtils.confirmModal();
cy.findAllByTestId("certificate").should("have.length", 1);
});
});
describe("SAML settings tab", () => {
const clientId = "saml-settings";
const settingsTab = new SettingsTab();
before(() => {
adminClient.createClient({
clientId,
protocol: "saml",
});
});
after(() => {
adminClient.deleteClient(clientId);
});
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToClients();
listingPage.searchItem(clientId).goToItemDetails(clientId);
});
it("should check SAML capabilities", () => {
cy.get(".pf-c-jump-links__list").contains("SAML capabilities").click();
settingsTab.assertNameIdFormatDropdown();
settingsTab.assertSAMLCapabilitiesSwitches();
});
it("should check signature and encryption", () => {
cy.get(".pf-c-jump-links__list")
.contains("Signature and Encryption")
.click();
settingsTab.assertSignatureAlgorithmDropdown();
settingsTab.assertSignatureKeyNameDropdown();
settingsTab.assertCanonicalizationDropdown();
settingsTab.assertSignatureEncryptionSwitches();
});
it("should check access settings", () => {
cy.get(".pf-c-jump-links__list").contains("Access settings").click();
const validUrl =
"http://localhost:8180/realms/master/protocol/" +
clientId +
"/clients/";
const rootUrlError =
"Client could not be updated: Root URL is not a valid URL";
const homeUrlError =
"Client could not be updated: Base URL is not a valid URL";
cy.get("#kc-root-url").type("Invalid URL");
settingsTab.clickSaveBtn();
masthead.checkNotificationMessage(rootUrlError);
cy.get("#kc-root-url").clear();
cy.get("#kc-home-url").type("Invalid URL");
settingsTab.clickSaveBtn();
masthead.checkNotificationMessage(homeUrlError);
cy.get("#kc-home-url").clear();
cy.get("#kc-root-url").type(validUrl);
cy.get("#kc-home-url").type(validUrl);
settingsTab.clickSaveBtn();
masthead.checkNotificationMessage("Client successfully updated");
settingsTab.assertAccessSettings();
});
it("should check login settings", () => {
cy.get(".pf-c-jump-links__list").contains("Login settings").click();
settingsTab.assertLoginThemeDropdown();
settingsTab.assertLoginSettings();
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,439 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import UserEventsTab, {
UserEventSearchData,
} from "../support/pages/admin-ui/manage/events/tabs/UserEventsTab";
import AdminEventsTab from "../support/pages/admin-ui/manage/events/tabs/AdminEventsTab";
import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import EventsPage, {
EventsTab,
} from "../support/pages/admin-ui/manage/events/EventsPage";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import adminClient from "../support/util/AdminClient";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const userEventsTab = new UserEventsTab();
const eventsPage = new EventsPage();
const adminEventsTab = new AdminEventsTab();
const realmSettingsPage = new RealmSettingsPage();
const masthead = new Masthead();
const listingPage = new ListingPage();
const dateFrom = new Date();
dateFrom.setDate(dateFrom.getDate() - 100);
const dateFromFormatted = `${dateFrom.getFullYear()}-${dateFrom.getMonth()}-${dateFrom.getDay()}`;
const dateTo = new Date();
dateTo.setDate(dateTo.getDate() + 100);
const dateToFormatted = `${dateTo.getFullYear()}-${dateTo.getMonth()}-${dateTo.getDay()}`;
describe("Events tests", () => {
const eventsTestUser = {
eventsTestUserId: "",
userRepresentation: {
username: "events-test" + crypto.randomUUID(),
enabled: true,
credentials: [{ value: "events-test" }],
},
};
const eventsTestUserClientId = "admin-cli";
before(async () => {
const result = await adminClient.createUser(
eventsTestUser.userRepresentation
);
eventsTestUser.eventsTestUserId = result.id;
});
after(() =>
adminClient.deleteUser(eventsTestUser.userRepresentation.username)
);
describe("User events list", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToEvents();
});
it("Show empty when no save events", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToUserEventsSettingsSubTab()
.enableSaveEventsSwitch()
.save()
.clearUserEvents();
cy.wait(5000);
sidebarPage.goToEvents();
eventsPage.goToUserEventsTab();
userEventsTab.assertNoSearchResultsExist(true);
});
it("Expand item to see more information", () => {
listingPage.expandRow(0).assertExpandedRowContainText(0, "code_id");
});
});
describe("Search user events list", () => {
const eventTypes = [
"LOGOUT",
"CODE_TO_TOKEN",
"CODE_TO_TOKEN_ERROR",
"LOGIN_ERROR",
"LOGIN",
];
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToEvents();
});
it("Check search dropdown display", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToUserEventsSettingsSubTab()
.disableSaveEventsSwitch()
.save();
cy.wait(5000);
masthead.signOut();
loginPage.logIn();
cy.visit("/");
sidebarPage.goToEvents();
eventsPage.tabUtils().checkIsCurrentTab(EventsTab.UserEvents);
userEventsTab.assertSearchUserEventDropdownMenuExist(true);
});
it("Check user events search form fields display", () => {
userEventsTab
.openSearchUserEventDropdownMenu()
.assertUserSearchDropdownMenuHasLabels();
});
it("Check event type dropdown options exist", () => {
userEventsTab
.openSearchUserEventDropdownMenu()
.openEventTypeSelectMenu()
.clickEventTypeSelectItem(eventTypes[0])
.clickEventTypeSelectItem(eventTypes[1])
.clickEventTypeSelectItem(eventTypes[2])
.clickEventTypeSelectItem(eventTypes[3])
.closeEventTypeSelectMenu();
});
it("Check `search events` button disabled by default", () => {
userEventsTab
.openSearchUserEventDropdownMenu()
.assertSearchEventBtnIsEnabled(false);
});
it("Check user events search and removal work", () => {
userEventsTab
.searchUserEventByEventType([eventTypes[0]])
.assertEventTypeChipGroupItemExist(eventTypes[0], true)
.assertEventTypeChipGroupItemExist(eventTypes[1], false)
.assertEventTypeChipGroupItemExist(eventTypes[2], false)
.assertEventTypeChipGroupItemExist(eventTypes[3], false)
.assertNoSearchResultsExist(true)
.removeEventTypeChipGroupItem(eventTypes[0])
.assertEventTypeChipGroupExist(false);
});
it("Check for no events logged", () => {
userEventsTab
.searchUserEventByUserId("test")
.assertNoSearchResultsExist(true);
});
it("Check `search events` button enabled", () => {
userEventsTab
.openSearchUserEventDropdownMenu()
.assertSearchEventBtnIsEnabled(false)
.typeUserId("11111")
.assertSearchEventBtnIsEnabled(true);
});
it("Search by user ID", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToUserEventsSettingsSubTab()
.enableSaveEventsSwitch()
.save();
sidebarPage.goToEvents();
cy.wrap(null).then(() =>
adminClient.loginUser(
eventsTestUser.userRepresentation.username,
eventsTestUser.userRepresentation.credentials[0].value,
eventsTestUserClientId
)
);
userEventsTab
.searchUserEventByUserId(eventsTestUser.eventsTestUserId)
.assertUserIdChipGroupExist(true)
.assertEventTypeChipGroupExist(false)
.assertClientChipGroupExist(false)
.assertDateFromChipGroupExist(false)
.assertDateToChipGroupExist(false);
listingPage.itemsGreaterThan(0);
});
it("Search by event type", () => {
userEventsTab
.searchUserEventByEventType([eventTypes[4]])
.assertUserIdChipGroupExist(false)
.assertEventTypeChipGroupExist(true)
.assertClientChipGroupExist(false)
.assertDateFromChipGroupExist(false)
.assertDateToChipGroupExist(false);
listingPage.itemsGreaterThan(0);
});
it("Search by client", () => {
userEventsTab
.searchUserEventByClient(eventsTestUserClientId)
.assertUserIdChipGroupExist(false)
.assertEventTypeChipGroupExist(false)
.assertClientChipGroupExist(true)
.assertDateFromChipGroupExist(false)
.assertDateToChipGroupExist(false);
listingPage.itemsGreaterThan(0);
});
it("Search by date from", () => {
userEventsTab
.searchUserEventByDateFrom(dateFromFormatted)
.assertUserIdChipGroupExist(false)
.assertEventTypeChipGroupExist(false)
.assertClientChipGroupExist(false)
.assertDateFromChipGroupExist(true)
.assertDateToChipGroupExist(false);
listingPage.itemsGreaterThan(0);
});
it("Search by dato to", () => {
userEventsTab
.searchUserEventByDateTo(dateToFormatted)
.assertUserIdChipGroupExist(false)
.assertEventTypeChipGroupExist(false)
.assertClientChipGroupExist(false)
.assertDateFromChipGroupExist(false)
.assertDateToChipGroupExist(true);
listingPage.itemsGreaterThan(0);
});
it("Search by all elements", () => {
const searchData = new UserEventSearchData();
searchData.client = eventsTestUserClientId;
searchData.userId = eventsTestUser.eventsTestUserId;
searchData.eventType = [eventTypes[4]];
searchData.dateFrom = dateFromFormatted;
searchData.dateTo = dateToFormatted;
userEventsTab
.searchUserEvent(searchData)
.assertUserIdChipGroupExist(true)
.assertEventTypeChipGroupExist(true)
.assertClientChipGroupExist(true)
.assertDateFromChipGroupExist(true)
.assertDateToChipGroupExist(true);
listingPage.itemsGreaterThan(0);
});
});
describe("Admin events list", () => {
const realmName = crypto.randomUUID();
before(() => adminClient.createRealm(realmName));
after(() => adminClient.deleteRealm(realmName));
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
});
it("Show events", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToAdminEventsSettingsSubTab()
.enableSaveEvents()
.save();
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
listingPage.itemsGreaterThan(0);
});
it("Show empty when no save events", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToAdminEventsSettingsSubTab()
.disableSaveEvents()
.save({ waitForRealm: false, waitForConfig: true })
.clearAdminEvents();
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
adminEventsTab.assertNoSearchResultsExist(true);
});
});
describe("Search admin events list", () => {
const resourceTypes = ["REALM"];
const operationTypes = ["UPDATE"];
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
});
it("Search by resource types", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToAdminEventsSettingsSubTab()
.enableSaveEvents()
.save({ waitForRealm: false, waitForConfig: true });
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
adminEventsTab
.searchAdminEventByResourceTypes([resourceTypes[0]])
.assertResourceTypesChipGroupExist(true);
listingPage.itemsGreaterThan(0);
});
it("Search by operation types", () => {
adminEventsTab
.searchAdminEventByOperationTypes([operationTypes[0]])
.assertOperationTypesChipGroupExist(true);
listingPage.itemsGreaterThan(0);
});
it("Search by resource path", () => {
adminEventsTab
.searchAdminEventByResourcePath("test")
.assertResourcePathChipGroupExist(true);
});
it("Search by realm", () => {
adminEventsTab
.searchAdminEventByRealm("master")
.assertRealmChipGroupExist(true);
});
it("Search by client", () => {
adminEventsTab
.searchAdminEventByClient("admin-cli")
.assertClientChipGroupExist(true);
});
it("Search by user ID", () => {
adminEventsTab
.searchAdminEventByUser("test")
.assertUserChipGroupExist(true);
});
it("Search by IP address", () => {
adminEventsTab
.searchAdminEventByIpAddress("test")
.assertIpAddressChipGroupExist(true);
});
it("Search by date from", () => {
adminEventsTab
.searchAdminEventByDateTo(dateToFormatted)
.assertDateToChipGroupExist(true);
});
it("Search by date to", () => {
adminEventsTab
.searchAdminEventByDateFrom(dateFromFormatted)
.assertDateFromChipGroupExist(true);
});
});
describe("Search admin events", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
});
it("Check admin events search form fields display", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToEventsTab()
.goToAdminEventsSettingsSubTab()
.disableSaveEvents()
.save({ waitForRealm: false, waitForConfig: true });
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
adminEventsTab
.openSearchAdminEventDropdownMenu()
.assertAdminSearchDropdownMenuHasLabels();
});
it("Check `search admin events` button disabled by default", () => {
adminEventsTab
.openSearchAdminEventDropdownMenu()
.assertSearchAdminBtnEnabled(false);
});
it("Check admin events search and removal work", () => {
sidebarPage.goToEvents();
eventsPage
.goToAdminEventsTab()
.searchAdminEventByResourcePath("events/config")
.assertResourcePathChipGroupItemExist("events/config", true)
.removeResourcePathChipGroup();
listingPage.itemContainValue("UPDATE", 3, "UPDATE");
});
it("Check for no events logged", () => {
adminEventsTab
.searchAdminEventByResourcePath("events/test")
.assertNoSearchResultsExist(true);
});
it("Check `search admin events` button enabled", () => {
adminEventsTab
.openSearchAdminEventDropdownMenu()
.typeIpAddress("11111")
.assertSearchAdminBtnEnabled(true);
});
});
describe("Check more button opens auth and representation dialogs", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToEvents();
eventsPage.goToAdminEventsTab();
});
it("Check auth dialog opens and is not empty", () => {
listingPage.clickRowDetails("UPDATE").clickDetailMenu("Auth");
adminEventsTab.assertAuthDialogIsNotEmpty();
});
it("Check representation dialog opens and is not empty", () => {
listingPage.clickRowDetails("UPDATE").clickDetailMenu("Representation");
adminEventsTab.assertRepresentationDialogIsNotEmpty();
});
});
});

View File

@ -0,0 +1,496 @@
import GroupModal from "../support/pages/admin-ui/manage/groups/GroupModal";
import GroupDetailPage from "../support/pages/admin-ui/manage/groups/group_details/GroupDetailPage";
import AttributesTab from "../support/pages/admin-ui/manage/AttributesTab";
import { SearchGroupPage } from "../support/pages/admin-ui/manage/groups/SearchGroupPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import GroupPage from "../support/pages/admin-ui/manage/groups/GroupPage";
import ChildGroupsTab from "../support/pages/admin-ui/manage/groups/group_details/tabs/ChildGroupsTab";
import MembersTab from "../support/pages/admin-ui/manage/groups/group_details/tabs/MembersTab";
import adminClient from "../support/util/AdminClient";
import { range } from "lodash-es";
import RoleMappingTab from "../support/pages/admin-ui/manage/RoleMappingTab";
import CommonPage from "../support/pages/CommonPage";
describe("Group test", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const groupModal = new GroupModal();
const searchGroupPage = new SearchGroupPage();
const attributesTab = new AttributesTab();
const groupPage = new GroupPage();
const groupDetailPage = new GroupDetailPage();
const childGroupsTab = new ChildGroupsTab();
const membersTab = new MembersTab();
const commonPage = new CommonPage();
const groupNamePrefix = "group_";
let groupName: string;
const groupNames: string[] = [];
const predefinedGroups = ["level", "level1", "level2", "level3"];
const emptyGroup = "empty-group";
let users: { id: string; username: string }[] = [];
const username = "test-user";
before(async () => {
users = await Promise.all(
range(5).map((index) => {
const user = adminClient
.createUser({
username: username + index,
enabled: true,
})
.then((user) => {
return { id: user.id, username: username + index };
});
return user;
})
);
});
after(() => adminClient.deleteGroups());
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToGroups();
groupName = groupNamePrefix + crypto.randomUUID();
groupNames.push(groupName);
});
describe("List", () => {
it("Create group from empty option", () => {
groupPage
.assertNoGroupsInThisRealmEmptyStateMessageExist(true)
.createGroup(groupName, true)
.assertNotificationGroupCreated()
.searchGroup(groupName, true)
.assertGroupItemExist(groupName, true);
});
it("Create group from search bar", () => {
groupPage
.assertNoGroupsInThisRealmEmptyStateMessageExist(false)
.createGroup(groupName, false)
.assertNotificationGroupCreated()
.searchGroup(groupName, true)
.assertGroupItemExist(groupName, true);
});
it("Fail to create group with empty name", () => {
groupPage
.assertNoGroupsInThisRealmEmptyStateMessageExist(false)
.createGroup(" ", false)
.assertNotificationCouldNotCreateGroupWithEmptyName();
groupModal.closeModal();
});
it("Fail to create group with duplicated name", () => {
groupPage
.assertNoGroupsInThisRealmEmptyStateMessageExist(false)
.createGroup(groupName, false)
.createGroup(groupName, false)
.assertNotificationCouldNotCreateGroupWithDuplicatedName(groupName);
groupModal.closeModal();
groupPage.searchGroup(groupName).assertGroupItemsEqual(1);
});
it("Empty search", () => {
groupPage.searchGroup(" ").assertNoSearchResultsMessageExist(true);
});
it("Search group that exists", () => {
groupPage
.searchGroup(groupNames[0])
.assertGroupItemExist(groupNames[0], true);
});
it("Search group that does not exists", () => {
groupPage
.searchGroup("not-existent-group")
.assertNoSearchResultsMessageExist(true);
});
it("Delete group from item bar", () => {
groupPage
.searchGroup(groupNames[0], true)
.deleteGroupItem(groupNames[0])
.assertNotificationGroupDeleted()
.searchGroup(groupNames[0], true)
.assertNoSearchResultsMessageExist(true);
});
it("Delete group from search bar", () => {
groupPage
.selectGroupItemCheckbox([groupNames[1]])
.deleteSelectedGroups()
.assertNotificationGroupDeleted()
.searchGroup(groupNames[1])
.assertNoSearchResultsMessageExist(true);
});
it("Delete groups from search bar", () => {
cy.wrap(null).then(() =>
adminClient.createGroup("group_multiple_deletion_test")
);
cy.reload();
groupPage
.selectGroupItemCheckboxAllRows()
.deleteSelectedGroups()
.assertNotificationGroupsDeleted()
.assertNoGroupsInThisRealmEmptyStateMessageExist(true);
});
});
describe("Search group under current group", () => {
before(async () => {
const createdGroups = await adminClient.createSubGroups(predefinedGroups);
await Promise.all([
range(5).map((index) => {
adminClient.addUserToGroup(
users[index].id!,
createdGroups[index % 3].id
);
}),
adminClient.createUser({ username: "new", enabled: true }),
]);
});
it("Search child group in group", () => {
groupPage
.goToGroupChildGroupsTab(predefinedGroups[0])
.searchGroup(predefinedGroups[1])
.assertGroupItemExist(predefinedGroups[1], true);
});
it("Search non existing child group in group", () => {
groupPage
.goToGroupChildGroupsTab(predefinedGroups[0])
.searchGroup("non-existent-sub-group")
.assertNoSearchResultsMessageExist(true);
});
it("Empty search in group", () => {
groupPage
.goToGroupChildGroupsTab(predefinedGroups[0])
.searchGroup(" ")
.assertNoSearchResultsMessageExist(true);
});
});
describe("Group Actions", () => {
const groupNameDeleteHeaderAction = "group_test_delete_header_action";
before(async () => {
await adminClient.createGroup(groupNameDeleteHeaderAction);
});
after(async () => {
await adminClient.deleteGroups();
});
describe("Search globally", () => {
it("Navigate to parent group details", () => {
searchGroupPage
.searchGroup(predefinedGroups[0])
.goToGroupChildGroupsTab(predefinedGroups[0])
.assertGroupItemExist(predefinedGroups[1], true);
});
it("Navigate to sub-group details", () => {
searchGroupPage
.searchGlobal(predefinedGroups[1])
.goToGroupChildGroupsFromTree(predefinedGroups[1])
.assertGroupItemExist(predefinedGroups[2], true);
});
});
it("Rename group", () => {
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
groupDetailPage
.renameGroup("new_group_name")
.assertNotificationGroupUpdated()
.assertHeaderGroupNameEqual("new_group_name")
.renameGroup(predefinedGroups[0])
.assertNotificationGroupUpdated()
.assertHeaderGroupNameEqual(predefinedGroups[0]);
});
it("Delete group from group details", () => {
groupPage.goToGroupChildGroupsTab(groupNameDeleteHeaderAction);
groupDetailPage
.headerActionDeleteGroup()
.assertNotificationGroupDeleted()
.assertGroupItemExist(groupNameDeleteHeaderAction, false);
});
});
describe("Child Groups", () => {
before(async () => {
await adminClient.createGroup(predefinedGroups[0]);
});
after(async () => {
await adminClient.deleteGroups();
});
beforeEach(() => {
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
});
it("Check empty state", () => {
childGroupsTab.assertNoGroupsInThisSubGroupEmptyStateMessageExist(true);
});
it("Create group from empty state", () => {
childGroupsTab
.createGroup(predefinedGroups[1], true)
.assertNotificationGroupCreated();
});
it("Create group from search bar", () => {
childGroupsTab
.createGroup(predefinedGroups[2], false)
.assertNotificationGroupCreated();
});
it("Fail to create group with empty name", () => {
childGroupsTab
.createGroup(" ", false)
.assertNotificationCouldNotCreateGroupWithEmptyName();
});
// https://github.com/keycloak/keycloak-admin-ui/issues/2726
it.skip("Fail to create group with duplicated name", () => {
childGroupsTab
.createGroup(predefinedGroups[2], false)
.assertNotificationCouldNotCreateGroupWithDuplicatedName(
predefinedGroups[2]
);
});
it("Move group from item bar", () => {
childGroupsTab
.moveGroupItemAction(predefinedGroups[1], [
predefinedGroups[0],
predefinedGroups[2],
])
.goToGroupChildGroupsTab(predefinedGroups[2])
.assertGroupItemExist(predefinedGroups[1], true);
});
it("Search group", () => {
childGroupsTab
.searchGroup(predefinedGroups[2])
.assertGroupItemExist(predefinedGroups[2], true);
});
it("Show child group in groups", () => {
childGroupsTab
.goToGroupChildGroupsTab(predefinedGroups[2])
.goToGroupChildGroupsTab(predefinedGroups[1])
.assertNoGroupsInThisSubGroupEmptyStateMessageExist(true);
});
it("Delete group from search bar", () => {
childGroupsTab
.goToGroupChildGroupsTab(predefinedGroups[2])
.selectGroupItemCheckbox([predefinedGroups[1]])
.deleteSelectedGroups()
.assertNotificationGroupDeleted();
});
it("Delete group from item bar", () => {
childGroupsTab
.deleteGroupItem(predefinedGroups[2])
.assertNotificationGroupDeleted()
.assertNoGroupsInThisSubGroupEmptyStateMessageExist(true);
});
});
describe("Members", () => {
before(async () => {
const createdGroups = await adminClient.createSubGroups(predefinedGroups);
await Promise.all([
range(5).map((index) => {
adminClient.addUserToGroup(
users[index].id!,
createdGroups[index % 3].id
);
}),
adminClient.createGroup(emptyGroup),
]);
});
beforeEach(() => {
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
childGroupsTab.goToMembersTab();
});
it("Add member from search bar", () => {
membersTab
.addMember(["new"], false)
.assertNotificationUserAddedToTheGroup(1);
});
it("Show members with sub-group users", () => {
membersTab
.assertUserItemExist(users[0].username, true)
.assertUserItemExist("new", true)
.assertUserItemExist(users[3].username, true)
.clickCheckboxIncludeSubGroupUsers()
.assertUserItemExist("new", true)
.assertUserItemExist(users[0].username, true)
.assertUserItemExist(users[1].username, true)
.assertUserItemExist(users[2].username, true)
.assertUserItemExist(users[3].username, true)
.assertUserItemExist(users[4].username, true)
.goToChildGroupsTab()
.goToGroupChildGroupsTab(predefinedGroups[1])
.goToMembersTab()
.assertUserItemExist(users[1].username, true)
.assertUserItemExist(users[4].username, true)
.goToChildGroupsTab()
.goToGroupChildGroupsTab(predefinedGroups[2])
.goToMembersTab()
.assertUserItemExist(users[2].username, true);
});
it("Add member from empty state", () => {
sidebarPage.goToGroups();
groupPage.goToGroupChildGroupsTab(emptyGroup);
childGroupsTab.goToMembersTab();
membersTab
.addMember([users[0].username, users[1].username], true)
.assertNotificationUserAddedToTheGroup(2);
});
it("Leave group from search bar", () => {
sidebarPage.goToGroups();
groupPage.goToGroupChildGroupsTab(emptyGroup);
childGroupsTab.goToMembersTab();
membersTab
.selectUserItemCheckbox([users[0].username])
.leaveGroupSelectedUsers()
.assertNotificationUserLeftTheGroup(1)
.assertUserItemExist(users[0].username, false);
});
it("Leave group from item bar", () => {
sidebarPage.goToGroups();
groupPage.goToGroupChildGroupsTab(emptyGroup);
childGroupsTab.goToMembersTab();
membersTab
.leaveGroupUserItem(users[1].username)
.assertNotificationUserLeftTheGroup(1)
.assertNoUsersFoundEmptyStateMessageExist(true);
});
});
describe("Breadcrumbs", () => {
it("Navigate to parent group", () => {
groupPage
.goToGroupChildGroupsTab(predefinedGroups[0])
.goToGroupChildGroupsTab(predefinedGroups[1])
.goToGroupChildGroupsTab(predefinedGroups[2])
.goToGroupChildGroupsTab(predefinedGroups[3]);
cy.reload();
groupPage.clickBreadcrumbItem(predefinedGroups[2]);
groupDetailPage.assertHeaderGroupNameEqual(predefinedGroups[2]);
groupPage.clickBreadcrumbItem(predefinedGroups[1]);
groupDetailPage.assertHeaderGroupNameEqual(predefinedGroups[1]);
groupPage.clickBreadcrumbItem(predefinedGroups[0]);
groupDetailPage.assertHeaderGroupNameEqual(predefinedGroups[0]);
groupPage
.clickBreadcrumbItem("Groups")
.assertGroupItemExist(predefinedGroups[0], true);
});
});
describe("Attributes", () => {
beforeEach(() => {
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
groupDetailPage.goToAttributesTab();
});
it("Add attribute", () => {
attributesTab.addAttribute("key", "value").save();
groupPage.assertNotificationGroupUpdated();
});
it("Remove attribute", () => {
attributesTab.deleteAttribute(1).assertRowItemsEqualTo(1);
groupPage.assertNotificationGroupUpdated();
});
it("Revert changes", () => {
attributesTab
.addAttribute("key", "value")
.addAnAttributeButton()
.revert()
.assertRowItemsEqualTo(1);
});
});
describe("'Move to' function", () => {
it("Move group to other group", () => {
groupPage
.moveGroupItemAction(predefinedGroups[0], [emptyGroup])
.goToGroupChildGroupsTab(emptyGroup)
.assertGroupItemExist(predefinedGroups[0], true);
});
it("Move group to root", () => {
groupPage
.goToGroupChildGroupsTab(emptyGroup)
.moveGroupItemAction(predefinedGroups[0], ["root"]);
sidebarPage.goToGroups();
groupPage.assertGroupItemExist(predefinedGroups[0], true);
});
});
describe("Role mappings", () => {
const roleMappingTab = new RoleMappingTab("group");
beforeEach(() => {
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
groupDetailPage.goToRoleMappingTab();
});
it("Check empty state", () => {
commonPage.emptyState().checkIfExists(true);
});
it("Assign roles from empty state", () => {
roleMappingTab.assignRole();
groupDetailPage.createRoleMapping();
roleMappingTab.assign();
});
it("Show and search roles", () => {
groupDetailPage.checkDefaultRole();
});
it("Check hide inherited roles option", () => {
roleMappingTab.unhideInheritedRoles();
roleMappingTab.hideInheritedRoles();
});
it("Remove roles", () => {
roleMappingTab.selectRow("default-roles");
roleMappingTab.unAssign();
groupDetailPage.deleteRole();
});
});
describe("Permissions", () => {
beforeEach(() => {
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
groupDetailPage.goToPermissionsTab();
});
it("enable/disable permissions", () => {
groupDetailPage.enablePermissionSwitch();
});
});
});

View File

@ -0,0 +1,148 @@
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import CreateProviderPage from "../support/pages/admin-ui/manage/identity_providers/CreateProviderPage";
import ModalUtils from "../support/util/ModalUtils";
import AddMapperPage from "../support/pages/admin-ui/manage/identity_providers/AddMapperPage";
import ProviderBaseGeneralSettingsPage from "../support/pages/admin-ui/manage/identity_providers/ProviderBaseGeneralSettingsPage";
import ProviderBaseAdvancedSettingsPage, {
ClientAssertionSigningAlg,
ClientAuthentication,
PromptSelect,
} from "../support/pages/admin-ui/manage/identity_providers/ProviderBaseAdvancedSettingsPage";
describe("OIDC identity provider test", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const listingPage = new ListingPage();
const createProviderPage = new CreateProviderPage();
const addMapperPage = new AddMapperPage();
const createSuccessMsg = "Identity provider successfully created";
const createMapperSuccessMsg = "Mapper created successfully.";
const deletePrompt = "Delete provider?";
const deleteSuccessMsg = "Provider successfully deleted.";
const keycloakServer = Cypress.env("KEYCLOAK_SERVER");
const discoveryUrl = `${keycloakServer}/realms/master/.well-known/openid-configuration`;
const authorizationUrl = `${keycloakServer}/realms/master/protocol/openid-connect/auth`;
describe("OIDC Identity provider creation", () => {
const oidcProviderName = "oidc";
const secret = "123";
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToIdentityProviders();
});
it("should create an OIDC provider using discovery url", () => {
createProviderPage
.checkVisible(oidcProviderName)
.clickCard(oidcProviderName);
createProviderPage.checkAddButtonDisabled();
createProviderPage
.fillDiscoveryUrl(discoveryUrl)
.shouldBeSuccessful()
.fillDisplayName(oidcProviderName)
.fill(oidcProviderName, secret)
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
createProviderPage.shouldHaveAuthorizationUrl(authorizationUrl);
});
it("should test all settings", () => {
const providerBaseGeneralSettingsPage =
new ProviderBaseGeneralSettingsPage();
const providerBaseAdvancedSettingsPage =
new ProviderBaseAdvancedSettingsPage();
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(oidcProviderName);
//general settings
cy.findByTestId("displayName").click().type("OIDC");
cy.findByTestId("jump-link-general-settings").click();
providerBaseGeneralSettingsPage.typeDisplayOrder("1");
createProviderPage.clickSave();
masthead.checkNotificationMessage("Provider successfully updated", true);
//OIDC Settings and save/revert buttons
providerBaseAdvancedSettingsPage.assertOIDCUrl("authorization");
providerBaseAdvancedSettingsPage.assertOIDCUrl("token");
//OIDC Switches
providerBaseAdvancedSettingsPage.assertOIDCSignatureSwitch();
providerBaseAdvancedSettingsPage.assertOIDCPKCESwitch();
//Client Authentication
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.basicAuth
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.jwt
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.jwtPrivKey
);
providerBaseAdvancedSettingsPage.assertOIDCClientAuthentication(
ClientAuthentication.post
);
//Client assertion signature algorithm
Object.entries(ClientAssertionSigningAlg).forEach(([, value]) => {
providerBaseAdvancedSettingsPage.assertOIDCClientAuthSignAlg(value);
});
//OIDC Advanced Settings
providerBaseAdvancedSettingsPage.assertOIDCSettingsAdvancedSwitches();
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.none);
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.consent);
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.login);
providerBaseAdvancedSettingsPage.selectPromptOption(PromptSelect.select);
providerBaseAdvancedSettingsPage.selectPromptOption(
PromptSelect.unspecified
);
//Advanced Settings
providerBaseAdvancedSettingsPage.assertAdvancedSettings();
});
it("should add OIDC mapper of type Attribute Importer", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(oidcProviderName);
addMapperPage.goToMappersTab();
addMapperPage.emptyStateAddMapper();
addMapperPage.addOIDCAttrImporterMapper("OIDC Attribute Importer");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add OIDC mapper of type Claim To Role", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(oidcProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addOIDCClaimToRoleMapper("OIDC Claim to Role");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should cancel the addition of the OIDC mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(oidcProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.cancelNewMapper();
addMapperPage.shouldGoToMappersTab();
});
it("clean up providers", () => {
const modalUtils = new ModalUtils();
sidebarPage.goToIdentityProviders();
listingPage.itemExist(oidcProviderName).deleteItem(oidcProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg, true);
});
});
});

View File

@ -0,0 +1,189 @@
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import CreateProviderPage from "../support/pages/admin-ui/manage/identity_providers/CreateProviderPage";
import ModalUtils from "../support/util/ModalUtils";
import AddMapperPage from "../support/pages/admin-ui/manage/identity_providers/AddMapperPage";
import ProviderSAMLSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderSAMLSettings";
describe("SAML identity provider test", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const listingPage = new ListingPage();
const createProviderPage = new CreateProviderPage();
const addMapperPage = new AddMapperPage();
const createSuccessMsg = "Identity provider successfully created";
const saveSuccessMsg = "Provider successfully updated";
const createMapperSuccessMsg = "Mapper created successfully.";
const saveMapperSuccessMsg = "Mapper saved successfully.";
const deletePrompt = "Delete provider?";
const deleteSuccessMsg = "Provider successfully deleted.";
const classRefName = "acClassRef-1";
const declRefName = "acDeclRef-1";
const keycloakServer = Cypress.env("KEYCLOAK_SERVER");
const samlDiscoveryUrl = `${keycloakServer}/realms/master/protocol/saml/descriptor`;
const samlDisplayName = "saml";
describe("SAML identity provider creation", () => {
const samlProviderName = "saml";
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToIdentityProviders();
});
it("should create a SAML provider using entity descriptor", () => {
createProviderPage
.checkVisible(samlProviderName)
.clickCard(samlProviderName);
createProviderPage.checkAddButtonDisabled();
createProviderPage
.fillDisplayName(samlDisplayName)
.fillDiscoveryUrl(samlDiscoveryUrl)
.shouldBeSuccessful()
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
});
it("should add auth constraints to existing SAML provider", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
createProviderPage
.fillAuthnContextClassRefs(classRefName)
.clickClassRefsAdd()
.fillAuthnContextDeclRefs(declRefName)
.clickDeclRefsAdd()
.clickSave();
masthead.checkNotificationMessage(saveSuccessMsg, true);
});
it("should add SAML mapper of type Advanced Attribute to Role", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.emptyStateAddMapper();
addMapperPage.addAdvancedAttrToRoleMapper("SAML mapper");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add SAML mapper of type Username Template Importer", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addUsernameTemplateImporterMapper(
"SAML Username Template Importer Mapper"
);
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add SAML mapper of type Hardcoded User Session Attribute", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addHardcodedUserSessionAttrMapper(
"Hardcoded User Session Attribute"
);
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add SAML mapper of type Attribute Importer", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addSAMLAttrImporterMapper("Attribute Importer");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add SAML mapper of type Hardcoded Role", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addHardcodedRoleMapper("Hardcoded Role");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add SAML mapper of type Hardcoded Attribute", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addHardcodedAttrMapper("Hardcoded Attribute");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add SAML mapper of type SAML Attribute To Role", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.addSAMLAttributeToRoleMapper("SAML Attribute To Role");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should edit Username Template Importer mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("SAML Username Template Importer Mapper");
addMapperPage.editUsernameTemplateImporterMapper();
masthead.checkNotificationMessage(saveMapperSuccessMsg, true);
});
it("should edit SAML mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("SAML mapper");
addMapperPage.editSAMLorOIDCMapper();
masthead.checkNotificationMessage(saveMapperSuccessMsg, true);
});
it("should edit SAML settings", () => {
const providerSAMLSettings = new ProviderSAMLSettings();
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails(samlProviderName);
providerSAMLSettings.disableProviderSwitch();
sidebarPage.goToIdentityProviders();
cy.findByText("Disabled");
listingPage.goToItemDetails(samlProviderName);
providerSAMLSettings.enableProviderSwitch();
cy.get(".pf-c-jump-links__list").contains("SAML settings").click();
providerSAMLSettings.assertIdAndURLFields();
providerSAMLSettings.assertNameIdPolicyFormat();
providerSAMLSettings.assertPrincipalType();
providerSAMLSettings.assertSAMLSwitches();
providerSAMLSettings.assertSignatureAlgorithm();
providerSAMLSettings.assertValidateSignatures();
providerSAMLSettings.assertTextFields();
cy.get(".pf-c-jump-links__list")
.contains("Requested AuthnContext Constraints")
.click();
providerSAMLSettings.assertAuthnContext();
});
it("clean up providers", () => {
const modalUtils = new ModalUtils();
sidebarPage.goToIdentityProviders();
listingPage.itemExist(samlProviderName).deleteItem(samlProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg, true);
});
});
});

View File

@ -0,0 +1,378 @@
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import CreateProviderPage from "../support/pages/admin-ui/manage/identity_providers/CreateProviderPage";
import ModalUtils from "../support/util/ModalUtils";
import OrderDialog from "../support/pages/admin-ui/manage/identity_providers/OrderDialog";
import AddMapperPage from "../support/pages/admin-ui/manage/identity_providers/AddMapperPage";
import ProviderFacebookGeneralSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderFacebookGeneralSettings";
import ProviderBaseGeneralSettingsPage from "../support/pages/admin-ui/manage/identity_providers/ProviderBaseGeneralSettingsPage";
import ProviderBaseAdvancedSettingsPage from "../support/pages/admin-ui/manage/identity_providers/ProviderBaseAdvancedSettingsPage";
import ProviderGithubGeneralSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderGithubGeneralSettings";
import ProviderGoogleGeneralSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderGoogleGeneralSettings";
import ProviderOpenshiftGeneralSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderOpenshiftGeneralSettings";
import ProviderPaypalGeneralSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderPaypalGeneralSettings";
import ProviderStackoverflowGeneralSettings from "../support/pages/admin-ui/manage/identity_providers/social/ProviderStackoverflowGeneralSettings";
import adminClient from "../support/util/AdminClient";
import GroupPage from "../support/pages/admin-ui/manage/groups/GroupPage";
import CommonPage from "../support/pages/CommonPage";
describe("Identity provider test", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const listingPage = new ListingPage();
const createProviderPage = new CreateProviderPage();
const addMapperPage = new AddMapperPage();
const groupPage = new GroupPage();
const commonPage = new CommonPage();
const createSuccessMsg = "Identity provider successfully created";
const createFailMsg =
"Could not create the identity provider: Identity Provider github already exists";
const createMapperSuccessMsg = "Mapper created successfully.";
const changeSuccessMsg =
"Successfully changed display order of identity providers";
const deletePrompt = "Delete provider?";
const deleteSuccessMsg = "Provider successfully deleted.";
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToIdentityProviders();
});
const socialLoginIdentityProvidersWithCustomFiels = {
Facebook: new ProviderFacebookGeneralSettings(),
Github: new ProviderGithubGeneralSettings(),
Google: new ProviderGoogleGeneralSettings(),
"Openshift-v3": new ProviderOpenshiftGeneralSettings(),
"Openshift-v4": new ProviderOpenshiftGeneralSettings(),
Paypal: new ProviderPaypalGeneralSettings(),
Stackoverflow: new ProviderStackoverflowGeneralSettings(),
};
function getSocialIdpClassInstance(idpTestName: string) {
let instance = new ProviderBaseGeneralSettingsPage();
Object.entries(socialLoginIdentityProvidersWithCustomFiels).find(
([key, value]) => {
if (key === idpTestName) {
instance = value;
return true;
}
return false;
}
);
return instance;
}
describe("Identity provider creation", () => {
const identityProviderName = "github";
describe("Custom fields tests", () => {
const socialLoginIdentityProviders = [
{ testName: "Bitbucket", displayName: "BitBucket", alias: "bitbucket" },
{ testName: "Facebook", displayName: "Facebook", alias: "facebook" },
{ testName: "Github", displayName: "GitHub", alias: "github" },
{ testName: "Gitlab", displayName: "Gitlab", alias: "gitlab" },
{ testName: "Google", displayName: "Google", alias: "google" },
{ testName: "Instagram", displayName: "Instagram", alias: "instagram" },
{ testName: "Linkedin", displayName: "LinkedIn", alias: "linkedin" },
{ testName: "Microsoft", displayName: "Microsoft", alias: "microsoft" },
{
testName: "Openshift-v3",
displayName: "Openshift v3",
alias: "openshift-v3",
},
{
testName: "Openshift-v4",
displayName: "Openshift v4",
alias: "openshift-v4",
},
{ testName: "Paypal", displayName: "PayPal", alias: "paypal" },
{
testName: "Stackoverflow",
displayName: "StackOverflow",
alias: "stackoverflow",
},
{ testName: "Twitter", displayName: "Twitter", alias: "twitter" },
];
after(async () => {
await Promise.all(
socialLoginIdentityProviders.map((idp) =>
adminClient.deleteIdentityProvider(idp.alias)
)
);
});
socialLoginIdentityProviders.forEach(($idp, linkedIdpsCount) => {
it(`should create social login provider ${$idp.testName} with custom fields`, () => {
if (linkedIdpsCount == 0) {
createProviderPage.clickCard($idp.alias);
} else {
createProviderPage.clickCreateDropdown().clickItem($idp.alias);
}
const instance = getSocialIdpClassInstance($idp.testName);
instance
.typeDisplayOrder("0")
.clickAdd()
.assertRequiredFieldsErrorsExist()
.fillData($idp.testName)
.clickAdd()
.assertNotificationIdpCreated()
.assertFilledDataEqual($idp.testName);
});
});
});
it("should create github provider", () => {
createProviderPage.checkGitHubCardVisible().clickGitHubCard();
createProviderPage.checkAddButtonDisabled();
createProviderPage
.fill(identityProviderName)
.clickAdd()
.checkClientIdRequiredMessage(true);
createProviderPage.fill(identityProviderName, "123").clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName);
});
it("fail to make duplicate github provider", () => {
createProviderPage
.clickCreateDropdown()
.clickItem("github")
.fill("github2", "123")
.clickAdd();
masthead.checkNotificationMessage(createFailMsg, true);
});
it("should create facebook provider", () => {
createProviderPage
.clickCreateDropdown()
.clickItem("facebook")
.fill("facebook", "123")
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
});
it("search for existing provider by name", () => {
sidebarPage.goToIdentityProviders();
listingPage.searchItem(identityProviderName, false);
listingPage.itemExist(identityProviderName, true);
});
it("search for non-existing provider by name", () => {
sidebarPage.goToIdentityProviders();
listingPage.searchItem("not-existing-provider", false);
groupPage.assertNoSearchResultsMessageExist(true);
});
it("create and delete provider by item details", () => {
createProviderPage
.clickCreateDropdown()
.clickItem("linkedin")
.fill("linkedin", "123")
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
commonPage
.actionToolbarUtils()
.clickActionToggleButton()
.clickDropdownItem("Delete");
const modalUtils = new ModalUtils();
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg, true);
});
it.skip("should change order of providers", () => {
const orderDialog = new OrderDialog();
const providers = [identityProviderName, "facebook", "bitbucket"];
sidebarPage.goToIdentityProviders();
listingPage.itemExist("facebook");
sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName);
createProviderPage
.clickCreateDropdown()
.clickItem("bitbucket")
.fill("bitbucket", "123")
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
cy.wait(2000);
sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName);
orderDialog.openDialog().checkOrder(providers);
orderDialog.moveRowTo("facebook", identityProviderName);
orderDialog.checkOrder(["bitbucket", identityProviderName, "facebook"]);
orderDialog.clickSave();
masthead.checkNotificationMessage(changeSuccessMsg);
});
it("should delete provider", () => {
const modalUtils = new ModalUtils();
listingPage.deleteItem(identityProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg, true);
});
it("should add facebook social mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("facebook");
addMapperPage.goToMappersTab();
addMapperPage.emptyStateAddMapper();
addMapperPage.fillSocialMapper("facebook mapper");
// addMapperPage.saveNewMapper();
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should add Social mapper of type Attribute Importer", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("facebook");
addMapperPage.goToMappersTab();
addMapperPage.addMapper();
addMapperPage.fillSocialMapper("facebook attribute importer");
masthead.checkNotificationMessage(createMapperSuccessMsg, true);
});
it("should edit facebook mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("facebook");
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("facebook attribute importer");
addMapperPage.editSocialMapper();
});
it("should delete facebook mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("facebook");
addMapperPage.goToMappersTab();
listingPage.deleteItem("facebook attribute importer");
cy.findByTestId("confirm").click();
});
it("clean up providers", () => {
const modalUtils = new ModalUtils();
// TODO: Re-enable this code when the 'should change order of providers' is no longer skipped.
// sidebarPage.goToIdentityProviders();
// listingPage.itemExist("bitbucket").deleteItem("bitbucket");
// modalUtils.checkModalTitle(deletePrompt).confirmModal();
// masthead.checkNotificationMessage(deleteSuccessMsg, true);
sidebarPage.goToIdentityProviders();
listingPage.itemExist("facebook").deleteItem("facebook");
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg, true);
});
});
describe("should check provider details", () => {
const identityProviderName = "github";
const githubSettings = new ProviderGithubGeneralSettings();
const advancedSettings = new ProviderBaseAdvancedSettingsPage();
it("creating github provider", () => {
createProviderPage.checkGitHubCardVisible().clickGitHubCard();
createProviderPage.checkAddButtonDisabled();
createProviderPage
.fill(identityProviderName)
.clickAdd()
.checkClientIdRequiredMessage(true);
createProviderPage.fill(identityProviderName, "123").clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName);
});
it("should check general settings", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("github");
githubSettings.fillData("github");
cy.findByTestId("save").click();
});
it("should check input switches and inputs", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("github");
advancedSettings.typeScopesInput("openid");
//advancedSettings.assertScopesInputEqual("openid"); //this line doesn't work
advancedSettings.assertStoreTokensSwitchTurnedOn(false);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
false
);
advancedSettings.assertDisableUserInfoSwitchTurnedOn(false);
advancedSettings.assertTrustEmailSwitchTurnedOn(false);
advancedSettings.assertAccountLinkingOnlySwitchTurnedOn(false);
advancedSettings.assertHideOnLoginPageSwitchTurnedOn(false);
advancedSettings.clickStoreTokensSwitch();
advancedSettings.clickAcceptsPromptNoneForwardFromClientSwitch();
advancedSettings.clickDisableUserInfoSwitch();
advancedSettings.clickTrustEmailSwitch();
advancedSettings.clickAccountLinkingOnlySwitch();
advancedSettings.clickHideOnLoginPageSwitch();
advancedSettings.assertStoreTokensSwitchTurnedOn(true);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
true
);
advancedSettings.assertDisableUserInfoSwitchTurnedOn(true);
advancedSettings.assertTrustEmailSwitchTurnedOn(true);
advancedSettings.assertAccountLinkingOnlySwitchTurnedOn(true);
advancedSettings.assertHideOnLoginPageSwitchTurnedOn(true);
cy.findByTestId("save").click();
});
it("should revert and save options", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("github");
cy.findByTestId("jump-link-advanced-settings").click();
advancedSettings.assertStoreTokensSwitchTurnedOn(true);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
true
);
advancedSettings.clickStoreTokensSwitch();
advancedSettings.clickAcceptsPromptNoneForwardFromClientSwitch();
advancedSettings.assertStoreTokensSwitchTurnedOn(false);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
false
);
cy.findByTestId("revert").click();
advancedSettings.assertStoreTokensSwitchTurnedOn(true);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
true
);
});
it("should delete providers", () => {
const modalUtils = new ModalUtils();
sidebarPage.goToIdentityProviders();
listingPage.itemExist("github").deleteItem("github");
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg, true);
});
});
});

View File

@ -0,0 +1,73 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import { keycloakBefore } from "../support/util/keycloak_hooks";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const helpLabel = ".pf-c-form__group-label-help";
describe("Masthead tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
});
describe("Desktop view", () => {
it("Go to account console and back to admin console", () => {
sidebarPage.waitForPageLoad();
masthead.accountManagement();
cy.get("h1").contains("Welcome to Keycloak account management");
masthead.goToAdminConsole();
masthead.checkIsAdminUI();
});
it("Sign out reachs to log in screen", () => {
sidebarPage.waitForPageLoad();
masthead.signOut();
sidebarPage.waitForPageLoad();
loginPage.isLogInPage();
});
it("Go to realm info", () => {
sidebarPage.goToClients();
masthead.toggleUsernameDropdown().clickRealmInfo();
cy.get(".pf-c-card__title").should("contain.text", "Server info");
});
it("Should go to documentation page", () => {
masthead.clickGlobalHelp();
masthead.clickDocumentationLink();
cy.get("#header").should("contain.text", "Server Administration Guide");
});
it("Enable/disable help mode in desktop mode", () => {
masthead.assertIsDesktopView();
cy.get(helpLabel).should("exist");
masthead.toggleGlobalHelp();
masthead.clickGlobalHelp();
cy.get(helpLabel).should("not.exist");
masthead.toggleGlobalHelp();
cy.get(helpLabel).should("exist");
});
});
describe("Mobile view", () => {
it("Mobile menu is shown when in mobile view", () => {
cy.viewport("samsung-s10");
masthead.assertIsMobileView();
});
it("Enable/disable help mode in mobile view", () => {
cy.viewport("samsung-s10");
masthead
.assertIsMobileView()
.toggleUsernameDropdown()
.toggleMobileViewHelp();
cy.get(helpLabel).should("not.exist");
masthead.toggleMobileViewHelp();
cy.get(helpLabel).should("exist");
});
});
});

View File

@ -0,0 +1,55 @@
import PartialExportModal from "../support/pages/admin-ui/configure/realm_settings/PartialExportModal";
import RealmSettings from "../support/pages/admin-ui/configure/realm_settings/RealmSettings";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import adminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_hooks";
describe("Partial realm export", () => {
const REALM_NAME = "Partial-export-test-realm";
before(() => adminClient.createRealm(REALM_NAME));
after(() => adminClient.deleteRealm(REALM_NAME));
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const modal = new PartialExportModal();
const realmSettings = new RealmSettings();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(REALM_NAME).goToRealmSettings();
realmSettings.clickActionMenu();
modal.open();
});
it("Closes the dialog", () => {
modal.cancelButton().click();
modal.exportButton().should("not.exist");
});
it("Shows a warning message", () => {
modal.warningMessage().should("not.exist");
modal.includeGroupsAndRolesSwitch().click({ force: true });
modal.warningMessage().should("exist");
modal.includeGroupsAndRolesSwitch().click({ force: true });
modal.warningMessage().should("not.exist");
modal.includeClientsSwitch().click({ force: true });
modal.warningMessage().should("exist");
modal.includeClientsSwitch().click({ force: true });
modal.warningMessage().should("not.exist");
});
it("Exports the realm", () => {
modal.includeGroupsAndRolesSwitch().click({ force: true });
modal.includeGroupsAndRolesSwitch().click({ force: true });
modal.exportButton().click();
cy.readFile(
Cypress.config("downloadsFolder") + "/realm-export.json"
).should("exist");
modal.exportButton().should("not.exist");
});
});

View File

@ -0,0 +1,129 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import PartialImportModal from "../support/pages/admin-ui/configure/realm_settings/PartialImportModal";
import RealmSettings from "../support/pages/admin-ui/configure/realm_settings/RealmSettings";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
describe("Partial import test", () => {
const TEST_REALM = "Partial-import-test-realm";
const TEST_REALM_2 = "Partial-import-test-realm-2";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const modal = new PartialImportModal();
const realmSettings = new RealmSettings();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(TEST_REALM);
sidebarPage.goToRealmSettings();
realmSettings.clickActionMenu();
});
before(() =>
Promise.all([
adminClient.createRealm(TEST_REALM),
adminClient.createRealm(TEST_REALM_2),
])
);
after(async () => {
await Promise.all([
adminClient.deleteRealm(TEST_REALM),
adminClient.deleteRealm(TEST_REALM_2),
]);
});
it("Opens and closes partial import dialog", () => {
modal.open();
modal.importButton().should("be.disabled");
modal.cancelButton().click();
modal.importButton().should("not.exist");
});
it("Import button only enabled if JSON has something to import", () => {
modal.open();
modal.textArea().type("{}");
modal.importButton().should("be.disabled");
modal.cancelButton().click();
});
it("Displays user options after multi-realm import", () => {
modal.open();
modal.typeResourceFile("multi-realm.json");
// Import button should be disabled if no checkboxes selected
modal.importButton().should("be.disabled");
modal.usersCheckbox().click();
modal.importButton().should("be.enabled");
modal.groupsCheckbox().click();
modal.importButton().should("be.enabled");
modal.groupsCheckbox().click();
modal.usersCheckbox().click();
modal.importButton().should("be.disabled");
// verify resource counts
modal.userCount().contains("1 Users");
modal.groupCount().contains("1 Groups");
modal.clientCount().contains("1 Clients");
modal.idpCount().contains("1 Identity providers");
modal.realmRolesCount().contains("2 Realm roles");
modal.clientRolesCount().contains("1 Client roles");
// import button should disable when switching realms
modal.usersCheckbox().click();
modal.importButton().should("be.enabled");
modal.selectRealm("realm2");
modal.importButton().should("be.disabled");
modal.clientCount().contains("2 Clients");
modal.clientsCheckbox().click();
modal.importButton().click();
cy.contains("2 records added");
cy.contains("customer-portal");
cy.contains("customer-portal2");
modal.closeButton().click();
});
it("Displays user options after realmless import and does the import", () => {
sidebarPage.goToRealm(TEST_REALM_2);
sidebarPage.goToRealmSettings();
realmSettings.clickActionMenu();
modal.open();
modal.typeResourceFile("client-only.json");
modal.realmSelector().should("not.exist");
modal.clientCount().contains("1 Clients");
modal.usersCheckbox().should("not.exist");
modal.groupsCheckbox().should("not.exist");
modal.idpCheckbox().should("not.exist");
modal.realmRolesCheckbox().should("not.exist");
modal.clientRolesCheckbox().should("not.exist");
modal.clientsCheckbox().click();
modal.importButton().click();
cy.contains("One record added");
cy.contains("customer-portal");
modal.closeButton().click();
});
it("Should clear the input with the button", () => {
modal.open();
//clear button should be disabled if there is nothing in the dialog
modal.clearButton().should("be.disabled");
modal.textArea().type("{}");
modal.textArea().get(".view-lines").should("have.text", "{}");
modal.clearButton().should("not.be.disabled");
modal.clearButton().click();
modal.clickClearConfirmButton();
modal.textArea().get(".view-lines").should("have.text", "");
});
});

View File

@ -0,0 +1,275 @@
import LoginPage from "../support/pages/LoginPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ModalUtils from "../support/util/ModalUtils";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import createRealmRolePage from "../support/pages/admin-ui/manage/realm_roles/CreateRealmRolePage";
import AssociatedRolesPage from "../support/pages/admin-ui/manage/realm_roles/AssociatedRolesPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
import ClientRolesTab from "../support/pages/admin-ui/manage/clients/ClientRolesTab";
import KeyValueInput from "../support/pages/admin-ui/manage/KeyValueInput";
let itemId = "realm_role_crud";
const loginPage = new LoginPage();
const masthead = new Masthead();
const modalUtils = new ModalUtils();
const sidebarPage = new SidebarPage();
const listingPage = new ListingPage();
const associatedRolesPage = new AssociatedRolesPage();
const rolesTab = new ClientRolesTab();
describe("Realm roles test", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealmRoles();
});
it("should fail creating realm role", () => {
listingPage.goToCreateItem();
createRealmRolePage.save().checkRealmRoleNameRequiredMessage();
createRealmRolePage.fillRealmRoleData("admin").save();
// The error should inform about duplicated name/id (THIS MESSAGE DOES NOT HAVE QUOTES AS THE OTHERS)
masthead.checkNotificationMessage(
"Could not create role: Role with name admin already exists",
true
);
});
it("shouldn't create a realm role based with only whitespace name", () => {
listingPage.goToCreateItem();
createRealmRolePage
.fillRealmRoleData(" ")
.checkRealmRoleNameRequiredMessage();
});
it("Realm role CRUD test", () => {
itemId += "_" + crypto.randomUUID();
// Create
listingPage.itemExist(itemId, false).goToCreateItem();
createRealmRolePage.fillRealmRoleData(itemId).save();
masthead.checkNotificationMessage("Role created", true);
sidebarPage.goToRealmRoles();
const fetchUrl = "/admin/realms/master/roles?first=0&max=11";
cy.intercept(fetchUrl).as("fetch");
listingPage.deleteItem(itemId);
cy.wait(["@fetch"]);
modalUtils.checkModalTitle("Delete role?").confirmModal();
masthead.checkNotificationMessage("The role has been deleted", true);
listingPage.itemExist(itemId, false);
itemId = "realm_role_crud";
});
it("should delete role from details action", () => {
itemId += "_" + crypto.randomUUID();
listingPage.goToCreateItem();
createRealmRolePage.fillRealmRoleData(itemId).save();
masthead.checkNotificationMessage("Role created", true);
createRealmRolePage.clickActionMenu("Delete this role");
modalUtils.confirmModal();
masthead.checkNotificationMessage("The role has been deleted", true);
itemId = "realm_role_crud";
});
it("should not be able to delete default role", () => {
const defaultRole = "default-roles-master";
listingPage.itemExist(defaultRole).deleteItem(defaultRole);
masthead.checkNotificationMessage(
"You cannot delete a default role.",
true
);
});
it("Add associated roles test", () => {
itemId += "_" + crypto.randomUUID();
// Create
listingPage.itemExist(itemId, false).goToCreateItem();
createRealmRolePage.fillRealmRoleData(itemId).save();
masthead.checkNotificationMessage("Role created", true);
// Add associated realm role from action dropdown
associatedRolesPage.addAssociatedRealmRole("create-realm");
masthead.checkNotificationMessage("Associated roles have been added", true);
// Add associated realm role from search bar
associatedRolesPage.addAssociatedRoleFromSearchBar("offline_access");
masthead.checkNotificationMessage("Associated roles have been added", true);
rolesTab.goToAssociatedRolesTab();
// Add associated client role from search bar
associatedRolesPage.addAssociatedRoleFromSearchBar("manage-account", true);
masthead.checkNotificationMessage("Associated roles have been added", true);
rolesTab.goToAssociatedRolesTab();
// Add associated client role
associatedRolesPage.addAssociatedRoleFromSearchBar("manage-consent", true);
masthead.checkNotificationMessage("Associated roles have been added", true);
rolesTab.goToAssociatedRolesTab();
// Add associated client role
associatedRolesPage.addAssociatedRoleFromSearchBar(
"manage-account-links",
true
);
masthead.checkNotificationMessage("Associated roles have been added", true);
});
it("Should search existing associated role by name", () => {
listingPage.searchItem("create-realm", false).itemExist("create-realm");
});
it("Should search non-existent associated role by name", () => {
const itemName = "non-existent-associated-role";
listingPage.searchItem(itemName, false);
cy.findByTestId(listingPage.emptyState).should("exist");
});
it("Should hide inherited roles test", () => {
listingPage.searchItem(itemId, false).goToItemDetails(itemId);
rolesTab.goToAssociatedRolesTab();
rolesTab.hideInheritedRoles();
});
it("Should fail to remove role when all unchecked from search bar", () => {
listingPage.searchItem(itemId, false).goToItemDetails(itemId);
rolesTab.goToAssociatedRolesTab();
associatedRolesPage.isRemoveAssociatedRolesBtnDisabled();
});
it("Should delete single non-inherited role item", () => {
listingPage.searchItem(itemId, false).goToItemDetails(itemId);
rolesTab.goToAssociatedRolesTab();
listingPage.removeItem("create-realm");
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove role?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Scope mapping successfully removed",
true
);
});
it("Should delete all roles from search bar", () => {
listingPage.searchItem(itemId, false).goToItemDetails(itemId);
sidebarPage.waitForPageLoad();
rolesTab.goToAssociatedRolesTab();
cy.get('input[name="check-all"]').check();
associatedRolesPage.removeAssociatedRoles();
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove role?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Scope mapping successfully removed",
true
);
});
it("Should delete associated roles from list test", () => {
itemId = "realm_role_crud";
itemId += "_" + crypto.randomUUID();
// Create
listingPage.itemExist(itemId, false).goToCreateItem();
createRealmRolePage.fillRealmRoleData(itemId).save();
masthead.checkNotificationMessage("Role created", true);
// Add associated realm role from action dropdown
associatedRolesPage.addAssociatedRealmRole("create-realm");
masthead.checkNotificationMessage("Associated roles have been added", true);
// Add associated realm role from search bar
associatedRolesPage.addAssociatedRoleFromSearchBar("offline_access");
masthead.checkNotificationMessage("Associated roles have been added", true);
rolesTab.goToAssociatedRolesTab();
// delete associated roles from list
listingPage.removeItem("create-realm");
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove role?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Scope mapping successfully removed",
true
);
listingPage.removeItem("offline_access");
sidebarPage.waitForPageLoad();
modalUtils.checkModalTitle("Remove role?").confirmModal();
sidebarPage.waitForPageLoad();
masthead.checkNotificationMessage(
"Scope mapping successfully removed",
true
);
});
describe("edit role details", () => {
const editRoleName = "going to edit";
const description = "some description";
const updateDescription = "updated description";
before(() =>
adminClient.createRealmRole({
name: editRoleName,
description,
})
);
after(() => adminClient.deleteRealmRole(editRoleName));
it("should edit realm role details", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.checkNameDisabled().checkDescription(description);
createRealmRolePage.updateDescription(updateDescription).save();
masthead.checkNotificationMessage("The role has been saved", true);
createRealmRolePage.checkDescription(updateDescription);
});
const keyValue = new KeyValueInput("attributes");
it("should add attribute", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.goToAttributesTab();
keyValue.fillKeyValue({ key: "one", value: "1" }).validateRows(2);
keyValue.save();
masthead.checkNotificationMessage("The role has been saved", true);
keyValue.validateRows(2);
});
it("should add attribute multiple", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.goToAttributesTab();
keyValue
.fillKeyValue({ key: "two", value: "2" }, 1)
.fillKeyValue({ key: "three", value: "3" }, 2)
.save()
.validateRows(4);
});
it("should delete attribute", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.goToAttributesTab();
keyValue.deleteRow(1).save().validateRows(3);
});
});
});

View File

@ -0,0 +1,186 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
import ModalUtils from "../support/util/ModalUtils";
import Masthead from "../support/pages/admin-ui/Masthead";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const modalUtils = new ModalUtils();
const masthead = new Masthead();
describe("Realm settings client policies tab tests", () => {
const realmName = "Realm_" + crypto.randomUUID();
const realmSettingsPage = new RealmSettingsPage(realmName);
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage
.waitForPageLoad()
.goToRealm(realmName)
.goToRealmSettings()
.waitForPageLoad();
realmSettingsPage.goToClientPoliciesTab().goToClientPoliciesList();
});
before(() => adminClient.createRealm(realmName));
after(() => {
adminClient.deleteRealm(realmName);
});
it("Complete new client form and cancel", () => {
realmSettingsPage
.checkDisplayPoliciesTab()
.createNewClientPolicyFromEmptyState("Test", "Test Description", true)
.checkNewClientPolicyForm()
.cancelNewClientPolicyCreation()
.checkEmptyPolicyList();
});
it("Complete new client form and submit", () => {
const url = `/admin/realms/${realmName}/client-policies/policies`;
cy.intercept("PUT", url).as("save");
realmSettingsPage.createNewClientPolicyFromEmptyState(
"Test",
"Test Description"
);
masthead.checkNotificationMessage("New policy created");
cy.wait("@save");
});
it("Should perform client profile search by profile name", () => {
realmSettingsPage.searchClientPolicy("Test");
});
it("Should not have conditions configured by default", () => {
realmSettingsPage.shouldNotHaveConditionsConfigured();
});
it("Should cancel adding a new condition to a client profile", () => {
realmSettingsPage.shouldCancelAddingCondition();
});
it("Should add a new client-roles condition to a client profile", () => {
realmSettingsPage.shouldAddClientRolesCondition();
});
it("Should add a new client-scopes condition to a client profile", () => {
realmSettingsPage.shouldAddClientScopesCondition();
});
it("Should edit the client-roles condition of a client profile", () => {
realmSettingsPage.shouldEditClientRolesCondition();
});
it("Should edit the client-scopes condition of a client profile", () => {
realmSettingsPage.shouldEditClientScopesCondition();
});
it("Should cancel deleting condition from a client profile", () => {
realmSettingsPage.deleteClientRolesCondition();
sidebarPage.waitForPageLoad();
modalUtils
.checkModalTitle("Delete condition?")
.checkModalMessage(
"This action will permanently delete client-roles. This cannot be undone."
)
.checkConfirmButtonText("Delete")
.cancelButtonContains("Cancel")
.cancelModal();
realmSettingsPage.checkConditionsListContains("client-roles");
});
it("Should delete client-roles condition from a client profile", () => {
realmSettingsPage.deleteClientRolesCondition();
sidebarPage.waitForPageLoad();
modalUtils.confirmModal();
realmSettingsPage.checkConditionsListContains("client-scopes");
});
it("Should delete client-scopes condition from a client profile", () => {
realmSettingsPage.shouldDeleteClientScopesCondition();
});
it("Check cancelling the client policy deletion", () => {
realmSettingsPage.deleteClientPolicyItemFromTable("Test");
modalUtils
.checkModalMessage(
"This action will permanently delete the policy Test. This cannot be undone."
)
.cancelModal();
realmSettingsPage.checkElementInList("Test");
});
it("Check deleting the client policy", () => {
realmSettingsPage.deleteClientPolicyItemFromTable("Test");
modalUtils.confirmModal();
masthead.checkNotificationMessage("Client policy deleted");
realmSettingsPage.checkEmptyPolicyList();
});
it("Check navigating between Form View and JSON editor", () => {
realmSettingsPage.shouldNavigateBetweenFormAndJSONViewPolicies();
});
it("Should not create duplicate client profile", () => {
const url = `admin/realms/${realmName}/client-policies/policies`;
cy.intercept("PUT", url).as("save");
realmSettingsPage.createNewClientPolicyFromEmptyState(
"Test",
"Test Description"
);
masthead.checkNotificationMessage("New policy created");
cy.wait("@save");
sidebarPage.goToRealmSettings();
realmSettingsPage.goToClientPoliciesTab().goToClientPoliciesList();
realmSettingsPage.createNewClientPolicyFromList(
"Test",
"Test Again Description",
true
);
realmSettingsPage.shouldShowErrorWhenDuplicate();
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToClientPoliciesTab()
.goToClientPoliciesList()
.deleteClientPolicyItemFromTable("Test");
modalUtils.confirmModal();
cy.wait("@save");
masthead.checkNotificationMessage("Client policy deleted");
realmSettingsPage.checkEmptyPolicyList();
});
it("Check deleting newly created client policy from create view via dropdown", () => {
const url = `admin/realms/${realmName}/client-policies/policies`;
cy.intercept("PUT", url).as("save");
realmSettingsPage.createNewClientPolicyFromEmptyState(
"Test again",
"Test Again Description"
);
masthead.checkNotificationMessage("New policy created");
sidebarPage.waitForPageLoad();
cy.wait("@save");
realmSettingsPage.deleteClientPolicyFromDetails();
modalUtils.confirmModal();
masthead.checkNotificationMessage("Client policy deleted");
sidebarPage.waitForPageLoad();
realmSettingsPage.checkEmptyPolicyList();
});
it("Check reloading JSON policies", () => {
realmSettingsPage.shouldReloadJSONPolicies();
});
});

View File

@ -0,0 +1,173 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
import ModalUtils from "../support/util/ModalUtils";
import Masthead from "../support/pages/admin-ui/Masthead";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const modalUtils = new ModalUtils();
const masthead = new Masthead();
describe("Realm settings client profiles tab tests", () => {
const profileName = "Test";
const editedProfileName = "Edit";
const realmName = "Realm_" + crypto.randomUUID();
const realmSettingsPage = new RealmSettingsPage(realmName);
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.waitForPageLoad().goToRealm(realmName).goToRealmSettings();
realmSettingsPage.goToClientPoliciesTab().goToClientProfilesList();
});
before(() => adminClient.createRealm(realmName));
after(() => adminClient.deleteRealm(realmName));
it("Go to client policies profiles tab", () => {
realmSettingsPage.shouldDisplayProfilesTab();
});
it("Check new client form is displaying", () => {
realmSettingsPage.shouldDisplayNewClientProfileForm();
});
it("Complete new client form and cancel", () => {
realmSettingsPage
.createClientProfile(profileName, "Test Description")
.cancelClientProfileCreation()
.checkElementNotInList(profileName);
});
it("Complete new client form and submit", () => {
const url = `admin/realms/${realmName}/client-policies/profiles`;
cy.intercept("PUT", url).as("save");
realmSettingsPage
.createClientProfile(profileName, "Test Description")
.saveClientProfileCreation();
cy.wait("@save");
masthead.checkNotificationMessage("New client profile created");
});
it("Should perform client profile search by profile name", () => {
realmSettingsPage.searchClientProfile(profileName);
});
it("Check navigating between Form View and JSON editor", () => {
realmSettingsPage.shouldNavigateBetweenFormAndJSONView();
});
it("Check saving changed JSON profiles", () => {
realmSettingsPage.shouldSaveChangedJSONProfiles();
realmSettingsPage.deleteClientPolicyItemFromTable(profileName);
modalUtils.confirmModal();
masthead.checkNotificationMessage("Client profile deleted");
realmSettingsPage.checkElementNotInList(profileName);
});
it("Should not create duplicate client profile", () => {
const url = `admin/realms/${realmName}/client-policies/profiles`;
cy.intercept("PUT", url).as("save");
realmSettingsPage
.createClientProfile(profileName, "Test Description")
.saveClientProfileCreation();
cy.wait("@save");
sidebarPage.goToRealmSettings();
realmSettingsPage.goToClientPoliciesTab().goToClientProfilesList();
sidebarPage.waitForPageLoad();
realmSettingsPage
.createClientProfile(profileName, "Test Description")
.saveClientProfileCreation();
cy.wait("@save");
masthead.checkNotificationMessage(
"Could not create client profile: 'proposed client profile name duplicated.'"
);
});
it("Should edit client profile", () => {
realmSettingsPage.shouldEditClientProfile();
});
it("Should check that edited client profile is now listed", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage
.goToClientPoliciesTab()
.goToClientProfilesList()
.shouldCheckEditedClientProfileListed();
});
it("Should show error when client profile left blank", () => {
realmSettingsPage.shouldShowErrorWhenNameBlank();
});
it("Should revert back to the previous profile name", () => {
realmSettingsPage.shouldReloadClientProfileEdits();
});
it("Should not have executors configured by default", () => {
realmSettingsPage.shouldNotHaveExecutorsConfigured();
});
it("Should cancel adding a new executor to a client profile", () => {
realmSettingsPage.shouldCancelAddingExecutor();
});
it("Should add a new executor to a client profile", () => {
realmSettingsPage.shouldAddExecutor();
});
it("Should cancel deleting executor from a client profile", () => {
realmSettingsPage.shouldCancelDeletingExecutor();
});
it("Should cancel editing executor", () => {
realmSettingsPage.openProfileDetails(editedProfileName).editExecutor(4000);
sidebarPage.waitForPageLoad();
realmSettingsPage
.cancelEditingExecutor()
.checkExecutorNotInList()
.editExecutor()
.checkAvailablePeriodExecutor(3600);
});
it("Should edit executor", () => {
realmSettingsPage
.openProfileDetails(editedProfileName)
.editExecutor(4000)
.saveExecutor();
masthead.checkNotificationMessage("Executor updated successfully");
realmSettingsPage.editExecutor();
// TODO: UNCOMMENT LINE WHEN ISSUE 2037 IS FIXED
//.checkAvailablePeriodExecutor(4000);
});
it("Should delete executor from a client profile", () => {
realmSettingsPage.shouldDeleteExecutor();
});
it("Check cancelling the client profile deletion", () => {
realmSettingsPage.deleteClientPolicyItemFromTable(editedProfileName);
modalUtils
.checkModalMessage(
"This action will permanently delete the profile " +
editedProfileName +
". This cannot be undone."
)
.cancelModal();
realmSettingsPage.checkElementInList(editedProfileName);
});
it("Check deleting the client profile", () => {
realmSettingsPage.deleteClientPolicyItemFromTable(editedProfileName);
modalUtils.confirmModal();
masthead.checkNotificationMessage("Client profile deleted");
sidebarPage.waitForPageLoad();
realmSettingsPage.checkElementNotInList(editedProfileName);
});
});

View File

@ -0,0 +1,455 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ModalUtils from "../support/util/ModalUtils";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import adminClient from "../support/util/AdminClient";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const modalUtils = new ModalUtils();
const realmSettingsPage = new RealmSettingsPage();
describe("Realm settings events tab tests", () => {
const realmName = "Realm_" + crypto.randomUUID();
const listingPage = new ListingPage();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
});
before(async () => {
await adminClient.createRealm(realmName);
});
after(async () => {
await adminClient.deleteRealm(realmName);
});
const goToDetails = () => {
const keysUrl = `/admin/realms/${realmName}/keys`;
cy.intercept(keysUrl).as("keysFetch");
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
cy.findAllByTestId("provider-name-link")
.contains("test_aes-generated")
.click();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
cy.findAllByTestId("provider-name-link")
.contains("test_hmac-generated")
.click();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
cy.findAllByTestId("provider-name-link").contains("test_rsa").click();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
cy.findAllByTestId("provider-name-link")
.contains("test_rsa-generated")
.click();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
cy.findAllByTestId("provider-name-link")
.contains("test_rsa-enc-generated")
.click();
cy.wait(["@keysFetch"]);
return this;
};
const goToKeys = () => {
const keysUrl = `/admin/realms/${realmName}/keys`;
cy.intercept(keysUrl).as("keysFetch");
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-keys-list-tab").click();
cy.wait(["@keysFetch"]);
return this;
};
const addBundle = () => {
realmSettingsPage.addKeyValuePair(
"key_" + crypto.randomUUID(),
"value_" + crypto.randomUUID()
);
return this;
};
it("Enable user events", () => {
cy.intercept("GET", `/admin/realms/${realmName}/events/config`).as("load");
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-realm-events-tab").click();
cy.findByTestId("rs-events-tab").click();
cy.wait("@load");
realmSettingsPage
.toggleSwitch(realmSettingsPage.enableEvents, false)
.save(realmSettingsPage.eventsUserSave);
masthead.checkNotificationMessage("Successfully saved configuration");
realmSettingsPage.clearEvents("user");
modalUtils
.checkModalMessage(
"If you clear all events of this realm, all records will be permanently cleared in the database"
)
.confirmModal();
masthead.checkNotificationMessage("The user events have been cleared");
const events = ["Client info", "Client info error"];
cy.intercept("GET", `/admin/realms/${realmName}/events/config`).as(
"fetchConfig"
);
realmSettingsPage.addUserEvents(events).clickAdd();
masthead.checkNotificationMessage("Successfully saved configuration");
cy.wait(["@fetchConfig"]);
sidebarPage.waitForPageLoad();
cy.wait(1000);
for (const event of events) {
listingPage.searchItem(event, false).itemExist(event);
}
});
it("Go to keys tab", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
});
it("add Providers", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
realmSettingsPage.toggleAddProviderDropdown();
cy.findByTestId("option-aes-generated").click();
realmSettingsPage.enterUIDisplayName("test_aes-generated");
realmSettingsPage.toggleSwitch("active", false);
realmSettingsPage.toggleSwitch("enabled", false);
realmSettingsPage.addProvider();
realmSettingsPage.toggleAddProviderDropdown();
cy.findByTestId("option-ecdsa-generated").click();
realmSettingsPage.enterUIDisplayName("test_ecdsa-generated");
realmSettingsPage.toggleSwitch("enabled", false);
realmSettingsPage.addProvider();
realmSettingsPage.toggleAddProviderDropdown();
cy.findByTestId("option-hmac-generated").click();
realmSettingsPage.enterUIDisplayName("test_hmac-generated");
realmSettingsPage.toggleSwitch("active", false);
realmSettingsPage.addProvider();
realmSettingsPage.toggleAddProviderDropdown();
cy.findByTestId("option-rsa-generated").click();
realmSettingsPage.enterUIDisplayName("test_rsa-generated");
realmSettingsPage.toggleSwitch("active", false);
realmSettingsPage.toggleSwitch("enabled", false);
realmSettingsPage.addProvider();
realmSettingsPage.toggleAddProviderDropdown();
cy.findByTestId("option-rsa-enc-generated").click();
realmSettingsPage.enterUIDisplayName("test_rsa-enc-generated");
realmSettingsPage.toggleSwitch("active", false);
realmSettingsPage.toggleSwitch("enabled", false);
realmSettingsPage.addProvider();
});
it("search providers", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
// search providers
cy.findByTestId("provider-search-input").type("rsa{enter}");
listingPage.checkTableLength(4, "kc-draggable-table");
cy.findByTestId("provider-search-input").clear().type("{enter}");
});
it("go to details", () => {
sidebarPage.goToRealmSettings();
goToDetails();
});
it("Test keys", () => {
sidebarPage.goToRealmSettings();
goToKeys();
realmSettingsPage.testSelectFilter();
});
it.skip("Should search active keys", () => {
sidebarPage.goToRealmSettings();
goToKeys();
realmSettingsPage.switchToActiveFilter();
listingPage.searchItem("rs", false);
listingPage.checkTableLength(3, "kc-keys-list");
});
it("Should search passive keys", () => {
sidebarPage.goToRealmSettings();
goToKeys();
realmSettingsPage.switchToPassiveFilter();
listingPage.searchItem("ec", false);
listingPage.checkTableLength(1, "kc-keys-list");
});
it("Should search disabled keys", () => {
sidebarPage.goToRealmSettings();
goToKeys();
realmSettingsPage.switchToDisabledFilter();
listingPage.searchItem("hs", false);
listingPage.checkTableLength(1, "kc-keys-list");
});
it("delete provider", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
cy.findByTestId("rs-providers-tab").click();
realmSettingsPage.deleteProvider("test_aes-generated");
});
it("list keys", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-keys-tab").click();
realmSettingsPage.checkKeyPublic();
});
it("add locale", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-localization-tab").click();
cy.findByTestId("internationalization-disabled").click({ force: true });
cy.get(realmSettingsPage.supportedLocalesTypeahead)
.click()
.get(".pf-c-select__menu-item")
.contains("dansk")
.click();
cy.get("#kc-l-supported-locales").click();
cy.intercept("GET", `/admin/realms/${realmName}/localization/en*`).as(
"load"
);
cy.findByTestId("localization-tab-save").click();
cy.wait("@load");
addBundle();
masthead.checkNotificationMessage(
"Success! The message bundle has been added."
);
realmSettingsPage.setDefaultLocale("dansk");
cy.findByTestId("localization-tab-save").click();
});
it("Realm header settings", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-security-defenses-tab").click();
cy.findByTestId("headers-form-tab-save").should("be.disabled");
cy.get("#xFrameOptions").clear().type("DENY");
cy.findByTestId("headers-form-tab-save").should("be.enabled").click();
masthead.checkNotificationMessage("Realm successfully updated");
});
it("Brute force detection", () => {
sidebarPage.goToRealmSettings();
cy.findAllByTestId("rs-security-defenses-tab").click();
cy.get("#pf-tab-20-bruteForce").click();
cy.findByTestId("brute-force-tab-save").should("be.disabled");
cy.get("#bruteForceProtected").click({ force: true });
cy.findByTestId("waitIncrementSeconds").type("1");
cy.findByTestId("maxFailureWaitSeconds").type("1");
cy.findByTestId("maxDeltaTimeSeconds").type("1");
cy.findByTestId("minimumQuickLoginWaitSeconds").type("1");
cy.findByTestId("brute-force-tab-save").should("be.enabled").click();
masthead.checkNotificationMessage("Realm successfully updated");
});
it("add session data", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-sessions-tab").click();
realmSettingsPage.populateSessionsPage();
realmSettingsPage.save("sessions-tab-save");
masthead.checkNotificationMessage("Realm successfully updated");
});
it("check that sessions data was saved", () => {
sidebarPage.goToAuthentication();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-sessions-tab").click();
cy.findByTestId(realmSettingsPage.ssoSessionIdleInput).should(
"have.value",
1
);
cy.findByTestId(realmSettingsPage.ssoSessionMaxInput).should(
"have.value",
2
);
cy.findByTestId(realmSettingsPage.ssoSessionIdleRememberMeInput).should(
"have.value",
3
);
cy.findByTestId(realmSettingsPage.ssoSessionMaxRememberMeInput).should(
"have.value",
4
);
cy.findByTestId(realmSettingsPage.clientSessionIdleInput).should(
"have.value",
5
);
cy.findByTestId(realmSettingsPage.clientSessionMaxInput).should(
"have.value",
6
);
cy.findByTestId(realmSettingsPage.offlineSessionIdleInput).should(
"have.value",
7
);
cy.findByTestId(realmSettingsPage.offlineSessionMaxSwitch).should(
"have.value",
"on"
);
cy.findByTestId(realmSettingsPage.loginTimeoutInput).should(
"have.value",
9
);
cy.findByTestId(realmSettingsPage.loginActionTimeoutInput).should(
"have.value",
10
);
});
it("add token data", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-tokens-tab").click();
realmSettingsPage.populateTokensPage();
realmSettingsPage.save("tokens-tab-save");
masthead.checkNotificationMessage("Realm successfully updated");
});
it("check that token data was saved", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-tokens-tab").click();
cy.findByTestId(realmSettingsPage.accessTokenLifespanInput).should(
"have.value",
1
);
cy.findByTestId(realmSettingsPage.accessTokenLifespanImplicitInput).should(
"have.value",
2
);
cy.findByTestId(realmSettingsPage.clientLoginTimeoutInput).should(
"have.value",
3
);
cy.findByTestId(realmSettingsPage.userInitiatedActionLifespanInput).should(
"have.value",
4
);
cy.findByTestId(realmSettingsPage.defaultAdminInitatedInput).should(
"have.value",
5
);
cy.findByTestId(realmSettingsPage.emailVerificationInput).should(
"have.value",
6
);
cy.findByTestId(realmSettingsPage.idpEmailVerificationInput).should(
"have.value",
7
);
cy.findByTestId(realmSettingsPage.forgotPasswordInput).should(
"have.value",
8
);
cy.findByTestId(realmSettingsPage.executeActionsInput).should(
"have.value",
9
);
});
});
describe("Realm settings events tab tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-realm-events-tab").click();
cy.findByTestId("rs-event-listeners-tab").click();
});
it("Should display event listeners form", () => {
realmSettingsPage.shouldDisplayEventListenersForm();
});
it("Should revert saving event listener", () => {
realmSettingsPage.shouldRevertSavingEventListener();
});
it("Should save event listener", () => {
realmSettingsPage.shouldSaveEventListener();
});
it("Should remove event from event listener", () => {
realmSettingsPage.shouldRemoveEventFromEventListener();
});
it("Should remove all events from event listener and re-save original", () => {
realmSettingsPage.shouldSaveEventListener();
realmSettingsPage.shouldRemoveAllEventListeners();
realmSettingsPage.shouldReSaveEventListener();
});
});

View File

@ -0,0 +1,179 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const realmSettingsPage = new RealmSettingsPage();
describe("Realm settings general tab tests", () => {
const realmName = "Realm_" + crypto.randomUUID();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
});
before(async () => {
await adminClient.createRealm(realmName);
});
after(async () => {
await adminClient.deleteRealm(realmName);
});
it("Test all general tab switches", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.toggleSwitch(
realmSettingsPage.managedAccessSwitch,
false
);
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated", true);
realmSettingsPage.toggleSwitch(
realmSettingsPage.managedAccessSwitch,
false
);
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated", true);
// Enable realm
realmSettingsPage.toggleSwitch(`${realmName}-switch`);
masthead.checkNotificationMessage("Realm successfully updated", true);
// Disable realm
realmSettingsPage.toggleSwitch(`${realmName}-switch`);
realmSettingsPage.disableRealm();
masthead.checkNotificationMessage("Realm successfully updated", true);
// Re-enable realm
realmSettingsPage.toggleSwitch(`${realmName}-switch`);
masthead.checkNotificationMessage("Realm successfully updated");
});
it("Modify Display name", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.fillDisplayName("display_name");
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated", true);
});
it("Check Display name value", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.getDisplayName("display_name");
});
it("Modify front end URL", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.fillFrontendURL("www.example.com");
// TODO: Fix internal server error 500 when front-end URL is saved
// realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
// masthead.checkNotificationMessage("Realm successfully updated", true);
realmSettingsPage.getFrontendURL("www.example.com");
realmSettingsPage.clearFrontendURL();
});
it("Select SSL all requests", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.fillRequireSSL("All requests");
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated", true);
});
it("Verify SSL all requests displays", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.getRequireSSL("All requests");
});
it("Select SSL external requests", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.fillRequireSSL("External requests");
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated", true);
});
it("Verify SSL external requests displays", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.getRequireSSL("External requests");
});
it("Select SSL None", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.fillRequireSSL("None");
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated", true);
});
it("Verify SSL None displays", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.getRequireSSL("None");
});
it("Check Access Endpoints OpenID Endpoint Configuration link", () => {
sidebarPage.goToRealmSettings();
// Check link exists
cy.get("a")
.contains("OpenID Endpoint Configuration")
.should(
"have.attr",
"href",
`${Cypress.env(
"KEYCLOAK_SERVER"
)}/realms/${realmName}/.well-known/openid-configuration`
)
.should("have.attr", "target", "_blank")
.should("have.attr", "rel", "noreferrer noopener");
});
it("Access Endpoints OpenID Endpoint Configuration link", () => {
sidebarPage.goToRealmSettings();
// Check the link is live
cy.get("a")
.contains("OpenID Endpoint Configuration")
.then((link) => {
cy.request(link.prop("href")).its("status").should("eq", 200);
});
});
it("Check if Access Endpoints SAML 2.0 Identity Provider Metadata link exists", () => {
sidebarPage.goToRealmSettings();
cy.get("a")
.contains("SAML 2.0 Identity Provider Metadata")
.should(
"have.attr",
"href",
`${Cypress.env(
"KEYCLOAK_SERVER"
)}/realms/${realmName}/protocol/saml/descriptor`
)
.should("have.attr", "target", "_blank")
.should("have.attr", "rel", "noreferrer noopener");
});
it("Access Endpoints SAML 2.0 Identity Provider Metadata link", () => {
sidebarPage.goToRealmSettings();
// Check the link is live
cy.get("a")
.contains("SAML 2.0 Identity Provider Metadata ")
.then((link) => {
cy.request(link.prop("href")).its("status").should("eq", 200);
});
});
it("Verify 'Revert' button works", () => {
sidebarPage.goToRealmSettings();
realmSettingsPage.fillDisplayName("should_be_reverted");
realmSettingsPage.revert(realmSettingsPage.generalRevertBtn);
realmSettingsPage.getDisplayName("display_name");
});
});

View File

@ -0,0 +1,127 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import adminClient from "../support/util/AdminClient";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const realmSettingsPage = new RealmSettingsPage();
describe("Realm settings tabs tests", () => {
const realmName = "Realm_" + crypto.randomUUID();
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
});
before(async () => {
await adminClient.createRealm(realmName);
});
after(async () => {
await adminClient.deleteRealm(realmName);
});
it("shows the 'user profile' tab if enabled", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId(realmSettingsPage.userProfileTab).should("not.exist");
realmSettingsPage.toggleSwitch(
realmSettingsPage.profileEnabledSwitch,
false
);
realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
masthead.checkNotificationMessage("Realm successfully updated");
cy.findByTestId(realmSettingsPage.userProfileTab).should("exist");
});
// Clicking multiple toggles in succession causes quick re-renderings of the screen
// and there will be a noticeable flicker during the test.
// Sometimes, this will screw up the test and cause Cypress to hang.
// Clicking to another section each time fixes the problem.
function reloadRealm() {
sidebarPage.goToClientScopes();
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-login-tab").click();
}
function testToggle(realmSwitch: string, expectedValue: string) {
realmSettingsPage.toggleSwitch(realmSwitch);
reloadRealm();
cy.findByTestId(realmSwitch).should("have.value", expectedValue);
}
it("Go to login tab", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-login-tab").click();
testToggle(realmSettingsPage.userRegSwitch, "on");
testToggle(realmSettingsPage.forgotPwdSwitch, "on");
testToggle(realmSettingsPage.rememberMeSwitch, "on");
testToggle(realmSettingsPage.loginWithEmailSwitch, "off");
testToggle(realmSettingsPage.duplicateEmailsSwitch, "on");
// Check other values
cy.findByTestId(realmSettingsPage.emailAsUsernameSwitch).should(
"have.value",
"off"
);
cy.findByTestId(realmSettingsPage.verifyEmailSwitch).should(
"have.value",
"off"
);
});
it("Go to email tab", () => {
// Configure an e-mail address so we can test the connection settings.
cy.wrap(null).then(async () => {
const adminUser = await adminClient.getAdminUser();
await adminClient.updateUser(adminUser.id!, {
email: "admin@example.com",
});
});
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-email-tab").click();
//required fields not filled in or not filled properly
realmSettingsPage.addSenderEmail("not a valid email");
realmSettingsPage.fillFromDisplayName("displayName");
realmSettingsPage.fillReplyToEmail("replyTo@email.com");
realmSettingsPage.fillPort("10");
cy.findByTestId("email-tab-save").click();
cy.get("#kc-display-name-helper").contains("You must enter a valid email.");
cy.get("#kc-host-helper").contains("Required field");
cy.findByTestId("email-tab-revert").click();
cy.findByTestId("sender-email-address").should("be.empty");
cy.findByTestId("from-display-name").should("be.empty");
cy.get("#kc-port").should("be.empty");
realmSettingsPage.addSenderEmail("example@example.com");
realmSettingsPage.toggleCheck(realmSettingsPage.enableSslCheck);
realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck);
realmSettingsPage.fillHostField("localhost");
cy.findByTestId(realmSettingsPage.testConnectionButton).click();
masthead.checkNotificationMessage("Error! Failed to send email", true);
});
it("Go to themes tab", () => {
sidebarPage.goToRealmSettings();
cy.findByTestId("rs-themes-tab").click();
realmSettingsPage.selectLoginThemeType("keycloak");
realmSettingsPage.selectAccountThemeType("keycloak");
realmSettingsPage.selectAdminThemeType("base");
realmSettingsPage.selectEmailThemeType("base");
realmSettingsPage.saveThemes();
});
});

View File

@ -0,0 +1,137 @@
import ListingPage from "../support/pages/admin-ui/ListingPage";
import UserProfile from "../support/pages/admin-ui/manage/realm_settings/UserProfile";
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import adminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import ModalUtils from "../support/util/ModalUtils";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const userProfileTab = new UserProfile();
const listingPage = new ListingPage();
const modalUtils = new ModalUtils();
const masthead = new Masthead();
// Selectors
const getUserProfileTab = () => userProfileTab.goToTab();
const getAttributesTab = () => userProfileTab.goToAttributesTab();
const getAttributesGroupTab = () => userProfileTab.goToAttributesGroupTab();
const getJsonEditorTab = () => userProfileTab.goToJsonEditorTab();
const clickCreateAttributeButton = () =>
userProfileTab.createAttributeButtonClick();
describe("User profile tabs", () => {
const realmName = "Realm_" + crypto.randomUUID();
const attributeName = "Test";
before(() =>
adminClient.createRealm(realmName, {
attributes: { userProfileEnabled: "true" },
})
);
after(() => adminClient.deleteRealm(realmName));
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealm(realmName);
sidebarPage.goToRealmSettings();
});
describe("Attributes sub tab tests", () => {
it("Goes to create attribute page", () => {
getUserProfileTab();
getAttributesTab();
clickCreateAttributeButton();
});
it("Completes new attribute form and performs cancel", () => {
getUserProfileTab();
getAttributesTab();
clickCreateAttributeButton();
userProfileTab
.createAttribute(attributeName, "Test display name")
.cancelAttributeCreation()
.checkElementNotInList(attributeName);
});
it("Completes new attribute form and performs submit", () => {
getUserProfileTab();
getAttributesTab();
clickCreateAttributeButton();
userProfileTab
.createAttribute(attributeName, "Display name")
.saveAttributeCreation();
masthead.checkNotificationMessage(
"Success! User Profile configuration has been saved."
);
});
it("Modifies existing attribute and performs save", () => {
getUserProfileTab();
getAttributesTab();
userProfileTab
.selectElementInList(attributeName)
.editAttribute("Edited display name")
.saveAttributeCreation();
masthead.checkNotificationMessage(
"Success! User Profile configuration has been saved."
);
});
it("Adds and removes validator to/from existing attribute and performs save", () => {
getUserProfileTab();
getAttributesTab();
userProfileTab.selectElementInList(attributeName).cancelAddingValidator();
userProfileTab.addValidator();
cy.get('tbody [data-label="Validator name"]').contains("email");
userProfileTab.cancelRemovingValidator();
userProfileTab.removeValidator();
cy.get(".kc-emptyValidators").contains("No validators.");
});
});
describe("Attribute groups sub tab tests", () => {
it("Deletes an attributes group", () => {
cy.wrap(null).then(() =>
adminClient.patchUserProfile(realmName, {
groups: [{ name: "Test" }],
})
);
getUserProfileTab();
getAttributesGroupTab();
listingPage.deleteItem("Test");
modalUtils.confirmModal();
listingPage.checkEmptyList();
});
});
describe("Json Editor sub tab tests", () => {
const removedThree = `
{ctrl+a}{backspace}
{
"attributes": [
{
"name": "username",
"validations": {
"length": {
"min": 3,
"max": 255 {downArrow},
"username-prohibited-characters": {
`;
it("Removes three validators with the editor", () => {
getUserProfileTab();
getJsonEditorTab();
userProfileTab.typeJSON(removedThree).saveJSON();
masthead.checkNotificationMessage(
"User profile settings successfully updated."
);
});
});
});

View File

@ -0,0 +1,124 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import CreateRealmPage from "../support/pages/admin-ui/CreateRealmPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import adminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import RealmSettings from "../support/pages/admin-ui/configure/realm_settings/RealmSettings";
import ModalUtils from "../support/util/ModalUtils";
const masthead = new Masthead();
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const createRealmPage = new CreateRealmPage();
const realmSettings = new RealmSettings();
const modalUtils = new ModalUtils();
const testRealmName = "Test-realm-" + crypto.randomUUID();
const newRealmName = "New-Test-realm-" + crypto.randomUUID();
const editedRealmName = "Edited-Test-realm-" + crypto.randomUUID();
const testDisabledName = "Test-Disabled";
describe("Realm tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
});
after(() =>
Promise.all(
[testRealmName, newRealmName, editedRealmName].map((realm) =>
adminClient.deleteRealm(realm)
)
)
);
it("should fail creating Master realm", () => {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName("master").createRealm();
masthead.checkNotificationMessage(
"Could not create realm Conflict detected. See logs for details"
);
createRealmPage.cancelRealmCreation();
});
it("should fail creating realm with empty name", () => {
sidebarPage.goToCreateRealm();
createRealmPage.createRealm();
createRealmPage.verifyRealmNameFieldInvalid();
});
it("should create Test realm", () => {
sidebarPage.goToCreateRealm();
// Test and clear resource field
createRealmPage.fillCodeEditor();
createRealmPage.clearTextField();
createRealmPage.fillRealmName(testRealmName).createRealm();
masthead.checkNotificationMessage("Realm created successfully");
});
it("should create Test Disabled realm", () => {
sidebarPage.goToCreateRealm();
sidebarPage.waitForPageLoad();
createRealmPage.fillRealmName(testDisabledName).createRealm();
createRealmPage.disableRealm();
masthead.checkNotificationMessage("Realm created successfully");
});
it("Should cancel deleting Test Disabled realm", () => {
sidebarPage.goToRealm(testDisabledName).goToRealmSettings();
realmSettings.clickActionMenu();
cy.findByText("Delete").click();
modalUtils.cancelModal();
});
it("Should delete Test Disabled realm", () => {
sidebarPage.goToRealm(testDisabledName).goToRealmSettings();
realmSettings.clickActionMenu();
cy.findByText("Delete").click();
modalUtils.confirmModal();
masthead.checkNotificationMessage("The realm has been deleted");
// Show current realms
sidebarPage.showCurrentRealms(2);
});
it("should create realm from new a realm", () => {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName(newRealmName).createRealm();
const fetchUrl = "/admin/realms?briefRepresentation=true";
cy.intercept(fetchUrl).as("fetch");
masthead.checkNotificationMessage("Realm created successfully");
cy.wait(["@fetch"]);
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName(editedRealmName).createRealm();
masthead.checkNotificationMessage("Realm created successfully");
cy.wait(["@fetch"]);
// Show current realms
sidebarPage.showCurrentRealms(4);
});
it("should change to Test realm", () => {
sidebarPage.goToRealm(editedRealmName);
sidebarPage.getCurrentRealm().should("eq", editedRealmName);
sidebarPage
.goToRealm(testRealmName)
.getCurrentRealm()
.should("eq", testRealmName);
});
});

View File

@ -0,0 +1,62 @@
import ListingPage from "../support/pages/admin-ui/ListingPage";
import UserRegistration, {
GroupPickerDialog,
} from "../support/pages/admin-ui/manage/realm_settings/UserRegistration";
import Masthead from "../support/pages/admin-ui/Masthead";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import adminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import ModalUtils from "../support/util/ModalUtils";
describe("Realm settings - User registration tab", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const modalUtils = new ModalUtils();
const masthead = new Masthead();
const listingPage = new ListingPage();
const groupPicker = new GroupPickerDialog();
const userRegistration = new UserRegistration();
const groupName = "The default group";
before(() => adminClient.createGroup(groupName));
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToRealmSettings();
userRegistration.goToTab();
});
after(() => adminClient.deleteGroups());
it("Add admin role", () => {
const role = "admin";
userRegistration.addRole();
sidebarPage.waitForPageLoad();
userRegistration.selectRow(role).assign();
masthead.checkNotificationMessage("Associated roles have been added");
listingPage.searchItem(role, false).itemExist(role);
});
it("Remove admin role", () => {
const role = "admin";
listingPage.markItemRow(role).removeMarkedItems("Unassign");
sidebarPage.waitForPageLoad();
modalUtils
.checkModalTitle("Remove role?")
.checkModalMessage("Are you sure you want to remove this role?")
.checkConfirmButtonText("Remove")
.confirmModal();
masthead.checkNotificationMessage("Scope mapping successfully removed");
});
it("Add default group", () => {
userRegistration.goToDefaultGroupTab().addDefaultGroup();
groupPicker.checkTitle("Add default groups").clickRow(groupName).clickAdd();
masthead.checkNotificationMessage("New group added to the default groups");
listingPage.itemExist(groupName);
});
});

View File

@ -0,0 +1,80 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import SessionsPage from "../support/pages/admin-ui/manage/sessions/SessionsPage";
import CommonPage from "../support/pages/CommonPage";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import GroupPage from "../support/pages/admin-ui/manage/groups/GroupPage";
import { keycloakBefore } from "../support/util/keycloak_hooks";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const sessionsPage = new SessionsPage();
const commonPage = new CommonPage();
const listingPage = new ListingPage();
const groupPage = new GroupPage();
describe("Sessions test", () => {
const admin = "admin";
const client = "security-admin-console";
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToSessions();
});
describe("Sessions list view", () => {
it("check item values", () => {
listingPage.searchItem(client, false);
commonPage
.tableUtils()
.checkRowItemExists(admin)
.checkRowItemExists(client);
});
it("go to item accessed clients link", () => {
listingPage.searchItem(client, false);
commonPage.tableUtils().clickRowItemLink(client);
});
});
describe("Search", () => {
it("search existing session", () => {
listingPage.searchItem(admin, false);
listingPage.itemExist(admin, true);
groupPage.assertNoSearchResultsMessageExist(false);
});
it("search non-existant session", () => {
listingPage.searchItem("non-existant-session", false);
groupPage.assertNoSearchResultsMessageExist(true);
});
});
describe("revocation", () => {
it("Clear revocation notBefore", () => {
sessionsPage.clearNotBefore();
});
it("Check if notBefore cleared", () => {
sessionsPage.checkNotBeforeCleared();
});
it("Set revocation notBefore", () => {
sessionsPage.setToNow();
});
it("Check if notBefore saved", () => {
sessionsPage.checkNotBeforeValueExists();
});
it("Push when URI not configured", () => {
sessionsPage.pushRevocation();
commonPage
.masthead()
.checkNotificationMessage(
"No push sent. No admin URI configured or no registered cluster nodes available"
);
});
});
});

View File

@ -0,0 +1,251 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import ProviderPage from "../support/pages/admin-ui/manage/providers/ProviderPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ModalUtils from "../support/util/ModalUtils";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import PriorityDialog from "../support/pages/admin-ui/manage/providers/PriorityDialog";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const providersPage = new ProviderPage();
const modalUtils = new ModalUtils();
const provider = "kerberos";
const initCapProvider = provider.charAt(0).toUpperCase() + provider.slice(1);
const kerberosName = "my-kerberos";
const kerberosRealm = "my-realm";
const kerberosPrincipal = "my-principal";
const kerberosKeytab = "my-keytab";
const firstKerberosName = `${kerberosName}-1`;
const firstKerberosRealm = `${kerberosRealm}-1`;
const firstKerberosPrincipal = `${kerberosPrincipal}-1`;
const firstKerberosKeytab = `${kerberosKeytab}-1`;
const secondKerberosName = `${kerberosName}-2`;
const secondKerberosRealm = `${kerberosRealm}-2`;
const secondKerberosPrincipal = `${kerberosPrincipal}-2`;
const secondKerberosKeytab = `${kerberosKeytab}-2`;
const defaultPolicy = "DEFAULT";
const weeklyPolicy = "EVICT_WEEKLY";
const dailyPolicy = "EVICT_DAILY";
const lifespanPolicy = "MAX_LIFESPAN";
const noCachePolicy = "NO_CACHE";
const defaultKerberosDay = "Sunday";
const defaultKerberosHour = "00";
const defaultKerberosMinute = "00";
const newKerberosDay = "Wednesday";
const newKerberosHour = "15";
const newKerberosMinute = "55";
const maxLifespan = 5;
const addProviderMenu = "Add new provider";
const createdSuccessMessage = "User federation provider successfully created";
const savedSuccessMessage = "User federation provider successfully saved";
const deletedSuccessMessage = "The user federation provider has been deleted.";
const deleteModalTitle = "Delete user federation provider?";
const disableModalTitle = "Disable user federation provider?";
const changeSuccessMsg =
"Successfully changed the priority order of user federation providers";
describe("User Fed Kerberos tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToUserFederation();
});
it("Should create Kerberos provider from empty state", () => {
// if tests don't start at empty state, e.g. user has providers configured locally,
// create a new card from the card view instead
cy.get("body").then(($body) => {
if ($body.find(`[data-testid=kerberos-card]`).length > 0) {
providersPage.clickNewCard(provider);
} else {
providersPage.clickMenuCommand(addProviderMenu, initCapProvider);
}
});
providersPage.fillKerberosRequiredData(
firstKerberosName,
firstKerberosRealm,
firstKerberosPrincipal,
firstKerberosKeytab
);
providersPage.save(provider);
masthead.checkNotificationMessage(createdSuccessMessage);
sidebarPage.goToUserFederation();
});
it("Should enable debug, password authentication, and first login", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.toggleSwitch(providersPage.debugSwitch);
providersPage.toggleSwitch(providersPage.passwordAuthSwitch);
providersPage.toggleSwitch(providersPage.firstLoginSwitch);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstKerberosName);
providersPage.verifyToggle(providersPage.debugSwitch, "on");
providersPage.verifyToggle(providersPage.passwordAuthSwitch, "on");
providersPage.verifyToggle(providersPage.firstLoginSwitch, "on");
});
it("Should set cache policy to evict_daily", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(dailyPolicy);
providersPage.changeCacheTime("hour", newKerberosHour);
providersPage.changeCacheTime("minute", newKerberosMinute);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstKerberosName);
expect(cy.contains(dailyPolicy).should("exist"));
expect(cy.contains(defaultPolicy).should("not.exist"));
});
it("Should set cache policy to default", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(defaultPolicy);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstKerberosName);
expect(cy.contains(defaultPolicy).should("exist"));
expect(cy.contains(dailyPolicy).should("not.exist"));
});
it("Should set cache policy to evict_weekly", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(weeklyPolicy);
providersPage.changeCacheTime("day", newKerberosDay);
providersPage.changeCacheTime("hour", newKerberosHour);
providersPage.changeCacheTime("minute", newKerberosMinute);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstKerberosName);
expect(cy.contains(weeklyPolicy).should("exist"));
expect(cy.contains(defaultPolicy).should("not.exist"));
});
it("Should set cache policy to max_lifespan", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(lifespanPolicy);
providersPage.fillMaxLifespanData(maxLifespan);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstKerberosName);
expect(cy.contains(lifespanPolicy).should("exist"));
expect(cy.contains(weeklyPolicy).should("not.exist"));
});
it("Should set cache policy to no_cache", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(noCachePolicy);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstKerberosName);
expect(cy.contains(noCachePolicy).should("exist"));
expect(cy.contains(lifespanPolicy).should("not.exist"));
});
it("Should edit existing Kerberos provider and cancel", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(weeklyPolicy);
providersPage.changeCacheTime("day", defaultKerberosDay);
providersPage.changeCacheTime("hour", defaultKerberosHour);
providersPage.changeCacheTime("minute", defaultKerberosMinute);
providersPage.cancel(provider);
providersPage.clickExistingCard(firstKerberosName);
providersPage.selectCacheType(weeklyPolicy);
providersPage.verifyChangedHourInput(newKerberosHour, defaultKerberosHour);
sidebarPage.goToUserFederation();
});
it("Should disable an existing Kerberos provider", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.disableEnabledSwitch(initCapProvider);
modalUtils.checkModalTitle(disableModalTitle).confirmModal();
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
expect(cy.contains("Disabled").should("exist"));
});
it("Should enable an existing previously-disabled Kerberos provider", () => {
providersPage.clickExistingCard(firstKerberosName);
providersPage.enableEnabledSwitch(initCapProvider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
expect(cy.contains("Enabled").should("exist"));
});
it("Should create new Kerberos provider using the New Provider dropdown", () => {
providersPage.clickMenuCommand(addProviderMenu, initCapProvider);
providersPage.fillKerberosRequiredData(
secondKerberosName,
secondKerberosRealm,
secondKerberosPrincipal,
secondKerberosKeytab
);
providersPage.save(provider);
masthead.checkNotificationMessage(createdSuccessMessage);
sidebarPage.goToUserFederation();
});
it.skip("Should change the priority order of Kerberos providers", () => {
const priorityDialog = new PriorityDialog();
const providers = [firstKerberosName, secondKerberosName];
sidebarPage.goToUserFederation();
providersPage.clickMenuCommand(addProviderMenu, initCapProvider);
sidebarPage.goToUserFederation();
priorityDialog.openDialog().checkOrder(providers);
priorityDialog.clickSave();
masthead.checkNotificationMessage(changeSuccessMsg, true);
});
it("Should delete a Kerberos provider from card view using the card's menu", () => {
providersPage.deleteCardFromCard(secondKerberosName);
modalUtils.checkModalTitle(deleteModalTitle).confirmModal();
masthead.checkNotificationMessage(deletedSuccessMessage);
});
it("Should delete a Kerberos provider using the Settings view's Action menu", () => {
providersPage.deleteCardFromMenu(firstKerberosName);
modalUtils.checkModalTitle(deleteModalTitle).confirmModal();
masthead.checkNotificationMessage(deletedSuccessMessage);
});
});

View File

@ -0,0 +1,237 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import GroupModal from "../support/pages/admin-ui/manage/groups/GroupModal";
import ProviderPage from "../support/pages/admin-ui/manage/providers/ProviderPage";
import CreateClientPage from "../support/pages/admin-ui/manage/clients/CreateClientPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ModalUtils from "../support/util/ModalUtils";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import GroupPage from "../support/pages/admin-ui/manage/groups/GroupPage";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const listingPage = new ListingPage();
const groupModal = new GroupModal();
const createClientPage = new CreateClientPage();
const groupPage = new GroupPage();
const providersPage = new ProviderPage();
const modalUtils = new ModalUtils();
const provider = "ldap";
const allCapProvider = provider.toUpperCase();
const ldapName = "ldap-mappers-testing";
const ldapVendor = "Active Directory";
// connection and authentication settings
const connectionUrlValid = "ldap://localhost:3004";
const bindTypeSimple = "simple";
const truststoreSpiOnlyLdaps = "Only for ldaps";
const connectionTimeoutTwoSecs = "2000";
const bindDnCnDc = "cn=user,dc=test";
const bindCredsValid = "user";
// ldap searching and updating
const editModeReadOnly = "READ_ONLY";
const firstUsersDn = "user-dn-1";
const firstUserLdapAtt = "uid";
const firstRdnLdapAtt = "uid";
const firstUuidLdapAtt = "entryUUID";
const firstUserObjClasses = "inetOrgPerson, organizationalPerson";
const addProviderMenu = "Add new provider";
const providerCreatedSuccess = "User federation provider successfully created";
const mapperCreatedSuccess = "Mapping successfully created";
const providerDeleteSuccess = "The user federation provider has been deleted.";
const providerDeleteTitle = "Delete user federation provider?";
const mapperDeletedSuccess = "Mapping successfully deleted";
const mapperDeleteTitle = "Delete mapping?";
const groupDeleteTitle = "Delete group?";
const groupCreatedSuccess = "Group created";
const groupDeletedSuccess = "Group deleted";
const clientCreatedSuccess = "Client created successfully";
const clientDeletedSuccess = "The client has been deleted";
const roleCreatedSuccess = "Role created";
const groupName = "aa-uf-mappers-group";
const clientName = "aa-uf-mappers-client";
const roleName = "aa-uf-mappers-role";
// mapperType variables
const hcAttMapper = "hardcoded-attribute-mapper";
const hcLdapGroupMapper = "hardcoded-ldap-group-mapper";
const hcLdapAttMapper = "hardcoded-ldap-attribute-mapper";
const roleLdapMapper = "role-ldap-mapper";
const hcLdapRoleMapper = "hardcoded-ldap-role-mapper";
// Used by "Delete default mappers" test
const creationDateMapper = "creation date";
const emailMapper = "email";
const lastNameMapper = "last name";
const modifyDateMapper = "modify date";
describe("User Fed LDAP mapper tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToUserFederation();
});
it("Create LDAP provider from empty state", () => {
// if tests don't start at empty state, e.g. user has providers configured locally,
// create a new card from the card view instead
cy.get("body").then(($body) => {
if ($body.find(`[data-testid=ldap-card]`).length > 0) {
providersPage.clickNewCard(provider);
} else {
providersPage.clickMenuCommand(addProviderMenu, allCapProvider);
}
});
providersPage.fillLdapGeneralData(ldapName, ldapVendor);
providersPage.fillLdapConnectionData(
connectionUrlValid,
bindTypeSimple,
truststoreSpiOnlyLdaps,
connectionTimeoutTwoSecs,
bindDnCnDc,
bindCredsValid
);
providersPage.toggleSwitch(providersPage.enableStartTls);
providersPage.toggleSwitch(providersPage.connectionPooling);
providersPage.fillLdapSearchingData(
editModeReadOnly,
firstUsersDn,
firstUserLdapAtt,
firstRdnLdapAtt,
firstUuidLdapAtt,
firstUserObjClasses
);
providersPage.save(provider);
masthead.checkNotificationMessage(providerCreatedSuccess);
sidebarPage.goToUserFederation();
});
// create a new group
it("Create group", () => {
sidebarPage.goToGroups();
groupPage.openCreateGroupModal(true);
groupModal.setGroupNameInput(groupName).create();
masthead.checkNotificationMessage(groupCreatedSuccess);
});
// create a new client and then new role for that client
it("Create client and role", () => {
sidebarPage.goToClients();
listingPage.goToCreateItem();
createClientPage
.selectClientType("openid-connect")
.fillClientData(clientName)
.continue()
.continue()
.save();
masthead.checkNotificationMessage(clientCreatedSuccess);
providersPage.createRole(roleName);
masthead.checkNotificationMessage(roleCreatedSuccess);
sidebarPage.goToClients();
listingPage.searchItem(clientName).itemExist(clientName);
});
// delete four default mappers
it("Delete default mappers", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.itemExist(creationDateMapper).deleteItem(creationDateMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(creationDateMapper, false);
listingPage.itemExist(emailMapper).deleteItem(emailMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(emailMapper, false);
listingPage.itemExist(lastNameMapper).deleteItem(lastNameMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(lastNameMapper, false);
listingPage.itemExist(modifyDateMapper).deleteItem(modifyDateMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(modifyDateMapper, false);
});
// create one of each hardcoded mapper type
it("Create hardcoded attribute mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(hcAttMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(hcAttMapper, true);
});
it("Create hardcoded ldap group mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(hcLdapGroupMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(hcLdapGroupMapper, true);
});
it("Create hardcoded ldap attribute mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(hcLdapAttMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(hcLdapAttMapper, true);
});
it("Create hardcoded ldap role mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(hcLdapRoleMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(hcLdapRoleMapper, true);
});
it("Create role ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(roleLdapMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(roleLdapMapper, true);
});
// *** test cleanup ***
it("Cleanup - delete LDAP provider", () => {
providersPage.deleteCardFromMenu(ldapName);
modalUtils.checkModalTitle(providerDeleteTitle).confirmModal();
masthead.checkNotificationMessage(providerDeleteSuccess);
});
it("Cleanup - delete group", () => {
sidebarPage.goToGroups();
listingPage.deleteItem(groupName);
modalUtils.checkModalTitle(groupDeleteTitle).confirmModal();
masthead.checkNotificationMessage(groupDeletedSuccess);
});
it("Cleanup - delete client", () => {
sidebarPage.goToClients();
listingPage.deleteItem(clientName);
modalUtils.checkModalTitle(`Delete ${clientName} ?`).confirmModal();
masthead.checkNotificationMessage(clientDeletedSuccess);
});
});

View File

@ -0,0 +1,276 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import ProviderPage from "../support/pages/admin-ui/manage/providers/ProviderPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ModalUtils from "../support/util/ModalUtils";
import { keycloakBefore } from "../support/util/keycloak_hooks";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const listingPage = new ListingPage();
const providersPage = new ProviderPage();
const modalUtils = new ModalUtils();
const provider = "ldap";
const allCapProvider = provider.toUpperCase();
const ldapName = "ldap-mappers-testing";
const ldapVendor = "Active Directory";
// connection and authentication settings
const connectionUrlValid = "ldap://localhost:3004";
const bindTypeSimple = "simple";
const truststoreSpiOnlyLdaps = "Only for ldaps";
const connectionTimeoutTwoSecs = "2000";
const bindDnCnDc = "cn=user,dc=test";
const bindCredsValid = "user";
// ldap searching and updating
const editModeReadOnly = "READ_ONLY";
const firstUsersDn = "user-dn-1";
const firstUserLdapAtt = "uid";
const firstRdnLdapAtt = "uid";
const firstUuidLdapAtt = "entryUUID";
const firstUserObjClasses = "inetOrgPerson, organizationalPerson";
const addProviderMenu = "Add new provider";
const providerCreatedSuccess = "User federation provider successfully created";
const mapperCreatedSuccess = "Mapping successfully created";
const mapperUpdatedSuccess = "Mapping successfully updated";
const providerDeleteSuccess = "The user federation provider has been deleted.";
const providerDeleteTitle = "Delete user federation provider?";
const mapperDeletedSuccess = "Mapping successfully deleted";
const mapperDeleteTitle = "Delete mapping?";
// mapperType variables
const msadUserAcctMapper = "msad-user-account-control-mapper";
const msadLdsUserAcctMapper = "msad-lds-user-account-control-mapper";
const userAttLdapMapper = "user-attribute-ldap-mapper";
const fullNameLdapMapper = "full-name-ldap-mapper";
const groupLdapMapper = "group-ldap-mapper";
const certLdapMapper = "certificate-ldap-mapper";
const mapperNames = [
`${msadUserAcctMapper}-test`,
`${msadLdsUserAcctMapper}-test`,
`${userAttLdapMapper}-test`,
`${fullNameLdapMapper}-test`,
`${groupLdapMapper}-test`,
];
const multiMapperNames = mapperNames.slice(2);
const singleMapperName = mapperNames.slice(4);
const uniqueSearchTerm = "group";
const multipleSearchTerm = "ldap";
const nonexistingSearchTerm = "redhat";
// Used by "Delete default mappers" test
const creationDateMapper = "creation date";
const emailMapper = "email";
const lastNameMapper = "last name";
const modifyDateMapper = "modify date";
const usernameMapper = "username";
const firstNameMapper = "first name";
const MsadAccountControlsMapper = "MSAD account controls";
describe("User Fed LDAP mapper tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToUserFederation();
});
it("Create LDAP provider from empty state", () => {
// if tests don't start at empty state, e.g. user has providers configured locally,
// create a new card from the card view instead
cy.get("body").then(($body) => {
if ($body.find(`[data-testid=ldap-card]`).length > 0) {
providersPage.clickNewCard(provider);
} else {
providersPage.clickMenuCommand(addProviderMenu, allCapProvider);
}
});
providersPage.fillLdapGeneralData(ldapName, ldapVendor);
providersPage.fillLdapConnectionData(
connectionUrlValid,
bindTypeSimple,
truststoreSpiOnlyLdaps,
connectionTimeoutTwoSecs,
bindDnCnDc,
bindCredsValid
);
providersPage.toggleSwitch(providersPage.enableStartTls);
providersPage.toggleSwitch(providersPage.connectionPooling);
providersPage.fillLdapSearchingData(
editModeReadOnly,
firstUsersDn,
firstUserLdapAtt,
firstRdnLdapAtt,
firstUuidLdapAtt,
firstUserObjClasses
);
providersPage.save(provider);
masthead.checkNotificationMessage(providerCreatedSuccess);
sidebarPage.goToUserFederation();
});
// delete default mappers
it("Delete default mappers", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.itemExist(creationDateMapper).deleteItem(creationDateMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(creationDateMapper, false);
listingPage.itemExist(emailMapper).deleteItem(emailMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(emailMapper, false);
listingPage.itemExist(lastNameMapper).deleteItem(lastNameMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(lastNameMapper, false);
listingPage.itemExist(modifyDateMapper).deleteItem(modifyDateMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(modifyDateMapper, false);
listingPage.itemExist(usernameMapper).deleteItem(usernameMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(usernameMapper, false);
listingPage.itemExist(firstNameMapper).deleteItem(firstNameMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
listingPage.itemExist(firstNameMapper, false);
listingPage
.itemExist(MsadAccountControlsMapper)
.deleteItem(MsadAccountControlsMapper);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess, true);
});
// mapper CRUD tests
// create mapper
it("Create certificate ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(certLdapMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(certLdapMapper, true);
});
// update mapper
it("Update certificate ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.goToItemDetails(`${certLdapMapper}-test`);
providersPage.updateMapper(certLdapMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperUpdatedSuccess);
});
// delete mapper
it("Delete certificate ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.deleteItem(`${certLdapMapper}-test`);
modalUtils.checkModalTitle(mapperDeleteTitle).confirmModal();
masthead.checkNotificationMessage(mapperDeletedSuccess);
});
// create one of each non-hardcoded mapper type except
// certificate ldap mapper which was already tested in CRUD section
it("Create user account control mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(msadUserAcctMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(msadUserAcctMapper, true);
});
it("Create msad lds user account control mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(msadLdsUserAcctMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(msadLdsUserAcctMapper, true);
});
it("Create user attribute ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(userAttLdapMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(userAttLdapMapper, true);
});
it("Create full name ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(fullNameLdapMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(fullNameLdapMapper, true);
});
it("Create group ldap mapper", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
providersPage.createNewMapper(groupLdapMapper);
providersPage.save("ldap-mapper");
masthead.checkNotificationMessage(mapperCreatedSuccess);
listingPage.itemExist(groupLdapMapper, true);
});
it("Should return one search result for mapper with unique string", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.searchItem(uniqueSearchTerm, false);
singleMapperName.map((mapperName) => listingPage.itemExist(mapperName));
});
it("Should return multiple search results for mappers that share common string", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.searchItem(multipleSearchTerm, false);
multiMapperNames.map((mapperName) => listingPage.itemExist(mapperName));
});
it("Should return all mappers in search results when no string is specified", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.searchItem("", false);
mapperNames.map((mapperName) => listingPage.itemExist(mapperName));
});
it("Should return no search results for string that does not exist in any mappers", () => {
providersPage.clickExistingCard(ldapName);
providersPage.goToMappers();
listingPage.searchItem(nonexistingSearchTerm, false);
cy.findByTestId(listingPage.emptyState).should("exist");
});
// *** test cleanup ***
it("Cleanup - delete LDAP provider", () => {
providersPage.deleteCardFromMenu(ldapName);
modalUtils.checkModalTitle(providerDeleteTitle).confirmModal();
masthead.checkNotificationMessage(providerDeleteSuccess);
});
});

View File

@ -0,0 +1,572 @@
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import ProviderPage from "../support/pages/admin-ui/manage/providers/ProviderPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ModalUtils from "../support/util/ModalUtils";
import { keycloakBefore } from "../support/util/keycloak_hooks";
const loginPage = new LoginPage();
const masthead = new Masthead();
const sidebarPage = new SidebarPage();
const providersPage = new ProviderPage();
const modalUtils = new ModalUtils();
const provider = "ldap";
const allCapProvider = provider.toUpperCase();
const firstLdapName = "my-ldap";
const firstLdapVendor = "Active Directory";
const secondLdapName = `${firstLdapName}-2`;
const secondLdapVendor = "Other";
const updatedLdapName = `${firstLdapName}-updated`;
// connection and authentication settings
const connectionUrlValid = "ldap://localhost:3004";
const bindTypeSimple = "simple";
const truststoreSpiOnlyLdaps = "Only for ldaps";
const connectionTimeoutTwoSecs = "2000";
const bindDnCnDc = "cn=user,dc=test";
const bindCredsValid = "user";
const connectionUrlInvalid = "ldap://nowhere.com";
const bindTypeNone = "none";
const truststoreSpiNever = "Never";
const bindDnCnOnly = "cn=read-only-admin";
const bindCredsInvalid = "not-my-password";
// kerberos integration settings
const kerberosRealm = "FOO.ORG";
const serverPrincipal = "HTTP/host.foo.org@FOO.ORG";
const keyTab = "/etc/krb5.keytab";
// ldap synchronization settings
const batchSize = "100";
const fullSyncPeriod = "604800";
const userSyncPeriod = "86400";
// ldap searching and updating
const editModeReadOnly = "READ_ONLY";
const editModeWritable = "WRITABLE";
const editModeUnsynced = "UNSYNCED";
const firstUsersDn = "user-dn-1";
const firstUserLdapAtt = "uid";
const firstRdnLdapAtt = "uid";
const firstUuidLdapAtt = "entryUUID";
const firstUserObjClasses = "inetOrgPerson, organizationalPerson";
const firstUserLdapFilter = "(first-filter)";
const firstReadTimeout = "5000";
const searchScopeOneLevel = "One Level";
const searchScopeSubtree = "Subtree";
const secondUsersDn = "user-dn-2";
const secondUserLdapAtt = "cn";
const secondRdnLdapAtt = "cn";
const secondUuidLdapAtt = "objectGUID";
const secondUserObjClasses = "person, organizationalPerson, user";
const secondUserLdapFilter = "(second-filter)";
const secondReadTimeout = "5000";
const defaultPolicy = "DEFAULT";
const weeklyPolicy = "EVICT_WEEKLY";
const dailyPolicy = "EVICT_DAILY";
const lifespanPolicy = "MAX_LIFESPAN";
const noCachePolicy = "NO_CACHE";
const defaultLdapDay = "Sunday";
const defaultLdapHour = "00";
const defaultLdapMinute = "00";
const newLdapDay = "Wednesday";
const newLdapHour = "15";
const newLdapMinute = "55";
const maxLifespan = 5;
const addProviderMenu = "Add new provider";
const createdSuccessMessage = "User federation provider successfully created";
const savedSuccessMessage = "User federation provider successfully saved";
const deletedSuccessMessage = "The user federation provider has been deleted.";
const deleteModalTitle = "Delete user federation provider?";
const disableModalTitle = "Disable user federation provider?";
const validatePasswordPolicyFailMessage =
"User federation provider could not be saved: Validate Password Policy is applicable only with WRITABLE edit mode";
const userImportingDisabledFailMessage =
"User federation provider could not be saved: Can not disable Importing users when LDAP provider mode is UNSYNCED";
const ldapTestSuccessMsg = "Successfully connected to LDAP";
const ldapTestFailMsg =
"Error when trying to connect to LDAP. See server.log for details. LDAP test error";
describe("User Federation LDAP tests", () => {
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToUserFederation();
cy.intercept("GET", "/admin/realms/master").as("getProvider");
});
it("Should create LDAP provider from empty state", () => {
// if tests don't start at empty state, e.g. user has providers configured locally,
// create a new card from the card view instead
cy.get("body").then(($body) => {
if ($body.find(`[data-testid=ldap-card]`).length > 0) {
providersPage.clickNewCard(provider);
} else {
providersPage.clickMenuCommand(addProviderMenu, allCapProvider);
}
});
providersPage.fillLdapGeneralData(firstLdapName, firstLdapVendor);
providersPage.fillLdapConnectionData(
connectionUrlInvalid,
bindTypeSimple,
truststoreSpiNever,
connectionTimeoutTwoSecs,
bindDnCnOnly,
bindCredsInvalid
);
providersPage.fillLdapSearchingData(
editModeReadOnly,
firstUsersDn,
firstUserLdapAtt,
firstRdnLdapAtt,
firstUuidLdapAtt,
firstUserObjClasses,
firstUserLdapFilter,
searchScopeOneLevel,
firstReadTimeout
);
providersPage.save(provider);
masthead.checkNotificationMessage(createdSuccessMessage);
sidebarPage.goToUserFederation();
});
it("Should fail updating advanced settings", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.toggleSwitch(providersPage.ldapv3PwSwitch);
providersPage.toggleSwitch(providersPage.validatePwPolicySwitch);
providersPage.toggleSwitch(providersPage.trustEmailSwitch);
providersPage.save(provider);
masthead.checkNotificationMessage(validatePasswordPolicyFailMessage);
sidebarPage.goToUserFederation();
});
it("Should update advanced settings", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.toggleSwitch(providersPage.ldapv3PwSwitch);
providersPage.toggleSwitch(providersPage.validatePwPolicySwitch);
providersPage.toggleSwitch(providersPage.trustEmailSwitch);
providersPage.fillLdapSearchingData(
editModeWritable,
secondUsersDn,
secondUserLdapAtt,
secondRdnLdapAtt,
secondUuidLdapAtt,
secondUserObjClasses
);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.verifyToggle(providersPage.ldapv3PwSwitch, "on");
providersPage.verifyToggle(providersPage.validatePwPolicySwitch, "on");
providersPage.verifyToggle(providersPage.trustEmailSwitch, "on");
});
it("Should set cache policy to evict_daily", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.selectCacheType(dailyPolicy);
providersPage.changeCacheTime("hour", newLdapHour);
providersPage.changeCacheTime("minute", newLdapMinute);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
expect(cy.contains(dailyPolicy).should("exist"));
expect(cy.contains(defaultPolicy).should("not.exist"));
});
it("Should set cache policy to default", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.selectCacheType(defaultPolicy);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
expect(cy.contains(defaultPolicy).should("exist"));
expect(cy.contains(dailyPolicy).should("not.exist"));
});
it("Should set cache policy to evict_weekly", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.selectCacheType(weeklyPolicy);
providersPage.changeCacheTime("day", newLdapDay);
providersPage.changeCacheTime("hour", newLdapHour);
providersPage.changeCacheTime("minute", newLdapMinute);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
expect(cy.contains(weeklyPolicy).should("exist"));
expect(cy.contains(defaultPolicy).should("not.exist"));
});
it("Update connection and authentication settings and save", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.fillLdapConnectionData(
connectionUrlInvalid,
bindTypeNone,
truststoreSpiNever,
connectionTimeoutTwoSecs
);
providersPage.toggleSwitch(providersPage.enableStartTls);
providersPage.toggleSwitch(providersPage.connectionPooling);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
// now verify
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.verifyTextField(
providersPage.connectionUrlInput,
connectionUrlInvalid
);
providersPage.verifyTextField(
providersPage.connectionTimeoutInput,
connectionTimeoutTwoSecs
);
providersPage.verifySelect(
providersPage.truststoreSpiInput,
truststoreSpiNever
);
providersPage.verifySelect(providersPage.bindTypeInput, bindTypeNone);
providersPage.verifyToggle(providersPage.enableStartTls, "on");
providersPage.verifyToggle(providersPage.connectionPooling, "on");
sidebarPage.goToUserFederation();
});
it("Should fail connection and authentication tests", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.testConnection();
masthead.checkNotificationMessage(ldapTestFailMsg);
providersPage.testAuthorization();
masthead.checkNotificationMessage(ldapTestFailMsg);
sidebarPage.goToUserFederation();
});
it("Should make changes and pass connection and authentication tests", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.fillLdapConnectionData(
connectionUrlValid,
bindTypeSimple,
truststoreSpiOnlyLdaps,
connectionTimeoutTwoSecs,
bindDnCnDc,
bindCredsValid
);
providersPage.toggleSwitch(providersPage.enableStartTls);
providersPage.toggleSwitch(providersPage.connectionPooling);
providersPage.save(provider);
providersPage.testConnection();
masthead.checkNotificationMessage(ldapTestSuccessMsg);
providersPage.testAuthorization();
masthead.checkNotificationMessage(ldapTestSuccessMsg);
sidebarPage.goToUserFederation();
});
it("Should update Kerberos integration settings and save", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.toggleSwitch(providersPage.allowKerberosAuth);
providersPage.toggleSwitch(providersPage.debug);
providersPage.toggleSwitch(providersPage.useKerberosForPwAuth);
providersPage.fillTextField(
providersPage.ldapKerberosRealmInput,
kerberosRealm
);
providersPage.fillTextField(
providersPage.ldapServerPrincipalInput,
serverPrincipal
);
providersPage.fillTextField(providersPage.ldapKeyTabInput, keyTab);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
// now verify
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.verifyTextField(
providersPage.ldapKerberosRealmInput,
kerberosRealm
);
providersPage.verifyTextField(
providersPage.ldapServerPrincipalInput,
serverPrincipal
);
providersPage.verifyTextField(providersPage.ldapKeyTabInput, keyTab);
providersPage.verifyToggle(providersPage.allowKerberosAuth, "on");
providersPage.verifyToggle(providersPage.debug, "on");
providersPage.verifyToggle(providersPage.useKerberosForPwAuth, "on");
sidebarPage.goToUserFederation();
});
it("Should update Synchronization settings and save", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.toggleSwitch(providersPage.importUsers);
providersPage.toggleSwitch(providersPage.periodicFullSync);
providersPage.toggleSwitch(providersPage.periodicUsersSync);
providersPage.fillTextField(providersPage.ldapBatchSizeInput, batchSize);
providersPage.fillTextField(
providersPage.ldapFullSyncPeriodInput,
fullSyncPeriod
);
providersPage.fillTextField(
providersPage.ldapUsersSyncPeriodInput,
userSyncPeriod
);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
// now verify
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.verifyTextField(providersPage.ldapBatchSizeInput, batchSize);
providersPage.verifyTextField(
providersPage.ldapFullSyncPeriodInput,
fullSyncPeriod
);
providersPage.verifyTextField(
providersPage.ldapUsersSyncPeriodInput,
userSyncPeriod
);
providersPage.verifyToggle(providersPage.periodicFullSync, "on");
providersPage.verifyToggle(providersPage.periodicUsersSync, "on");
providersPage.verifyToggle(providersPage.importUsers, "on");
sidebarPage.goToUserFederation();
});
it("Should update LDAP searching and updating settings and save", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.fillLdapSearchingData(
editModeWritable,
secondUsersDn,
secondUserLdapAtt,
secondRdnLdapAtt,
secondUuidLdapAtt,
secondUserObjClasses,
secondUserLdapFilter,
searchScopeSubtree,
secondReadTimeout
);
providersPage.toggleSwitch(providersPage.ldapPagination);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
// now verify
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.verifySelect(
providersPage.ldapEditModeInput,
editModeWritable
);
providersPage.verifyTextField(
providersPage.ldapUsersDnInput,
secondUsersDn
);
providersPage.verifyTextField(
providersPage.ldapUserLdapAttInput,
secondUserLdapAtt
);
providersPage.verifyTextField(
providersPage.ldapRdnLdapAttInput,
secondRdnLdapAtt
);
providersPage.verifyTextField(
providersPage.ldapUuidLdapAttInput,
secondUuidLdapAtt
);
providersPage.verifyTextField(
providersPage.ldapUserObjClassesInput,
secondUserObjClasses
);
providersPage.verifyTextField(
providersPage.ldapUserLdapFilter,
secondUserLdapFilter
);
providersPage.verifySelect(
providersPage.ldapSearchScopeInput,
searchScopeSubtree
);
providersPage.verifyTextField(
providersPage.ldapReadTimeout,
secondReadTimeout
);
providersPage.verifyToggle(providersPage.ldapPagination, "on");
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.fillSelect(providersPage.ldapEditModeInput, editModeUnsynced);
providersPage.toggleSwitch(providersPage.importUsers);
providersPage.save(provider);
masthead.checkNotificationMessage(validatePasswordPolicyFailMessage);
providersPage.toggleSwitch(providersPage.importUsers);
providersPage.toggleSwitch(providersPage.validatePwPolicySwitch);
providersPage.save(provider);
masthead.checkNotificationMessage(userImportingDisabledFailMessage);
providersPage.toggleSwitch(providersPage.importUsers);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
// now verify
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
providersPage.verifySelect(
providersPage.ldapEditModeInput,
editModeUnsynced
);
});
it("Should update display name", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.fillLdapGeneralData(updatedLdapName);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(updatedLdapName);
sidebarPage.goToUserFederation();
});
it("Should update existing LDAP provider and cancel", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.selectCacheType(weeklyPolicy);
providersPage.changeCacheTime("day", defaultLdapDay);
providersPage.changeCacheTime("hour", defaultLdapHour);
providersPage.changeCacheTime("minute", defaultLdapMinute);
providersPage.cancel(provider);
providersPage.clickExistingCard(updatedLdapName);
providersPage.selectCacheType(weeklyPolicy);
providersPage.verifyChangedHourInput(newLdapHour, defaultLdapHour);
sidebarPage.goToUserFederation();
});
it("Should set cache policy to max_lifespan", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.selectCacheType(lifespanPolicy);
providersPage.fillMaxLifespanData(maxLifespan);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
expect(cy.contains(lifespanPolicy).should("exist"));
expect(cy.contains(weeklyPolicy).should("not.exist"));
});
it("Should set cache policy to no_cache", () => {
providersPage.clickExistingCard(firstLdapName);
providersPage.selectCacheType(noCachePolicy);
providersPage.save(provider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
providersPage.clickExistingCard(firstLdapName);
expect(cy.contains(noCachePolicy).should("exist"));
expect(cy.contains(lifespanPolicy).should("not.exist"));
});
it("Should disable an existing LDAP provider", () => {
providersPage.clickExistingCard(firstLdapName);
cy.wait("@getProvider");
providersPage.disableEnabledSwitch(allCapProvider);
modalUtils.checkModalTitle(disableModalTitle).confirmModal();
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
expect(cy.contains("Disabled").should("exist"));
});
it("Should enable a previously-disabled LDAP provider", () => {
providersPage.clickExistingCard(firstLdapName);
cy.wait("@getProvider");
providersPage.enableEnabledSwitch(allCapProvider);
masthead.checkNotificationMessage(savedSuccessMessage);
sidebarPage.goToUserFederation();
expect(cy.contains("Enabled").should("exist"));
});
it("Should create new LDAP provider using New Provider dropdown", () => {
providersPage.clickMenuCommand(addProviderMenu, allCapProvider);
providersPage.fillLdapGeneralData(secondLdapName, secondLdapVendor);
providersPage.fillLdapConnectionData(
connectionUrlValid,
bindTypeSimple,
truststoreSpiNever,
connectionTimeoutTwoSecs,
bindDnCnOnly,
bindCredsInvalid
);
providersPage.fillLdapSearchingData(
editModeWritable,
secondUsersDn,
secondUserLdapAtt,
secondRdnLdapAtt,
secondUuidLdapAtt,
secondUserObjClasses
);
providersPage.save(provider);
masthead.checkNotificationMessage(createdSuccessMessage);
sidebarPage.goToUserFederation();
});
it("Should delete LDAP provider from card view using card menu", () => {
providersPage.deleteCardFromCard(secondLdapName);
modalUtils.checkModalTitle(deleteModalTitle).confirmModal();
masthead.checkNotificationMessage(deletedSuccessMessage);
});
it("Should delete LDAP provider using Settings view Action menu", () => {
providersPage.deleteCardFromMenu(firstLdapName);
modalUtils.checkModalTitle(deleteModalTitle).confirmModal();
masthead.checkNotificationMessage(deletedSuccessMessage);
});
});

View File

@ -0,0 +1,445 @@
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import CreateUserPage from "../support/pages/admin-ui/manage/users/CreateUserPage";
import Masthead from "../support/pages/admin-ui/Masthead";
import ListingPage from "../support/pages/admin-ui/ListingPage";
import UserDetailsPage from "../support/pages/admin-ui/manage/users/user_details/UserDetailsPage";
import AttributesTab from "../support/pages/admin-ui/manage/AttributesTab";
import ModalUtils from "../support/util/ModalUtils";
import { keycloakBefore } from "../support/util/keycloak_hooks";
import UserGroupsPage from "../support/pages/admin-ui/manage/users/UserGroupsPage";
import adminClient from "../support/util/AdminClient";
import CredentialsPage from "../support/pages/admin-ui/manage/users/CredentialsPage";
import UsersPage from "../support/pages/admin-ui/manage/users/UsersPage";
import IdentityProviderLinksTab from "../support/pages/admin-ui/manage/users/user_details/tabs/IdentityProviderLinksTab";
let groupName = "group";
let groupsList: string[] = [];
describe("User creation", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const createUserPage = new CreateUserPage();
const userGroupsPage = new UserGroupsPage();
const masthead = new Masthead();
const modalUtils = new ModalUtils();
const listingPage = new ListingPage();
const userDetailsPage = new UserDetailsPage();
const credentialsPage = new CredentialsPage();
const attributesTab = new AttributesTab();
const usersPage = new UsersPage();
const identityProviderLinksTab = new IdentityProviderLinksTab();
let itemId = "user_crud";
let itemIdWithGroups = "user_with_groups_crud";
let itemIdWithCred = "user_crud_cred";
const itemCredential = "Password";
before(async () => {
for (let i = 0; i <= 2; i++) {
groupName += "_" + crypto.randomUUID();
await adminClient.createGroup(groupName);
groupsList = [...groupsList, groupName];
}
});
beforeEach(() => {
loginPage.logIn();
keycloakBefore();
sidebarPage.goToUsers();
});
after(() => adminClient.deleteGroups());
it("Go to create User page", () => {
createUserPage.goToCreateUser();
cy.url().should("include", "users/add-user");
// Verify Cancel button works
createUserPage.cancel();
cy.url().should("not.include", "/add-user");
});
it("Create user test", () => {
itemId += "_" + crypto.randomUUID();
// Create
createUserPage.goToCreateUser();
createUserPage.createUser(itemId);
createUserPage.save();
masthead.checkNotificationMessage("The user has been created");
});
it("Create user with groups test", () => {
itemIdWithGroups += crypto.randomUUID();
// Add user from search bar
createUserPage.goToCreateUser();
createUserPage.createUser(itemIdWithGroups);
createUserPage.toggleAddGroupModal();
const groupsListCopy = groupsList.slice(0, 1);
groupsListCopy.forEach((element) => {
cy.findByTestId(`${element}-check`).click();
});
createUserPage.joinGroups();
createUserPage.save();
masthead.checkNotificationMessage("The user has been created");
});
it("Create user with credentials test", () => {
itemIdWithCred += "_" + crypto.randomUUID();
// Add user from search bar
createUserPage.goToCreateUser();
createUserPage.createUser(itemIdWithCred);
userDetailsPage.fillUserData();
createUserPage.save();
masthead.checkNotificationMessage("The user has been created");
sidebarPage.waitForPageLoad();
credentialsPage
.goToCredentialsTab()
.clickEmptyStatePasswordBtn()
.fillPasswordForm()
.clickConfirmationBtn()
.clickSetPasswordBtn();
});
it("Search existing user test", () => {
listingPage.searchItem(itemId).itemExist(itemId);
});
it("Search non-existing user test", () => {
listingPage.searchItem("user_DNE");
cy.findByTestId(listingPage.emptyState).should("exist");
});
it("User details test", () => {
sidebarPage.waitForPageLoad();
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
userDetailsPage.fillUserData().save();
masthead.checkNotificationMessage("The user has been saved");
sidebarPage.waitForPageLoad();
sidebarPage.goToUsers();
listingPage.searchItem(itemId).itemExist(itemId);
});
it("User attributes test", () => {
listingPage.goToItemDetails(itemId);
attributesTab.goToAttributesTab().addAttribute("key", "value").save();
masthead.checkNotificationMessage("The user has been saved");
});
it("User attributes with multiple values test", () => {
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
cy.intercept("PUT", `/admin/realms/master/users/*`).as("save-user");
const attributeKey = "key-multiple";
attributesTab
.goToAttributesTab()
.addAttribute(attributeKey, "other value")
.save();
masthead.checkNotificationMessage("The user has been saved");
cy.wait("@save-user").should(({ request, response }) => {
expect(response?.statusCode).to.equal(204);
expect(request.body.attributes, "response body").deep.equal({
key: ["value"],
"key-multiple": ["other value"],
});
});
});
it("Add user to groups test", () => {
// Go to user groups
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
userGroupsPage.goToGroupsTab();
userGroupsPage.toggleAddGroupModal();
const groupsListCopy = groupsList.slice(0, 3);
groupsListCopy.forEach((element) => {
cy.findByTestId(`${element}-check`).click();
});
userGroupsPage.joinGroups();
});
it("Leave group test", () => {
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
// Go to user groups
userGroupsPage.goToGroupsTab();
cy.findByTestId(`leave-${groupsList[0]}`).click();
cy.findByTestId("confirm").click({ force: true });
});
it("search and leave group", () => {
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
userGroupsPage.goToGroupsTab();
listingPage.searchItem("group");
userGroupsPage.leaveGroupButtonDisabled();
listingPage.clickTableHeaderItemCheckboxAllRows();
userGroupsPage.leaveGroupButtonEnabled();
userGroupsPage.leaveGroup();
});
it("Go to user consents test", () => {
listingPage.searchItem(itemId).itemExist(itemId);
sidebarPage.waitForPageLoad();
listingPage.goToItemDetails(itemId);
cy.findByTestId("user-consents-tab").click();
cy.findByTestId("empty-state").contains("No consents");
});
describe("Identity provider links", () => {
const usernameIdpLinksTest = "user_idp_link_test";
const identityProviders = [
{ testName: "Bitbucket", displayName: "BitBucket", alias: "bitbucket" },
{ testName: "Facebook", displayName: "Facebook", alias: "facebook" },
{
testName: "Keycloak-oidc",
displayName: "Keycloak OpenID Connect",
alias: "keycloak-oidc",
},
];
before(async () => {
await Promise.all([
adminClient.createUser({
username: usernameIdpLinksTest,
enabled: true,
}),
identityProviders.forEach((idp) =>
adminClient.createIdentityProvider(idp.displayName, idp.alias)
),
]);
});
after(async () => {
await adminClient.deleteUser(usernameIdpLinksTest);
await Promise.all(
identityProviders.map((idp) =>
adminClient.deleteIdentityProvider(idp.alias)
)
);
});
beforeEach(() => {
usersPage.goToUserListTab().goToUserDetailsPage(usernameIdpLinksTest);
userDetailsPage.goToIdentityProviderLinksTab();
});
identityProviders.forEach(($idp, linkedIdpsCount) => {
it(`Link account to IdP: ${$idp.testName}`, () => {
const availableIdpsCount = identityProviders.length - linkedIdpsCount;
if (linkedIdpsCount == 0) {
identityProviderLinksTab.assertNoIdentityProvidersLinkedMessageExist(
true
);
}
identityProviderLinksTab
.assertAvailableIdentityProvidersItemsEqual(availableIdpsCount)
.clickLinkAccount($idp.testName)
.assertLinkAccountModalTitleEqual($idp.testName)
.assertLinkAccountModalIdentityProviderInputEqual($idp.testName)
.typeLinkAccountModalUserId("testUserId")
.typeLinkAccountModalUsername("testUsername")
.clickLinkAccountModalLinkBtn()
.assertNotificationIdentityProviderLinked()
.assertLinkedIdentityProvidersItemsEqual(linkedIdpsCount + 1)
.assertAvailableIdentityProvidersItemsEqual(availableIdpsCount - 1)
.assertLinkedIdentityProviderExist($idp.testName, true)
.assertAvailableIdentityProviderExist($idp.testName, false);
if (availableIdpsCount - 1 == 0) {
identityProviderLinksTab.assertNoAvailableIdentityProvidersMessageExist(
true
);
}
});
});
it("Link account to already added IdP fail", () => {
cy.wrap(null).then(() =>
adminClient.unlinkAccountIdentityProvider(
usernameIdpLinksTest,
identityProviders[0].displayName
)
);
sidebarPage.goToUsers();
usersPage.goToUserListTab().goToUserDetailsPage(usernameIdpLinksTest);
userDetailsPage.goToIdentityProviderLinksTab();
cy.wrap(null).then(() =>
adminClient.linkAccountIdentityProvider(
usernameIdpLinksTest,
identityProviders[0].displayName
)
);
identityProviderLinksTab
.clickLinkAccount(identityProviders[0].testName)
.assertLinkAccountModalTitleEqual(identityProviders[0].testName)
.assertLinkAccountModalIdentityProviderInputEqual(
identityProviders[0].testName
)
.typeLinkAccountModalUserId("testUserId")
.typeLinkAccountModalUsername("testUsername")
.clickLinkAccountModalLinkBtn()
.assertNotificationAlreadyLinkedError();
modalUtils.cancelModal();
});
identityProviders.forEach(($idp, availableIdpsCount) => {
it(`Unlink account from IdP: ${$idp.testName}`, () => {
const linkedIdpsCount = identityProviders.length - availableIdpsCount;
if (availableIdpsCount == 0) {
identityProviderLinksTab.assertNoAvailableIdentityProvidersMessageExist(
true
);
}
identityProviderLinksTab
.assertAvailableIdentityProvidersItemsEqual(availableIdpsCount)
.clickUnlinkAccount($idp.testName)
.assertUnLinkAccountModalTitleEqual($idp.testName)
.clickUnlinkAccountModalUnlinkBtn()
.assertNotificationPoviderLinkRemoved()
.assertLinkedIdentityProvidersItemsEqual(linkedIdpsCount - 1)
.assertAvailableIdentityProvidersItemsEqual(availableIdpsCount + 1)
.assertLinkedIdentityProviderExist($idp.testName, false)
.assertAvailableIdentityProviderExist($idp.testName, true);
if (linkedIdpsCount - 1 == 0) {
identityProviderLinksTab.assertNoIdentityProvidersLinkedMessageExist(
true
);
}
});
});
});
it("Reset credential of User with empty state", () => {
listingPage.goToItemDetails(itemId);
credentialsPage
.goToCredentialsTab()
.clickEmptyStateResetBtn()
.fillResetCredentialForm();
masthead.checkNotificationMessage(
"Failed: Failed to send execute actions email"
);
});
it("Reset credential of User with existing credentials", () => {
listingPage.goToItemDetails(itemIdWithCred);
credentialsPage
.goToCredentialsTab()
.clickResetBtn()
.fillResetCredentialForm();
masthead.checkNotificationMessage(
"Failed: Failed to send execute actions email"
);
});
it("Edit credential label", () => {
listingPage.goToItemDetails(itemIdWithCred);
credentialsPage
.goToCredentialsTab()
.clickEditCredentialLabelBtn()
.fillEditCredentialForm()
.clickEditConfirmationBtn();
masthead.checkNotificationMessage(
"The user label has been changed successfully."
);
});
it("Show credential data dialog", () => {
listingPage.goToItemDetails(itemIdWithCred);
credentialsPage
.goToCredentialsTab()
.clickShowDataDialogBtn()
.clickCloseDataDialogBtn();
});
it("Delete credential", () => {
listingPage.goToItemDetails(itemIdWithCred);
credentialsPage.goToCredentialsTab();
cy.wait(2000);
listingPage.deleteItem(itemCredential);
modalUtils.checkModalTitle("Delete credentials?").confirmModal();
masthead.checkNotificationMessage(
"The credentials has been deleted successfully."
);
});
it("Delete user from search bar test", () => {
// Delete
sidebarPage.waitForPageLoad();
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.deleteItemFromSearchBar(itemId);
modalUtils.checkModalTitle("Delete user?").confirmModal();
masthead.checkNotificationMessage("The user has been deleted");
sidebarPage.waitForPageLoad();
listingPage.itemExist(itemId, false);
});
it("Delete user with groups test", () => {
// Delete
listingPage.deleteItem(itemIdWithGroups);
modalUtils.checkModalTitle("Delete user?").confirmModal();
masthead.checkNotificationMessage("The user has been deleted");
sidebarPage.waitForPageLoad();
listingPage.itemExist(itemIdWithGroups, false);
});
it("Delete user with credential test", () => {
// Delete
listingPage.deleteItem(itemIdWithCred);
modalUtils.checkModalTitle("Delete user?").confirmModal();
masthead.checkNotificationMessage("The user has been deleted");
sidebarPage.waitForPageLoad();
listingPage.itemExist(itemIdWithCred, false);
});
});

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,4 @@
{
"port": 3004,
"searchBase": "dc=test"
}

View File

@ -0,0 +1,7 @@
[
{
"dn": "cn=user,dc=test",
"objectClass": "person",
"cn": "user-login"
}
]

View File

@ -0,0 +1,13 @@
{
"clients": [
{
"clientId": "customer-portal",
"enabled": true,
"adminUrl": "/customer-portal",
"baseUrl": "/customer-portal",
"redirectUris": [
"/customer-portal/*"
],
"secret": "password"
}]
}

View File

@ -0,0 +1,48 @@
{
"clientId": "identical",
"name": "",
"description": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"oidc.ciba.grant.enabled": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"backchannel.logout.revoke.offline.tokens": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"acr",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
[
{
"id": "realm1",
"realm": "realm1",
"clients": [
{
"clientId": "customer-portal",
"enabled": true,
"adminUrl": "/customer-portal",
"baseUrl": "/customer-portal",
"redirectUris": ["/customer-portal/*"],
"secret": "password"
}
],
"users": [
{
"username": "ssilvert",
"enabled": true,
"email": "ssilvert@redhat.com",
"firstName": "Stan",
"lastName": "Silvert",
"credentials": [{ "type": "password", "value": "password" }],
"realmRoles": ["user", "offline_access"],
"clientRoles": {
"account": ["manage-account"]
}
}
],
"groups": [
{
"id": "7a20471c-c695-4778-adb0-4ee4ae88d198",
"name": "group1",
"path": "/group1",
"attributes": {
"foo": ["bar"]
},
"realmRoles": ["create-realm"],
"clientRoles": {},
"subGroups": []
}
],
"identityProviders": [
{
"alias": "keycloak-oidc",
"internalId": "721b5dae-5b98-4284-bcd1-f872ce2ac174",
"providerId": "keycloak-oidc",
"enabled": true,
"updateProfileFirstLoginMode": "on",
"trustEmail": false,
"storeToken": false,
"addReadTokenRoleOnCreate": false,
"authenticateByDefault": false,
"config": {
"clientSecret": "foo",
"clientId": "foo",
"tokenUrl": "https://foo.bar",
"authorizationUrl": "https://foo.bar"
}
}
],
"roles": {
"realm": [
{
"id": "9d2638c8-4c62-4c42-90ea-5f3c836d0cc8",
"name": "offline_access",
"scopeParamRequired": false,
"composite": false
},
{
"id": "9d2638c8-4c62-4c42-90ea-5f3c836d0cc8",
"name": "another",
"scopeParamRequired": false,
"composite": false
}
],
"client": {
"realm-management": [
{
"id": "3b939f75-d013-4096-8462-48aa39261293",
"name": "create-client",
"description": "${role_create-client}",
"scopeParamRequired": false,
"composite": false
}
]
}
}
},
{
"id": "realm2",
"realm": "realm2",
"clients": [
{
"clientId": "customer-portal",
"enabled": true,
"adminUrl": "/customer-portal",
"baseUrl": "/customer-portal",
"redirectUris": ["/customer-portal/*"],
"secret": "password"
},
{
"clientId": "customer-portal2",
"enabled": true,
"adminUrl": "/customer-portal",
"baseUrl": "/customer-portal",
"redirectUris": ["/customer-portal/*"],
"secret": "password"
}
]
}
]

View File

@ -0,0 +1,28 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
import "@testing-library/cypress/add-commands";
import "@4tw/cypress-drag-drop";

Some files were not shown because too many files have changed in this diff Show More