mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 02:08:20 +02:00 
			
		
		
		
	* Add SSO functionality using OpenID Connect Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com> * Improvements and error handling * Stop rolling device token * Add playwright tests * Activate PKCE by default * Ensure result order when searching for sso_user * add SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION * Toggle SSO button in scss * Base64 encode state before sending it to providers * Prevent disabled User from SSO login * Review fixes * Remove unused UserOrganization.invited_by_email * Split SsoUser::find_by_identifier_or_email * api::Accounts::verify_password add the policy even if it's ignored * Disable signups if SSO_ONLY is activated * Add verifiedDate to organizations::get_org_domain_sso_details * Review fixes * Remove OrganizationId guard from get_master_password_policy * Add wrapper type OIDCCode OIDCState OIDCIdentifier * Membership::confirm_user_invitations fix and tests * Allow set-password only if account is unitialized * Review fixes * Prevent accepting another user invitation * Log password change event on SSO account creation * Unify master password policy resolution * Upgrade openidconnect to 4.0.0 * Revert "Remove unused UserOrganization.invited_by_email" This reverts commit 548e19995e141314af98a10d170ea7371f02fab4. * Process org enrollment in accounts::post_set_password * Improve tests * Pass the claim invited_by_email in case it was not in db * Add Slack configuration hints * Fix playwright tests * Skip broken tests * Add sso identifier in admin user panel * Remove duplicate expiration check, add a log * Augment mobile refresh_token validity * Rauthy configuration hints * Fix playwright tests * Playwright upgrade and conf improvement * Playwright tests improvements * 2FA email and device creation change * Fix and improve Playwright tests * Minor improvements * Fix enforceOnLogin org policies * Run playwright sso tests against correct db * PKCE should now work with Zitadel * Playwright upgrade maildev to use MailBuffer.expect * Upgrades playwright tests deps * Check email_verified in id_token and user_info * Add sso verified endpoint for v2025.6.0 * Fix playwright tests * Create a separate sso_client * Upgrade openidconnect to 4.0.1 * Server settings for login fields toggle * Use only css for login fields * Fix playwright test * Review fix * More review fix * Perform same checks when setting kdf --------- Co-authored-by: Felix Eckhofer <felix@eckhofer.com> Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com> Co-authored-by: Timshel <timshel@480s>
		
			
				
	
	
		
			247 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			247 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { expect, type Browser, type TestInfo } from '@playwright/test';
 | |
| import { EventEmitter } from "events";
 | |
| import { type Mail, MailServer } from 'maildev';
 | |
| import { execSync } from 'node:child_process';
 | |
| 
 | |
| import dotenv from 'dotenv';
 | |
| import dotenvExpand from 'dotenv-expand';
 | |
| 
 | |
| const fs = require("fs");
 | |
| const { spawn } = require('node:child_process');
 | |
| 
 | |
| export function loadEnv(){
 | |
|     var myEnv = dotenv.config({ path: 'test.env' });
 | |
|     dotenvExpand.expand(myEnv);
 | |
| 
 | |
|     return {
 | |
|         user1: {
 | |
|             email: process.env.TEST_USER_MAIL,
 | |
|             name: process.env.TEST_USER,
 | |
|             password: process.env.TEST_USER_PASSWORD,
 | |
|         },
 | |
|         user2: {
 | |
|             email: process.env.TEST_USER2_MAIL,
 | |
|             name: process.env.TEST_USER2,
 | |
|             password: process.env.TEST_USER2_PASSWORD,
 | |
|         },
 | |
|         user3: {
 | |
|             email: process.env.TEST_USER3_MAIL,
 | |
|             name: process.env.TEST_USER3,
 | |
|             password: process.env.TEST_USER3_PASSWORD,
 | |
|         },
 | |
|     }
 | |
| }
 | |
| 
 | |
| export async function waitFor(url: String, browser: Browser) {
 | |
|     var ready = false;
 | |
|     var context;
 | |
| 
 | |
|     do {
 | |
|         try {
 | |
|             context = await browser.newContext();
 | |
|             const page = await context.newPage();
 | |
|             await page.waitForTimeout(500);
 | |
|             const result = await page.goto(url);
 | |
|             ready = result.status() === 200;
 | |
|         } catch(e) {
 | |
|             if( !e.message.includes("CONNECTION_REFUSED") ){
 | |
|                 throw e;
 | |
|             }
 | |
|         } finally {
 | |
|             await context.close();
 | |
|         }
 | |
|     } while(!ready);
 | |
| }
 | |
