`src/api/core/ciphers.rs:170` comment said "similar to the the
userDecryptionOptions" -> "similar to the userDecryptionOptions".
Comment-only.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Follow-up to the previous two commits, all four issues were introduced by them:
- The legacy `access_all`-group authority was accepted by
`caller_may_grant_collection_manage`, so a Custom member with manageGroups +
editAnyCollection could create an access_all group holding itself, use the
derived authority to persist `collections_groups.manage`, then leave the group
and keep collection deletion without ever holding deleteAnyCollection. The
clamp now takes only a real stored grant again, and the legacy exception is
limited to members holding none of the three collection permissions -- which
is the shape the migration leaves a group-derived Manager in, and never a
member who can create such a group in the first place.
- `Collection::to_json_details` reported `manage: false` for a member reaching
every collection even when a real grant existed, so the client rendered
controls that disagree with the guards. Both the cached and the uncached path
now read the effective grant.
- The SQLite rollback script continued after a failed statement, so a second run
dropped `users_organizations` and committed an empty replacement. It now sets
`.bail on`, refuses up front unless the permission columns are present, and
the README documents `-bail` instead of claiming the run is atomic regardless.
- The downgrade acknowledgement was never consumed, so one approval authorized
every later revert. It is now dropped by the revert it permits and cleared
again on re-upgrade.
- SQLite: the access_all drop used ALTER TABLE ... DROP COLUMN, which needs
SQLite 3.35 while a sqlite_system build links whatever the host provides and
libsqlite3-sys accepts 3.34.1. Use the portable table rebuild instead.
- Preflight: it returned Proceed for every database carrying the repair
migration. Both the access_all drop and the access-permission columns run
after that repair, so their interrupted MySQL/MariaDB states went undetected
and every restart failed with 1091/1060. Check schema and ledger after the
repair too, complete a recorded-drop-without-ledger in place, and refuse the
remaining states with a recovery path.
- Keep deriving a legacy Manager's collection edit/delete from the
organization-local access_all group it came from, rather than dropping the
authority when the repair removes its 0/1/1 copy. Stays revocable with the
group; collection creation remains the independent permission.
- post_org_import: Bitwarden does not require accessImportExport either, it
authorizes on that permission *or* per-collection create/import authority.
* Update GHA and pre-commit
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update admin diagnostics
Added a check if the templates are overridden and return which specific folder, `admin`, `email` or `scss`.
This way we could more quickly point users to possible outdated templates which they are using.
Also updated the Support String to use some emojis so we should be able to quicker see if there is something wrong.
Just checking `true` or `false` could be difficult sometimes, and sometimes what we had as `false` wasn't bad either.
Also adjusted the eslint comments so it will work with the latest version of eslint.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix updating collections for a cipher
The newer clients expect a `cipherDetails` response on the `collections-admin` endpoints.
Without it, the client will cause an error and stops handling the update correctly.
This will fix this by returning the cipher json.
Fixes#7545Fixes#7546
Signed-off-by: BlackDex <black.dex@gmail.com>
* Cache CSS file in a different way
Currently we set a cache ttl of 24 hours, and users need to do a force refresh if there is anything changed to the CSS file.
In the past we have had several issue reported which were related to a still cached CSS file.
This commit will change the caching and also cache the generated CSS file in memory.
Instead of letting the browser cache it for 24 hours we generate an ETag, this is just a hash of the contents.
This ETag is returned by the browser during a request, and we can match this, and if so, just return a `304` `Not Modified`.
If the ETag is not known, we return the new content.
This should make simple refreshes by clients get updated settings or a new version of Vaultwarden which has other CSS entries get updated instantly.
If a user does a hard refresh, we will not receive the ETag and the content will be served.
The same goes if someone has the `reload_templates` feature enabled, since then we should not cache anyway.
If someone adjust settings via the `/admin` interface, the cache will be invalidated and a new CSS will be generated.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix showing events for a specific user
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates and adjust code.
- Updated opendal and adjusted code where needed.
- Updated yubico_ng and adjusted code where needed.
This version now supports using an own HttpClient and it pulls in no reqwest dependency anymore.
Now it will use our own client which uses custom hickory DNS and other features.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update web-vault to v2026.7.0
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix hadolint warnings
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
- Migrate legacy `access_all` Users losslessly into per-collection
assignments instead of refusing to start the server.
- Keep provisioning of Admin/Owner memberships Owner-only (invite,
confirm, delete); state changes stay as they were.
- Drop the new organization-import gate; the import stays authorized per
target collection, and now requires a confirmed membership.
- Scope `accessReports` to the caller's own collections instead of
handing out every cipher in the organization.
- Let `createNewCollections` read the collection list it needs.
- Reject group changes a caller may not make instead of silently
dropping them.
- Detect partially applied permission-column migrations on MySQL.
- Drop the migration bookkeeping table and remove dead code.
The client event whitelist no longer drops the newer Bitwarden item-type and
organization events that the previous range check still logged. The admin
console collection lists echo a plain User's stored users_collections.manage
bit again, so saving an unrelated dialog no longer strips it.
Clippy (with --all-targets) reported two issues introduced by this branch:
events.rs:356 items_after_test_module
events.rs:395 redundant_clone
Move the test module to the end of the file and drop the redundant clone.
The relocation is content-identical; the only semantic change is the removed
`org_id.clone()`, which was the last use of the binding. Note that CI runs
clippy without --all-targets, so neither of these was failing the pipeline.
Remove `AccessReportsHeaders` and its `can_access_reports` helper. The guard
was never applied to any route and could not be: Vaultwarden has no
server-side report endpoints, because the clients compute every report
locally from the organization cipher list. `accessReports` is therefore
enforced inline in `get_org_details`, where that list is actually served. A
note at both former sites records this, so the guard is not reintroduced and
an endpoint is not gated on "may call reports" instead of "may read these
ciphers".
Document the resulting authorization semantics above `get_org_details`:
the endpoint serializes with CipherSyncType::Organization, which skips the
per-cipher access restrictions, so readOnly/hidePasswords are not applied and
collection assignments are ignored. `accessReports` is by design a full
organization read permission. This matches Bitwarden's CanAccessAllCiphersAsync,
which admits Owner/Admin plus Custom members holding AccessImportExport,
EditAnyCollection or AccessReports. We stay deliberately stricter than
Bitwarden for accessImportExport: it does not open this endpoint, and
get_org_export scopes its output to the caller's own collections, so
"may export" never widens what a member can read.
No behavioural change.
A security review of this branch (finding F1) pointed out that the
'Access Import/Export' permission, which this branch added and which
opens GET /organizations/<id>/export via AccessImportExportHeaders,
decided *whether* a member may export but not *what* they get: the
handler always dumped Cipher::find_by_org(), and CipherSyncType::
Organization deliberately skips the per-cipher access restrictions.
A confirmed Custom member holding only accessImportExport - assigned to
no collection at all - therefore received every cipher of the
organization, including collections they were explicitly excluded from.
Every confirmed member holds the organization key, so the exported blobs
are decryptable by the caller.
The export is now built from the caller's own assignments unless they
already reach every collection anyway (Admin/Owner, or a Custom member
with 'Edit any collection'), which is what Bitwarden's export controller
does. Group-based access is covered: both scoped queries honour group
assignments and group access_all.
* new helper may_export_entire_organization() names the decision and
keeps it unit-testable
* get_org_details_impl() split into ciphers_to_org_json() so the export
can serialize an already-authorized cipher list; the serializer's doc
comment states that requirement
* new regression test access_import_export_alone_does_not_widen_the_export
Verified: cargo check, cargo clippy --features sqlite (clean),
cargo test --features sqlite (48 passed), cargo fmt --all -- --check.
- Updated API response to more closely match v2026.6.0+ server versions.
- Updated all the crates
- Updated Rust to v1.97.1
- Updated the web-vault to v2026.6.4
- Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
Implements the three remaining Bitwarden Custom-role permissions on top of the
existing set. Each is an independent, persisted flag on the membership, gated on
the Custom role in code (stale flags on other roles grant nothing); Owners/Admins
hold every permission implicitly. They are parsed from and emitted in the
`permissions` object (replacing the previously hard-coded `false`) so the
unmodified web-vault shows and round-trips them.
Server-side enforcement:
- accessEventLogs: the organization event-log endpoints (`GET .../events` and
`GET .../users/<id>/events`) now use a new `AccessEventLogsHeaders` guard
(Admin/Owner or the permission) instead of `AdminHeaders`.
- accessImportExport: `GET .../export` uses a new `AccessImportExportHeaders`
guard, and `post_org_import` gains an explicit permission check.
NOTE: this tightens org import, which previously accepted any confirmed member
(with per-collection gating). It now requires Admin/Owner or the permission,
matching Bitwarden and the web-vault, which only offers org import to permitted
members. Flagged here for maintainer review.
- accessReports has no server endpoint in Vaultwarden (reports are computed
client-side from vault data the member already has), so it is stored and
reported in the permissions object and enforced by the client UI, matching
Bitwarden's own model. No server route gates it.
New migration adds the three columns (down drops them). Unit tests cover
independence, type-gating, parsing and change-detection; a black-box probe over
HTTP confirms the event-log/export/import gating and the permission round-trip
(16/16), and the existing custom-role suite still passes (30/30).
The per-membership `access_all` flag was Vaultwarden's pre-permissions patch for
"this member reaches every collection". It is now fully represented by the role
model: Owners/Admins hold it implicitly, and a Custom member holds it via
`edit_any_collection`. Every authorization query that read
`users_organizations.access_all` now reads `edit_any_collection = true OR
atype <= Admin` instead, which is exactly the set the flag ever identified, so
admin/owner and edit-any access is preserved. The column is dropped via a new
migration (down-migration restores it from the role/permission model).
`groups.access_all` is a separate, still-supported group feature and is untouched.
The legacy Manager role (wire value 3) is folded into Custom: the
`MembershipType::Manager` variant is removed, all `>= Manager` rank checks become
`>= Custom` (identical authorization rank), the `LegacyManager` collection-access
path collapses into the Custom rules, and an incoming wire `type=3` is mapped onto
Custom for backward compatibility. Custom stays `type=4` because that is the only
role modern Bitwarden clients render with custom permissions. Existing type-3
members were already converted to Custom by the earlier migration.
Server-only change; verified against the unmodified web-vault. Unit tests, clippy
and rustfmt pass; migration up/down verified against SQLite.
A per-collection `manage` row carries collection delete authority via
`has_explicit_collection_manage_access` -> `CollectionDeleteHeaders`, so
only a caller who could delete the collection may confer it. Two write
paths still passed the client-supplied `manage` bit through unguarded:
- `send_invite` gated the initial collection assignments only on
`has_full_access()`, which `edit_any_collection` alone satisfies, so a
Custom member with manage-users and edit-any-collection could plant a
manage row on any collection for an account they control.
- `post_organization_collections` wrote `group.manage` / `user.manage`
directly. With the create guard narrowed to `can_create_new_collections()`,
a create-only Custom member could grant manage on the new collection to
itself, another member, or a group.
Both now AND the requested bit with `caller_may_grant_collection_manage`,
the same gate `post_bulk_access_collections`,
`post_organization_collection_update`, `edit_member` and `add_update_group`
already use. In the create handler the gate is evaluated once, before both
assignment loops, so no grant can bootstrap the next. The check is strictly
subtractive: Admin/Owner, Custom-with-delete-any-collection and the
access_all Manager are unaffected.
post_bulk_access_collections authorized each requested collection via the
legacy Collection::is_manageable_by_user helper, which also accepts a member's
membership/group access_all. For a Custom member this diverged from the
single-collection edit endpoint (ManagerHeaders -> collection_edit_access),
which requires a real per-collection Manage grant and never treats a Custom
member's access_all as one. A flagless Custom member placed in an access_all
group could therefore rewrite collection user/group assignments in bulk while
the single-collection edit endpoint denied the exact same change.
Add auth::can_edit_collection - the collection_edit_access + can_manage_collection
pair the ManagerHeaders guard already uses - and call it per collection in the
bulk-access endpoint. Legacy Managers, Admins/Owners and Edit-any-collection
members are unaffected; only a Custom member's access_all shortcut is removed,
so bulk-access now enforces exactly what the single-collection edit endpoint does.
A Custom member holding only `edit_any_collection` (whose `access_all` mirror
makes every collection "manageable") could grant a per-collection `manage` row
to a group it belongs to -- via post_bulk_access_collections,
post_organization_collection_update, add_update_group or edit_member -- and then
delete that collection through CollectionDeleteHeaders'
`has_explicit_collection_manage_access` path, despite holding no
`delete_any_collection` permission. This defeated the intended edit != delete
separation (confirmed end-to-end: the delete returned 200 before this change).
Gate every caller-controlled collection-assignment `manage` write behind a new
`caller_may_grant_collection_manage()` check that mirrors the delete
authorization exactly: a caller may confer `manage` (which carries delete
authority) on a collection only if it could delete that collection itself --
Admin/Owner or `delete_any_collection` always; an exact legacy Manager via its
per-collection manage helper; any other Custom member only with a real persisted
`users_collections.manage` / `collections_groups.manage` grant.
`edit_any_collection`'s `access_all` mirror deliberately does not count.
The change is strictly subtractive: it can only ever downgrade a requested
`manage` to false, never grant it, so it opens no new access and leaves
Admins/Owners and genuinely delete-capable members unaffected. The create,
organization-import and invite paths are intentionally left alone (new-collection
creators must manage their own collection; non-owner invites can only create
plain Users, which cannot delete).
Adds a unit test for the collection-independent part of the gate.
Follow-up hardening on the custom-role work, found during a static review:
- Admin panel type change (apply_membership_type_change): when converting a
Custom member to the legacy Manager role, only preserve `access_all` if the
member actually held the full "manage all collections" grant (all three
collection flags). Previously an Edit-any-collection-only Custom member
(whose access_all is just the Edit mirror) became a Manager with a broad
access_all grant, silently escalating Edit-only into Create + Edit + Delete.
Mirrors the collection-permissions down-migration. Unit test updated.
- Bulk collection access (post_bulk_access_collections): drop the blanket
has_full_access() requirement and rely on the existing per-collection
is_manageable_by_user check (as the single-collection endpoint and the
pre-existing behavior do). Pure manage_users / manage_groups / manage_policies
Custom members hold no per-collection manage grant and are still rejected,
while a Manager/Custom member who manages some collections regains the ability
to bulk-edit exactly those collections.
- Group details (GET /organizations/<org>/groups/<id>/details): align the guard
with the list endpoint (/groups/details) to ManageUsersOrGroups, so a
manage_users member is not denied the single-group view of the same data it
can already read in bulk.
Addresses the confirmed High/Medium findings of the granular-collection-
permissions security audit:
- H-1: bind direct (non-sync) cipher access to a confirmed membership in the
cipher's organization, and harden the user/group collection access-flag
queries to require org consistency plus confirmed status. Revoked/invited
members can no longer reach ciphers via stale assignment rows.
- H-2: validate group ids against the org before mutating group membership in
edit_member, and reject cross-org group<->membership links in GroupUser::save
and Group::is_in_full_access_group.
- H-3: fully pre-validate collections/groups/users in bulk-access and collection
create before any mutation, and reject cross-org collection<->group links in
CollectionGroup::save.
- M-1: require Manage Users for the full member list and Manage Users/Groups for
group details (new ManageUsersOrGroupsHeaders guard).
- M-2: validate the whole bulk-access request before the destructive
delete/replace so an invalid element can't leave partial state behind.
- M-3: bounds-validate import collection relationships before writing and
propagate cipher-save errors instead of discarding them.
A Custom member with the manage_users permission reaches edit_member via
ManageUsersHeaders. Every mutated field there (access_all, the manage_* flags,
collection and group assignments) is gated behind an Admin/collection-management
check -- except the role type itself, which was written unconditionally aside
from the existing Admin/Owner-elevation guard.
Because collection "manage" rights are role-derived (`atype >= Manager` grants
manage on any collection the member can write), a manage_users caller with no
collection access could promote a plain User to Manager/Custom to grant them
collection administration (rename/delete/re-share), or demote to revoke it --
a separation-of-duties break between the user-management and data planes.
Add a may_change_member_type() guard so callers below Admin may only submit an
unchanged role (keeping the regular edit dialog working), and cover it with a
regression unit test.
* Misc updates and fixes
- Updated Rust to v1.96.1
- Updated all the crates
- Updated GitHub Actions
- Updated the web-vault to v2026.6.2
- Updated Alpine to v3.24
- Fixed several clippy lints
- The `send` UUID wrappers didn't need the special namespace anymore since an updated crate, so removed this extra mod.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update MSRV to v1.94.1
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
A Custom member with only the manage_groups permission could revoke other
members' collection access via two endpoints that were missing the
collection-access check enforced elsewhere (put_group_members, edit_member):
- POST /organizations/<org>/groups/<group>/delete-user/<member>
(post_delete_group_member) removed a member from any group, including
collection-bearing ones.
- DELETE /organizations/<org>/groups/<group> and its bulk variant
(delete_group_impl / bulk_delete_groups) deleted collection-bearing groups
outright, revoking access for all their members.
Neither path can grant access, so confidentiality was never at risk, but both
let a manage_groups-only user tamper with other members' collection access,
contradicting the permission's invariant. Both now require Admin/Owner or full
collection access before touching a group that confers collection access (via
access_all or assigned collections).
send_invite computed access_all from the invite permissions but never
wrote the manage_users / manage_groups / manage_policies flags onto the
new membership, so a Custom member invited with a management permission
checked was created without it (the web-vault sends everything in a
single invite POST, with no follow-up edit). Mirror the handling in
edit_member and persist the flags at invite time.
Only Owners can invite Custom members, so the caller is always
authorized to grant these; the flags are gated on the Custom type and
forced false for every other type.
A Custom member holding manage_users + manage_groups (but without collection
access) could add a member -- including themselves via edit_member, or an
attacker-controlled invitee via send_invite -- to an access_all or
collection-bearing group, and thereby indirectly gain read access to those
collections' cipher contents. This bypassed the collection-access boundary
already enforced in put_group_members and add_update_group.
Gate both member-centric group-assignment paths on collection-management
rights via a shared `may_change_group_membership` predicate: callers who
cannot manage collections may only add/remove membership of groups that
confer no collection access, and collection-bearing memberships are left
untouched (neither granted nor revoked). Admins/Owners and full-access
callers keep the previous full-replace behaviour.
Add a regression unit test for the predicate.
Tighten two custom-role read paths that were broader than intended:
- get_group, get_group_details and get_group_members only required
ManagerHeadersLoose, exposing group metadata, collection mappings and
membership to any confirmed Manager/Custom member. Require
ManageGroupsHeaders (Admin/Owner or manage_groups) instead.
- list_policies returned the full policy configuration to any manage_*
member. Keep the endpoint reachable so the Admin Console still loads,
but return an empty list to callers without manage_policies.
Security fixes and hardening following a review of the custom-role
permissions feature:
- Gate the manage_* flags on the Custom membership type via new
Membership::has_manage_* helpers. Previously a stale flag (e.g. left
over after changing a member's type through the admin panel, which
does not go through edit_member) would keep granting management
permissions to a member of any type.
- Clear the manage_* flags in admin::update_membership_type when a
member is changed away from the Custom type.
- edit_member: reject any *change* to the manage_* flags by non-admin
callers. This keeps the previous protection against granting flags,
and additionally prevents a manage_users member from silently
stripping flags an Admin/Owner granted to fellow Custom members.
Unchanged flags still pass, so the regular edit dialog keeps working.
- list_policies: restrict the full policy list to Admins/Owners and
Custom members holding at least one management permission. The
previous loosening to ManagerHeadersLoose also exposed all policies
(including their configuration data) to plain Managers.
- Collection list metadata (get_org_collections{,_details}) is now only
readable with manage_users or manage_groups; manage_policies does not
need the collection list.
- find_confirmed_and_manage_all_by_org: include Custom members with
access_all, matching the pre-existing behaviour for Managers.
- Fix the partial_cmp_MembershipType unit test (Custom is no longer
converted to Manager) and document that Manager and Custom share an
access level in Ord while remaining distinct roles under PartialEq.
- Replace the remaining stale "HACK" comments and the raw "4" type
comparison in send_invite; drop unrelated .gitignore entries.
A Custom member with only manage_users could set the "manage all
collections" child permissions (createNewCollections / editAnyCollection /
deleteAnyCollection) on any member, including themselves, via
POST /organizations/<org_id>/users/<member_id>. edit_member wrote
member_to_edit.access_all unconditionally, so the resulting access_all=true
granted full access to every collection's contents - defeating the
"manage users without collection access" guarantee.
Gate the access_all change on caller_can_manage_collections (Admins/Owners
or full-access members), mirroring how put_group preserves a group's
access_all for callers without collection rights. For everyone else the
member's existing access_all is left untouched. The collection- and
group-assignment paths were already gated; this closes the remaining
direct path.
* Update to Rust 2024 Edition
Updated to the Rust 2024 Edition and added and fixed several lint checks.
This is a large change which, because of the extra lints, added some possible fixes for issues.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Reorder and merge imports
Signed-off-by: BlackDex <black.dex@gmail.com>
* Remove "db_run!" macro calls where possible
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates and gha
Updated all the crates
Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix restoring revoked user
A new endpoint is used to restore a revoked user.
This commit fixes that.
Fixes#7224
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update datatables
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>