The member permissions object could not be written back. The read shape emits
permissions null for anyone who is not a manage-all member, but the write models
declared a plain map with serde(default), which only covers a missing field: an
explicit null failed to deserialize and the request was rejected before the
handler ran. Reading a member and sending it straight back therefore failed for
every member except the manage-all case the previous test covered. The field is
optional now, and the smoke test round-trips a plain member as well.
Reinvite was the one member write that could still reach an owner, and it can
change an owner from invited to accepted, so it takes the same guard as the rest.
Group updates log before mutating, so a failure part way through the collection
associations cannot leave an unaudited change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The write endpoints derive the access_all flag from the raw type plus the three
collection permissions, but the member serializer emitted neither the custom
role nor the permissions object. A client that read a manage-all member and
wrote it back unchanged therefore dropped that member from every collection,
because the information needed to round-trip was not in the response.
The member object now reports the custom role and its permissions the same way
the internal serializers do, which is also the shape upstream uses: Permissions
is part of the shared member model and is returned on reads as well as accepted
on writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adversarial review of the new endpoints turned up several defects.
Ownership is now out of reach of a Public API client. The internal endpoints
only let an Owner grant, change or remove Owner, and that check cannot be
applied here because there is no user behind the request. Since the
organization API key can be created by an Admin, a client could previously
promote itself to Owner and take over the organization. Granting the Owner role
and acting on an existing owner are both refused now, which subsumes the
narrower last confirmed owner guard.
An omitted groups list on a member update no longer unassigns every group.
Upstream leaves group access untouched when the field is absent and only
collections reset on omission, so groups is optional now.
An omitted accessAll on a group update no longer clears the flag, and an
omitted externalId no longer clears the directory matching key that
/public/organization/import relies on to match members and groups.
Revoked members are reported with the upstream status of -1 instead of the
internal offset encoding, matching every other serializer in the codebase.
Restore refuses a member who is not revoked, rather than saving nothing and
logging a restore that did not happen, and group creation is logged before its
collection associations so a failure cannot leave an unaudited group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Boots a throwaway instance against a seeded SQLite database and exercises every
member and group write endpoint end to end, asserting on the resulting state
rather than just the status code.
Covers the guards that protect organization integrity: the last confirmed owner
cannot be demoted, revoked or deleted; collections, groups and members from
another organization are rejected; ids belonging to another organization return
404; and writes require a token. It also pins two behaviours that are easy to
regress: a group update leaves member assignments alone, and every write is
recorded in the event log with no acting user and no device type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the write side of the organization Public API for members and groups, so
an organization API client can make incremental changes instead of pushing a
full directory snapshot through /public/organization/import, which revokes any
member missing from the payload when overwriteExisting is set.
Members: create (invite), update, delete, replace group ids, reinvite, revoke
and restore. Groups: create, update, delete and replace member ids.
Routes and request models follow the upstream Bitwarden Public API controllers.
The handlers mirror the equivalent internal endpoints in organizations.rs but
are guarded by PublicToken instead of AdminHeaders. A PublicToken carries only
an organization, so the per-actor permission checks do not apply, while the
guards protecting organization integrity are kept: the last confirmed owner
cannot be demoted, revoked or deleted, org policies are still enforced on
member changes, and group endpoints still require group support to be enabled.
Public API writes have no acting user or device, so log_event_impl now takes
both as optional and a new log_public_event records these events with a null
actingUserId rather than leaving them out of the organization event log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Boots a throwaway instance against a temporary SQLite database seeded
with two organizations (each with members, groups, collections and
their access associations), mints an organization API token, and
asserts on every read endpoint: member/group/collection lists and
details, the direct member-to-collection grant on member detail, the
group-to-collection grant on group and collection detail, and that
collection responses omit the encrypted name.
It also asserts the organization scoping boundary (ids owned by the
second organization return 404 through the first org's token) and that
an unauthenticated request returns 401. The script exits non-zero if any
assertion fails, so it can be run as a check. It builds the binary when
one is not supplied via VW_BIN, uses a throwaway port and temp directory,
and cleans up on exit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public organization API previously exposed only the write-side
"/public/organization/import" endpoint. This adds the corresponding
read endpoints so an organization-scoped API client can read back the
members, groups, and collections it manages, along with their access
associations:
- GET /public/members and /public/members/<id> (with collection grants)
- GET /public/members/<id>/group-ids
- GET /public/groups and /public/groups/<id> (with collection grants)
- GET /public/groups/<id>/member-ids
- GET /public/collections and /public/collections/<id> (with group grants)
All handlers reuse the existing PublicToken guard, so they are
authorized by the same organization API key as the import endpoint, and
every handler is scoped to the token's organization: a resource id that
belongs to another organization returns 404 rather than leaking data.
Collection responses intentionally omit the end-to-end encrypted name
and key collections by id and externalId only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bundled web vault (2026.6.4) requires seven query parameters in the
accept-organization URL and rejects the invite client-side when any of them is
null, showing only "Unable to accept invitation" without sending a request to
the server.
send_invite() never appended initOrganization, and appended
orgUserHasExistingUser only for users who already had an account, so every
organization invitation e-mail produced a link that could not be accepted.
Web vault 2026.4.1 (shipped with 1.36.0) read these parameters null-safely,
which is why this only appeared in 1.37.0.
Fixes#7481
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
- 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>
* 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>
* ci: remove dead BASE_TAGS reference in release bake step
steps.determine-version doesn't exist in docker-build; the expression
resolves to empty string. The HCL default (testing) would have
applied, but it's moot - the bake uses push-by-digest=true so tags are
only set in merge-manifests. Dead code.
* ci: replace unsecured curl hadolint download with an official action
hadolint/hadolint-action uses a Docker-based runner with hadolint
pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian,so no binary
downloaded at runtime. Pinning the action to a commit SHA covers the
Dockerfile that specifies the image version, closing the supply-chain
gap from the previous unverified curl | sudo install.
Split {debian,alpine}: the action takes a single dockerfile argument,
so debian and alpine are linted separately.
* ci: pin ubuntu-latest to ubuntu-24.04 in merge-manifests and zizmor
ubuntu-latest is a moving target that can silently change the runner OS
on the next GitHub-side update. All other jobs in this repo already pin
to ubuntu-24.04; this makes merge-manifests and zizmor consistent.
* ci: return BASE_TAGS - it's needed for bake step
The /api/sync response serialized a Send hide_email field directly from
Option<bool>, so a NULL value in the sends table (the column is
Nullable<Bool> with no default) produced "hideEmail": null.
The Bitwarden Android client deserializes SyncResponseJson.Send.hideEmail
as a non-null Kotlin Boolean and aborts the entire sync with a
JsonDecodingException when it encounters null. Web, desktop and CLI
clients coerce null to false, so only accounts with at least one Send
are affected and only on Android.
Default None to false at the serialization boundary, matching the
official Bitwarden server where hideEmail is non-nullable. This needs no
database migration and fixes both legacy NULL rows and any future NULLs.
The hide_email field stays Option<bool> internally.
- Updated Rust to v1.96.0
- Updated all the crates, and adjusted code where needed
- Fixed some nightly reported clippy lints
- Updated all the GitHub actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* 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>
* Panic on unrecognised DATABASE_URL instead of silent SQLite fallback
Previously, any DATABASE_URL that did not match the mysql: or postgresql:
prefix was silently treated as a SQLite file path. This caused data loss
in containerised environments when the URL was misconfigured (typos,
quoting issues), as vaultwarden would create an ephemeral SQLite database
that was wiped on restart.
Now, an explicit sqlite:// prefix is supported and used as the default.
Bare paths without a recognised scheme are still accepted for backwards
compatibility, but only if the database file already exists. If not, the
process panics with a clear error message.
Relates to #2835, #1910, #860.
* Use err!() instead of panic!() for unrecognised DATABASE_URL
Follow the established codebase convention where configuration
validation errors use err!() to propagate gracefully, rather than
panic!(). The error propagates through from_config() and is caught
by create_db_pool() which logs and calls exit(1).
* Use 'scheme' instead of 'prefix' in DATABASE_URL messages
Per review feedback, 'scheme' is the more accurate term for the
sqlite:// portion of the URL.
* deps: upgrade the reqwest stack to 0.13
The reqwest 0.13 rustls feature selects the aws-lc provider. Use
rustls-no-provider instead, add rustls 0.23 with the ring provider, and
install that provider at process startup. This keeps Vaultwarden on the
existing ring crypto provider while giving reqwest, OpenDAL and lettre a
process-wide rustls provider.
Disable openidconnect default features and provide a small
AsyncHttpClient wrapper around Vaultwarden's shared reqwest client
builder. This preserves custom DNS, request blocking, timeouts and the
no-redirect OIDC behavior without openidconnect enabling its own reqwest
stack.
Upgrade yubico_ng to 0.15.0 and OpenDAL to 0.56.0. OpenDAL 0.56 also
moves S3 signing to reqsign 3, so switch the optional S3 dependencies
from reqsign/anyhow to reqsign-core and reqsign-aws-v4 and adapt the AWS
SDK credential bridge to the new ProvideCredential API.
Adjust the local OpenDAL call sites for the 0.56 API: use the FS_SCHEME
constant for filesystem checks and replace deprecated remove_all() with
delete_with(...).recursive(true) for Send file cleanup.
* storage: add OpenDAL S3 URI options
OpenDAL S3 storage accepts bucket and root path data today, but
serverless deployments also need URI query parameters to describe provider
behavior in one DATA_FOLDER value.
Update OpenDAL to 0.56.0 and build S3 operators with
S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by
installing a reqsign provider when the URI does not explicitly request
OpenDAL-native credential handling.
Move path handling and operator construction into storage.rs so S3-specific
parsing, credential setup, and URI path manipulation stay out of
configuration handling. Local filesystem behavior is unchanged, and S3
child paths are derived before query strings.
- Update crates including fixing a regression of Diesel
- Update web-vault to v2026.4.1
- Adjusted the README to address the secure context and needing HTTPS
Fixes#7132Closes#7137
Signed-off-by: BlackDex <black.dex@gmail.com>
Keeping the default behaviour of SQLite being built statically,
so as not to break anyone's workflow, but allowing for downstream
packagers to link dynamically against SQLite (where it's fine because
that's the point of package managers).
Note that SQLite is still *not* enabled by default, thanks to the `?` operator.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
`Cipher::to_json()` returns `Result<Value, Error>` but its match arm for
unknown `atype` values called `panic!("Wrong type")` instead of
propagating an error. This means if a cipher with an invalid/unknown type
ends up in the database (via direct DB edits, data migration issues, or
future type additions in the upstream Bitwarden protocol), the entire
server process would crash on the next sync request.
Replace the `panic!` with `err!()` so callers receive a proper `Err` and
can handle or log it gracefully without taking down the server.
Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
The collection update endpoints (post_collections_update and
post_collections_admin) call .unwrap() on cipher.organization_uuid
in four places. If a user-owned cipher without an organization
somehow reaches these code paths, the server would panic.
Extract the organization UUID early with a descriptive error message
instead of relying on .unwrap(), preventing potential panics and
providing a clear API error response.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Add archiving
* Update Diesel macros and remove unnecessary SUPPORTED_FEATURE_FLAG
* Add IF EXISTS to down.sql migratinos
* Rename migration folders, separate logic based on PR threads
* Ensure SSO token is only usable on the same client
This commit adds an extra check via cookies to ensure the same browser/client is used to request and provide the SSO token.
Previously it would be able to provide a custom link which attackers could use to steal data.
While an attacker would still need the Master Password to be able to decrypt or execute specific actions, they were able to fetch encrypted data.
Solved with some help of Claude Code.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Check email-verified on SSO login/create
This commit prevents possible account takeover via SSO which doesn't check/validate or provide validated status of the email.
It was checked at other locations, but was skipped here.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Prevent data disclosure via SSO endpoints
This commit prevents some data disclosure and user enumeration by only returning the fake SSO identifier.
Since we do not check the identifier anywhere useful, returning the fake one is just fine.
During an invite to an org, that link contains the correct UUID and will be used for the master password requirements.
For anything else, server admins should set the `SSO_MASTER_PASSWORD_POLICY` env variable.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust admin layout to fix issues when SSO is enabled
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
IPv4 addresses can also be in decimal or hex formats.
These were not checked during the Global IP check, and could bypass it.
We now convert everything to the right format before running this check and it will catch these formats.
Also updated the `is_global()` function to match Rust's still unstable version.
And updated the Image Magic checks to be more precise and filter out any possible broken or invalid formats.
While at it, also added several checks to ensure these special formatted IPv4 addresses are still blocked and punycode domains are also correctly resolved.
Signed-off-by: BlackDex <black.dex@gmail.com>
Quote from the lint description:
"More flexibility, better memory optimization, and more idiomatic Rust code.
&Option<T> in a function signature breaks encapsulation because the caller must own T and move it into an Option to call with it. When returned, the owner must internally store it as Option<T> in order to return it. At a lower level, &Option<T> points to memory with the presence bit flag plus the T value, whereas Option<&T> is usually optimized to a single pointer, so it may be more optimal."
Quote from lint description:
"Using a smaller unit for a duration that is evenly divisible by a larger unit reduces readability. Readers have to mentally convert values, which can be error-prone and makes the code less clear."