Browse Source
When the "Centralise organisation ownership" (PersonalOwnership) policy is active, a non-admin/owner member can't own personal items and must create organization items instead. The official web-vault already gates the Import destination on this: it offers an org as an import target if the member has `manage: true` on at least one of its collections in their sync data — but Vaultwarden's Collection::to_json_details() only ever set `manage: true` for members with the org-level "Manager" role, ignoring the actual per-collection Manage permission granted to a plain "User" role member. That left the whole Import form disabled for exactly the members this policy is meant to still let use it. - collection.rs: compute `manage` from the per-collection flag (or group grant) directly, independent of the member's org role. Keeps the existing Manager-with-full-access fallback. - organizations.rs (post_org_import): require the same "Manage" permission (not just write access) for existing target collections, and reject the whole import up front if a member without full org access would leave any item unassigned to a collection they can manage — no cipher is created before that's confirmed. - Adds a Playwright test (against a real HTTPS-enabled Vaultwarden + the current official web-vault) proving the import destination, collection, file format and content fields are enabled for such a member, with the org's collection pre-selected instead of the personal vault. Depends on the HTTPS/selector fixes on playwright-https-and-selector-fixes. - Adds unit tests for the new per-cipher-collection-assignment check. Security: this only widens who can be offered as an import target in line with permissions Vaultwarden already enforces elsewhere for manual collection management (is_manageable_by_user); it does not change who is authorized to write to a collection, and the personal-ownership policy itself is unaffected.pull/7559/head
3 changed files with 159 additions and 6 deletions
@ -0,0 +1,80 @@ |
|||
import { test, expect, type TestInfo } from '@playwright/test'; |
|||
|
|||
import * as utils from "../global-utils"; |
|||
import * as orgs from './setups/orgs'; |
|||
import { createAccount, logUser } from './setups/user'; |
|||
|
|||
let users = utils.loadEnv(); |
|||
|
|||
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { |
|||
await utils.startVault(browser, testInfo); |
|||
}); |
|||
|
|||
test.afterAll('Teardown', async ({}) => { |
|||
utils.stopVault(); |
|||
}); |
|||
|
|||
test('A member with Manage permission on a collection can import into it under the ownership policy', async ({ page }) => { |
|||
test.setTimeout(300_000); |
|||
|
|||
// The member account has to exist before being invited (no SMTP configured for this suite).
|
|||
await createAccount(test, page, users.user2); |
|||
await createAccount(test, page, users.user1); |
|||
|
|||
await orgs.create(test, page, 'ImportOrg'); |
|||
|
|||
await test.step('Create a managed and a locked collection', async () => { |
|||
await page.getByRole('button', { name: 'New', exact: true }).click(); |
|||
await page.getByRole('menuitem', { name: 'Collection' }).click(); |
|||
await page.getByRole('textbox', { name: 'Name * (required)', exact: true }).fill('Managed'); |
|||
await page.getByRole('button', { name: 'Save' }).click(); |
|||
await utils.checkNotification(page, 'Created collection Managed'); |
|||
|
|||
await page.getByRole('button', { name: 'New', exact: true }).click(); |
|||
await page.getByRole('menuitem', { name: 'Collection' }).click(); |
|||
await page.getByRole('textbox', { name: 'Name * (required)', exact: true }).fill('Locked'); |
|||
await page.getByRole('button', { name: 'Save' }).click(); |
|||
await utils.checkNotification(page, 'Created collection Locked'); |
|||
}); |
|||
|
|||
await test.step('Enable the organisation ownership policy', async () => { |
|||
await orgs.policies(test, page, 'ImportOrg'); |
|||
await page.getByRole('button', { name: /^Centralise organisation ownership/ }).click(); |
|||
await page.getByRole('checkbox', { name: 'Turn on' }).check(); |
|||
await page.getByRole('button', { name: 'Save' }).click(); |
|||
}); |
|||
|
|||
await orgs.members(test, page, 'ImportOrg'); |
|||
await test.step(`Invite ${users.user2.email} with Manage collection on Managed only`, async () => { |
|||
await page.getByRole('button', { name: 'Invite member' }).click(); |
|||
await page.getByRole('textbox', { name: 'Email * (required)', exact: true }).fill(users.user2.email); |
|||
await page.getByRole('tab', { name: 'Collections' }).click(); |
|||
await page.getByRole('combobox', { name: 'Permission' }).click(); |
|||
await page.getByRole('option', { name: 'Manage collection', exact: true }).click(); |
|||
await page.getByRole('combobox', { name: 'Select collections' }).click(); |
|||
await page.getByLabel('Options List').getByText('Managed', { exact: true }).click(); |
|||
await page.getByRole('columnheader', { name: 'Collection', exact: true }).click(); |
|||
await page.getByRole('button', { name: 'Save' }).click(); |
|||
await utils.checkNotification(page, 'User(s) invited'); |
|||
}); |
|||
|
|||
await orgs.confirm(test, page, 'ImportOrg', users.user2.email); |
|||
|
|||
await logUser(test, page, users.user2); |
|||
|
|||
await test.step('The import destination and file fields are enabled for the managed collection', async () => { |
|||
await page.goto('/#/tools/import'); |
|||
|
|||
const vaultSelect = page.getByRole('combobox', { name: /^Vault/ }); |
|||
await expect(vaultSelect).toBeEnabled(); |
|||
// The member has exactly one org they can manage a collection in, so it's preselected
|
|||
// instead of "My vault".
|
|||
await expect(page.getByText('ImportOrg', { exact: true })).toBeVisible(); |
|||
|
|||
const collectionSelect = page.getByRole('combobox', { name: 'Collection' }); |
|||
await expect(collectionSelect).toBeEnabled(); |
|||
|
|||
await expect(page.getByRole('combobox', { name: /^File format/ })).toBeEnabled(); |
|||
await expect(page.getByRole('textbox', { name: /or copy\/paste the import file contents/ })).toBeEnabled(); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue