Skip to content

Commit

Permalink
Add ORGANIZATION_INVITE_AUTO_ACCEPT
Browse files Browse the repository at this point in the history
  • Loading branch information
Timshel committed Nov 19, 2024
1 parent 2e1b6cd commit ff3bf2a
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
#########################
Expand Down
1 change: 1 addition & 0 deletions SSO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions playwright/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
environment:
- DATABASE_URL
- I_REALLY_WANT_VOLATILE_STORAGE
- ORGANIZATION_INVITE_AUTO_ACCEPT
- SMTP_HOST
- SMTP_FROM
- SMTP_DEBUG
Expand Down
38 changes: 38 additions & 0 deletions playwright/tests/sso_groups.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
39 changes: 39 additions & 0 deletions playwright/tests/sso_organization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
1 change: 1 addition & 0 deletions src/api/core/organizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,7 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
data.access_all,
&collections,
headers.user.email.clone(),
false,
&mut conn,
)
.await?;
Expand Down
12 changes: 9 additions & 3 deletions src/business/organization_logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ pub async fn invite(
access_all: bool,
collections: &Vec<CollectionData>,
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;
}

Expand Down Expand Up @@ -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 => (),
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand Down
12 changes: 12 additions & 0 deletions src/mail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/sso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ pub async fn sync_groups(
CONFIG.sso_organizations_all_collections(),
&org_collections,
org.billing_email.clone(),
true,
conn,
)
.await?;
Expand Down
7 changes: 7 additions & 0 deletions src/static/templates/email/send_org_enrolled.hbs
Original file line number Diff line number Diff line change
@@ -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 }}
16 changes: 16 additions & 0 deletions src/static/templates/email/send_org_enrolled.html.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Enrolled in {{{org_name}}}
<!---------------->
{{> email/email_header }}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
You have been enrolled in the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> organization.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
An admin still need to confirm your account before you can access it.
</td>
</tr>
</table>
{{> email/email_footer }}

0 comments on commit ff3bf2a

Please sign in to comment.