| 
 | |
| export function startComposeService(serviceName: String){
 | |
|     console.log(`Starting ${serviceName}`);
 | |
|     execSync(`docker compose --profile playwright --env-file test.env  up -d ${serviceName}`);
 | |
| }
 | |
| 
 | |
| export function stopComposeService(serviceName: String){
 | |
|     console.log(`Stopping ${serviceName}`);
 | |
|     execSync(`docker compose --profile playwright --env-file test.env  stop ${serviceName}`);
 | |
| }
 | |
| 
 | |
| function wipeSqlite(){
 | |
|     console.log(`Delete Vaultwarden container to wipe sqlite`);
 | |
|     execSync(`docker compose --env-file test.env stop Vaultwarden`);
 | |
|     execSync(`docker compose --env-file test.env rm -f Vaultwarden`);
 | |
| }
 | |
| 
 | |
| async function wipeMariaDB(){
 | |
|     var mysql = require('mysql2/promise');
 | |
|     var ready = false;
 | |
|     var connection;
 | |
| 
 | |
|     do {
 | |
|         try {
 | |
|             connection = await mysql.createConnection({
 | |
|                 user: process.env.MARIADB_USER,
 | |
|                 host: "127.0.0.1",
 | |
|                 database: process.env.MARIADB_DATABASE,
 | |
|                 password: process.env.MARIADB_PASSWORD,
 | |
|                 port: process.env.MARIADB_PORT,
 | |
|             });
 | |
| 
 | |
|             await connection.execute(`DROP DATABASE ${process.env.MARIADB_DATABASE}`);
 | |
|             await connection.execute(`CREATE DATABASE ${process.env.MARIADB_DATABASE}`);
 | |
|             console.log('Successfully wiped mariadb');
 | |
|             ready = true;
 | |
|         } catch (err) {
 | |
|             console.log(`Error when wiping mariadb: ${err}`);
 | |
|         } finally {
 | |
|             if( connection ){
 | |
|                 connection.end();
 | |
|             }
 | |
|         }
 | |
|         await new Promise(r => setTimeout(r, 1000));
 | |
|     } while(!ready);
 | |
| }
 | |
| 
 | |
| async function wipeMysqlDB(){
 | |
|     var mysql = require('mysql2/promise');
 | |
|     var ready = false;
 | |
|     var connection;
 | |
| 
 | |
|     do{
 | |
|         try {
 | |
|             connection = await mysql.createConnection({
 | |
|                 user: process.env.MYSQL_USER,
 | |
|                 host: "127.0.0.1",
 | |
|                 database: process.env.MYSQL_DATABASE,
 | |
|                 password: process.env.MYSQL_PASSWORD,
 | |
|                 port: process.env.MYSQL_PORT,
 | |
|             });
 | |
| 
 | |
|             await connection.execute(`DROP DATABASE ${process.env.MYSQL_DATABASE}`);
 | |
|             await connection.execute(`CREATE DATABASE ${process.env.MYSQL_DATABASE}`);
 | |
|             console.log('Successfully wiped mysql');
 | |
|             ready = true;
 | |
|         } catch (err) {
 | |
|             console.log(`Error when wiping mysql: ${err}`);
 | |
|         } finally {
 | |
|             if( connection ){
 | |
|                 connection.end();
 | |
|             }
 | |
|         }
 | |
|         await new Promise(r => setTimeout(r, 1000));
 | |
|     } while(!ready);
 | |
| }
 | |
| 
 | |
| async function wipePostgres(){
 | |
|     const { Client } = require('pg');
 | |
| 
 | |
|     const client = new Client({
 | |
|         user: process.env.POSTGRES_USER,
 | |
|         host: "127.0.0.1",
 | |
|         database: "postgres",
 | |
|         password: process.env.POSTGRES_PASSWORD,
 | |
|         port: process.env.POSTGRES_PORT,
 | |
|     });
 | |
| 
 | |
|     try {
 | |
|         await client.connect();
 | |
|         await client.query(`DROP DATABASE ${process.env.POSTGRES_DB}`);
 | |
|         await client.query(`CREATE DATABASE ${process.env.POSTGRES_DB}`);
 | |
|         console.log('Successfully wiped postgres');
 | |
|     } catch (err) {
 | |
|         console.log(`Error when wiping postgres: ${err}`);
 | |
|     } finally {
 | |
|         client.end();
 | |
|     }
 | |
| }
 | |
