From b263cdd25d0bf3920f242c476a520d9f1e56e014 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 10 Sep 2024 17:03:43 +0200 Subject: [PATCH] Add ORGANIZATION_INVITE_AUTO_ACCEPT --- .env.template | 3 ++ SSO.md | 1 + playwright/docker-compose.yml | 1 + playwright/tests/sso_groups.spec.ts | 38 ++++++++++++++++++ playwright/tests/sso_organization.spec.ts | 39 +++++++++++++++++++ src/api/core/organizations.rs | 1 + src/business/organization_logic.rs | 12 ++++-- src/config.rs | 4 ++ src/mail.rs | 12 ++++++ src/sso.rs | 1 + .../templates/email/send_org_enrolled.hbs | 7 ++++ .../email/send_org_enrolled.html.hbs | 16 ++++++++ 12 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/static/templates/email/send_org_enrolled.hbs create mode 100644 src/static/templates/email/send_org_enrolled.html.hbs diff --git a/.env.template b/.env.template index 8cb928dc95..7ec94cc70c 100644 --- a/.env.template +++ b/.env.template @@ -293,6 +293,9 @@ ## unauthenticated access to potentially sensitive data. # SHOW_PASSWORD_HINT=false +## Auto accept Organization invitation +# ORGANIZATION_INVITE_AUTO_ACCEPT=false + ######################### ### Advanced settings ### ######################### diff --git a/SSO.md b/SSO.md index 8ae8b706f8..beb4d033ae 100644 --- a/SSO.md +++ b/SSO.md @@ -34,6 +34,7 @@ The following configurations are available - `SSO_ORGANIZATIONS_TOKEN_PATH`: path to read groups/organization in the Id token - `SSO_ORGANIZATIONS_ID_MAPPING`: Optional, allow to map provider group to a Vaultwarden organization `uuid` (default `""`, format: `"ProviderId:VaultwardenId;"`) - `SSO_ORGANIZATIONS_ALL_COLLECTIONS`: Grant access to all collections, default is `true` + - `ORGANIZATION_INVITE_AUTO_ACCEPT`: Bypass the invitation logic and as users as `Accepted` (Apply to non SSO logic too) - `SSO_CLIENT_CACHE_EXPIRATION`: Cache calls to the discovery endpoint, duration in seconds, `0` to disable (default `0`); - `SSO_DEBUG_TOKENS`: Log all tokens for easier debugging (default `false`, `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` need to be set) diff --git a/playwright/docker-compose.yml b/playwright/docker-compose.yml index a33930f2e1..f7fe2a2913 100644 --- a/playwright/docker-compose.yml +++ b/playwright/docker-compose.yml @@ -24,6 +24,7 @@ services: environment: - DATABASE_URL - I_REALLY_WANT_VOLATILE_STORAGE + - ORGANIZATION_INVITE_AUTO_ACCEPT - SMTP_HOST - SMTP_FROM - SMTP_DEBUG diff --git a/playwright/tests/sso_groups.spec.ts b/playwright/tests/sso_groups.spec.ts index d600ef38e8..ec6f2b5edb 100644 --- a/playwright/tests/sso_groups.spec.ts +++ b/playwright/tests/sso_groups.spec.ts @@ -52,3 +52,41 @@ test('User auto invite', async ({ context, page }) => { mail2Buffer.close(); } }); + +test('Org invite auto accept', async ({ context, page }, testInfo: TestInfo) => { + test.setTimeout(40000); + let mail1Buffer = mailServer.buffer(users.user1.email); + let mail2Buffer = mailServer.buffer(users.user2.email); + try { + await utils.restartVaultwarden(page, testInfo, { + ORGANIZATION_INVITE_AUTO_ACCEPT: true, + SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM, + SMTP_HOST: process.env.MAILDEV_HOST, + SSO_ENABLED: true, + SSO_FRONTEND: "override", + SSO_ONLY: true, + SSO_ORGANIZATIONS_INVITE: true, + SSO_SCOPES: "email profile groups", + }, true); + + await logNewUser(test, page, users.user1, { mailBuffer: mail1Buffer, override: true }); + + await test.step('Create Org', async () => { + await page.getByRole('link', { name: 'New organisation' }).click(); + await page.getByLabel('Organisation name (required)').fill('Test'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); + }); + + await test.step('Invite user2', async () => { + await context.clearCookies(); + await logNewUser(test, page, users.user2, { mailBuffer: mail2Buffer, override: true }); + + await expect(mail2Buffer.next((m) => m.subject === "Enrolled in Test")).resolves.toBeDefined(); + await expect(mail1Buffer.next((m) => m.subject === "Invitation to Test accepted")).resolves.toBeDefined(); + }); + } finally { + mail1Buffer.close(); + mail2Buffer.close(); + } +}); diff --git a/playwright/tests/sso_organization.spec.ts b/playwright/tests/sso_organization.spec.ts index d7578f8dec..b482812dfc 100644 --- a/playwright/tests/sso_organization.spec.ts +++ b/playwright/tests/sso_organization.spec.ts @@ -140,3 +140,42 @@ test('invited with new account', async ({ page }) => { await expect(mail1Buffer.next((m) => m.subject === "Invitation to Test accepted")).resolves.toBeDefined(); }); }); + +test('Org invite auto accept', async ({ page }, testInfo: TestInfo) => { + test.setTimeout(40000); + + await utils.restartVaultwarden(page, testInfo, { + ORGANIZATION_INVITE_AUTO_ACCEPT: true, + SMTP_HOST: process.env.MAILDEV_HOST, + SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM, + SSO_ENABLED: true, + SSO_FRONTEND: "override", + }, true); + + await logNewUser(test, page, users.user1, { mailBuffer: mail1Buffer, override: true }); + + await test.step('Create Org', async () => { + await page.getByRole('link', { name: 'New organisation' }).click(); + await page.getByLabel('Organisation name (required)').fill('Test'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); + }); + + await test.step('Invite user2', async () => { + await page.getByRole('button', { name: 'Invite member' }).click(); + await page.getByLabel('Email (required)').fill(users.user2.email); + await page.getByRole('tab', { name: 'Collections' }).click(); + await page.locator('label').filter({ hasText: 'Grant access to all current' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText('User(s) invited'); + }); + + await expect(page.getByText('Needs confirmation', { exact: true })).toBeVisible(); + + await page.getByRole('row', { name: users.user2.email }).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Confirm' }).click(); + + await expect(page.getByTestId("toast-title")).toHaveText('Failed to confirm user'); + + await expect(mail2Buffer.next((m) => m.subject === "Enrolled in Test")).resolves.toBeDefined(); +}); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index e63901982f..2b16354898 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -963,6 +963,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders data.access_all, &collections, headers.user.email.clone(), + false, &mut conn, ) .await?; diff --git a/src/business/organization_logic.rs b/src/business/organization_logic.rs index f53ef811aa..8e76ab7280 100644 --- a/src/business/organization_logic.rs +++ b/src/business/organization_logic.rs @@ -17,12 +17,13 @@ pub async fn invite( access_all: bool, collections: &Vec, invited_by_email: String, + auto: bool, conn: &mut DbConn, ) -> ApiResult<()> { let mut user_org_status = UserOrgStatus::Invited; - // automatically accept existing users if mail is disabled - if !user.password_hash.is_empty() && !CONFIG.mail_enabled() { + // automatically accept existing users if mail is disabled or config if set + if !CONFIG.mail_enabled() || CONFIG.organization_invite_auto_accept() { user_org_status = UserOrgStatus::Accepted; } @@ -73,7 +74,12 @@ pub async fn invite( ) .await? } - UserOrgStatus::Accepted => mail::send_invite_accepted(&user.email, &invited_by_email, &org.name).await?, + UserOrgStatus::Accepted => { + mail::send_enrolled(&user.email, &org.name).await?; + if auto { + mail::send_invite_accepted(&user.email, &invited_by_email, &org.name).await?; + } + } UserOrgStatus::Revoked | UserOrgStatus::Confirmed => (), } } diff --git a/src/config.rs b/src/config.rs index fd3aa4b78b..fcf00828b3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -508,6 +508,9 @@ make_config! { /// because this provides unauthenticated access to potentially sensitive data. show_password_hint: bool, true, def, false; + /// Organization invitation auto accept |> Activated if email is disabled + organization_invite_auto_accept: bool, true, def, false; + /// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session! admin_token: Pass, true, option; @@ -1518,6 +1521,7 @@ where reg!("email/pw_hint_some", ".html"); reg!("email/send_2fa_removed_from_org", ".html"); reg!("email/send_emergency_access_invite", ".html"); + reg!("email/send_org_enrolled", ".html"); reg!("email/send_org_invite", ".html"); reg!("email/send_single_org_removed_from_org", ".html"); reg!("email/set_password", ".html"); diff --git a/src/mail.rs b/src/mail.rs index 97face07f9..c0379fdc1b 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -308,6 +308,18 @@ pub async fn send_invite( send_email(&user.email, &subject, body_html, body_text).await } +pub async fn send_enrolled(user_email: &str, org_name: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/send_org_enrolled", + json!({ + "img_src": CONFIG._smtp_img_src(), + "org_name": org_name, + }), + )?; + + send_email(user_email, &subject, body_html, body_text).await +} + pub async fn send_emergency_access_invite( address: &str, uuid: &str, diff --git a/src/sso.rs b/src/sso.rs index e30a8e6454..3f38480230 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -693,6 +693,7 @@ pub async fn sync_groups( CONFIG.sso_organizations_all_collections(), &org_collections, org.billing_email.clone(), + true, conn, ) .await?; diff --git a/src/static/templates/email/send_org_enrolled.hbs b/src/static/templates/email/send_org_enrolled.hbs new file mode 100644 index 0000000000..5bfe42a5a9 --- /dev/null +++ b/src/static/templates/email/send_org_enrolled.hbs @@ -0,0 +1,7 @@ +Enrolled in {{{org_name}}} + +You have been enrolled in the *{{org_name}}* organization. + +An admin still need to confirm your account before you can access it. + +{{> email/email_footer_text }} diff --git a/src/static/templates/email/send_org_enrolled.html.hbs b/src/static/templates/email/send_org_enrolled.html.hbs new file mode 100644 index 0000000000..fd10e9d4dc --- /dev/null +++ b/src/static/templates/email/send_org_enrolled.html.hbs @@ -0,0 +1,16 @@ +Enrolled in {{{org_name}}} + +{{> email/email_header }} + + + + + + + +
+ You have been enrolled in the {{org_name}} organization. +
+ An admin still need to confirm your account before you can access it. +
+{{> email/email_footer }}