| 
 | |
| function dbConfig(testInfo: TestInfo){
 | |
|     switch(testInfo.project.name) {
 | |
|         case "postgres":
 | |
|         case "sso-postgres":
 | |
|             return { DATABASE_URL: `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@127.0.0.1:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DB}` };
 | |
|         case "mariadb":
 | |
|         case "sso-mariadb":
 | |
|             return { DATABASE_URL: `mysql://${process.env.MARIADB_USER}:${process.env.MARIADB_PASSWORD}@127.0.0.1:${process.env.MARIADB_PORT}/${process.env.MARIADB_DATABASE}` };
 | |
|         case "mysql":
 | |
|         case "sso-mysql":
 | |
|             return { DATABASE_URL: `mysql://${process.env.MYSQL_USER}:${process.env.MYSQL_PASSWORD}@127.0.0.1:${process.env.MYSQL_PORT}/${process.env.MYSQL_DATABASE}`};
 | |
|         case "sqlite":
 | |
|         case "sso-sqlite":
 | |
|             return { I_REALLY_WANT_VOLATILE_STORAGE: true };
 | |
|         default:
 | |
|             throw new Error(`Unknow database name: ${testInfo.project.name}`);
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *  All parameters passed in `env` need to be added to the docker-compose.yml
 | |
|  **/
 | |
| export async function startVault(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) {
 | |
|     if( resetDB ){
 | |
|         switch(testInfo.project.name) {
 | |
|             case "postgres":
 | |
|             case "sso-postgres":
 | |
|                 await wipePostgres();
 | |
|                 break;
 | |
|             case "mariadb":
 | |
|             case "sso-mariadb":
 | |
|                 await wipeMariaDB();
 | |
|                 break;
 | |
|             case "mysql":
 | |
|             case "sso-mysql":
 | |
|                 await wipeMysqlDB();
 | |
|                 break;
 | |
|             case "sqlite":
 | |
|             case "sso-sqlite":
 | |
|                 wipeSqlite();
 | |
|                 break;
 | |
|             default:
 | |
|                 throw new Error(`Unknow database name: ${testInfo.project.name}`);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     console.log(`Starting Vaultwarden`);
 | |
|     execSync(`docker compose --profile playwright --env-file test.env up -d Vaultwarden`, {
 | |
|         env: { ...env, ...dbConfig(testInfo) },
 | |
|     });
 | |
|     await waitFor("/", browser);
 | |
|     console.log(`Vaultwarden running on: ${process.env.DOMAIN}`);
 | |
| }
 | |
| 
 | |
| export async function stopVault(force: boolean = false) {
 | |
|     if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
 | |
|         console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
 | |
|     } else {
 | |
|         console.log(`Vaultwarden stopping`);
 | |
|         execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`);
 | |
|     }
 | |
| }
 | |
| 
 | |
| export async function restartVault(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) {
 | |
|     stopVault(true);
 | |
|     return startVault(page.context().browser(), testInfo, env, resetDB);
 | |
| }
 | |
| 
 | |
| export async function checkNotification(page: Page, hasText: string) {
 | |
|     await expect(page.locator('bit-toast').filter({ hasText })).toBeVisible();
 | |
|     await page.locator('bit-toast').filter({ hasText }).getByRole('button').click();
 | |
|     await expect(page.locator('bit-toast').filter({ hasText })).toHaveCount(0);
 | |
| }
 | |
| 
 | |
| export async function cleanLanding(page: Page) {
 | |
|     await page.goto('/', { waitUntil: 'domcontentloaded' });
 | |
|     await expect(page.getByRole('button').nth(0)).toBeVisible();
 | |
| 
 | |
|     const logged = await page.getByRole('button', { name: 'Log out' }).count();
 | |
|     if( logged > 0 ){
 | |
|         await page.getByRole('button', { name: 'Log out' }).click();
 | |
|         await page.getByRole('button', { name: 'Log out' }).click();
 | |
|     }
 | |
| }
 | |
| 
 | |
| export async function logout(test: Test, page: Page, user: { name: string }) {
 | |
|     await test.step('logout', async () => {
 | |
|         await page.getByRole('button', { name: user.name, exact: true }).click();
 | |
|         await page.getByRole('menuitem', { name: 'Log out' }).click();
 | |
|         await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();
 | |
|     });
 | |
| }
 |