Compare commits

...

85 Commits
1.5.0 ... 1.6.0

Author SHA1 Message Date
Daniel García
1d034749f7 Fix AArch64 build by disabling yubico 2019-01-10 23:54:01 +01:00
Daniel García
08c55f636a Mention HTTPS needed for Chrome 2019-01-08 21:51:09 +01:00
Daniel García
0dc5d1a1c6 Of course I forgot the year... 2019-01-08 20:39:46 +01:00
Daniel García
1b11445bb2 Update dependencies and web vault 2019-01-08 20:28:17 +01:00
Daniel García
1596e1d4c5 Update README 2019-01-08 20:27:55 +01:00
Daniel García
320266606e Implement put collections 2019-01-08 20:27:28 +01:00
Daniel García
a0a08c4c5a Include IP in invalid admin token error 2019-01-08 16:17:18 +01:00
Daniel García
4309df8334 Only create invitations when SMTP is disabled, and ignore invitations if we have a token.
Disallow users from accepting invitation twice
2019-01-08 15:42:26 +01:00
Daniel García
f1161c65fb Make sure an invitation is created when reinviting 2019-01-08 14:05:05 +01:00
Daniel García
50eeb4f651 Remove whitespace before processing tokens 2019-01-07 20:37:14 +01:00
Daniel García
21b85b78b1 Changed reinvite check and removed obsolete comment 2019-01-07 15:29:57 +01:00
Daniel García
673adde9f1 Make the admin retype the user name before deleting 2019-01-07 14:48:10 +01:00
Daniel García
c9063a06b4 Print unauthorized error message 2019-01-07 02:18:51 +01:00
Daniel García
62b8500aae Fix missing .env error in Dockerfile, the .env file is not needed for the Docker images 2019-01-07 01:43:37 +01:00
Daniel García
f645e5381c Rename included .env file to .env.template and ignored .env 2019-01-06 22:50:30 +01:00
Daniel García
5e37471488 Merge pull request #323 from njfox/invite_accepted_email
Send email notifications when invitations are accepted/confirmed
2019-01-06 14:12:24 +01:00
Nick Fox
0a74e79cea Refactor generate_invite_claims, make org_name and org_id optional 2019-01-05 23:03:49 -05:00
Nick Fox
7db66f73f0 Refactor invited_by_email check 2019-01-05 13:46:45 -05:00
Nick Fox
2f5bdc23f6 Fix formatting and add vault link to notification emails 2019-01-05 13:36:08 -05:00
Daniel García
94adf063ad Merge pull request #326 from mprasil/vault_2.8.0
Update Vault to 2.8.0
2019-01-05 19:16:34 +01:00
Miroslav Prasil
77cada4085 Update Vault to 2.8.0 2019-01-05 18:54:59 +01:00
Nick Fox
cec28a85ac Update admin page to work with new invitation flow 2019-01-04 10:32:51 -05:00
Daniel García
5f49ecd7f3 Updated dependencies to use u2f crate directly, and some style changes 2019-01-04 00:25:38 +01:00
Nick Fox
736c0e62f2 Send emails to inviters/invitees when invites are accepted/confirmed 2019-01-02 22:20:39 -05:00
Nick Fox
43eb064351 Replace invite/reinvite email functions with generic send_email 2019-01-02 22:19:44 -05:00
Daniel García
c8af62ed48 Merge pull request #321 from njfox/migrate_readme_to_wiki
Remove redundant information from README after moving it to the wiki
2019-01-01 23:41:39 +01:00
Nick Fox
6e47535c2e Remove redundant docs files 2019-01-01 16:33:19 -05:00
Nick Fox
0448d98afc Remove redundant information from README after moving it to the wiki 2018-12-31 21:03:06 -05:00
Daniel García
b8326a15a3 Add build.rs to dockerfile 2018-12-31 14:07:12 +01:00
Daniel García
a6631c2ea8 Update documentation for admin page 2018-12-30 23:47:20 +01:00
Daniel García
30e768613b Start using rustfmt and some style changes to make some lines shorter 2018-12-30 23:34:31 +01:00
Daniel García
72ed05c4a4 Add build script to get some Git info to later show in admin panel, and add some metadata to Cargo.toml 2018-12-30 21:43:57 +01:00
Daniel García
adb8052689 Updated Error to implement Display and Debug, instead of using custom methods 2018-12-30 21:43:56 +01:00
Daniel García
1483829c94 Removed invalid todo 2018-12-30 21:43:56 +01:00
Daniel García
acb9d1b3c6 Remove config option for admin email, embdedded admin page, managed IO::Error, and added security and cache headers globally 2018-12-30 21:43:56 +01:00
Daniel García
301919d9d4 Modified navigation bar and corrected some comments 2018-12-30 21:43:55 +01:00
Daniel García
2bb0b15e04 Implemented better errors for JWT 2018-12-30 21:43:55 +01:00
Daniel García
250a2b340f Use new Errors in latest changes 2018-12-30 21:43:55 +01:00
Daniel García
b2fc0499f6 Finish invite functionality, and remove virtual organization 2018-12-30 21:40:26 +01:00
Daniel García
6a99849a1e Implemented proper error handling, now we can do user.save($conn)?; and it works.
In the future, maybe we can do the same with the `find_by_id` methods that return an Option.
2018-12-30 21:31:12 +01:00
Daniel García
172f1770cf Embed the icon in the binary, no need to download when it's not going to change 2018-12-30 21:31:12 +01:00
Daniel García
1b5134dfe2 Fixed delete user when 2FA is enabled, implemented delete user for admin panel, and the front-end part for invite user. Secured admin panel behind a configurable token. 2018-12-30 21:31:11 +01:00
Daniel García
5fecf09631 Initial version of admin panel, list users and reload user list works. No serious auth method yet, password is 'token123' 2018-12-30 21:31:11 +01:00
Daniel García
9a8cae836b Remove vault patch file, and point users to the vault builds repo for the patch 2018-12-30 21:31:03 +01:00
Daniel García
7d7d8afed9 Merge pull request #318 from njfox/reinvite_endpoint
Add email reinvite endpoint
2018-12-30 21:22:26 +01:00
Nick Fox
f20c4705d9 Refactor invite claims and disallow reinvites to virtual_org 2018-12-30 00:19:01 -05:00
Nick Fox
3142d8d01f Add more detail to invitation not found error 2018-12-29 23:28:19 -05:00
Nick Fox
84fa5a4ed6 Implement reinvite endpoint 2018-12-29 23:24:38 -05:00
Daniel García
004a3f891f Merge pull request #315 from aksdb/master
Restrict join on users_collections to current user (fixes #313)
2018-12-28 21:06:30 +01:00
Daniel García
e197f372b5 Merge pull request #314 from mprasil/find_by_user_uuid
Rewrite find_by_user_uuid to use one query
2018-12-28 15:47:31 +01:00
Andreas Schneider
e7ea5097f4 Restrict join on users_collections to current user (fixes #313) 2018-12-28 15:25:51 +01:00
Miroslav Prasil
8451a70de6 Rewrite find_by_user_uuid to use one query 2018-12-27 18:56:01 +01:00
Daniel García
9f0357ce82 Merge pull request #312 from njfox/logging_timestamp
Change logging timestamp format so fail2ban can parse it
2018-12-26 20:20:50 +01:00
Nick Fox
cd6e4a0ebd Remove reference to syslog 2018-12-26 12:14:54 -05:00
Nick Fox
473740c13a Update fail2ban documentation for new logging format 2018-12-26 12:07:16 -05:00
Nick Fox
ec715d78fb Change log timestamp format so fail2ban can parse it 2018-12-26 11:54:31 -05:00
Daniel García
3f5df3ef8d Merge pull request #311 from njfox/update_documentation
Document new features and update outdated parts of README
2018-12-26 17:27:10 +01:00
Nick Fox
f694d6f839 Add missing trailing slash 2018-12-26 10:30:21 -05:00
Nick Fox
b20516d645 Update ToC with Logging section 2018-12-26 10:28:46 -05:00
Nick Fox
c04300651a Document new logging setup 2018-12-25 23:00:50 -05:00
Nick Fox
2f058d3ff5 Remove old/incorrect statement about SMTP not being implemented 2018-12-25 22:30:35 -05:00
Nick Fox
582f967a59 Update invitations documentation for new email invitations 2018-12-25 22:29:31 -05:00
Daniel García
00bc355220 Merge pull request #308 from njfox/invite_existing_user_fix
Fix email invitations for existing users and update README
2018-12-23 22:31:00 +01:00
Nick Fox
e3395ee910 Mention that invite links are valid for 5 days only 2018-12-23 15:25:05 -05:00
Nick Fox
cb78ba9bb2 Document domain/email invitation requirements 2018-12-23 15:21:19 -05:00
Nick Fox
b0ac640d8b Use JWT to validate existing user invites 2018-12-23 15:15:44 -05:00
Daniel García
2b24b17609 Merge pull request #295 from njfox/invite_emails
Add Email Invite Functionality
2018-12-21 16:04:19 +01:00
Nick Fox
2cd736ab81 Validate JWT if a user registers with SMTP invites enabled 2018-12-20 22:16:41 -05:00
Nick Fox
99256b9b3a Prefix unused params with underscore 2018-12-20 21:37:03 -05:00
Nick Fox
26bf7bc12f Use upstream jslib invite/registration workflow 2018-12-18 23:16:03 -05:00
Daniel García
b3ec8f2611 Merge pull request #302 from tycho/icon-cache-ttl
implement TTLs for icon cache
2018-12-18 23:34:16 +01:00
Steven Noonan
a55c048a62 icons: implement positive/negative cache TTLs
Signed-off-by: Steven Noonan <steven@uplinklabs.net>
2018-12-18 13:33:32 -08:00
Steven Noonan
848cd1dbec add environment variables for ICON_CACHE_TTL and ICON_CACHE_NEGTTL
These aren't used yet, but will be utilized by the icon caching service
in a subsequent patch.

Signed-off-by: Steven Noonan <steven@uplinklabs.net>
2018-12-18 13:33:31 -08:00
Daniel García
149e69414f Merge pull request #293 from dheimerl/patch-1
Update web.rs
2018-12-18 19:00:43 +01:00
dheimerl
9a7d3634d5 Changed frame-ancestors to use 'self' 2018-12-18 10:19:35 -06:00
dheimerl
7f7c936049 Fixed web.rs 2018-12-17 22:59:53 -06:00
Nick Fox
9479108fb7 Remove CONFIG.email_invitations 2018-12-17 17:10:09 -05:00
Nick Fox
042c1072d9 Remove CONFIG.email_invitation option 2018-12-17 17:02:15 -05:00
Daniel García
1f4edb38e6 Add extra headers to Nginx (#303) 2018-12-17 19:29:07 +01:00
Nick Fox
e245e965ba Fix broken rebase 2018-12-15 22:45:39 -05:00
dheimerl
037eb0b790 Update web.rs
Add frame-ancestors to allow U2F to work in Chrome (and possibly Firefox) extension
2018-12-15 13:23:07 -06:00
Nick Fox
4910b14d57 Implement email invitations and registration workflow 2018-12-14 21:56:00 -05:00
Nick Fox
d428120ec6 Add email_invitations config option 2018-12-14 21:54:44 -05:00
Nick Fox
e2907f4250 Add invite email functionality 2018-12-14 21:54:03 -05:00
Nick Fox
680f5e83d8 Add Invite JWT struct and supporting functions 2018-12-14 21:52:16 -05:00
46 changed files with 2941 additions and 2659 deletions

View File

@@ -10,6 +10,11 @@
# ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
# ICON_CACHE_TTL=2592000
## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
# ICON_CACHE_NEGTTL=259200
## Web vault settings
# WEB_VAULT_FOLDER=web-vault/
# WEB_VAULT_ENABLED=true
@@ -29,15 +34,23 @@
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log
## Controls if new users can register
# SIGNUPS_ALLOWED=true
## Use a local favicon extractor
## Set to false to use bitwarden's official icon servers
## Set to true to use the local version, which is not as smart,
## but it doesn't send the cipher domains to bitwarden's servers
# LOCAL_ICON_EXTRACTOR=false
## Controls if new users can register
# SIGNUPS_ALLOWED=true
## Token for the admin interface, preferably use a long random string
## One option is to use 'openssl rand -base64 48'
## If not set, the admin panel is disabled
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
## Invitations org admins to invite users, even when signups are disabled
# INVITATIONS_ALLOWED=true
## Controls the PBBKDF password iterations to apply on the server
## The change only applies when the password is changed
# PASSWORD_ITERATIONS=100000

4
.gitignore vendored
View File

@@ -10,7 +10,7 @@ data
*.iml
# Environment file
# .env
.env
# Web vault
web-vault
web-vault

View File

@@ -1,77 +0,0 @@
# Build instructions
## Dependencies
- `Rust nightly` (strongly recommended to use [rustup](https://rustup.rs/))
- `OpenSSL` (should be available in path, install through your system's package manager or use the [prebuilt binaries](https://wiki.openssl.org/index.php/Binaries))
- `NodeJS` (only when compiling the web-vault, install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/))
## Run/Compile
```sh
# Compile and run
cargo run --release
# or just compile (binary located in target/release/bitwarden_rs)
cargo build --release
```
When run, the server is accessible in [http://localhost:80](http://localhost:80).
### Install the web-vault
A compiled version of the web vault can be downloaded from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
If you prefer to compile it manually, follow these steps:
*Note: building the Vault needs ~1.5GB of RAM. On systems like a RaspberryPI with 1GB or less, please [enable swapping](https://www.tecmint.com/create-a-linux-swap-file/) or build it on a more powerful machine and copy the directory from there. This much memory is only needed for building it, running bitwarden_rs with vault needs only about 10MB of RAM.*
- Clone the git repository at [bitwarden/web](https://github.com/bitwarden/web) and checkout the latest release tag (e.g. v2.1.1):
```sh
# clone the repository
git clone https://github.com/bitwarden/web.git web-vault
cd web-vault
# switch to the latest tag
git checkout "$(git tag | tail -n1)"
```
- Apply the patch file from `docker/set-vault-baseurl.patch`:
```sh
# In the Vault repository directory
git apply /path/to/bitwarden_rs/docker/set-vault-baseurl.patch
```
- Then, build the Vault:
```sh
npm run sub:init
npm install
npm run dist
```
Finally copy the contents of the `build` folder into the `bitwarden_rs/web-vault` folder.
# Configuration
The available configuration options are documented in the default `.env` file, and they can be modified by uncommenting the desired options in that file or by setting their respective environment variables. Look at the README file for the main configuration options available.
Note: the environment variables override the values set in the `.env` file.
## How to recreate database schemas (for developers)
Install diesel-cli with cargo:
```sh
cargo install diesel_cli --no-default-features --features sqlite-bundled
```
Make sure that the correct path to the database is in the `.env` file.
If you want to modify the schemas, create a new migration with:
```
diesel migration generate <name>
```
Modify the *.sql files, making sure that any changes are reverted in the down.sql file.
Apply the migrations and save the generated schemas as follows:
```sh
diesel migration redo
# This step should be done automatically when using diesel-cli > 1.3.0
# diesel print-schema > src/db/schema.rs
```

744
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,16 @@ version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2018"
repository = "https://github.com/dani-garcia/bitwarden_rs"
readme = "README.md"
license = "GPL-3.0-only"
publish = false
build = "build.rs"
[features]
default = ["enable_yubikey"]
enable_syslog = ["syslog", "fern/syslog-4"]
enable_yubikey = ["yubico"]
[dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
@@ -13,7 +21,7 @@ rocket = { version = "0.4.0", features = ["tls"], default-features = false }
rocket_contrib = "0.4.0"
# HTTP client
reqwest = "0.9.5"
reqwest = "0.9.6"
# multipart/form-data support
multipart = "0.15.4"
@@ -28,9 +36,9 @@ rmpv = "0.4.0"
chashmap = "2.2.0"
# A generic serialization/deserialization framework
serde = "1.0.82"
serde_derive = "1.0.82"
serde_json = "1.0.33"
serde = "1.0.84"
serde_derive = "1.0.84"
serde_json = "1.0.34"
# Logging
log = "0.4.6"
@@ -57,16 +65,16 @@ chrono = "0.4.6"
oath = "0.10.2"
# Data encoding library
data-encoding = "2.1.1"
data-encoding = "2.1.2"
# JWT library
jsonwebtoken = "5.0.1"
# U2F library
u2f = "0.1.2"
u2f = "0.1.4"
# Yubico Library
yubico = { version = "=0.4.0", features = ["online"], default-features = false }
yubico = { version = "=0.4.0", features = ["online"], default-features = false, optional = true }
# A `dotenv` implementation for Rust
dotenv = { version = "0.13.0", default-features = false }
@@ -74,6 +82,9 @@ dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro
lazy_static = { version = "1.2.0", features = ["nightly"] }
# More derives
derive_more = "0.13.0"
# Numerical libraries
num-traits = "0.2.6"
num-derive = "0.2.3"
@@ -94,8 +105,5 @@ rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
# Version 0.1.2 from crates.io lacks a commit that fixes a certificate error
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '75b9fa5afb4c5' }
# Allows optional libusb support
yubico = { git = 'https://github.com/dani-garcia/yubico-rs' }

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.7.1"
ENV VAULT_VERSION "v2.8.0b"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
@@ -33,9 +33,10 @@ FROM rust as build
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
# Builds your dependencies and removes the
# dummy project, except the target folder
@@ -75,9 +76,8 @@ VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (env file and web-vault)
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build app/target/release/bitwarden_rs .

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.7.1"
ENV VAULT_VERSION "v2.8.0b"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
@@ -55,7 +55,8 @@ COPY . .
# Build
RUN rustup target add aarch64-unknown-linux-gnu
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
# TODO: Enable yubico when #262 is fixed
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v --no-default-features
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
@@ -82,9 +83,8 @@ RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (env file and web-vault)
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.7.1"
ENV VAULT_VERSION "v2.8.0b"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
@@ -56,9 +56,8 @@ VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (env file and web-vault)
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.7.1"
ENV VAULT_VERSION "v2.8.0b"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
@@ -82,9 +82,8 @@ RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (env file and web-vault)
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .

View File

@@ -1,94 +0,0 @@
# Proxy examples
In this document, `<SERVER>` refers to the IP or domain where bitwarden_rs is accessible from. If both the proxy and bitwarden_rs are running in the same system, simply use `localhost`.
The ports proxied by default are `80` for the web server and `3012` for the WebSocket server. The proxies are configured to listen in port `443` with HTTPS enabled, which is recommended.
When using a proxy, it's preferrable to configure HTTPS at the proxy level and not at the application level, this way the WebSockets connection is also secured.
## Caddy
```nginx
localhost:443 {
# The negotiation endpoint is also proxied to Rocket
proxy /notifications/hub/negotiate <SERVER>:80 {
transparent
}
# Notifications redirected to the websockets server
proxy /notifications/hub <SERVER>:3012 {
websocket
}
# Proxy the Root directory to Rocket
proxy / <SERVER>:80 {
transparent
}
tls ${SSLCERTIFICATE} ${SSLKEY}
}
```
## Nginx (by shauder)
```nginx
server {
listen 443 ssl http2;
server_name vault.*;
# Specify SSL config if using a shared one.
#include conf.d/ssl/ssl.conf;
location / {
proxy_pass http://<SERVER>:80;
}
location /notifications/hub {
proxy_pass http://<SERVER>:3012;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub/negotiate {
proxy_pass http://<SERVER>:80;
}
}
```
## Apache (by fbartels)
```apache
<VirtualHost *:443>
SSLEngine on
ServerName bitwarden.$hostname.$domainname
SSLCertificateFile ${SSLCERTIFICATE}
SSLCertificateKeyFile ${SSLKEY}
SSLCACertificateFile ${SSLCA}
${SSLCHAIN}
ErrorLog \${APACHE_LOG_DIR}/bitwarden-error.log
CustomLog \${APACHE_LOG_DIR}/bitwarden-access.log combined
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://<SERVER>:3012/$1 [P,L]
ProxyPass / http://<SERVER>:80/
ProxyPreserveHost On
ProxyRequests Off
</VirtualHost>
```
## Traefik (docker-compose example)
```traefik
labels:
- 'traefik.frontend.rule=Host:vault.example.local'
- 'traefik.docker.network=traefik'
- 'traefik.port=80'
- 'traefik.enable=true'
- 'traefik.web.frontend.rule=Host:vault.example.local'
- 'traefik.web.port=80'
- 'traefik.hub.frontend.rule=Path:/notifications/hub'
- 'traefik.hub.port=3012'
- 'traefik.negotiate.frontend.rule=Path:/notifications/hub/negotiate'
- 'traefik.negotiate.port=80'
```

661
README.md
View File

@@ -3,10 +3,11 @@
---
[![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs)
[![Docker Pulls](https://img.shields.io/docker/pulls/mprasil/bitwarden.svg)](https://hub.docker.com/r/mprasil/bitwarden)
[![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
[![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
[![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
[![Matrix Chat](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#bitwarden_rs:matrix.org)
[![Matrix Chat](https://img.shields.io/matrix/bitwarden_rs:matrix.org.svg?logo=matrix)](https://matrix.to/#/#bitwarden_rs:matrix.org)
Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/bitwarden_rs).
@@ -14,58 +15,6 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
---
**Table of contents**
- [Features](#features)
- [Missing features](#missing-features)
- [Docker image usage](#docker-image-usage)
- [Starting a container](#starting-a-container)
- [Updating the bitwarden image](#updating-the-bitwarden-image)
- [Configuring bitwarden service](#configuring-bitwarden-service)
- [Disable registration of new users](#disable-registration-of-new-users)
- [Disable invitations](#disable-invitations)
- [Configure server administrator](#configure-server-administrator)
- [Enabling HTTPS](#enabling-https)
- [Enabling WebSocket notifications](#enabling-websocket-notifications)
- [Enabling U2F authentication](#enabling-u2f-authentication)
- [Enabling YubiKey OTP authentication](#enabling-yubikey-otp-authentication)
- [Changing persistent data location](#changing-persistent-data-location)
- [/data prefix:](#data-prefix)
- [database name and location](#database-name-and-location)
- [attachments location](#attachments-location)
- [icons cache](#icons-cache)
- [Changing the API request size limit](#changing-the-api-request-size-limit)
- [Changing the number of workers](#changing-the-number-of-workers)
- [SMTP configuration](#smtp-configuration)
- [Password hint display](#password-hint-display)
- [Disabling or overriding the Vault interface hosting](#disabling-or-overriding-the-vault-interface-hosting)
- [Other configuration](#other-configuration)
- [Fail2Ban Setup](#fail2ban-setup)
- [Logging Failed Login Attempts to Syslog](#logging-failed-login-attempts-to-syslog)
- [Fail2Ban Filter](#fail2ban-filter)
- [Fail2Ban Jail](#fail2ban-jail)
- [Testing Fail2Ban](#testing-fail2ban)
- [Running with systemd-docker](#running-with-systemd-docker)
- [Setting environment variables](#setting-environment-variables)
- [Running the service](#running-the-service)
- [Building your own image](#building-your-own-image)
- [Building binary](#building-binary)
- [Available packages](#available-packages)
- [Arch Linux](#arch-linux)
- [Kubernetes deployment](#kubernetes-deployment)
- [Backing up your vault](#backing-up-your-vault)
- [1. the sqlite3 database](#1-the-sqlite3-database)
- [2. the attachments folder](#2-the-attachments-folder)
- [3. the key files](#3-the-key-files)
- [4. Icon Cache](#4-icon-cache)
- [Running the server with non-root user](#running-the-server-with-non-root-user)
- [Differences from upstream API implementation](#differences-from-upstream-api-implementation)
- [Changing user email](#changing-user-email)
- [Creating organization](#creating-organization)
- [Inviting users into organization](#inviting-users-into-organization)
- [Running on unencrypted connection](#running-on-unencrypted-connection)
- [Get in touch](#get-in-touch)
## Features
Basically full implementation of Bitwarden API is provided including:
@@ -79,609 +28,23 @@ Basically full implementation of Bitwarden API is provided including:
* Authenticator and U2F support
* YubiKey OTP
## Missing features
* Email confirmation
* Other two-factor systems:
* Duo
* Email codes
## Installation
Pull the docker image and mount a volume from the host for persistent storage:
## Docker image usage
### Starting a container
The persistent data is stored under /data inside the container, so the only requirement for persistent deployment using Docker is to mount persistent volume at the path:
```
```sh
docker pull mprasil/bitwarden:latest
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
```
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
This will preserve any persistent data under `/bw-data/`, you can adapt the path to whatever suits you.
**IMPORTANT**: Some web browsers, like Chrome, disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault from HTTPS.
The service will be exposed on port 80.
This can be configured in [bitwarden_rs directly](https://github.com/dani-garcia/bitwarden_rs/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/bitwarden_rs/wiki/Proxy-examples)).
### Updating the bitwarden image
If you have an available domain name, you can get HTTPS certificates with [Let's Encrypt](https://letsencrypt.org/), or you can generate self-signed certificates with utilities like [mkcert](https://github.com/FiloSottile/mkcert). Some proxies automatically do this step, like Caddy (see examples linked above).
Updating is straightforward, you just make sure to preserve the mounted volume. If you used the bind-mounted path as in the example above, you just need to `pull` the latest image, `stop` and `rm` the current container and then start a new one the same way as before:
```sh
# Pull the latest version
docker pull mprasil/bitwarden:latest
# Stop and remove the old container
docker stop bitwarden
docker rm bitwarden
# Start new container with the data mounted
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
```
Then visit [http://localhost:80](http://localhost:80)
In case you didn't bind mount the volume for persistent data, you need an intermediate step where you preserve the data with an intermediate container:
```sh
# Pull the latest version
docker pull mprasil/bitwarden:latest
# Create intermediate container to preserve data
docker run --volumes-from bitwarden --name bitwarden_data busybox true
# Stop and remove the old container
docker stop bitwarden
docker rm bitwarden
# Start new container with the data mounted
docker run -d --volumes-from bitwarden_data --name bitwarden -p 80:80 mprasil/bitwarden:latest
# Optionally remove the intermediate container
docker rm bitwarden_data
# Alternatively you can keep data container around for future updates in which case you can skip last step.
```
## Configuring bitwarden service
### Disable registration of new users
By default new users can register, if you want to disable that, set the `SIGNUPS_ALLOWED` env variable to `false`:
```sh
docker run -d --name bitwarden \
-e SIGNUPS_ALLOWED=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note: While users can't register on their own, they can still be invited by already registered users. Read below if you also want to disable that.
### Disable invitations
Even when registration is disabled, organization administrators or owners can invite users to join organization. This won't send email invitation to the users, but after they are invited, they can register with the invited email even if `SIGNUPS_ALLOWED` is actually set to `false`. You can disable this functionality completely by setting `INVITATIONS_ALLOWED` env variable to `false`:
```sh
docker run -d --name bitwarden \
-e SIGNUPS_ALLOWED=false \
-e INVITATIONS_ALLOWED=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Configure server administrator
**Warning:** *Never* use your regular account for the admin functionality. This is a bit of a hack using the Vault interface for something it's not intended to do and it breaks any other functionality for the account. Please set up and use separate account just for this functionality.
You can configure one email account to be server administrator via the `SERVER_ADMIN_EMAIL` environment variable:
```sh
docker run -d --name bitwarden \
-e SERVER_ADMIN_EMAIL=admin@example.com \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
This will give the user extra functionality and privileges to manage users on the server. In the Vault, the user will see a special (virtual) organization called `bitwarden_rs`. This organization doesn't actually exist and can't be used for most things. (can't have collections or ciphers) Instead it just contains all the users registered on the server. Deleting users from this organization will actually completely delete the user from the server. Inviting users into this organization will just invite the user so they are able to register, but will not grant any organization membership. (unlike inviting user to regular organization)
You can think of the `bitwarden_rs` organization as sort of Admin interface to manage users on the server. Keep in mind that deleting user this way removes the user permanently without any way to restore the deleted data just as if user deleted their own account.
### Enabling HTTPS
To enable HTTPS, you need to configure the `ROCKET_TLS`.
The values to the option must follow the format:
```
ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
```
Where:
- certs: a path to a certificate chain in PEM format
- key: a path to a private key file in PEM format for the certificate in certs
```sh
docker run -d --name bitwarden \
-e ROCKET_TLS='{certs="/ssl/certs.pem",key="/ssl/key.pem"}' \
-v /ssl/keys/:/ssl/ \
-v /bw-data/:/data/ \
-p 443:80 \
mprasil/bitwarden:latest
```
Note that you need to mount ssl files and you need to forward appropriate port.
Due to what is likely a certificate validation bug in Android, you need to make sure that your certificate includes the full chain of trust. In the case of certbot, this means using `fullchain.pem` instead of `cert.pem`.
Softwares used for getting certs are often using symlinks. If that is the case, both locations need to be accessible to the docker container.
Example: [certbot](https://certbot.eff.org/) will create a folder that contains the needed `fullchain.pem` and `privkey.pem` files in `/etc/letsencrypt/live/mydomain/`
These files are symlinked to `../../archive/mydomain/privkey.pem`
So to use from bitwarden container:
```sh
docker run -d --name bitwarden \
-e ROCKET_TLS='{certs="/ssl/live/mydomain/fullchain.pem",key="/ssl/live/mydomain/privkey.pem"}' \
-v /etc/letsencrypt/:/ssl/ \
-v /bw-data/:/data/ \
-p 443:80 \
mprasil/bitwarden:latest
```
### Enabling WebSocket notifications
*Important: This does not apply to the mobile clients, which use push notifications.*
To enable WebSockets notifications, an external reverse proxy is necessary, and it must be configured to do the following:
- Route the `/notifications/hub` endpoint to the WebSocket server, by default at port `3012`, making sure to pass the `Connection` and `Upgrade` headers. (Note the port can be changed with `WEBSOCKET_PORT` variable)
- Route everything else, including `/notifications/hub/negotiate`, to the standard Rocket server, by default at port `80`.
- If using Docker, you may need to map both ports with the `-p` flag
Example configurations are included in the [PROXY.md](https://github.com/dani-garcia/bitwarden_rs/blob/master/PROXY.md) file.
Then you need to enable WebSockets negotiation on the bitwarden_rs side by setting the `WEBSOCKET_ENABLED` variable to `true`:
```sh
docker run -d --name bitwarden \
-e WEBSOCKET_ENABLED=true \
-v /bw-data/:/data/ \
-p 80:80 \
-p 3012:3012 \
mprasil/bitwarden:latest
```
Note: The reason for this workaround is the lack of support for WebSockets from Rocket (though [it's a planned feature](https://github.com/SergioBenitez/Rocket/issues/90)), which forces us to launch a secondary server on a separate port.
### Enabling U2F authentication
To enable U2F authentication, you must be serving bitwarden_rs from an HTTPS domain with a valid certificate (Either using the included
HTTPS options or with a reverse proxy). We recommend using a free certificate from Let's Encrypt.
After that, you need to set the `DOMAIN` environment variable to the same address from where bitwarden_rs is being served:
```sh
docker run -d --name bitwarden \
-e DOMAIN=https://bw.domain.tld \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note that the value has to include the `https://` and it may include a port at the end (in the format of `https://bw.domain.tld:port`) when not using `443`.
### Enabling YubiKey OTP authentication
To enable YubiKey authentication, you must set the `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` env variables.
If `YUBICO_SERVER` is not specified, it will use the default YubiCloud servers. You can generate `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` for the default YubiCloud [here](https://upgrade.yubico.com/getapikey/).
Note: In order to generate API keys or use a YubiKey with an OTP server, it must be registered. After configuring your key in the [YubiKey Personalization Tool](https://www.yubico.com/products/services-software/personalization-tools/use/), you can register it with the default servers [here](https://upload.yubico.com/).
```sh
docker run -d --name bitwarden \
-e YUBICO_CLIENT_ID=12345 \
-e YUBICO_SECRET_KEY=ABCDEABCDEABCDEABCDE= \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Changing persistent data location
#### /data prefix:
By default all persistent data is saved under `/data`, you can override this path by setting the `DATA_FOLDER` env variable:
```sh
docker run -d --name bitwarden \
-e DATA_FOLDER=/persistent \
-v /bw-data/:/persistent/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Notice, that you need to adapt your volume mount accordingly.
#### database name and location
Default is `$DATA_FOLDER/db.sqlite3`, you can change the path specifically for database using `DATABASE_URL` variable:
```sh
docker run -d --name bitwarden \
-e DATABASE_URL=/database/bitwarden.sqlite3 \
-v /bw-data/:/data/ \
-v /bw-database/:/database/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note, that you need to remember to mount the volume for both database and other persistent data if they are different.
#### attachments location
Default is `$DATA_FOLDER/attachments`, you can change the path using `ATTACHMENTS_FOLDER` variable:
```sh
docker run -d --name bitwarden \
-e ATTACHMENTS_FOLDER=/attachments \
-v /bw-data/:/data/ \
-v /bw-attachments/:/attachments/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note, that you need to remember to mount the volume for both attachments and other persistent data if they are different.
#### icons cache
Default is `$DATA_FOLDER/icon_cache`, you can change the path using `ICON_CACHE_FOLDER` variable:
```sh
docker run -d --name bitwarden \
-e ICON_CACHE_FOLDER=/icon_cache \
-v /bw-data/:/data/ \
-v /icon_cache/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note, that in the above example we don't mount the volume locally, which means it won't be persisted during the upgrade unless you use intermediate data container using `--volumes-from`. This will impact performance as bitwarden will have to re-download the icons on restart, but might save you from having stale icons in cache as they are not automatically cleaned.
### Changing the API request size limit
By default the API calls are limited to 10MB. This should be sufficient for most cases, however if you want to support large imports, this might be limiting you. On the other hand you might want to limit the request size to something smaller than that to prevent API abuse and possible DOS attack, especially if running with limited resources.
To set the limit, you can use the `ROCKET_LIMITS` variable. Example here shows 10MB limit for posted json in the body (this is the default):
```sh
docker run -d --name bitwarden \
-e ROCKET_LIMITS={json=10485760} \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Changing the number of workers
When you run bitwarden_rs, it spawns `2 * <number of cpu cores>` workers to handle requests. On some systems this might lead to low number of workers and hence slow performance, so the default in the docker image is changed to spawn 10 threads. You can override this setting to increase or decrease the number of workers by setting the `ROCKET_WORKERS` variable.
In the example below, we're starting with 20 workers:
```sh
docker run -d --name bitwarden \
-e ROCKET_WORKERS=20 \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### SMTP configuration
You can configure bitwarden_rs to send emails via a SMTP agent:
```sh
docker run -d --name bitwarden \
-e SMTP_HOST=<smtp.domain.tld> \
-e SMTP_FROM=<bitwarden@domain.tld> \
-e SMTP_PORT=587 \
-e SMTP_SSL=true \
-e SMTP_USERNAME=<username> \
-e SMTP_PASSWORD=<password> \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
When `SMTP_SSL` is set to `true`(this is the default), only TLSv1.1 and TLSv1.2 protocols will be accepted and `SMTP_PORT` will default to `587`. If set to `false`, `SMTP_PORT` will default to `25` and the connection won't be encrypted. This can be very insecure, use this setting only if you know what you're doing.
### Password hint display
Usually, password hints are sent by email. But as bitwarden_rs is made with small or personal deployment in mind, hints are also available from the password hint page, so you don't have to configure an email service. If you want to disable this feature, you can use the `SHOW_PASSWORD_HINT` variable:
```sh
docker run -d --name bitwarden \
-e SHOW_PASSWORD_HINT=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Disabling or overriding the Vault interface hosting
As a convenience bitwarden_rs image will also host static files for Vault web interface. You can disable this static file hosting completely by setting the WEB_VAULT_ENABLED variable.
```sh
docker run -d --name bitwarden \
-e WEB_VAULT_ENABLED=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Alternatively you can override the Vault files and provide your own static files to host. You can do that by mounting a path with your files over the `/web-vault` directory in the container. Just make sure the directory contains at least `index.html` file.
```sh
docker run -d --name bitwarden \
-v /path/to/static/files_directory:/web-vault \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note that you can also change the path where bitwarden_rs looks for static files by providing the `WEB_VAULT_FOLDER` environment variable with the path.
### Other configuration
Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables).
### Fail2Ban Setup
Bitwarden_rs logs failed login attempts to stdout. We need to set this so the host OS can see these. Then we can setup Fail2Ban.
#### Logging Failed Login Attempts to Syslog
We need to set the logging driver to syslog so the host OS and Fail2Ban can see them.
If you are using docker commands, you will need to add: `--log-driver syslog --log-opt tag=$TAG` to your command.
If you are using docker-compose, add this to you yaml file:
```
bitwarden:
logging:
driver: "syslog"
options:
tag: "$TAG"
```
With the above settings in the docker-compose file. Any failed login attempts will look like this in your syslog file:
```
MMM DD hh:mm:ss server-hostname $TAG[773]: [YYYY-MM-DD][hh:mm:ss][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: XXX.XXX.XXX.XXX. Username: email@domain.com.
```
You can change the '$TAG' to anything you like. Just remember it because it will be in the Fail2Ban filter.
#### Fail2Ban Filter
Create the filter file
```
sudo nano /etc/fail2ban/filter.d/bitwarden.conf
```
And add the following
```
[INCLUDES]
before = common.conf
[Definition]
_daemon = $TAG
failregex = ^%(__prefix_line)s.*Username or password is incorrect\. Try again\. IP: <HOST>\. Username:.*$
ignoreregex =
```
Dont forget to change the '$TAG' to what you set it as from above.
#### Fail2Ban Jail
Now we need the jail, create the jail file
```
sudo nano /etc/fail2ban/jail.d/bitwarden.local
```
and add:
```
[bitwarden]
enabled = true
port = 80,443,8081
filter = bitwarden
action = iptables-allports[name=bitwarden]
logpath = /var/log/syslog
maxretry = 3
bantime = 14400
findtime = 14400
```
Feel free to change the options as you see fit.
#### Testing Fail2Ban
Now just try to login to bitwarden using any email (it doesnt have to be a valid email, just an email format)
If it works correctly and your IP is banned, you can unban the ip by running:
```
sudo fail2ban-client unban XX.XX.XX.XX bitwarden
```
### Running with systemd-docker
These instructions allow you to have systemd manage the lifecycle of the docker container, if you prefer.
First, install the `systemd-docker` package using your system package manager.
This is a wrapper which improves docker integration with systemd.
For full instructions and configuration options, see the [GitHub repository](https://github.com/ibuildthecloud/systemd-docker).
As root, create `/etc/systemd/system/bitwarden.service` using your preferred editor with the following contents:
```ini
[Unit]
Description=Bitwarden
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
ExecStartPre=/usr/bin/docker pull mprasil/bitwarden:latest
ExecStart=/usr/bin/systemd-docker --cgroups name=systemd --env run \
-p 8080:80 \
-p 8081:3012 \
-v /opt/bw-data:/data/ \
--rm --name %n mprasil/bitwarden:latest
Restart=always
RestartSec=10s
Type=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target
```
Adjust the above example as necessary. In particular, pay attention to the `-p` and `-v` options,
as these control the port and volume bindings between the container and the host.
Explanation of options which may not be self-explanatory:
- A `TimeoutStartSec` value of 0 stops systemd from considering the service failed
after waiting for the default startup time. This is required as it may take a while for the `docker pull` in `ExecStartPre` to finish.
- `ExecStartPre`: Pull the docker tag before running.
- A `Type` value of `notify` tells systemd to expect a notification from the service that it is ready.
- A `NotifyAccess` value of `all` is required by `systemd-docker`.
#### Setting environment variables
It's possible to directly specify environment variables in the unit file in two ways:
- Using an `Environment` directive in the `[Service]` block.
- Using the `-e` option of `docker`. In this case, you can omit the `--env` option shown in the example above.
To verify that your environment variables are set correctly, check the output of `systemctl show bitwarden.service`
for an `Environment` line.
It's also possible to store environment variables in a separate file using the `EnvironmentFile` directive in the unit file.
Systemd can source a file of the form:
```shell
Key="Value"
```
However, the systemd project does not mandate where this file should be stored. Consult your distribution's documentation for the
best location for this file. For example, RedHat based distributions typically place these files in `/etc/sysconfig/`
If you're unsure, just create a file as root in `/etc/` e.g. `/etc/bitwarden.service.conf`.
In your unit file, add an `EnvironmentFile` directive in the `[Service]` block, the value being the full path to the
file created above. Example:
```ini
[Unit]
Description=Bitwarden
After=docker.service
Requires=docker.service
[Service]
EnvironmentFile=/etc/bitwarden.service.conf
TimeoutStartSec=0
-snip-
```
#### Running the service
After the above installation and configuration is complete, reload systemd using `sudo systemctl daemon-reload`.
Then, start the Bitwarden service using `sudo systemctl start bitwarden`.
To have the service start with the system, use `sudo systemctl enable bitwarden`.
Verify that the container has started using `systemctl status bitwarden`.
## Building your own image
Clone the repository, then from the root of the repository run:
```sh
# Build the docker image:
docker build -t bitwarden_rs .
```
## Building binary
For building binary outside the Docker environment and running it locally without docker, please see [build instructions](https://github.com/dani-garcia/bitwarden_rs/blob/master/BUILD.md).
## Available packages
### Arch Linux
Bitwarden_rs is already packaged for Archlinux thanks to @mqus. There is an [AUR package](https://aur.archlinux.org/packages/bitwarden_rs) (optionally with the [vault web interface](https://aur.archlinux.org/packages/bitwarden_rs-vault/) ) available.
## Kubernetes deployment
Please check the [kubernetes-bitwarden_rs](https://github.com/icicimov/kubernetes-bitwarden_rs) repository for example deployment in Kubernetes.
It will setup a fully functional and secure `bitwarden_rs` application in Kubernetes behind [nginx-ingress-controller](https://github.com/kubernetes/ingress-nginx) and AWS [ELBv1](https://aws.amazon.com/elasticloadbalancing/features/#Details_for_Elastic_Load_Balancing_Products). It provides little bit more than just simple deployment but you can use all or just part of the manifests depending on your needs and setup.
## Backing up your vault
### 1. the sqlite3 database
The sqlite3 database should be backed up using the proper sqlite3 backup command. This will ensure the database does not become corrupted if the backup happens during a database write.
```
mkdir $DATA_FOLDER/db-backup
sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sqlite3'"
```
This command can be run via a CRON job everyday, however note that it will overwrite the same `backup.sqlite3` file each time. This backup file should therefore be saved via incremental backup either using a CRON job command that appends a timestamp or from another backup app such as Duplicati. To restore simply overwrite `db.sqlite3` with `backup.sqlite3` (while bitwarden_rs is stopped).
Running the above command requires sqlite3 to be installed on the docker host system. You can achieve the same result with a sqlite3 docker container using the following command.
```
docker run --rm --volumes-from=bitwarden bruceforce/bw_backup /backup.sh
```
You can also run a container with integrated cron daemon to automatically backup your database. See https://gitlab.com/1O/bitwarden_rs-backup for examples.
### 2. the attachments folder
By default, this is located in `$DATA_FOLDER/attachments`
### 3. the key files
This is optional, these are only used to store tokens of users currently logged in, deleting them would simply log each user out forcing them to log in again. By default, these are located in the `$DATA_FOLDER` (by default /data in the docker). There are 3 files: rsa_key.der, rsa_key.pem, rsa_key.pub.der.
### 4. Icon Cache
This is optional, the icon cache can re-download itself however if you have a large cache, it may take a long time. By default it is located in `$DATA_FOLDER/icon_cache`
## Running the server with non-root user
The root user inside the container is already pretty limited in what it can do, so the default setup should be secure enough. However if you wish to go the extra mile to avoid using root even in container, here's how you can do that:
1. Create a data folder that's owned by non-root user, so you can use that user to write persistent data. Get the user `id`. In linux you can run `stat <folder_name>` to get/verify the owner ID.
2. When you run the container, you need to provide the user ID as one of the parameters. Note that this needs to be in the numeric form and not the username, because docker would try to find such user-defined inside the image, which would likely not be there or it would have different ID than your local user and hence wouldn't be able to write the persistent data. This can be done with the `--user` parameter.
3. bitwarden_rs listens on port `80` inside the container by default, this [won't work with non-root user](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html), because regular users aren't allowed to open port below `1024`. To overcome this, you need to configure server to listen on a different port, you can use `ROCKET_PORT` to do that.
Here's sample docker run, that uses user with id `1000` and with the port redirection configured, so that inside container the service is listening on port `8080` and docker translates that to external (host) port `80`:
```sh
docker run -d --name bitwarden \
--user 1000 \
-e ROCKET_PORT=8080 \
-v /bw-data/:/data/ \
-p 80:8080 \
mprasil/bitwarden:latest
```
## Differences from upstream API implementation
### Changing user email
Because we don't have any SMTP functionality at the moment, there's no way to deliver the verification token when you try to change the email. User just needs to enter any random token to continue and the change will be applied.
### Creating organization
We use upstream Vault interface directly without any (significant) changes, this is why user is presented with paid options when creating organization. To create an organization, just use the free option, none of the limits apply when using bitwarden_rs as back-end API and after the organization is created it should behave like Enterprise organization.
### Inviting users into organization
The invited users won't get the invitation email, instead all already registered users will appear in the interface as if they already accepted the invitation. Organization admin then just needs to confirm them to be proper Organization members and to give them access to the shared secrets.
Invited users, that aren't registered yet will show up in the Organization admin interface as "Invited". At the same time an invitation record is created that allows the users to register even if [user registration is disabled](#disable-registration-of-new-users). (unless you [disable this functionality](#disable-invitations)) They will automatically become "Accepted" once they register. From there Organization admin can confirm them to give them access to Organization.
### Running on unencrypted connection
It is strongly recommended to run bitwarden_rs service over HTTPS. However the server itself while [supporting it](#enabling-https) does not strictly require such setup. This makes it a bit easier to spin up the service in cases where you can generally trust the connection (internal and secure network, access over VPN,..) or when you want to put the service behind HTTP proxy, that will do the encryption on the proxy end.
Running over HTTP is still reasonably secure provided you use really strong master password and that you avoid using web Vault over connection that is vulnerable to MITM attacks where attacker could inject javascript into your interface. However some forms of 2FA might not work in this setup and [Vault doesn't work in this configuration in Chrome](https://github.com/bitwarden/web/issues/254).
## Usage
See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) for more information on how to configure and run the bitwarden_rs server.
## Get in touch

39
build.rs Normal file
View File

@@ -0,0 +1,39 @@
use std::process::Command;
fn main() {
read_git_info().expect("Unable to read Git info");
}
fn run(args: &[&str]) -> Result<String, std::io::Error> {
let out = Command::new(args[0]).args(&args[1..]).output()?;
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
}
/// This method reads info from Git, namely tags, branch, and revision
fn read_git_info() -> Result<(), std::io::Error> {
// The exact tag for the current commit, can be empty when
// the current commit doesn't have an associated tag
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"])?;
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact_tag);
// The last available tag, equal to exact_tag when
// the current commit is tagged
let last_tag = run(&["git", "describe", "--abbrev=0", "--tags"])?;
println!("cargo:rustc-env=GIT_LAST_TAG={}", last_tag);
// The current branch name
let branch = run(&["git", "rev-parse", "--abbrev-ref", "HEAD"])?;
println!("cargo:rustc-env=GIT_BRANCH={}", branch);
// The current git commit hash
let rev = run(&["git", "rev-parse", "HEAD"])?;
let rev_short = rev.get(..12).unwrap_or_default();
println!("cargo:rustc-env=GIT_REV={}", rev_short);
// To access these values, use:
// env!("GIT_EXACT_TAG")
// env!("GIT_LAST_TAG")
// env!("GIT_BRANCH")
// env!("GIT_REV")
Ok(())
}

View File

@@ -1,27 +0,0 @@
--- a/src/app/services/services.module.ts
+++ b/src/app/services/services.module.ts
@@ -120,20 +120,16 @@ const notificationsService = new NotificationsService(userService, syncService,
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
-const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(),
+const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost() || true,
platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window);
export function initFactory(): Function {
return async () => {
await (storageService as HtmlStorageService).init();
- const isDev = platformUtilsService.isDev();
- if (!isDev && platformUtilsService.isSelfHost()) {
- environmentService.baseUrl = window.location.origin;
- } else {
- environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
- 'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
- }
+ const isDev = false;
+ environmentService.baseUrl = window.location.origin;
+ environmentService.notificationsUrl = window.location.origin + '/notifications';
apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://localhost:4000' : null,

View File

@@ -1 +1 @@
nightly-2018-12-01
nightly-2019-01-08

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
max_width = 120

107
src/api/admin.rs Normal file
View File

@@ -0,0 +1,107 @@
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::api::{JsonResult, JsonUpcase};
use crate::CONFIG;
use crate::db::models::*;
use crate::db::DbConn;
use crate::mail;
use rocket::request::{self, FromRequest, Request};
use rocket::{Outcome, Route};
pub fn routes() -> Vec<Route> {
routes![get_users, invite_user, delete_user]
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct InviteData {
Email: String,
}
#[get("/users")]
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
let users = User::get_all(&conn);
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
Ok(Json(Value::Array(users_json)))
}
#[post("/invite", data = "<data>")]
fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
let data: InviteData = data.into_inner().data;
let email = data.Email.clone();
if User::find_by_mail(&data.Email, &conn).is_some() {
err!("User already exists")
}
if !CONFIG.invitations_allowed {
err!("Invitations are not allowed")
}
if let Some(ref mail_config) = CONFIG.mail {
let mut user = User::new(email);
user.save(&conn)?;
let org_name = "bitwarden_rs";
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None, mail_config)?;
} else {
let mut invitation = Invitation::new(data.Email);
invitation.save(&conn)?;
}
Ok(Json(json!({})))
}
#[post("/users/<uuid>/delete")]
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&uuid, &conn) {
Some(user) => user,
None => err!("User doesn't exist"),
};
user.delete(&conn)?;
Ok(Json(json!({})))
}
pub struct AdminToken {}
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let config_token = match CONFIG.admin_token.as_ref() {
Some(token) => token,
None => err_handler!("Admin panel is disabled"),
};
// Get access_token
let access_token: &str = match request.headers().get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
},
None => err_handler!("No access token provided"),
};
// TODO: What authentication to use?
// Option 1: Make it a config option
// Option 2: Generate random token, and
// Option 2a: Send it to admin email, like upstream
// Option 2b: Print in console or save to data dir, so admin can check
use crate::auth::ClientIp;
let ip = match request.guard::<ClientIp>() {
Outcome::Success(ip) => ip,
_ => err_handler!("Error getting Client IP"),
};
if access_token != config_token {
err_handler!("Invalid admin token", format!("IP: {}.", ip.ip))
}
Outcome::Success(AdminToken {})
}
}

View File

@@ -3,13 +3,13 @@ use rocket_contrib::json::Json;
use crate::db::models::*;
use crate::db::DbConn;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, UpdateType, WebSocketUsers};
use crate::auth::Headers;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
use crate::auth::{decode_invite_jwt, Headers, InviteJWTClaims};
use crate::mail;
use crate::CONFIG;
use rocket::{Route, State};
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
@@ -44,6 +44,8 @@ struct RegisterData {
MasterPasswordHash: String,
MasterPasswordHint: Option<String>,
Name: Option<String>,
Token: Option<String>,
OrganizationUserId: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -59,13 +61,23 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
let mut user = match User::find_by_mail(&data.Email, &conn) {
Some(user) => {
if Invitation::take(&data.Email, &conn) {
if !user.password_hash.is_empty() {
err!("User already exists")
}
if let Some(token) = data.Token {
let claims: InviteJWTClaims = decode_invite_jwt(&token)?;
if claims.email == data.Email {
user
} else {
err!("Registration email does not match invite email")
}
} else if Invitation::take(&data.Email, &conn) {
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
user_org.status = UserOrgStatus::Accepted as i32;
if user_org.save(&conn).is_err() {
err!("Failed to accept user to organization")
}
user_org.save(&conn)?;
}
user
} else if CONFIG.signups_allowed {
err!("Account with this email already exists")
@@ -75,13 +87,16 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
}
None => {
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
User::new(data.Email)
User::new(data.Email.clone())
} else {
err!("Registration not allowed")
}
}
};
// Make sure we don't leave a lingering invitation.
Invitation::take(&data.Email, &conn);
if let Some(client_kdf_iter) = data.KdfIterations {
user.client_kdf_iter = client_kdf_iter;
}
@@ -107,10 +122,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
user.public_key = Some(keys.PublicKey);
}
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save user"),
}
user.save(&conn)
}
#[get("/accounts/profile")]
@@ -143,10 +155,8 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
Some(ref h) if h.is_empty() => None,
_ => data.MasterPasswordHint,
};
match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save user profile"),
}
user.save(&conn)?;
Ok(Json(user.to_json(&conn)))
}
#[get("/users/<uuid>/public-key")]
@@ -172,10 +182,8 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
user.private_key = Some(data.EncryptedPrivateKey);
user.public_key = Some(data.PublicKey);
match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save the user's keys"),
}
user.save(&conn)?;
Ok(Json(user.to_json(&conn)))
}
#[derive(Deserialize)]
@@ -197,10 +205,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save password"),
}
user.save(&conn)
}
#[derive(Deserialize)]
@@ -227,10 +232,7 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
user.client_kdf_type = data.Kdf;
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save password settings"),
}
user.save(&conn)
}
#[derive(Deserialize)]
@@ -253,7 +255,7 @@ struct KeyData {
}
#[post("/accounts/key", data = "<data>")]
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data: KeyData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
@@ -274,9 +276,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws:
}
saved_folder.name = folder_data.Name;
if saved_folder.save(&conn).is_err() {
err!("Failed to save folder")
}
saved_folder.save(&conn)?
}
// Update cipher data
@@ -292,7 +292,15 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws:
err!("The cipher is not owned by the user")
}
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?
update_cipher_from_data(
&mut saved_cipher,
cipher_data,
&headers,
false,
&conn,
&nt,
UpdateType::CipherUpdate,
)?
}
// Update user data
@@ -302,11 +310,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws:
user.private_key = Some(data.PrivateKey);
user.reset_security_stamp();
if user.save(&conn).is_err() {
err!("Failed modify user key");
}
Ok(())
user.save(&conn)
}
#[post("/accounts/security-stamp", data = "<data>")]
@@ -319,10 +323,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
}
user.reset_security_stamp();
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to reset security stamp"),
}
user.save(&conn)
}
#[derive(Deserialize)]
@@ -377,10 +378,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save email address"),
}
user.save(&conn)
}
#[post("/accounts/delete", data = "<data>")]
@@ -397,10 +395,7 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
err!("Invalid password")
}
match user.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?"),
}
user.delete(&conn)
}
#[get("/accounts/revision-date")]
@@ -425,9 +420,7 @@ fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResul
};
if let Some(ref mail_config) = CONFIG.mail {
if let Err(e) = mail::send_password_hint(&data.Email, hint, mail_config) {
err!(format!("There have been a problem sending the email: {}", e));
}
mail::send_password_hint(&data.Email, hint, mail_config)?;
} else if CONFIG.show_password_hint {
if let Some(hint) = hint {
err!(format!("Your password hint is: {}", &hint));

View File

@@ -1,8 +1,8 @@
use std::collections::{HashSet, HashMap};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use rocket::http::ContentType;
use rocket::{request::Form, Data, Route, State};
use rocket::{request::Form, Data, Route};
use rocket_contrib::json::Json;
use serde_json::Value;
@@ -17,7 +17,7 @@ use crate::db::DbConn;
use crate::crypto;
use crate::api::{self, EmptyResult, JsonResult, JsonUpcase, PasswordData, UpdateType, WebSocketUsers};
use crate::api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType};
use crate::auth::Headers;
use crate::CONFIG;
@@ -56,7 +56,7 @@ pub fn routes() -> Vec<Route> {
delete_all,
move_cipher_selected,
move_cipher_selected_put,
put_collections_update,
post_collections_update,
post_collections_admin,
put_collections_admin,
@@ -80,9 +80,16 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
let collections_json: Vec<Value> = collections.iter().map(|c| c.to_json()).collect();
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
let ciphers_json: Vec<Value> = ciphers
.iter()
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
.collect();
let domains_json = if data.exclude_domains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
let domains_json = if data.exclude_domains {
Value::Null
} else {
api::core::get_eq_domains(headers).unwrap().into_inner()
};
Ok(Json(json!({
"Profile": user_json,
@@ -98,7 +105,10 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
let ciphers_json: Vec<Value> = ciphers
.iter()
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
.collect();
Ok(Json(json!({
"Data": ciphers_json,
@@ -111,7 +121,7 @@ fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
fn get_cipher(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
None => err!("Cipher doesn't exist"),
};
if !cipher.is_accessible_to_user(&headers.user.uuid, &conn) {
@@ -166,7 +176,7 @@ pub struct CipherData {
// These are used during key rotation
#[serde(rename = "Attachments")]
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
Attachments2: Option<HashMap<String, Attachments2Data>>
Attachments2: Option<HashMap<String, Attachments2Data>>,
}
#[derive(Deserialize, Debug)]
@@ -177,45 +187,53 @@ pub struct Attachments2Data {
}
#[post("/ciphers/admin", data = "<data>")]
fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
let data: ShareCipherData = data.into_inner().data;
let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone());
cipher.user_uuid = Some(headers.user.uuid.clone());
match cipher.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed saving cipher")
};
cipher.save(&conn)?;
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &ws)
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &nt)
}
#[post("/ciphers/create", data = "<data>")]
fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
post_ciphers_admin(data, headers, conn, ws)
fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
post_ciphers_admin(data, headers, conn, nt)
}
#[post("/ciphers", data = "<data>")]
fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
let data: CipherData = data.into_inner().data;
let mut cipher = Cipher::new(data.Type, data.Name.clone());
update_cipher_from_data(&mut cipher, data, &headers, false, &conn, &ws, UpdateType::SyncCipherCreate)?;
update_cipher_from_data(&mut cipher, data, &headers, false, &conn, &nt, UpdateType::CipherCreate)?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
}
pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Headers, shared_to_collection: bool, conn: &DbConn, ws: &State<WebSocketUsers>, ut: UpdateType) -> EmptyResult {
pub fn update_cipher_from_data(
cipher: &mut Cipher,
data: CipherData,
headers: &Headers,
shared_to_collection: bool,
conn: &DbConn,
nt: &Notify,
ut: UpdateType,
) -> EmptyResult {
if let Some(org_id) = data.OrganizationId {
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
None => err!("You don't have permission to add item to organization"),
Some(org_user) => if shared_to_collection
|| org_user.has_full_access()
|| cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
cipher.organization_uuid = Some(org_id);
cipher.user_uuid = None;
} else {
err!("You don't have permission to add cipher directly to organization")
Some(org_user) => {
if shared_to_collection
|| org_user.has_full_access()
|| cipher.is_write_accessible_to_user(&headers.user.uuid, &conn)
{
cipher.organization_uuid = Some(org_id);
cipher.user_uuid = None;
} else {
err!("You don't have permission to add cipher directly to organization")
}
}
}
} else {
@@ -229,7 +247,7 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
err!("Folder is not owned by user")
}
}
None => err!("Folder doesn't exist")
None => err!("Folder doesn't exist"),
}
}
@@ -238,7 +256,7 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
for (id, attachment) in attachments {
let mut saved_att = match Attachment::find_by_id(&id, &conn) {
Some(att) => att,
None => err!("Attachment doesn't exist")
None => err!("Attachment doesn't exist"),
};
if saved_att.cipher_uuid != cipher.uuid {
@@ -248,10 +266,7 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
saved_att.key = Some(attachment.Key);
saved_att.file_name = attachment.FileName;
match saved_att.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed to save attachment")
};
saved_att.save(&conn)?;
}
}
@@ -260,12 +275,12 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
2 => data.SecureNote,
3 => data.Card,
4 => data.Identity,
_ => err!("Invalid type")
_ => err!("Invalid type"),
};
let mut type_data = match type_data_opt {
Some(data) => data,
None => err!("Data missing")
None => err!("Data missing"),
};
// TODO: ******* Backwards compat start **********
@@ -284,17 +299,11 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
cipher.data = type_data.to_string();
cipher.password_history = data.PasswordHistory.map(|f| f.to_string());
match cipher.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed to save cipher")
};
ws.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
cipher.save(&conn)?;
if cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn).is_err() {
err!("Error saving the folder information")
}
nt.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
Ok(())
cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn)
}
use super::folders::FolderData;
@@ -316,20 +325,17 @@ struct RelationsData {
Value: usize,
}
#[post("/ciphers/import", data = "<data>")]
fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data: ImportData = data.into_inner().data;
// Read and create the folders
let mut folders: Vec<_> = Vec::new();
for folder in data.Folders.into_iter() {
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.Name);
if new_folder.save(&conn).is_err() {
err!("Failed importing folders")
} else {
folders.push(new_folder);
}
new_folder.save(&conn)?;
folders.push(new_folder);
}
// Read the relations between folders and ciphers
@@ -341,52 +347,67 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
// Read and create the ciphers
for (index, cipher_data) in data.Ciphers.into_iter().enumerate() {
let folder_uuid = relations_map.get(&index)
.map(|i| folders[*i].uuid.clone());
let folder_uuid = relations_map.get(&index).map(|i| folders[*i].uuid.clone());
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherCreate)?;
update_cipher_from_data(
&mut cipher,
cipher_data,
&headers,
false,
&conn,
&nt,
UpdateType::CipherCreate,
)?;
cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn).ok();
cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn)?;
}
let mut user = headers.user;
match user.update_revision(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to update the revision, please log out and log back in to finish import.")
}
user.update_revision(&conn)
}
#[put("/ciphers/<uuid>/admin", data = "<data>")]
fn put_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
put_cipher(uuid, data, headers, conn, ws)
fn put_cipher_admin(
uuid: String,
data: JsonUpcase<CipherData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> JsonResult {
put_cipher(uuid, data, headers, conn, nt)
}
#[post("/ciphers/<uuid>/admin", data = "<data>")]
fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
post_cipher(uuid, data, headers, conn, ws)
fn post_cipher_admin(
uuid: String,
data: JsonUpcase<CipherData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> JsonResult {
post_cipher(uuid, data, headers, conn, nt)
}
#[post("/ciphers/<uuid>", data = "<data>")]
fn post_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
put_cipher(uuid, data, headers, conn, ws)
fn post_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
put_cipher(uuid, data, headers, conn, nt)
}
#[put("/ciphers/<uuid>", data = "<data>")]
fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
let data: CipherData = data.into_inner().data;
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
None => err!("Cipher doesn't exist"),
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
err!("Cipher is not write accessible")
}
update_cipher_from_data(&mut cipher, data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?;
update_cipher_from_data(&mut cipher, data, &headers, false, &conn, &nt, UpdateType::CipherUpdate)?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
}
@@ -397,23 +418,48 @@ struct CollectionsAdminData {
CollectionIds: Vec<String>,
}
#[put("/ciphers/<uuid>/collections", data = "<data>")]
fn put_collections_update(
uuid: String,
data: JsonUpcase<CollectionsAdminData>,
headers: Headers,
conn: DbConn,
) -> EmptyResult {
post_collections_admin(uuid, data, headers, conn)
}
#[post("/ciphers/<uuid>/collections", data = "<data>")]
fn post_collections_update(uuid: String, data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn) -> EmptyResult {
fn post_collections_update(
uuid: String,
data: JsonUpcase<CollectionsAdminData>,
headers: Headers,
conn: DbConn,
) -> EmptyResult {
post_collections_admin(uuid, data, headers, conn)
}
#[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
fn put_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn) -> EmptyResult {
fn put_collections_admin(
uuid: String,
data: JsonUpcase<CollectionsAdminData>,
headers: Headers,
conn: DbConn,
) -> EmptyResult {
post_collections_admin(uuid, data, headers, conn)
}
#[post("/ciphers/<uuid>/collections-admin", data = "<data>")]
fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn) -> EmptyResult {
fn post_collections_admin(
uuid: String,
data: JsonUpcase<CollectionsAdminData>,
headers: Headers,
conn: DbConn,
) -> EmptyResult {
let data: CollectionsAdminData = data.into_inner().data;
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
None => err!("Cipher doesn't exist"),
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
@@ -421,23 +467,23 @@ fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>,
}
let posted_collections: HashSet<String> = data.CollectionIds.iter().cloned().collect();
let current_collections: HashSet<String> = cipher.get_collections(&headers.user.uuid ,&conn).iter().cloned().collect();
let current_collections: HashSet<String> = cipher
.get_collections(&headers.user.uuid, &conn)
.iter()
.cloned()
.collect();
for collection in posted_collections.symmetric_difference(&current_collections) {
match Collection::find_by_uuid(&collection, &conn) {
None => err!("Invalid collection ID provided"),
Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
if posted_collections.contains(&collection.uuid) { // Add to collection
match CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn) {
Ok(()) => (),
Err(_) => err!("Failed to add cipher to collection")
};
} else { // Remove from collection
match CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn) {
Ok(()) => (),
Err(_) => err!("Failed to remove cipher from collection")
};
if posted_collections.contains(&collection.uuid) {
// Add to collection
CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn)?;
} else {
// Remove from collection
CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn)?;
}
} else {
err!("No rights to modify the collection")
@@ -457,28 +503,45 @@ struct ShareCipherData {
}
#[post("/ciphers/<uuid>/share", data = "<data>")]
fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn post_cipher_share(
uuid: String,
data: JsonUpcase<ShareCipherData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> JsonResult {
let data: ShareCipherData = data.into_inner().data;
share_cipher_by_uuid(&uuid, data, &headers, &conn, &ws)
share_cipher_by_uuid(&uuid, data, &headers, &conn, &nt)
}
#[put("/ciphers/<uuid>/share", data = "<data>")]
fn put_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn put_cipher_share(
uuid: String,
data: JsonUpcase<ShareCipherData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> JsonResult {
let data: ShareCipherData = data.into_inner().data;
share_cipher_by_uuid(&uuid, data, &headers, &conn, &ws)
share_cipher_by_uuid(&uuid, data, &headers, &conn, &nt)
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ShareSelectedCipherData {
Ciphers: Vec<CipherData>,
CollectionIds: Vec<String>
CollectionIds: Vec<String>,
}
#[put("/ciphers/share", data = "<data>")]
fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn put_cipher_share_seleted(
data: JsonUpcase<ShareSelectedCipherData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> EmptyResult {
let mut data: ShareSelectedCipherData = data.into_inner().data;
let mut cipher_ids: Vec<String> = Vec::new();
@@ -493,7 +556,7 @@ fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers:
for cipher in data.Ciphers.iter() {
match cipher.Id {
Some(ref id) => cipher_ids.push(id.to_string()),
None => err!("Request missing ids field")
None => err!("Request missing ids field"),
};
}
@@ -506,20 +569,25 @@ fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers:
while let Some(cipher) = data.Ciphers.pop() {
let mut shared_cipher_data = ShareCipherData {
Cipher: cipher,
CollectionIds: data.CollectionIds.clone()
CollectionIds: data.CollectionIds.clone(),
};
match shared_cipher_data.Cipher.Id.take() {
Some(id) => share_cipher_by_uuid(&id, shared_cipher_data , &headers, &conn, &ws)?,
None => err!("Request missing ids field")
Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt)?,
None => err!("Request missing ids field"),
};
}
Ok(())
}
fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> JsonResult {
fn share_cipher_by_uuid(
uuid: &str,
data: ShareCipherData,
headers: &Headers,
conn: &DbConn,
nt: &Notify,
) -> JsonResult {
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => {
if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
@@ -527,8 +595,8 @@ fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, co
} else {
err!("Cipher is not write accessible")
}
},
None => err!("Cipher doesn't exist")
}
None => err!("Cipher doesn't exist"),
};
match data.Cipher.OrganizationId.clone() {
@@ -540,10 +608,7 @@ fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, co
None => err!("Invalid collection ID provided"),
Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
match CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn) {
Ok(()) => (),
Err(_) => err!("Failed to add cipher to collection")
};
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn)?;
shared_to_collection = true;
} else {
err!("No rights to modify the collection")
@@ -551,7 +616,15 @@ fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, co
}
}
}
update_cipher_from_data(&mut cipher, data.Cipher, &headers, shared_to_collection, &conn, &ws, UpdateType::SyncCipherUpdate)?;
update_cipher_from_data(
&mut cipher,
data.Cipher,
&headers,
shared_to_collection,
&conn,
&nt,
UpdateType::CipherUpdate,
)?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
}
@@ -562,7 +635,7 @@ fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, co
fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn) -> JsonResult {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
None => err!("Cipher doesn't exist"),
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
@@ -577,120 +650,152 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
let mut attachment_key = None;
Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| {
match field.headers.name.as_str() {
"key" => {
use std::io::Read;
let mut key_buffer = String::new();
if field.data.read_to_string(&mut key_buffer).is_ok() {
attachment_key = Some(key_buffer);
}
},
"data" => {
// This is provided by the client, don't trust it
let name = field.headers.filename.expect("No filename provided");
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
let path = base_path.join(&file_name);
let size = match field.data.save()
.memory_threshold(0)
.size_limit(None)
.with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => {
error!("Attachment is not a file: {:?}", other);
return;
},
SaveResult::Partial(_, reason) => {
error!("Partial result: {:?}", reason);
return;
},
SaveResult::Error(e) => {
error!("Error: {:?}", e);
return;
Multipart::with_body(data.open(), boundary)
.foreach_entry(|mut field| {
match field.headers.name.as_str() {
"key" => {
use std::io::Read;
let mut key_buffer = String::new();
if field.data.read_to_string(&mut key_buffer).is_ok() {
attachment_key = Some(key_buffer);
}
};
}
"data" => {
// This is provided by the client, don't trust it
let name = field.headers.filename.expect("No filename provided");
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
attachment.key = attachment_key.clone();
match attachment.save(&conn) {
Ok(()) => (),
Err(_) => error!("Failed to save attachment")
};
},
_ => error!("Invalid multipart name")
}
}).expect("Error processing multipart data");
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
let path = base_path.join(&file_name);
let size = match field.data.save().memory_threshold(0).size_limit(None).with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => {
error!("Attachment is not a file: {:?}", other);
return;
}
SaveResult::Partial(_, reason) => {
error!("Partial result: {:?}", reason);
return;
}
SaveResult::Error(e) => {
error!("Error: {:?}", e);
return;
}
};
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
attachment.key = attachment_key.clone();
attachment.save(&conn).expect("Error saving attachment");
}
_ => error!("Invalid multipart name"),
}
})
.expect("Error processing multipart data");
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
}
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
fn post_attachment_admin(uuid: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn) -> JsonResult {
fn post_attachment_admin(
uuid: String,
data: Data,
content_type: &ContentType,
headers: Headers,
conn: DbConn,
) -> JsonResult {
post_attachment(uuid, data, content_type, headers, conn)
}
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
fn post_attachment_share(uuid: String, attachment_id: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &ws)?;
#[post(
"/ciphers/<uuid>/attachment/<attachment_id>/share",
format = "multipart/form-data",
data = "<data>"
)]
fn post_attachment_share(
uuid: String,
attachment_id: String,
data: Data,
content_type: &ContentType,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> JsonResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &nt)?;
post_attachment(uuid, data, content_type, headers, conn)
}
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
fn delete_attachment_post_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
delete_attachment(uuid, attachment_id, headers, conn, ws)
fn delete_attachment_post_admin(
uuid: String,
attachment_id: String,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> EmptyResult {
delete_attachment(uuid, attachment_id, headers, conn, nt)
}
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
fn delete_attachment_post(uuid: String, attachment_id: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
delete_attachment(uuid, attachment_id, headers, conn, ws)
fn delete_attachment_post(
uuid: String,
attachment_id: String,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> EmptyResult {
delete_attachment(uuid, attachment_id, headers, conn, nt)
}
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &ws)
fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &nt)
}
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
fn delete_attachment_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &ws)
fn delete_attachment_admin(
uuid: String,
attachment_id: String,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> EmptyResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &nt)
}
#[post("/ciphers/<uuid>/delete")]
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
}
#[post("/ciphers/<uuid>/delete-admin")]
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
}
#[delete("/ciphers/<uuid>")]
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
}
#[delete("/ciphers/<uuid>/admin")]
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
}
#[delete("/ciphers", data = "<data>")]
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data: Value = data.into_inner().data;
let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array")
None => err!("Posted ids field is not an array"),
},
None => err!("Request missing ids field")
None => err!("Request missing ids field"),
};
for uuid in uuids {
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &ws) {
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &nt) {
return error;
};
}
@@ -699,46 +804,42 @@ fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbCon
}
#[post("/ciphers/delete", data = "<data>")]
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
delete_cipher_selected(data, headers, conn, ws)
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
delete_cipher_selected(data, headers, conn, nt)
}
#[post("/ciphers/move", data = "<data>")]
fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data = data.into_inner().data;
let folder_id = match data.get("FolderId") {
Some(folder_id) => {
match folder_id.as_str() {
Some(folder_id) => {
match Folder::find_by_uuid(folder_id, &conn) {
Some(folder) => {
if folder.user_uuid != headers.user.uuid {
err!("Folder is not owned by user")
}
Some(folder.uuid)
}
None => err!("Folder doesn't exist")
Some(folder_id) => match folder_id.as_str() {
Some(folder_id) => match Folder::find_by_uuid(folder_id, &conn) {
Some(folder) => {
if folder.user_uuid != headers.user.uuid {
err!("Folder is not owned by user")
}
Some(folder.uuid)
}
None => err!("Folder id provided in wrong format")
}
}
None => None
None => err!("Folder doesn't exist"),
},
None => err!("Folder id provided in wrong format"),
},
None => None,
};
let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array")
None => err!("Posted ids field is not an array"),
},
None => err!("Request missing ids field")
None => err!("Request missing ids field"),
};
for uuid in uuids {
let mut cipher = match Cipher::find_by_uuid(uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
None => err!("Cipher doesn't exist"),
};
if !cipher.is_accessible_to_user(&headers.user.uuid, &conn) {
@@ -746,26 +847,22 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn,
}
// Move cipher
if cipher.move_to_folder(folder_id.clone(), &headers.user.uuid, &conn).is_err() {
err!("Error saving the folder information")
}
match cipher.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed to save cipher")
};
ws.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn));
cipher.move_to_folder(folder_id.clone(), &headers.user.uuid, &conn)?;
cipher.save(&conn)?;
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
}
Ok(())
}
#[put("/ciphers/move", data = "<data>")]
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
move_cipher_selected(data, headers, conn, ws)
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
move_cipher_selected(data, headers, conn, nt)
}
#[post("/ciphers/purge", data = "<data>")]
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data: PasswordData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
@@ -777,27 +874,20 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, ws
// Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
if cipher.delete(&conn).is_err() {
err!("Failed deleting cipher")
}
else {
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
}
cipher.delete(&conn)?;
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
}
// Delete folders
for f in Folder::find_by_user(&user.uuid, &conn) {
if f.delete(&conn).is_err() {
err!("Failed deleting folder")
} else {
ws.send_folder_update(UpdateType::SyncFolderCreate, &f);
}
f.delete(&conn)?;
nt.send_folder_update(UpdateType::FolderCreate, &f);
}
Ok(())
}
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> EmptyResult {
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist"),
@@ -807,19 +897,21 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, ws: &Sta
err!("Cipher can't be deleted by user")
}
match cipher.delete(&conn) {
Ok(()) => {
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(())
}
Err(_) => err!("Failed deleting cipher")
}
cipher.delete(&conn)?;
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(())
}
fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> EmptyResult {
fn _delete_cipher_attachment_by_id(
uuid: &str,
attachment_id: &str,
headers: &Headers,
conn: &DbConn,
nt: &Notify,
) -> EmptyResult {
let attachment = match Attachment::find_by_id(&attachment_id, &conn) {
Some(attachment) => attachment,
None => err!("Attachment doesn't exist")
None => err!("Attachment doesn't exist"),
};
if attachment.cipher_uuid != uuid {
@@ -828,7 +920,7 @@ fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &He
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
None => err!("Cipher doesn't exist"),
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
@@ -836,11 +928,7 @@ fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &He
}
// Delete attachment
match attachment.delete(&conn) {
Ok(()) => {
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(())
}
Err(_) => err!("Deleting attachment failed")
}
attachment.delete(&conn)?;
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(())
}

View File

@@ -1,11 +1,10 @@
use rocket::State;
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::db::DbConn;
use crate::db::models::*;
use crate::db::DbConn;
use crate::api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType};
use crate::auth::Headers;
use rocket::Route;
@@ -39,7 +38,7 @@ fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
let folder = match Folder::find_by_uuid(&uuid, &conn) {
Some(folder) => folder,
_ => err!("Invalid folder")
_ => err!("Invalid folder"),
};
if folder.user_uuid != headers.user.uuid {
@@ -53,35 +52,33 @@ fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
#[allow(non_snake_case)]
pub struct FolderData {
pub Name: String
pub Name: String,
}
#[post("/folders", data = "<data>")]
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
let data: FolderData = data.into_inner().data;
let mut folder = Folder::new(headers.user.uuid.clone(), data.Name);
if folder.save(&conn).is_err() {
err!("Failed to save folder")
}
ws.send_folder_update(UpdateType::SyncFolderCreate, &folder);
folder.save(&conn)?;
nt.send_folder_update(UpdateType::FolderCreate, &folder);
Ok(Json(folder.to_json()))
}
#[post("/folders/<uuid>", data = "<data>")]
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
put_folder(uuid, data, headers, conn, ws)
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
put_folder(uuid, data, headers, conn, nt)
}
#[put("/folders/<uuid>", data = "<data>")]
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
let data: FolderData = data.into_inner().data;
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
Some(folder) => folder,
_ => err!("Invalid folder")
_ => err!("Invalid folder"),
};
if folder.user_uuid != headers.user.uuid {
@@ -90,24 +87,22 @@ fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn
folder.name = data.Name;
if folder.save(&conn).is_err() {
err!("Failed to save folder")
}
ws.send_folder_update(UpdateType::SyncFolderUpdate, &folder);
folder.save(&conn)?;
nt.send_folder_update(UpdateType::FolderUpdate, &folder);
Ok(Json(folder.to_json()))
}
#[post("/folders/<uuid>/delete")]
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
delete_folder(uuid, headers, conn, ws)
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
delete_folder(uuid, headers, conn, nt)
}
#[delete("/folders/<uuid>")]
fn delete_folder(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn delete_folder(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let folder = match Folder::find_by_uuid(&uuid, &conn) {
Some(folder) => folder,
_ => err!("Invalid folder")
_ => err!("Invalid folder"),
};
if folder.user_uuid != headers.user.uuid {
@@ -115,11 +110,8 @@ fn delete_folder(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSock
}
// Delete the actual folder entry
match folder.delete(&conn) {
Ok(()) => {
ws.send_folder_update(UpdateType::SyncFolderDelete, &folder);
Ok(())
}
Err(_) => err!("Failed deleting folder")
}
folder.delete(&conn)?;
nt.send_folder_update(UpdateType::FolderDelete, &folder);
Ok(())
}

View File

@@ -8,7 +8,6 @@ pub fn routes() -> Vec<Route> {
let mut mod_routes = routes![
clear_device_token,
put_device_token,
get_eq_domains,
post_eq_domains,
put_eq_domains,
@@ -25,9 +24,9 @@ pub fn routes() -> Vec<Route> {
routes
}
///
/// Move this somewhere else
///
//
// Move this somewhere else
//
use rocket::Route;
use rocket_contrib::json::Json;
@@ -77,7 +76,7 @@ struct GlobalDomain {
Excluded: bool,
}
const GLOBAL_DOMAINS: &str = include_str!("global_domains.json");
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
#[get("/settings/domains")]
fn get_eq_domains(headers: Headers) -> JsonResult {
@@ -120,10 +119,9 @@ fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: Db
user.excluded_globals = to_string(&excluded_globals).unwrap_or("[]".to_string());
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or("[]".to_string());
match user.save(&conn) {
Ok(()) => Ok(Json(json!({}))),
Err(_) => err!("Failed to save user"),
}
user.save(&conn)?;
Ok(Json(json!({})))
}
#[put("/settings/domains", data = "<data>")]

View File

@@ -1,14 +1,15 @@
use rocket::State;
use rocket::request::Form;
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::CONFIG;
use crate::db::DbConn;
use crate::db::models::*;
use crate::db::DbConn;
use crate::CONFIG;
use crate::api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
use crate::auth::{Headers, AdminHeaders, OwnerHeaders};
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
use crate::auth::{decode_invite_jwt, AdminHeaders, Headers, InviteJWTClaims, OwnerHeaders};
use crate::mail;
use serde::{Deserialize, Deserializer};
@@ -37,18 +38,18 @@ pub fn routes() -> Vec<Route> {
get_org_details,
get_org_users,
send_invite,
reinvite_user,
confirm_invite,
accept_invite,
get_user,
edit_user,
put_organization_user,
delete_user,
post_delete_user,
post_reinvite_user,
post_org_import,
]
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct OrgData {
@@ -78,32 +79,28 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
let data: OrgData = data.into_inner().data;
let mut org = Organization::new(data.Name, data.BillingEmail);
let mut user_org = UserOrganization::new(
headers.user.uuid.clone(), org.uuid.clone());
let mut collection = Collection::new(
org.uuid.clone(), data.CollectionName);
let mut user_org = UserOrganization::new(headers.user.uuid.clone(), org.uuid.clone());
let mut collection = Collection::new(org.uuid.clone(), data.CollectionName);
user_org.key = data.Key;
user_org.access_all = true;
user_org.type_ = UserOrgType::Owner as i32;
user_org.status = UserOrgStatus::Confirmed as i32;
if org.save(&conn).is_err() {
err!("Failed creating organization")
}
if user_org.save(&conn).is_err() {
err!("Failed to add user to organization")
}
if collection.save(&conn).is_err() {
err!("Failed creating Collection");
}
org.save(&conn)?;
user_org.save(&conn)?;
collection.save(&conn)?;
Ok(Json(org.to_json()))
}
#[delete("/organizations/<org_id>", data = "<data>")]
fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers: OwnerHeaders, conn: DbConn) -> EmptyResult {
fn delete_organization(
org_id: String,
data: JsonUpcase<PasswordData>,
headers: OwnerHeaders,
conn: DbConn,
) -> EmptyResult {
let data: PasswordData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
@@ -113,15 +110,17 @@ fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers:
match Organization::find_by_uuid(&org_id, &conn) {
None => err!("Organization not found"),
Some(org) => match org.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting the organization")
}
Some(org) => org.delete(&conn),
}
}
#[post("/organizations/<org_id>/delete", data = "<data>")]
fn post_delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers: OwnerHeaders, conn: DbConn) -> EmptyResult {
fn post_delete_organization(
org_id: String,
data: JsonUpcase<PasswordData>,
headers: OwnerHeaders,
conn: DbConn,
) -> EmptyResult {
delete_organization(org_id, data, headers, conn)
}
@@ -131,19 +130,15 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
None => err!("User not part of organization"),
Some(user_org) => {
if user_org.type_ == UserOrgType::Owner {
let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn)
.len();
let num_owners =
UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len();
if num_owners <= 1 {
err!("The last owner can't leave")
}
}
match user_org.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed leaving the organization")
}
user_org.delete(&conn)
}
}
}
@@ -152,37 +147,44 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult {
match Organization::find_by_uuid(&org_id, &conn) {
Some(organization) => Ok(Json(organization.to_json())),
None => err!("Can't find organization details")
None => err!("Can't find organization details"),
}
}
#[put("/organizations/<org_id>", data = "<data>")]
fn put_organization(org_id: String, headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, conn: DbConn) -> JsonResult {
fn put_organization(
org_id: String,
headers: OwnerHeaders,
data: JsonUpcase<OrganizationUpdateData>,
conn: DbConn,
) -> JsonResult {
post_organization(org_id, headers, data, conn)
}
#[post("/organizations/<org_id>", data = "<data>")]
fn post_organization(org_id: String, _headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, conn: DbConn) -> JsonResult {
fn post_organization(
org_id: String,
_headers: OwnerHeaders,
data: JsonUpcase<OrganizationUpdateData>,
conn: DbConn,
) -> JsonResult {
let data: OrganizationUpdateData = data.into_inner().data;
let mut org = match Organization::find_by_uuid(&org_id, &conn) {
Some(organization) => organization,
None => err!("Can't find organization details")
None => err!("Can't find organization details"),
};
org.name = data.Name;
org.billing_email = data.BillingEmail;
match org.save(&conn) {
Ok(()) => Ok(Json(org.to_json())),
Err(_) => err!("Failed to modify organization")
}
org.save(&conn)?;
Ok(Json(org.to_json()))
}
// GET /api/collections?writeOnly=false
#[get("/collections")]
fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
Ok(Json(json!({
"Data":
Collection::find_by_user_uuid(&headers.user.uuid, &conn)
@@ -208,40 +210,54 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
}
#[post("/organizations/<org_id>/collections", data = "<data>")]
fn post_organization_collections(org_id: String, _headers: AdminHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn) -> JsonResult {
fn post_organization_collections(
org_id: String,
_headers: AdminHeaders,
data: JsonUpcase<NewCollectionData>,
conn: DbConn,
) -> JsonResult {
let data: NewCollectionData = data.into_inner().data;
let org = match Organization::find_by_uuid(&org_id, &conn) {
Some(organization) => organization,
None => err!("Can't find organization details")
None => err!("Can't find organization details"),
};
let mut collection = Collection::new(org.uuid.clone(), data.Name);
if collection.save(&conn).is_err() {
err!("Failed saving Collection");
}
collection.save(&conn)?;
Ok(Json(collection.to_json()))
}
#[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
fn put_organization_collection_update(org_id: String, col_id: String, headers: AdminHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn) -> JsonResult {
fn put_organization_collection_update(
org_id: String,
col_id: String,
headers: AdminHeaders,
data: JsonUpcase<NewCollectionData>,
conn: DbConn,
) -> JsonResult {
post_organization_collection_update(org_id, col_id, headers, data, conn)
}
#[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
fn post_organization_collection_update(org_id: String, col_id: String, _headers: AdminHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn) -> JsonResult {
fn post_organization_collection_update(
org_id: String,
col_id: String,
_headers: AdminHeaders,
data: JsonUpcase<NewCollectionData>,
conn: DbConn,
) -> JsonResult {
let data: NewCollectionData = data.into_inner().data;
let org = match Organization::find_by_uuid(&org_id, &conn) {
Some(organization) => organization,
None => err!("Can't find organization details")
None => err!("Can't find organization details"),
};
let mut collection = match Collection::find_by_uuid(&col_id, &conn) {
Some(collection) => collection,
None => err!("Collection not found")
None => err!("Collection not found"),
};
if collection.org_uuid != org.uuid {
@@ -249,22 +265,27 @@ fn post_organization_collection_update(org_id: String, col_id: String, _headers:
}
collection.name = data.Name.clone();
if collection.save(&conn).is_err() {
err!("Failed updating Collection");
}
collection.save(&conn)?;
Ok(Json(collection.to_json()))
}
#[delete("/organizations/<org_id>/collections/<col_id>/user/<org_user_id>")]
fn delete_organization_collection_user(org_id: String, col_id: String, org_user_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
fn delete_organization_collection_user(
org_id: String,
col_id: String,
org_user_id: String,
_headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
let collection = match Collection::find_by_uuid(&col_id, &conn) {
None => err!("Collection not found"),
Some(collection) => if collection.org_uuid == org_id {
collection
} else {
err!("Collection and Organization id do not match")
Some(collection) => {
if collection.org_uuid == org_id {
collection
} else {
err!("Collection and Organization id do not match")
}
}
};
@@ -273,19 +294,20 @@ fn delete_organization_collection_user(org_id: String, col_id: String, org_user_
Some(user_org) => {
match CollectionUser::find_by_collection_and_user(&collection.uuid, &user_org.user_uuid, &conn) {
None => err!("User not assigned to collection"),
Some(col_user) => {
match col_user.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed removing user from collection")
}
}
Some(col_user) => col_user.delete(&conn),
}
}
}
}
#[post("/organizations/<org_id>/collections/<col_id>/delete-user/<org_user_id>")]
fn post_organization_collection_delete_user(org_id: String, col_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
fn post_organization_collection_delete_user(
org_id: String,
col_id: String,
org_user_id: String,
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
delete_organization_collection_user(org_id, col_id, org_user_id, headers, conn)
}
@@ -293,13 +315,12 @@ fn post_organization_collection_delete_user(org_id: String, col_id: String, org_
fn delete_organization_collection(org_id: String, col_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
match Collection::find_by_uuid(&col_id, &conn) {
None => err!("Collection not found"),
Some(collection) => if collection.org_uuid == org_id {
match collection.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting collection")
Some(collection) => {
if collection.org_uuid == org_id {
collection.delete(&conn)
} else {
err!("Collection and Organization id do not match")
}
} else {
err!("Collection and Organization id do not match")
}
}
}
@@ -312,7 +333,13 @@ struct DeleteCollectionData {
}
#[post("/organizations/<org_id>/collections/<col_id>/delete", data = "<_data>")]
fn post_organization_collection_delete(org_id: String, col_id: String, headers: AdminHeaders, _data: JsonUpcase<DeleteCollectionData>, conn: DbConn) -> EmptyResult {
fn post_organization_collection_delete(
org_id: String,
col_id: String,
headers: AdminHeaders,
_data: JsonUpcase<DeleteCollectionData>,
conn: DbConn,
) -> EmptyResult {
delete_organization_collection(org_id, col_id, headers, conn)
}
@@ -335,16 +362,18 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
// Get org and collection, check that collection is from org
let collection = match Collection::find_by_uuid_and_org(&coll_id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => collection
Some(collection) => collection,
};
// Get the users from collection
let user_list: Vec<Value> = CollectionUser::find_by_collection(&collection.uuid, &conn)
.iter().map(|col_user| {
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
.unwrap()
.to_json_collection_user_details(col_user.read_only, &conn)
}).collect();
.iter()
.map(|col_user| {
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
.unwrap()
.to_json_collection_user_details(col_user.read_only, &conn)
})
.collect();
Ok(Json(json!({
"Data": user_list,
@@ -356,13 +385,16 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
#[derive(FromForm)]
struct OrgIdData {
#[form(field = "organizationId")]
organization_id: String
organization_id: String,
}
#[get("/ciphers/organization-details?<data..>")]
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
let ciphers_json: Vec<Value> = ciphers
.iter()
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
.collect();
Ok(Json(json!({
"Data": ciphers_json,
@@ -388,7 +420,7 @@ where
D: Deserializer<'de>,
{
// Deserialize null to empty Vec
Deserialize::deserialize(deserializer).or(Ok(vec![]))
Deserialize::deserialize(deserializer).or(Ok(vec![]))
}
#[derive(Deserialize)]
@@ -414,85 +446,201 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
Some(new_type) => new_type as i32,
None => err!("Invalid type")
None => err!("Invalid type"),
};
if new_type != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner {
if new_type != UserOrgType::User && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can invite Managers, Admins or Owners")
}
for email in data.Emails.iter() {
let mut user_org_status = UserOrgStatus::Accepted as i32;
let mut user_org_status = match CONFIG.mail {
Some(_) => UserOrgStatus::Invited as i32,
None => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
};
let user = match User::find_by_mail(&email, &conn) {
None => if CONFIG.invitations_allowed { // Invite user if that's enabled
let mut invitation = Invitation::new(email.clone());
match invitation.save(&conn) {
Ok(()) => {
let mut user = User::new(email.clone());
if user.save(&conn).is_err() {
err!("Failed to create placeholder for invited user")
} else {
user_org_status = UserOrgStatus::Invited as i32;
user
}
}
Err(_) => err!(format!("Failed to invite: {}", email))
None => {
if !CONFIG.invitations_allowed {
err!(format!("User email does not exist: {}", email))
}
} else {
err!(format!("User email does not exist: {}", email))
},
Some(user) => if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
err!(format!("User already in organization: {}", email))
} else {
if CONFIG.mail.is_none() {
let mut invitation = Invitation::new(email.clone());
invitation.save(&conn)?;
}
let mut user = User::new(email.clone());
user.save(&conn)?;
user_org_status = UserOrgStatus::Invited as i32;
user
}
Some(user) => {
if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
err!(format!("User already in organization: {}", email))
} else {
user
}
}
};
// Don't create UserOrganization in virtual organization
if org_id != Organization::VIRTUAL_ID {
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
let access_all = data.AccessAll.unwrap_or(false);
new_user.access_all = access_all;
new_user.type_ = new_type;
new_user.status = user_org_status;
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
let access_all = data.AccessAll.unwrap_or(false);
new_user.access_all = access_all;
new_user.type_ = new_type;
new_user.status = user_org_status;
// If no accessAll, add the collections received
if !access_all {
for col in &data.Collections {
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => {
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
}
}
// If no accessAll, add the collections received
if !access_all {
for col in &data.Collections {
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => {
CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn)?;
}
}
}
}
if new_user.save(&conn).is_err() {
err!("Failed to add user to organization")
new_user.save(&conn)?;
if let Some(ref mail_config) = CONFIG.mail {
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
None => err!("Error looking up organization"),
};
mail::send_invite(
&email,
&user.uuid,
Some(org_id.clone()),
Some(new_user.uuid),
&org_name,
Some(headers.user.email.clone()),
mail_config,
)?;
}
}
Ok(())
}
#[post("/organizations/<org_id>/users/<user_org>/reinvite")]
fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
if !CONFIG.invitations_allowed {
err!("Invitations are not allowed.")
}
if CONFIG.mail.is_none() {
err!("SMTP is not configured.")
}
let user_org = match UserOrganization::find_by_uuid(&user_org, &conn) {
Some(user_org) => user_org,
None => err!("The user hasn't been invited to the organization."),
};
if user_org.status != UserOrgStatus::Invited as i32 {
err!("The user is already accepted or confirmed to the organization")
}
let user = match User::find_by_uuid(&user_org.user_uuid, &conn) {
Some(user) => user,
None => err!("User not found."),
};
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
None => err!("Error looking up organization."),
};
if let Some(ref mail_config) = CONFIG.mail {
mail::send_invite(
&user.email,
&user.uuid,
Some(org_id),
Some(user_org.uuid),
&org_name,
Some(headers.user.email),
mail_config,
)?;
} else {
let mut invitation = Invitation::new(user.email.clone());
invitation.save(&conn)?;
}
Ok(())
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct AcceptData {
Token: String,
}
#[post("/organizations/<_org_id>/users/<_org_user_id>/accept", data = "<data>")]
fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptData>, conn: DbConn) -> EmptyResult {
// The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead
let data: AcceptData = data.into_inner().data;
let token = &data.Token;
let claims: InviteJWTClaims = decode_invite_jwt(&token)?;
match User::find_by_mail(&claims.email, &conn) {
Some(_) => {
Invitation::take(&claims.email, &conn);
if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) {
let mut user_org = match UserOrganization::find_by_uuid_and_org(user_org, org, &conn) {
Some(user_org) => user_org,
None => err!("Error accepting the invitation"),
};
if user_org.status != UserOrgStatus::Invited as i32 {
err!("User already accepted the invitation")
}
user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(&conn)?;
}
}
None => err!("Invited user not found"),
}
if let Some(ref mail_config) = CONFIG.mail {
let mut org_name = String::from("bitwarden_rs");
if let Some(org_id) = &claims.org_id {
org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
None => err!("Organization not found."),
};
};
if let Some(invited_by_email) = &claims.invited_by_email {
// User was invited to an organization, so they must be confirmed manually after acceptance
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name, mail_config)?;
} else {
// User was invited from /admin, so they are automatically confirmed
mail::send_invite_confirmed(&claims.email, &org_name, mail_config)?;
}
}
Ok(())
}
#[post("/organizations/<org_id>/users/<org_user_id>/confirm", data = "<data>")]
fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
fn confirm_invite(
org_id: String,
org_user_id: String,
data: JsonUpcase<Value>,
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
let data = data.into_inner().data;
let mut user_to_confirm = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user,
None => err!("The specified user isn't a member of the organization")
None => err!("The specified user isn't a member of the organization"),
};
if user_to_confirm.type_ != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner {
if user_to_confirm.type_ != UserOrgType::User && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can confirm Managers, Admins or Owners")
}
@@ -503,20 +651,29 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
user_to_confirm.status = UserOrgStatus::Confirmed as i32;
user_to_confirm.key = match data["Key"].as_str() {
Some(key) => key.to_string(),
None => err!("Invalid key provided")
None => err!("Invalid key provided"),
};
match user_to_confirm.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to add user to organization")
if let Some(ref mail_config) = CONFIG.mail {
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
None => err!("Error looking up organization."),
};
let address = match User::find_by_uuid(&user_to_confirm.user_uuid, &conn) {
Some(user) => user.email,
None => err!("Error looking up user."),
};
mail::send_invite_confirmed(&address, &org_name, mail_config)?;
}
user_to_confirm.save(&conn)
}
#[get("/organizations/<org_id>/users/<org_user_id>")]
fn get_user(org_id: String, org_user_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
let user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user,
None => err!("The specified user isn't a member of the organization")
None => err!("The specified user isn't a member of the organization"),
};
Ok(Json(user.to_json_details(&conn)))
@@ -532,44 +689,50 @@ struct EditUserData {
}
#[put("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
fn put_organization_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
fn put_organization_user(
org_id: String,
org_user_id: String,
data: JsonUpcase<EditUserData>,
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
edit_user(org_id, org_user_id, data, headers, conn)
}
#[post("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
fn edit_user(
org_id: String,
org_user_id: String,
data: JsonUpcase<EditUserData>,
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
let data: EditUserData = data.into_inner().data;
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
Some(new_type) => new_type,
None => err!("Invalid type")
None => err!("Invalid type"),
};
let mut user_to_edit = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user,
None => err!("The specified user isn't member of the organization")
None => err!("The specified user isn't member of the organization"),
};
if new_type != user_to_edit.type_ && (
user_to_edit.type_ >= UserOrgType::Admin ||
new_type >= UserOrgType::Admin
) &&
headers.org_user_type != UserOrgType::Owner {
if new_type != user_to_edit.type_
&& (user_to_edit.type_ >= UserOrgType::Admin || new_type >= UserOrgType::Admin)
&& headers.org_user_type != UserOrgType::Owner
{
err!("Only Owners can grant and remove Admin or Owner privileges")
}
if user_to_edit.type_ == UserOrgType::Owner &&
headers.org_user_type != UserOrgType::Owner {
if user_to_edit.type_ == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can edit Owner users")
}
if user_to_edit.type_ == UserOrgType::Owner &&
new_type != UserOrgType::Owner {
if user_to_edit.type_ == UserOrgType::Owner && new_type != UserOrgType::Owner {
// Removing owner permmission, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn)
.len();
let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len();
if num_owners <= 1 {
err!("Can't delete the last owner")
@@ -581,9 +744,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
// Delete all the odd collections
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
if c.delete(&conn).is_err() {
err!("Failed deleting old collection assignment")
}
c.delete(&conn)?;
}
// If no accessAll, add the collections received
@@ -592,64 +753,36 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => {
if CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
}
CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, &conn)?;
}
}
}
}
match user_to_edit.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save user data")
}
user_to_edit.save(&conn)
}
#[delete("/organizations/<org_id>/users/<org_user_id>")]
fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
// We're deleting user in virtual Organization. Delete User, not UserOrganization
if org_id == Organization::VIRTUAL_ID {
match User::find_by_uuid(&org_user_id, &conn) {
Some(user_to_delete) => {
if user_to_delete.uuid == headers.user.uuid {
err!("Delete your account in the account settings")
} else {
match user_to_delete.delete(&conn) {
Ok(()) => return Ok(()),
Err(_) => err!("Failed to delete user - likely because it's the only owner of organization")
}
}
},
None => err!("User not found")
}
}
let user_to_delete = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user,
None => err!("User to delete isn't member of the organization")
None => err!("User to delete isn't member of the organization"),
};
if user_to_delete.type_ != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner {
if user_to_delete.type_ != UserOrgType::User && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can delete Admins or Owners")
}
if user_to_delete.type_ == UserOrgType::Owner {
// Removing owner, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn)
.len();
let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len();
if num_owners <= 1 {
err!("Can't delete the last owner")
}
}
match user_to_delete.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting user from organization")
}
user_to_delete.delete(&conn)
}
#[post("/organizations/<org_id>/users/<org_user_id>/delete")]
@@ -657,13 +790,8 @@ fn post_delete_user(org_id: String, org_user_id: String, headers: AdminHeaders,
delete_user(org_id, org_user_id, headers, conn)
}
#[post("/organizations/<_org_id>/users/<_org_user_id>/reinvite")]
fn post_reinvite_user(_org_id: String, _org_user_id: String, _headers: AdminHeaders, _conn: DbConn) -> EmptyResult {
err!("This functionality is not implemented. The user needs to manually register before they can be accepted into the organization.")
}
use super::ciphers::CipherData;
use super::ciphers::update_cipher_from_data;
use super::ciphers::CipherData;
#[derive(Deserialize)]
#[allow(non_snake_case)]
@@ -683,13 +811,19 @@ struct RelationsData {
}
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
fn post_org_import(
query: Form<OrgIdData>,
data: JsonUpcase<ImportData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> EmptyResult {
let data: ImportData = data.into_inner().data;
let org_id = query.into_inner().organization_id;
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(user) => user,
None => err!("User is not part of the organization")
None => err!("User is not part of the organization"),
};
if org_user.type_ < UserOrgType::Admin {
@@ -697,14 +831,18 @@ fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers
}
// Read and create the collections
let collections: Vec<_> = data.Collections.into_iter().map(|coll| {
let mut collection = Collection::new(org_id.clone(), coll.Name);
if collection.save(&conn).is_err() {
err!("Failed to create Collection");
}
Ok(collection)
}).collect();
let collections: Vec<_> = data
.Collections
.into_iter()
.map(|coll| {
let mut collection = Collection::new(org_id.clone(), coll.Name);
if collection.save(&conn).is_err() {
err!("Failed to create Collection");
}
Ok(collection)
})
.collect();
// Read the relations between collections and ciphers
let mut relations = Vec::new();
@@ -713,11 +851,24 @@ fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers
}
// Read and create the ciphers
let ciphers: Vec<_> = data.Ciphers.into_iter().map(|cipher_data| {
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherCreate).ok();
cipher
}).collect();
let ciphers: Vec<_> = data
.Ciphers
.into_iter()
.map(|cipher_data| {
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
update_cipher_from_data(
&mut cipher,
cipher_data,
&headers,
false,
&conn,
&nt,
UpdateType::CipherCreate,
)
.ok();
cipher
})
.collect();
// Assign the collections
for (cipher_index, coll_index) in relations {
@@ -725,18 +876,12 @@ fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers
let coll = &collections[coll_index];
let coll_id = match coll {
Ok(coll) => coll.uuid.as_str(),
Err(_) => err!("Failed to assign to collection")
};
match CollectionCipher::save(cipher_id, coll_id, &conn) {
Ok(()) => (),
Err(_) => err!("Failed to add cipher to collection")
Err(_) => err!("Failed to assign to collection"),
};
CollectionCipher::save(cipher_id, coll_id, &conn)?;
}
let mut user = headers.user;
match user.update_revision(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to update the revision, please log out and log back in to finish import.")
}
user.update_revision(&conn)
}

View File

@@ -3,7 +3,6 @@ use rocket_contrib::json::Json;
use serde_json;
use serde_json::Value;
use crate::db::{
models::{TwoFactor, TwoFactorType, User},
DbConn,
@@ -11,13 +10,23 @@ use crate::db::{
use crate::crypto;
use crate::api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use crate::auth::Headers;
use rocket::Route;
#[cfg(feature = "enable_yubikey")]
fn yubi_routes() -> Vec<Route> {
routes![generate_yubikey, activate_yubikey, activate_yubikey_put]
}
#[cfg(not(feature = "enable_yubikey"))]
fn yubi_routes() -> Vec<Route> {
Vec::new()
}
pub fn routes() -> Vec<Route> {
routes![
let mut routes = routes![
get_twofactor,
get_recover,
recover,
@@ -30,10 +39,11 @@ pub fn routes() -> Vec<Route> {
generate_u2f_challenge,
activate_u2f,
activate_u2f_put,
generate_yubikey,
activate_yubikey,
activate_yubikey_put,
]
];
routes.append(&mut yubi_routes());
routes
}
#[get("/two-factor")]
@@ -99,10 +109,8 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
// Remove the recovery code, not needed without twofactors
user.totp_recover = None;
match user.save(&conn) {
Ok(()) => Ok(Json(json!({}))),
Err(_) => err!("Failed to remove the user's two factor recovery code")
}
user.save(&conn)?;
Ok(Json(json!({})))
}
#[derive(Deserialize)]
@@ -113,11 +121,7 @@ struct DisableTwoFactorData {
}
#[post("/two-factor/disable", data = "<data>")]
fn disable_twofactor(
data: JsonUpcase<DisableTwoFactorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: DisableTwoFactorData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
@@ -139,20 +143,12 @@ fn disable_twofactor(
}
#[put("/two-factor/disable", data = "<data>")]
fn disable_twofactor_put(
data: JsonUpcase<DisableTwoFactorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
disable_twofactor(data, headers, conn)
}
#[post("/two-factor/get-authenticator", data = "<data>")]
fn generate_authenticator(
data: JsonUpcase<PasswordData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
@@ -183,11 +179,7 @@ struct EnableAuthenticatorData {
}
#[post("/two-factor/authenticator", data = "<data>")]
fn activate_authenticator(
data: JsonUpcase<EnableAuthenticatorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableAuthenticatorData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
let key = data.Key;
@@ -230,11 +222,7 @@ fn activate_authenticator(
}
#[put("/two-factor/authenticator", data = "<data>")]
fn activate_authenticator_put(
data: JsonUpcase<EnableAuthenticatorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
activate_authenticator(data, headers, conn)
}
@@ -242,9 +230,7 @@ fn _generate_recover_code(user: &mut User, conn: &DbConn) {
if user.totp_recover.is_none() {
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
user.totp_recover = Some(totp_recover);
if user.save(conn).is_err() {
error!("Failed to save the user's two factor recovery code")
}
user.save(conn).ok();
}
}
@@ -312,23 +298,22 @@ struct EnableU2FData {
}
// This struct is copied from the U2F lib
// because challenge is not always sent
// to add an optional error code
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RegisterResponseCopy {
pub registration_data: String,
pub version: String,
pub challenge: Option<String>,
pub error_code: Option<NumberOrString>,
pub client_data: String,
pub error_code: Option<NumberOrString>,
}
impl RegisterResponseCopy {
fn into_response(self, challenge: String) -> RegisterResponse {
fn into_response(self) -> RegisterResponse {
RegisterResponse {
registration_data: self.registration_data,
version: self.version,
challenge,
client_data: self.client_data,
}
}
@@ -342,22 +327,15 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
err!("Invalid password");
}
let tf_challenge = TwoFactor::find_by_user_and_type(
&headers.user.uuid,
TwoFactorType::U2fRegisterChallenge as i32,
&conn,
);
let tf_challenge =
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2fRegisterChallenge as i32, &conn);
if let Some(tf_challenge) = tf_challenge {
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)
.expect("Can't parse U2fRegisterChallenge data");
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
tf_challenge
.delete(&conn)
.expect("Error deleting U2F register challenge");
tf_challenge.delete(&conn)?;
let response_copy: RegisterResponseCopy =
serde_json::from_str(&data.DeviceResponse).expect("Can't parse RegisterResponse data");
let response_copy: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
let error_code = response_copy
.error_code
@@ -368,42 +346,33 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
err!("Error registering U2F token")
}
let response = response_copy.into_response(challenge.challenge.clone());
let response = response_copy.into_response();
match U2F.register_response(challenge.clone(), response) {
Ok(registration) => {
// TODO: Allow more than one U2F device
let mut registrations = Vec::new();
registrations.push(registration);
let registration = U2F.register_response(challenge.clone(), response)?;
// TODO: Allow more than one U2F device
let mut registrations = Vec::new();
registrations.push(registration);
let tf_registration = TwoFactor::new(
headers.user.uuid.clone(),
TwoFactorType::U2f,
serde_json::to_string(&registrations).unwrap(),
);
tf_registration
.save(&conn)
.expect("Error saving U2F registration");
let tf_registration = TwoFactor::new(
headers.user.uuid.clone(),
TwoFactorType::U2f,
serde_json::to_string(&registrations).unwrap(),
);
tf_registration.save(&conn)?;
let mut user = headers.user;
_generate_recover_code(&mut user, &conn);
let mut user = headers.user;
_generate_recover_code(&mut user, &conn);
Ok(Json(json!({
"Enabled": true,
"Challenge": {
"UserId": user.uuid,
"AppId": APP_ID.to_string(),
"Challenge": challenge,
"Version": U2F_VERSION,
},
"Object": "twoFactorU2f"
})))
}
Err(e) => {
error!("{:#?}", e);
err!("Error activating u2f")
}
}
Ok(Json(json!({
"Enabled": true,
"Challenge": {
"UserId": user.uuid,
"AppId": APP_ID.to_string(),
"Challenge": challenge,
"Version": U2F_VERSION,
},
"Object": "twoFactorU2f"
})))
} else {
err!("Can't recover challenge")
}
@@ -411,17 +380,14 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
#[put("/two-factor/u2f", data = "<data>")]
fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
activate_u2f(data,headers, conn)
activate_u2f(data, headers, conn)
}
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
let challenge = U2F.generate_challenge().unwrap();
TwoFactor::new(
user_uuid.into(),
type_,
serde_json::to_string(&challenge).unwrap(),
).save(conn)
TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
.save(conn)
.expect("Error saving challenge");
challenge
@@ -469,7 +435,7 @@ pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRe
Ok(signed_request)
}
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> {
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
let u2f_type = TwoFactorType::U2f as i32;
@@ -477,11 +443,8 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
let challenge = match tf_challenge {
Some(tf_challenge) => {
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)
.expect("Can't parse U2fLoginChallenge data");
tf_challenge
.delete(&conn)
.expect("Error deleting U2F login challenge");
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
tf_challenge.delete(&conn)?;
challenge
}
None => err!("Can't recover login challenge"),
@@ -494,13 +457,11 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
let registrations = _parse_registrations(&twofactor.data);
let response: SignResponse =
serde_json::from_str(response).expect("Can't parse SignResponse data");
let response: SignResponse = serde_json::from_str(response)?;
let mut _counter: u32 = 0;
for registration in registrations {
let response =
U2F.sign_response(challenge.clone(), registration, response.clone(), _counter);
let response = U2F.sign_response(challenge.clone(), registration, response.clone(), _counter);
match response {
Ok(new_counter) => {
_counter = new_counter;
@@ -516,7 +477,6 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
err!("error verifying response")
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct EnableYubikeyData {
@@ -536,8 +496,8 @@ pub struct YubikeyMetadata {
pub Nfc: bool,
}
use yubico::Yubico;
use yubico::config::Config;
#[cfg(feature = "enable_yubikey")]
use yubico::{config::Config, Yubico};
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
let mut yubikeys: Vec<String> = Vec::new();
@@ -569,24 +529,26 @@ fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
let mut result = json!({});
for (i, key) in yubikeys.into_iter().enumerate() {
result[format!("Key{}", i+1)] = Value::String(key);
result[format!("Key{}", i + 1)] = Value::String(key);
}
result
}
#[cfg(feature = "enable_yubikey")]
fn verify_yubikey_otp(otp: String) -> JsonResult {
if !CONFIG.yubico_cred_set {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
Yubikey OTP Disabled")
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
}
let yubico = Yubico::new();
let config = Config::default().set_client_id(CONFIG.yubico_client_id.to_owned()).set_key(CONFIG.yubico_secret_key.to_owned());
let config = Config::default()
.set_client_id(CONFIG.yubico_client_id.to_owned())
.set_key(CONFIG.yubico_secret_key.to_owned());
let result = match CONFIG.yubico_server {
Some(ref server) => yubico.verify(otp, config.set_api_hosts(vec![server.to_owned()])),
None => yubico.verify(otp, config)
None => yubico.verify(otp, config),
};
match result {
@@ -595,11 +557,11 @@ fn verify_yubikey_otp(otp: String) -> JsonResult {
}
}
#[cfg(feature = "enable_yubikey")]
#[post("/two-factor/get-yubikey", data = "<data>")]
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.yubico_cred_set {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
Yubikey OTP Disabled")
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
}
let data: PasswordData = data.into_inner().data;
@@ -614,8 +576,7 @@ fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbCo
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
if let Some(r) = r {
let yubikey_metadata: YubikeyMetadata =
serde_json::from_str(&r.data).expect("Can't parse YubikeyMetadata data");
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
@@ -632,6 +593,7 @@ fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbCo
}
}
#[cfg(feature = "enable_yubikey")]
#[post("/two-factor/yubikey", data = "<data>")]
fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableYubikeyData = data.into_inner().data;
@@ -641,14 +603,10 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
}
// Check if we already have some data
let yubikey_data = TwoFactor::find_by_user_and_type(
&headers.user.uuid,
TwoFactorType::YubiKey as i32,
&conn,
);
let yubikey_data = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::YubiKey as i32, &conn);
if let Some(yubikey_data) = yubikey_data {
yubikey_data.delete(&conn).expect("Error deleting current Yubikeys");
yubikey_data.delete(&conn)?;
}
let yubikeys = parse_yubikeys(&data);
@@ -664,7 +622,7 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
for yubikey in &yubikeys {
if yubikey.len() == 12 {
// YubiKey ID
continue
continue;
}
let result = verify_yubikey_otp(yubikey.to_owned());
@@ -686,8 +644,7 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
TwoFactorType::YubiKey,
serde_json::to_string(&yubikey_metadata).unwrap(),
);
yubikey_registration
.save(&conn).expect("Failed to save Yubikey info");
yubikey_registration.save(&conn)?;
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
@@ -698,12 +655,19 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
Ok(Json(result))
}
#[cfg(feature = "enable_yubikey")]
#[put("/two-factor/yubikey", data = "<data>")]
fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
activate_yubikey(data, headers, conn)
}
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> {
#[cfg(not(feature = "enable_yubikey"))]
pub fn validate_yubikey_login(_: &str, _: &str, _: &DbConn) -> EmptyResult {
err!("Yubikey functionality is disabled. If you are using AArch64, check #262")
}
#[cfg(feature = "enable_yubikey")]
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
if response.len() != 44 {
err!("Invalid Yubikey OTP length");
}
@@ -715,7 +679,8 @@ pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) ->
None => err!("No YubiKey devices registered"),
};
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let yubikey_metadata: YubikeyMetadata =
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let response_id = &response[..12];
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {

View File

@@ -1,9 +1,11 @@
use std::error::Error;
use std::fs::{create_dir_all, remove_file, symlink_metadata, File};
use std::io::prelude::*;
use std::fs::{create_dir_all, File};
use std::time::SystemTime;
use rocket::Route;
use rocket::response::Content;
use rocket::http::ContentType;
use rocket::response::Content;
use rocket::Route;
use reqwest;
@@ -13,13 +15,15 @@ pub fn routes() -> Vec<Route> {
routes![icon]
}
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
#[get("/<domain>/icon.png")]
fn icon(domain: String) -> Content<Vec<u8>> {
let icon_type = ContentType::new("image", "x-icon");
// Validate the domain to avoid directory traversal attacks
if domain.contains('/') || domain.contains("..") {
return Content(icon_type, get_fallback_icon());
return Content(icon_type, FALLBACK_ICON.to_vec());
}
let icon = get_icon(&domain);
@@ -27,7 +31,7 @@ fn icon(domain: String) -> Content<Vec<u8>> {
Content(icon_type, icon)
}
fn get_icon (domain: &str) -> Vec<u8> {
fn get_icon(domain: &str) -> Vec<u8> {
let path = format!("{}/{}.png", CONFIG.icon_cache_folder, domain);
if let Some(icon) = get_cached_icon(&path) {
@@ -41,15 +45,26 @@ fn get_icon (domain: &str) -> Vec<u8> {
Ok(icon) => {
save_icon(&path, &icon);
icon
},
}
Err(e) => {
error!("Error downloading icon: {:?}", e);
get_fallback_icon()
mark_negcache(&path);
FALLBACK_ICON.to_vec()
}
}
}
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
// Check for expiration of negatively cached copy
if icon_is_negcached(path) {
return Some(FALLBACK_ICON.to_vec());
}
// Check for expiration of successfully cached copy
if icon_is_expired(path) {
return None;
}
// Try to read the cached icon, and return it if it exists
if let Ok(mut f) = File::open(path) {
let mut buffer = Vec::new();
@@ -62,6 +77,43 @@ fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
None
}
fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Box<Error>> {
let meta = symlink_metadata(path)?;
let modified = meta.modified()?;
let age = SystemTime::now().duration_since(modified)?;
Ok(ttl > 0 && ttl <= age.as_secs())
}
fn icon_is_negcached(path: &str) -> bool {
let miss_indicator = path.to_owned() + ".miss";
let expired = file_is_expired(&miss_indicator, CONFIG.icon_cache_negttl);
match expired {
// No longer negatively cached, drop the marker
Ok(true) => {
if let Err(e) = remove_file(&miss_indicator) {
error!("Could not remove negative cache indicator for icon {:?}: {:?}", path, e);
}
false
}
// The marker hasn't expired yet.
Ok(false) => true,
// The marker is missing or inaccessible in some way.
Err(_) => false,
}
}
fn mark_negcache(path: &str) {
let miss_indicator = path.to_owned() + ".miss";
File::create(&miss_indicator).expect("Error creating negative cache marker");
}
fn icon_is_expired(path: &str) -> bool {
let expired = file_is_expired(path, CONFIG.icon_cache_ttl);
expired.unwrap_or(true)
}
fn get_icon_url(domain: &str) -> String {
if CONFIG.local_icon_extractor {
format!("http://{}/favicon.ico", domain)
@@ -89,24 +141,3 @@ fn save_icon(path: &str, icon: &[u8]) {
f.write_all(icon).expect("Error writing icon file");
};
}
const FALLBACK_ICON_URL: &str = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
fn get_fallback_icon() -> Vec<u8> {
let path = format!("{}/default.png", CONFIG.icon_cache_folder);
if let Some(icon) = get_cached_icon(&path) {
return icon;
}
match download_icon(FALLBACK_ICON_URL) {
Ok(icon) => {
save_icon(&path, &icon);
icon
},
Err(e) => {
error!("Error downloading fallback icon: {:?}", e);
vec![]
}
}
}

View File

@@ -61,17 +61,16 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
match device.save(&conn) {
Ok(()) => Ok(Json(json!({
"access_token": access_token,
"expires_in": expires_in,
"token_type": "Bearer",
"refresh_token": device.refresh_token,
"Key": user.key,
"PrivateKey": user.private_key,
}))),
Err(e) => err!("Failed to add device to user", e),
}
device.save(&conn)?;
Ok(Json(json!({
"access_token": access_token,
"expires_in": expires_in,
"token_type": "Bearer",
"refresh_token": device.refresh_token,
"Key": user.key,
"PrivateKey": user.private_key,
})))
}
fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
@@ -85,19 +84,19 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
let username = data.username.as_ref().unwrap();
let user = match User::find_by_mail(username, &conn) {
Some(user) => user,
None => err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip.ip, username
)),
None => err!(
"Username or password is incorrect. Try again",
format!("IP: {}. Username: {}.", ip.ip, username)
),
};
// Check password
let password = data.password.as_ref().unwrap();
if !user.check_valid_password(password) {
err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip.ip, username
))
err!(
"Username or password is incorrect. Try again",
format!("IP: {}. Username: {}.", ip.ip, username)
)
}
// On iOS, device_type sends "iOS", on others it sends a number
@@ -126,9 +125,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
if let Err(e) = device.save(&conn) {
err!("Failed to add device to user", e)
}
device.save(&conn)?;
let mut result = json!({
"access_token": access_token,
@@ -148,7 +145,12 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
Ok(Json(result))
}
fn twofactor_auth(user_uuid: &str, data: &ConnectData, device: &mut Device, conn: &DbConn) -> ApiResult<Option<String>> {
fn twofactor_auth(
user_uuid: &str,
data: &ConnectData,
device: &mut Device,
conn: &DbConn,
) -> ApiResult<Option<String>> {
let twofactors_raw = TwoFactor::find_by_user(user_uuid, conn);
// Remove u2f challenge twofactors (impl detail)
let twofactors: Vec<_> = twofactors_raw.iter().filter(|tf| tf.type_ < 1000).collect();
@@ -255,13 +257,14 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
}
Some(TwoFactorType::YubiKey) => {
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::YubiKey as i32, &conn) {
Some(tf_type @ TwoFactorType::YubiKey) => {
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) {
Some(tf) => tf,
None => err!("No YubiKey devices registered"),
};
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let yubikey_metadata: two_factor::YubikeyMetadata =
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let mut map = JsonMap::new();
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc));

View File

@@ -1,24 +1,25 @@
mod admin;
pub(crate) mod core;
mod icons;
mod identity;
mod web;
mod notifications;
mod web;
pub use self::admin::routes as admin_routes;
pub use self::core::routes as core_routes;
pub use self::icons::routes as icons_routes;
pub use self::identity::routes as identity_routes;
pub use self::web::routes as web_routes;
pub use self::notifications::routes as notifications_routes;
pub use self::notifications::{start_notification_server, WebSocketUsers, UpdateType};
pub use self::notifications::{start_notification_server, Notify, UpdateType};
pub use self::web::routes as web_routes;
use rocket::response::status::BadRequest;
use rocket_contrib::json::Json;
use serde_json::Value;
// Type aliases for API methods results
type ApiResult<T> = Result<T, BadRequest<Json<Value>>>;
type JsonResult = ApiResult<Json<Value>>;
type EmptyResult = ApiResult<()>;
type ApiResult<T> = Result<T, crate::error::Error>;
pub type JsonResult = ApiResult<Json<Value>>;
pub type EmptyResult = ApiResult<()>;
use crate::util;
type JsonUpcase<T> = Json<util::UpCase<T>>;
@@ -27,7 +28,7 @@ type JsonUpcase<T> = Json<util::UpCase<T>>;
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct PasswordData {
MasterPasswordHash: String
MasterPasswordHash: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -41,14 +42,14 @@ impl NumberOrString {
fn into_string(self) -> String {
match self {
NumberOrString::Number(n) => n.to_string(),
NumberOrString::String(s) => s
NumberOrString::String(s) => s,
}
}
fn into_i32(self) -> Option<i32> {
match self {
NumberOrString::Number(n) => Some(n),
NumberOrString::String(s) => s.parse().ok()
}
NumberOrString::String(s) => s.parse().ok(),
}
}
}

View File

@@ -14,7 +14,7 @@ pub fn routes() -> Vec<Route> {
#[get("/hub")]
fn websockets_err() -> JsonResult {
err!("'/notifications/hub' should be proxied towards the websocket server, otherwise notifications will not work. Go to the README for more info.")
err!("'/notifications/hub' should be proxied to the websocket server or notifications won't work. Go to the README for more info.")
}
#[post("/hub/negotiate")]
@@ -40,9 +40,9 @@ fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
})))
}
///
/// Websockets server
///
//
// Websockets server
//
use std::sync::Arc;
use std::thread;
@@ -94,9 +94,7 @@ fn serialize_date(date: NaiveDateTime) -> Value {
use byteorder::{BigEndian, WriteBytesExt};
let mut bs = [0u8; 8];
bs.as_mut()
.write_i64::<BigEndian>(timestamp)
.expect("Unable to write");
bs.as_mut().write_i64::<BigEndian>(timestamp).expect("Unable to write");
// -1 is Timestamp
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
@@ -142,12 +140,7 @@ impl Handler for WSHandler {
use crate::auth;
let claims = match auth::decode_jwt(access_token) {
Ok(claims) => claims,
Err(_) => {
return Err(ws::Error::new(
ws::ErrorKind::Internal,
"Invalid access token provided",
))
}
Err(_) => return Err(ws::Error::new(ws::ErrorKind::Internal, "Invalid access token provided")),
};
// Assign the user to the handler
@@ -158,11 +151,9 @@ impl Handler for WSHandler {
let handler_insert = self.out.clone();
let handler_update = self.out.clone();
self.users.map.upsert(
user_uuid,
|| vec![handler_insert],
|ref mut v| v.push(handler_update),
);
self.users
.map
.upsert(user_uuid, || vec![handler_insert], |ref mut v| v.push(handler_update));
// Schedule a ping to keep the connection alive
self.out.timeout(PING_MS, PING)
@@ -238,7 +229,7 @@ impl Factory for WSFactory {
#[derive(Clone)]
pub struct WebSocketUsers {
pub map: Arc<CHashMap<String, Vec<Sender>>>,
map: Arc<CHashMap<String, Vec<Sender>>>,
}
impl WebSocketUsers {
@@ -338,32 +329,32 @@ fn create_ping() -> Vec<u8> {
#[allow(dead_code)]
pub enum UpdateType {
SyncCipherUpdate = 0,
SyncCipherCreate = 1,
SyncLoginDelete = 2,
SyncFolderDelete = 3,
SyncCiphers = 4,
CipherUpdate = 0,
CipherCreate = 1,
LoginDelete = 2,
FolderDelete = 3,
Ciphers = 4,
SyncVault = 5,
SyncOrgKeys = 6,
SyncFolderCreate = 7,
SyncFolderUpdate = 8,
SyncCipherDelete = 9,
Vault = 5,
OrgKeys = 6,
FolderCreate = 7,
FolderUpdate = 8,
CipherDelete = 9,
SyncSettings = 10,
LogOut = 11,
}
use rocket::State;
pub type Notify<'a> = State<'a, WebSocketUsers>;
pub fn start_notification_server() -> WebSocketUsers {
let factory = WSFactory::init();
let users = factory.users.clone();
if CONFIG.websocket_enabled {
thread::spawn(move || {
WebSocket::new(factory)
.unwrap()
.listen(&CONFIG.websocket_url)
.unwrap();
WebSocket::new(factory).unwrap().listen(&CONFIG.websocket_url).unwrap();
});
}

View File

@@ -1,10 +1,10 @@
use std::io;
use std::path::{Path, PathBuf};
use rocket::http::ContentType;
use rocket::request::Request;
use rocket::response::{self, NamedFile, Responder};
use rocket::response::content::Content;
use rocket::http::{ContentType, Status};
use rocket::response::{self, NamedFile, Responder};
use rocket::Route;
use rocket_contrib::json::Json;
use serde_json::Value;
@@ -13,56 +13,79 @@ use crate::CONFIG;
pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled {
routes![web_index, app_id, web_files, attachments, alive]
routes![web_index, app_id, web_files, admin_page, attachments, alive]
} else {
routes![attachments, alive]
}
}
// TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache
#[get("/")]
fn web_index() -> WebHeaders<io::Result<NamedFile>> {
web_files("index.html".into())
fn web_index() -> Cached<io::Result<NamedFile>> {
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join("index.html")))
}
#[get("/app-id.json")]
fn app_id() -> WebHeaders<Content<Json<Value>>> {
fn app_id() -> Cached<Content<Json<Value>>> {
let content_type = ContentType::new("application", "fido.trusted-apps+json");
WebHeaders(Content(content_type, Json(json!({
"trustedFacets": [
{
"version": { "major": 1, "minor": 0 },
"ids": [
&CONFIG.domain,
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
}]
}))))
Cached::long(Content(
content_type,
Json(json!({
"trustedFacets": [
{
"version": { "major": 1, "minor": 0 },
"ids": [
&CONFIG.domain,
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
}]
})),
))
}
const ADMIN_PAGE: &'static str = include_str!("../static/admin.html");
use rocket::response::content::Html;
#[get("/admin")]
fn admin_page() -> Cached<Html<&'static str>> {
Cached::short(Html(ADMIN_PAGE))
}
/* // Use this during Admin page development
#[get("/admin")]
fn admin_page() -> Cached<io::Result<NamedFile>> {
Cached::short(NamedFile::open("src/static/admin.html"))
}
*/
#[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
fn web_files(p: PathBuf) -> WebHeaders<io::Result<NamedFile>> {
WebHeaders(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p)))
fn web_files(p: PathBuf) -> Cached<io::Result<NamedFile>> {
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p)))
}
struct WebHeaders<R>(R);
struct Cached<R>(R, &'static str);
impl<'r, R: Responder<'r>> Responder<'r> for WebHeaders<R> {
impl<R> Cached<R> {
fn long(r: R) -> Cached<R> {
// 7 days
Cached(r, "public, max-age=604800")
}
fn short(r: R) -> Cached<R> {
// 10 minutes
Cached(r, "public, max-age=600")
}
}
impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
fn respond_to(self, req: &Request) -> response::Result<'r> {
match self.0.respond_to(req) {
Ok(mut res) => {
res.set_raw_header("Referrer-Policy", "same-origin");
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-XSS-Protection", "1; mode=block");
res.set_raw_header("Cache-Control", self.1);
Ok(res)
},
Err(_) => {
Err(Status::NotFound)
}
}
e @ Err(_) => e,
}
}
}
@@ -71,7 +94,6 @@ fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new(&CONFIG.attachments_folder).join(uuid).join(file))
}
#[get("/alive")]
fn alive() -> Json<String> {
use crate::util::format_date;

View File

@@ -1,12 +1,13 @@
///
/// JWT Handling
///
//
// JWT Handling
//
use crate::util::read_file;
use chrono::Duration;
use chrono::{Duration, Utc};
use jsonwebtoken::{self, Algorithm, Header};
use serde::ser::Serialize;
use crate::error::{Error, MapResult};
use crate::CONFIG;
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
@@ -14,28 +15,31 @@ const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
lazy_static! {
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
pub static ref JWT_ISSUER: String = CONFIG.domain.clone();
static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
Ok(key) => key,
Err(e) => panic!("Error loading private RSA Key from {}\n Error: {}", CONFIG.private_rsa_key, e)
Err(e) => panic!(
"Error loading private RSA Key from {}\n Error: {}",
CONFIG.private_rsa_key, e
),
};
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key) {
Ok(key) => key,
Err(e) => panic!("Error loading public RSA Key from {}\n Error: {}", CONFIG.public_rsa_key, e)
Err(e) => panic!(
"Error loading public RSA Key from {}\n Error: {}",
CONFIG.public_rsa_key, e
),
};
}
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
Ok(token) => token,
Err(e) => panic!("Error encoding jwt {}", e)
Err(e) => panic!("Error encoding jwt {}", e),
}
}
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
pub fn decode_jwt(token: &str) -> Result<JWTClaims, Error> {
let validation = jsonwebtoken::Validation {
leeway: 30, // 30 seconds
validate_exp: true,
@@ -47,13 +51,30 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
algorithms: vec![JWT_ALGORITHM],
};
match jsonwebtoken::decode(token, &PUBLIC_RSA_KEY, &validation) {
Ok(decoded) => Ok(decoded.claims),
Err(msg) => {
error!("Error validating jwt - {:#?}", msg);
Err(msg.to_string())
}
}
let token = token.replace(char::is_whitespace, "");
jsonwebtoken::decode(&token, &PUBLIC_RSA_KEY, &validation)
.map(|d| d.claims)
.map_res("Error decoding login JWT")
}
pub fn decode_invite_jwt(token: &str) -> Result<InviteJWTClaims, Error> {
let validation = jsonwebtoken::Validation {
leeway: 30, // 30 seconds
validate_exp: true,
validate_iat: false, // IssuedAt is the same as NotBefore
validate_nbf: true,
aud: None,
iss: Some(JWT_ISSUER.clone()),
sub: None,
algorithms: vec![JWT_ALGORITHM],
};
let token = token.replace(char::is_whitespace, "");
jsonwebtoken::decode(&token, &PUBLIC_RSA_KEY, &validation)
.map(|d| d.claims)
.map_res("Error decoding invite JWT")
}
#[derive(Debug, Serialize, Deserialize)]
@@ -87,14 +108,50 @@ pub struct JWTClaims {
pub amr: Vec<String>,
}
///
/// Bearer token authentication
///
use rocket::Outcome;
use rocket::request::{self, Request, FromRequest};
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteJWTClaims {
// Not before
pub nbf: i64,
// Expiration time
pub exp: i64,
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub email: String,
pub org_id: Option<String>,
pub user_org_id: Option<String>,
pub invited_by_email: Option<String>,
}
pub fn generate_invite_claims(uuid: String,
email: String,
org_id: Option<String>,
org_user_id: Option<String>,
invited_by_email: Option<String>,
) -> InviteJWTClaims {
let time_now = Utc::now().naive_utc();
InviteJWTClaims {
nbf: time_now.timestamp(),
exp: (time_now + Duration::days(5)).timestamp(),
iss: JWT_ISSUER.to_string(),
sub: uuid.clone(),
email: email.clone(),
org_id: org_id.clone(),
user_org_id: org_user_id.clone(),
invited_by_email: invited_by_email.clone(),
}
}
//
// Bearer token authentication
//
use rocket::request::{self, FromRequest, Request};
use rocket::Outcome;
use crate::db::models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization};
use crate::db::DbConn;
use crate::db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
pub struct Headers {
pub host: String,
@@ -113,7 +170,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
CONFIG.domain.clone()
} else if let Some(referer) = headers.get_one("Referer") {
referer.to_string()
} else {
} else {
// Try to guess from the headers
use std::env;
@@ -137,7 +194,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
};
// Get access_token
let access_token: &str = match request.headers().get_one("Authorization") {
let access_token: &str = match headers.get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
@@ -148,7 +205,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
// Check JWT token is valid and get device and user from it
let claims: JWTClaims = match decode_jwt(access_token) {
Ok(claims) => claims,
Err(_) => err_handler!("Invalid claim")
Err(_) => err_handler!("Invalid claim"),
};
let device_uuid = claims.device;
@@ -156,17 +213,17 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
let conn = match request.guard::<DbConn>() {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB")
_ => err_handler!("Error getting DB"),
};
let device = match Device::find_by_uuid(&device_uuid, &conn) {
Some(device) => device,
None => err_handler!("Invalid device id")
None => err_handler!("Invalid device id"),
};
let user = match User::find_by_uuid(&user_uuid, &conn) {
Some(user) => user,
None => err_handler!("Device has no user associated")
None => err_handler!("Device has no user associated"),
};
if user.security_stamp != claims.sstamp {
@@ -197,10 +254,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
Some(Ok(org_id)) => {
let conn = match request.guard::<DbConn>() {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB")
_ => err_handler!("Error getting DB"),
};
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
let user = headers.user;
let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn) {
Some(user) => {
if user.status == UserOrgStatus::Confirmed as i32 {
user
@@ -208,28 +266,23 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
err_handler!("The current user isn't confirmed member of the organization")
}
}
None => {
if headers.user.is_server_admin() && org_id == Organization::VIRTUAL_ID {
UserOrganization::new_virtual(headers.user.uuid.clone(), UserOrgType::Owner, UserOrgStatus::Confirmed)
} else {
err_handler!("The current user isn't member of the organization")
}
}
None => err_handler!("The current user isn't member of the organization"),
};
Outcome::Success(Self{
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
org_user_type: {
user,
org_user_type: {
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.type_) {
org_usr_type
} else { // This should only happen if the DB is corrupted
} else {
// This should only happen if the DB is corrupted
err_handler!("Unknown user type in the database")
}
},
})
},
}
_ => err_handler!("Error getting the organization id"),
}
}
@@ -295,9 +348,9 @@ impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
}
}
///
/// Client IP address detection
///
//
// Client IP address detection
//
use std::net::IpAddr;
pub struct ClientIp {

View File

@@ -1,6 +1,6 @@
///
/// PBKDF2 derivation
///
//
// PBKDF2 derivation
//
use ring::{digest, pbkdf2};
@@ -19,9 +19,9 @@ pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterati
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
}
///
/// Random values
///
//
// Random values
//
pub fn get_random_64() -> Vec<u8> {
get_random(vec![0u8; 64])
@@ -30,7 +30,9 @@ pub fn get_random_64() -> Vec<u8> {
pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
use ring::rand::{SecureRandom, SystemRandom};
SystemRandom::new().fill(&mut array).expect("Error generating random values");
SystemRandom::new()
.fill(&mut array)
.expect("Error generating random values");
array
}

View File

@@ -1,9 +1,9 @@
use std::ops::Deref;
use diesel::{Connection as DieselConnection, ConnectionError};
use diesel::sqlite::SqliteConnection;
use diesel::r2d2;
use diesel::r2d2::ConnectionManager;
use diesel::sqlite::SqliteConnection;
use diesel::{Connection as DieselConnection, ConnectionError};
use rocket::http::Status;
use rocket::request::{self, FromRequest};
@@ -20,16 +20,14 @@ type Pool = r2d2::Pool<ConnectionManager<Connection>>;
/// Connection request guard type: a wrapper around an r2d2 pooled connection.
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>);
pub mod schema;
pub mod models;
pub mod schema;
/// Initializes a database pool.
pub fn init_pool() -> Pool {
let manager = ConnectionManager::new(&*CONFIG.database_url);
r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool")
r2d2::Pool::builder().build(manager).expect("Failed to create pool")
}
pub fn get_connection() -> Result<Connection, ConnectionError> {
@@ -46,7 +44,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
let pool = request.guard::<State<Pool>>()?;
match pool.get() {
Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
}
}
}

View File

@@ -12,7 +12,7 @@ pub struct Attachment {
pub cipher_uuid: String,
pub file_name: String,
pub file_size: i32,
pub key: Option<String>
pub key: Option<String>,
}
/// Local methods
@@ -23,7 +23,7 @@ impl Attachment {
cipher_uuid,
file_name,
file_size,
key: None
key: None,
}
}
@@ -49,34 +49,35 @@ impl Attachment {
}
}
use crate::db::schema::attachments;
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::attachments;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Attachment {
pub fn save(&self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(attachments::table)
.values(self)
.execute(&**conn)
.and(Ok(()))
.map_res("Error saving attachment")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(self, conn: &DbConn) -> EmptyResult {
crate::util::retry(
|| {
diesel::delete(attachments::table.filter(attachments::id.eq(&self.id)))
.execute(&**conn)
},
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(&**conn),
10,
)?;
)
.map_res("Error deleting attachment")?;
crate::util::delete_file(&self.get_file_path());
crate::util::delete_file(&self.get_file_path())?;
Ok(())
}
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
for attachment in Attachment::find_by_cipher(&cipher_uuid, &conn) {
attachment.delete(&conn)?;
}
@@ -86,18 +87,21 @@ impl Attachment {
pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> {
attachments::table
.filter(attachments::id.eq(id))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> {
attachments::table
.filter(attachments::cipher_uuid.eq(cipher_uuid))
.load::<Self>(&**conn).expect("Error loading attachments")
.load::<Self>(&**conn)
.expect("Error loading attachments")
}
pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> {
attachments::table
.filter(attachments::cipher_uuid.eq_any(cipher_uuids))
.load::<Self>(&**conn).expect("Error loading attachments")
.load::<Self>(&**conn)
.expect("Error loading attachments")
}
}

View File

@@ -1,7 +1,9 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value;
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrganization, UserOrgType, UserOrgStatus};
use super::{
Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization,
};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers"]
@@ -59,28 +61,35 @@ impl Cipher {
}
}
use crate::db::schema::*;
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::*;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Cipher {
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
use serde_json;
use crate::util::format_date;
use super::Attachment;
use crate::util::format_date;
use serde_json;
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
let fields_json: Value = if let Some(ref fields) = self.fields {
serde_json::from_str(fields).unwrap()
} else { Value::Null };
} else {
Value::Null
};
let password_history_json: Value = if let Some(ref password_history) = self.password_history {
serde_json::from_str(password_history).unwrap()
} else { Value::Null };
} else {
Value::Null
};
let mut data_json: Value = serde_json::from_str(&self.data).unwrap();
@@ -134,71 +143,70 @@ impl Cipher {
Some(ref user_uuid) => {
User::update_uuid_revision(&user_uuid, conn);
user_uuids.push(user_uuid.clone())
},
None => { // Belongs to Organization, need to update affected users
}
None => {
// Belongs to Organization, need to update affected users
if let Some(ref org_uuid) = self.organization_uuid {
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
user_uuids.push(user_org.user_uuid.clone())
});
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
user_uuids.push(user_org.user_uuid.clone())
});
}
}
};
user_uuids
}
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
self.updated_at = Utc::now().naive_utc();
diesel::replace_into(ciphers::table)
.values(&*self)
.execute(&**conn)
.and(Ok(()))
.map_res("Error saving cipher")
}
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?;
Attachment::delete_all_by_cipher(&self.uuid, &conn)?;
diesel::delete(
ciphers::table.filter(
ciphers::uuid.eq(&self.uuid)
)
).execute(&**conn).and(Ok(()))
diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid)))
.execute(&**conn)
.map_res("Error deleting cipher")
}
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
for cipher in Self::find_by_org(org_uuid, &conn) {
cipher.delete(&conn)?;
}
Ok(())
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
for cipher in Self::find_owned_by_user(user_uuid, &conn) {
cipher.delete(&conn)?;
}
Ok(())
}
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> Result<(), &str> {
match self.get_folder_uuid(&user_uuid, &conn) {
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
match self.get_folder_uuid(&user_uuid, &conn) {
None => {
match folder_uuid {
Some(new_folder) => {
self.update_users_revision(conn);
let folder_cipher = FolderCipher::new(&new_folder, &self.uuid);
folder_cipher.save(&conn).or(Err("Couldn't save folder setting"))
},
None => Ok(()) //nothing to do
folder_cipher.save(&conn)
}
None => Ok(()), //nothing to do
}
},
}
Some(current_folder) => {
match folder_uuid {
Some(new_folder) => {
@@ -206,24 +214,19 @@ impl Cipher {
Ok(()) //nothing to do
} else {
self.update_users_revision(conn);
match FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn) {
Some(current_folder) => {
current_folder.delete(&conn).or(Err("Failed removing old folder mapping"))
},
None => Ok(()) // Weird, but nothing to do
}.and_then(
|()| FolderCipher::new(&new_folder, &self.uuid)
.save(&conn).or(Err("Couldn't save folder setting"))
)
if let Some(current_folder) =
FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn)
{
current_folder.delete(&conn)?;
}
FolderCipher::new(&new_folder, &self.uuid).save(&conn)
}
},
}
None => {
self.update_users_revision(conn);
match FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn) {
Some(current_folder) => {
current_folder.delete(&conn).or(Err("Failed removing old folder mapping"))
},
None => Err("Couldn't move from previous folder")
Some(current_folder) => current_folder.delete(&conn),
None => err!("Couldn't move from previous folder"),
}
}
}
@@ -233,67 +236,82 @@ impl Cipher {
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
ciphers::table
.filter(ciphers::uuid.eq(&self.uuid))
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
users_organizations::user_uuid.eq(user_uuid)
.filter(ciphers::uuid.eq(&self.uuid))
.left_join(
users_organizations::table.on(ciphers::organization_uuid
.eq(users_organizations::org_uuid.nullable())
.and(users_organizations::user_uuid.eq(user_uuid))),
)
))
.left_join(ciphers_collections::table)
.left_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
))
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
users_collections::user_uuid.eq(user_uuid).and(
users_collections::read_only.eq(false) //R/W access to collection
)
)
.left_join(ciphers_collections::table)
.left_join(
users_collections::table
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
)
))
.select(ciphers::all_columns)
.first::<Self>(&**conn).ok().is_some()
.filter(ciphers::user_uuid.eq(user_uuid).or(
// Cipher owner
users_organizations::access_all.eq(true).or(
// access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or(
// Org admin or owner
users_collections::user_uuid.eq(user_uuid).and(
users_collections::read_only.eq(false), //R/W access to collection
),
),
),
))
.select(ciphers::all_columns)
.first::<Self>(&**conn)
.ok()
.is_some()
}
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
ciphers::table
.filter(ciphers::uuid.eq(&self.uuid))
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
users_organizations::user_uuid.eq(user_uuid)
.filter(ciphers::uuid.eq(&self.uuid))
.left_join(
users_organizations::table.on(ciphers::organization_uuid
.eq(users_organizations::org_uuid.nullable())
.and(users_organizations::user_uuid.eq(user_uuid))),
)
))
.left_join(ciphers_collections::table)
.left_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
))
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
users_collections::user_uuid.eq(user_uuid) // Access to Collection
)
.left_join(ciphers_collections::table)
.left_join(
users_collections::table
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
)
))
.select(ciphers::all_columns)
.first::<Self>(&**conn).ok().is_some()
.filter(ciphers::user_uuid.eq(user_uuid).or(
// Cipher owner
users_organizations::access_all.eq(true).or(
// access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or(
// Org admin or owner
users_collections::user_uuid.eq(user_uuid), // Access to Collection
),
),
))
.select(ciphers::all_columns)
.first::<Self>(&**conn)
.ok()
.is_some()
}
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
folders_ciphers::table.inner_join(folders::table)
folders_ciphers::table
.inner_join(folders::table)
.filter(folders::user_uuid.eq(&user_uuid))
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
.select(folders_ciphers::folder_uuid)
.first::<String>(&**conn).ok()
.first::<String>(&**conn)
.ok()
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
ciphers::table
.filter(ciphers::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
// Find all ciphers accesible to user
// Find all ciphers accessible to user
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
ciphers::table
.left_join(users_organizations::table.on(
@@ -303,7 +321,9 @@ impl Cipher {
)
)
))
.left_join(ciphers_collections::table)
.left_join(ciphers_collections::table.on(
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
))
.left_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
))
@@ -352,7 +372,9 @@ impl Cipher {
)
))
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
users_collections::user_uuid.eq(user_id)
)
))
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection

View File

@@ -1,6 +1,6 @@
use serde_json::Value;
use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus};
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "collections"]
@@ -33,39 +33,40 @@ impl Collection {
}
}
use crate::db::schema::*;
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::*;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Collection {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
// Update affected users revision
UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
diesel::replace_into(collections::table)
.values(&*self)
.execute(&**conn)
.and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error saving collection")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(self, conn: &DbConn) -> EmptyResult {
CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?;
CollectionUser::delete_all_by_collection(&self.uuid, &conn)?;
diesel::delete(
collections::table.filter(
collections::uuid.eq(self.uuid)
)
).execute(&**conn).and(Ok(()))
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
.execute(&**conn)
.map_res("Error deleting collection")
}
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
for collection in Self::find_by_organization(org_uuid, &conn) {
collection.delete(&conn)?;
}
@@ -75,47 +76,54 @@ impl Collection {
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
collections::table
.filter(collections::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
let mut all_access_collections = users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.filter(users_organizations::access_all.eq(true))
.inner_join(collections::table.on(collections::org_uuid.eq(users_organizations::org_uuid)))
.select(collections::all_columns)
.load::<Self>(&**conn).expect("Error loading collections");
let mut assigned_collections = users_collections::table.inner_join(collections::table)
.left_join(users_organizations::table.on(
users_collections::user_uuid.eq(users_organizations::user_uuid)
))
.filter(users_collections::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.select(collections::all_columns)
.load::<Self>(&**conn).expect("Error loading collections");
all_access_collections.append(&mut assigned_collections);
all_access_collections
collections::table
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(collections::uuid).and(
users_collections::user_uuid.eq(user_uuid)
)
))
.left_join(users_organizations::table.on(
collections::org_uuid.eq(users_organizations::org_uuid).and(
users_organizations::user_uuid.eq(user_uuid)
)
))
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
.filter(
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
users_organizations::access_all.eq(true) // access_all in Organization
)
).select(collections::all_columns)
.load::<Self>(&**conn).expect("Error loading collections")
}
pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> {
Self::find_by_user_uuid(user_uuid, conn).into_iter().filter(|c| c.org_uuid == org_uuid).collect()
Self::find_by_user_uuid(user_uuid, conn)
.into_iter()
.filter(|c| c.org_uuid == org_uuid)
.collect()
}
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
collections::table
.filter(collections::org_uuid.eq(org_uuid))
.load::<Self>(&**conn).expect("Error loading collections")
.load::<Self>(&**conn)
.expect("Error loading collections")
}
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
collections::table
.filter(collections::uuid.eq(uuid))
.filter(collections::org_uuid.eq(org_uuid))
.select(collections::all_columns)
.first::<Self>(&**conn).ok()
.filter(collections::uuid.eq(uuid))
.filter(collections::org_uuid.eq(org_uuid))
.select(collections::all_columns)
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
@@ -145,22 +153,25 @@ impl Collection {
match UserOrganization::find_by_user_and_org(&user_uuid, &self.org_uuid, &conn) {
None => false, // Not in Org
Some(user_org) => {
if user_org.access_all {
true
} else {
users_collections::table.inner_join(collections::table)
.filter(users_collections::collection_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(&user_uuid))
.filter(users_collections::read_only.eq(false))
.select(collections::all_columns)
.first::<Self>(&**conn).ok().is_some() // Read only or no access to collection
}
if user_org.access_all {
true
} else {
users_collections::table
.inner_join(collections::table)
.filter(users_collections::collection_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(&user_uuid))
.filter(users_collections::read_only.eq(false))
.select(collections::all_columns)
.first::<Self>(&**conn)
.ok()
.is_some() // Read only or no access to collection
}
}
}
}
}
use super::User;
use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "users_collections"]
@@ -181,66 +192,72 @@ impl CollectionUser {
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
.filter(collections::org_uuid.eq(org_uuid))
.select(users_collections::all_columns)
.load::<Self>(&**conn).expect("Error loading users_collections")
.load::<Self>(&**conn)
.expect("Error loading users_collections")
}
pub fn save(user_uuid: &str, collection_uuid: &str, read_only:bool, conn: &DbConn) -> QueryResult<()> {
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn);
diesel::replace_into(users_collections::table)
.values((
users_collections::user_uuid.eq(user_uuid),
users_collections::collection_uuid.eq(collection_uuid),
users_collections::read_only.eq(read_only),
)).execute(&**conn).and(Ok(()))
.values((
users_collections::user_uuid.eq(user_uuid),
users_collections::collection_uuid.eq(collection_uuid),
users_collections::read_only.eq(read_only),
))
.execute(&**conn)
.map_res("Error adding user to collection")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
diesel::delete(users_collections::table
.filter(users_collections::user_uuid.eq(&self.user_uuid))
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)))
.execute(&**conn).and(Ok(()))
diesel::delete(
users_collections::table
.filter(users_collections::user_uuid.eq(&self.user_uuid))
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)),
)
.execute(&**conn)
.map_res("Error removing user from collection")
}
pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
.select(users_collections::all_columns)
.load::<Self>(&**conn).expect("Error loading users_collections")
.filter(users_collections::collection_uuid.eq(collection_uuid))
.select(users_collections::all_columns)
.load::<Self>(&**conn)
.expect("Error loading users_collections")
}
pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
.filter(users_collections::user_uuid.eq(user_uuid))
.select(users_collections::all_columns)
.first::<Self>(&**conn).ok()
}
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
CollectionUser::find_by_collection(&collection_uuid, conn)
.iter()
.for_each(|collection| {
User::update_uuid_revision(&collection.user_uuid, conn)
});
diesel::delete(users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
).execute(&**conn).and(Ok(()))
.filter(users_collections::user_uuid.eq(user_uuid))
.select(users_collections::all_columns)
.first::<Self>(&**conn)
.ok()
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
CollectionUser::find_by_collection(&collection_uuid, conn)
.iter()
.for_each(|collection| User::update_uuid_revision(&collection.user_uuid, conn));
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
.execute(&**conn)
.map_res("Error deleting users from collection")
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn);
diesel::delete(users_collections::table
.filter(users_collections::user_uuid.eq(user_uuid))
).execute(&**conn).and(Ok(()))
diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid)))
.execute(&**conn)
.map_res("Error removing user from collections")
}
}
use super::Cipher;
use super::Cipher;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers_collections"]
@@ -254,30 +271,35 @@ pub struct CollectionCipher {
/// Database methods
impl CollectionCipher {
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::replace_into(ciphers_collections::table)
.values((
ciphers_collections::cipher_uuid.eq(cipher_uuid),
ciphers_collections::collection_uuid.eq(collection_uuid),
)).execute(&**conn).and(Ok(()))
))
.execute(&**conn)
.map_res("Error adding cipher to collection")
}
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::delete(ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
.execute(&**conn).and(Ok(()))
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(
ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)),
)
.execute(&**conn)
.map_res("Error deleting cipher from collection")
}
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::delete(ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
).execute(&**conn).and(Ok(()))
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
.execute(&**conn)
.map_res("Error removing cipher from collections")
}
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::delete(ciphers_collections::table
.filter(ciphers_collections::collection_uuid.eq(collection_uuid))
).execute(&**conn).and(Ok(()))
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
.execute(&**conn)
.map_res("Error removing ciphers from collection")
}
}
}

View File

@@ -44,8 +44,8 @@ impl Device {
}
pub fn refresh_twofactor_remember(&mut self) -> String {
use data_encoding::BASE64;
use crate::crypto;
use data_encoding::BASE64;
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
self.twofactor_remember = Some(twofactor_remember.clone());
@@ -57,12 +57,11 @@ impl Device {
self.twofactor_remember = None;
}
pub fn refresh_tokens(&mut self, user: &super::User, orgs: Vec<super::UserOrganization>) -> (String, i64) {
// If there is no refresh token, we create one
if self.refresh_token.is_empty() {
use data_encoding::BASE64URL;
use crate::crypto;
use data_encoding::BASE64URL;
self.refresh_token = BASE64URL.encode(&crypto::get_random_64());
}
@@ -105,34 +104,33 @@ impl Device {
}
}
use crate::db::schema::devices;
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::devices;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Device {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
crate::util::retry(
|| {
diesel::replace_into(devices::table)
.values(&*self)
.execute(&**conn)
},
|| diesel::replace_into(devices::table).values(&*self).execute(&**conn),
10,
)
.and(Ok(()))
.map_res("Error saving device")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
diesel::delete(devices::table.filter(
devices::uuid.eq(self.uuid)
)).execute(&**conn).and(Ok(()))
pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid)))
.execute(&**conn)
.map_res("Error removing device")
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
for device in Self::find_by_user(user_uuid, &conn) {
device.delete(&conn)?;
}
@@ -142,18 +140,21 @@ impl Device {
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
devices::table
.filter(devices::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {
devices::table
.filter(devices::refresh_token.eq(refresh_token))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).expect("Error loading devices")
.load::<Self>(&**conn)
.expect("Error loading devices")
}
}

View File

@@ -1,7 +1,7 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value;
use super::{User, Cipher};
use super::{Cipher, User};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "folders"]
@@ -61,33 +61,36 @@ impl FolderCipher {
}
}
use crate::db::schema::{folders, folders_ciphers};
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::{folders, folders_ciphers};
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Folder {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc();
diesel::replace_into(folders::table)
.values(&*self).execute(&**conn).and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error saving folder")
}
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
diesel::delete(
folders::table.filter(
folders::uuid.eq(&self.uuid)
)
).execute(&**conn).and(Ok(()))
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
.execute(&**conn)
.map_res("Error deleting folder")
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
for folder in Self::find_by_user(user_uuid, &conn) {
folder.delete(&conn)?;
}
@@ -97,52 +100,60 @@ impl Folder {
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
folders::table
.filter(folders::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
folders::table
.filter(folders::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).expect("Error loading folders")
.load::<Self>(&**conn)
.expect("Error loading folders")
}
}
impl FolderCipher {
pub fn save(&self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(folders_ciphers::table)
.values(&*self)
.execute(&**conn).and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error adding cipher to folder")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
diesel::delete(folders_ciphers::table
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid))
).execute(&**conn).and(Ok(()))
pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(
folders_ciphers::table
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)),
)
.execute(&**conn)
.map_res("Error removing cipher from folder")
}
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::delete(folders_ciphers::table
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
).execute(&**conn).and(Ok(()))
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
.execute(&**conn)
.map_res("Error removing cipher from folders")
}
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::delete(folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
).execute(&**conn).and(Ok(()))
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
.execute(&**conn)
.map_res("Error removing ciphers from folder")
}
pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> {
folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.load::<Self>(&**conn).expect("Error loading folders")
.load::<Self>(&**conn)
.expect("Error loading folders")
}
}

View File

@@ -10,10 +10,10 @@ mod two_factor;
pub use self::attachment::Attachment;
pub use self::cipher::Cipher;
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
pub use self::device::Device;
pub use self::folder::{Folder, FolderCipher};
pub use self::user::{User, Invitation};
pub use self::organization::Organization;
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::organization::{UserOrgStatus, UserOrgType, UserOrganization};
pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::user::{Invitation, User};

View File

@@ -1,7 +1,7 @@
use std::cmp::Ordering;
use serde_json::Value;
use std::cmp::Ordering;
use super::{User, CollectionUser, Invitation};
use super::{CollectionUser, User};
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "organizations"]
@@ -32,9 +32,7 @@ pub enum UserOrgStatus {
Confirmed = 2,
}
#[derive(Copy, Clone)]
#[derive(PartialEq)]
#[derive(Eq)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum UserOrgType {
Owner = 0,
Admin = 1,
@@ -51,13 +49,13 @@ impl Ord for UserOrgType {
UserOrgType::Owner => Ordering::Greater,
UserOrgType::Admin => match other {
UserOrgType::Owner => Ordering::Less,
_ => Ordering::Greater
_ => Ordering::Greater,
},
UserOrgType::Manager => match other {
UserOrgType::Owner | UserOrgType::Admin => Ordering::Less,
_ => Ordering::Greater
_ => Ordering::Greater,
},
UserOrgType::User => Ordering::Less
UserOrgType::User => Ordering::Less,
}
}
}
@@ -78,7 +76,7 @@ impl PartialEq<i32> for UserOrgType {
impl PartialOrd<i32> for UserOrgType {
fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
if let Some(other) = Self::from_i32(*other) {
return Some(self.cmp(&other))
return Some(self.cmp(&other));
}
None
}
@@ -96,7 +94,6 @@ impl PartialOrd<i32> for UserOrgType {
_ => true,
}
}
}
impl PartialEq<UserOrgType> for i32 {
@@ -108,7 +105,7 @@ impl PartialEq<UserOrgType> for i32 {
impl PartialOrd<UserOrgType> for i32 {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
if let Some(self_type) = UserOrgType::from_i32(*self) {
return Some(self_type.cmp(other))
return Some(self_type.cmp(other));
}
None
}
@@ -126,7 +123,6 @@ impl PartialOrd<UserOrgType> for i32 {
_ => false,
}
}
}
impl UserOrgType {
@@ -149,13 +145,10 @@ impl UserOrgType {
_ => None,
}
}
}
/// Local methods
impl Organization {
pub const VIRTUAL_ID: &'static str = "00000000-0000-0000-0000-000000000000";
pub fn new(name: String, billing_email: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
@@ -165,14 +158,6 @@ impl Organization {
}
}
pub fn new_virtual() -> Self {
Self {
uuid: String::from(Organization::VIRTUAL_ID),
name: String::from("bitwarden_rs"),
billing_email: String::from("none@none.none")
}
}
pub fn to_json(&self) -> Value {
json!({
"Id": self.uuid,
@@ -216,70 +201,48 @@ impl UserOrganization {
type_: UserOrgType::User as i32,
}
}
pub fn new_virtual(user_uuid: String, type_: UserOrgType, status: UserOrgStatus) -> Self {
Self {
uuid: user_uuid.clone(),
user_uuid,
org_uuid: String::from(Organization::VIRTUAL_ID),
access_all: true,
key: String::new(),
status: status as i32,
type_: type_ as i32,
}
}
}
use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations};
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Organization {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
if self.uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
UserOrganization::find_by_org(&self.uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
diesel::replace_into(organizations::table)
.values(&*self).execute(&**conn).and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error saving organization")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(self, conn: &DbConn) -> EmptyResult {
use super::{Cipher, Collection};
if self.uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
Cipher::delete_all_by_organization(&self.uuid, &conn)?;
Collection::delete_all_by_organization(&self.uuid, &conn)?;
UserOrganization::delete_all_by_organization(&self.uuid, &conn)?;
diesel::delete(
organizations::table.filter(
organizations::uuid.eq(self.uuid)
)
).execute(&**conn).and(Ok(()))
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
.execute(&**conn)
.map_res("Error saving organization")
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
if uuid == Organization::VIRTUAL_ID {
return Some(Self::new_virtual())
};
organizations::table
.filter(organizations::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
}
@@ -344,12 +307,15 @@ impl UserOrganization {
})
}
pub fn to_json_details(&self, conn: &DbConn) -> Value {
let coll_uuids = if self.access_all {
pub fn to_json_details(&self, conn: &DbConn) -> Value {
let coll_uuids = if self.access_all {
vec![] // If we have complete access, no need to fill the array
} else {
let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn);
collections.iter().map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only})).collect()
collections
.iter()
.map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only}))
.collect()
};
json!({
@@ -365,39 +331,33 @@ impl UserOrganization {
})
}
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
if self.org_uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
diesel::replace_into(users_organizations::table)
.values(&*self).execute(&**conn).and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error adding user to organization")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
if self.org_uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
pub fn delete(self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
diesel::delete(
users_organizations::table.filter(
users_organizations::uuid.eq(self.uuid)
)
).execute(&**conn).and(Ok(()))
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
.execute(&**conn)
.map_res("Error removing user from organization")
}
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
for user_org in Self::find_by_org(&org_uuid, &conn) {
user_org.delete(&conn)?;
}
Ok(())
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
for user_org in Self::find_any_state_by_user(&user_uuid, &conn) {
user_org.delete(&conn)?;
}
@@ -411,67 +371,62 @@ impl UserOrganization {
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.load::<Self>(&**conn).unwrap_or_default()
.load::<Self>(&**conn)
.unwrap_or_default()
}
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
.load::<Self>(&**conn).unwrap_or_default()
.load::<Self>(&**conn)
.unwrap_or_default()
}
pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).unwrap_or_default()
.load::<Self>(&**conn)
.unwrap_or_default()
}
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
if org_uuid == Organization::VIRTUAL_ID {
User::get_all(&*conn).iter().map(|user| {
Self::new_virtual(
user.uuid.clone(),
UserOrgType::User,
if Invitation::find_by_mail(&user.email, &conn).is_some() {
UserOrgStatus::Invited
} else {
UserOrgStatus::Confirmed
})
}).collect()
} else {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<Self>(&**conn).expect("Error loading user organizations")
}
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<Self>(&**conn)
.expect("Error loading user organizations")
}
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::type_.eq(type_))
.load::<Self>(&**conn).expect("Error loading user organizations")
.load::<Self>(&**conn)
.expect("Error loading user organizations")
}
pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
@@ -508,7 +463,4 @@ impl UserOrganization {
.select(users_organizations::all_columns)
.load::<Self>(&**conn).expect("Error loading user organizations")
}
}

View File

@@ -50,7 +50,7 @@ impl TwoFactor {
let decoded_secret = match BASE32.decode(totp_secret) {
Ok(s) => s,
Err(_) => return false
Err(_) => return false,
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
@@ -74,37 +74,47 @@ impl TwoFactor {
}
}
use crate::db::schema::twofactor;
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::twofactor;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl TwoFactor {
pub fn save(&self, conn: &DbConn) -> QueryResult<usize> {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(twofactor::table)
.values(self)
.execute(&**conn)
.map_res("Error saving twofactor")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<usize> {
diesel::delete(
twofactor::table.filter(
twofactor::uuid.eq(self.uuid)
)
).execute(&**conn)
pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
.execute(&**conn)
.map_res("Error deleting twofactor")
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).expect("Error loading twofactor")
.load::<Self>(&**conn)
.expect("Error loading twofactor")
}
pub fn find_by_user_and_type(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<Self> {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::type_.eq(type_))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
.execute(&**conn)
.map_res("Error deleting twofactors")
}
}

View File

@@ -4,7 +4,6 @@ use serde_json::Value;
use crate::crypto;
use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "users"]
#[primary_key(uuid)]
@@ -24,7 +23,7 @@ pub struct User {
pub key: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
#[column_name = "totp_secret"]
_totp_secret: Option<String>,
pub totp_recover: Option<String>,
@@ -33,7 +32,7 @@ pub struct User {
pub equivalent_domains: String,
pub excluded_globals: String,
pub client_kdf_type: i32,
pub client_kdf_iter: i32,
}
@@ -64,23 +63,25 @@ impl User {
password_hint: None,
private_key: None,
public_key: None,
_totp_secret: None,
totp_recover: None,
equivalent_domains: "[]".to_string(),
excluded_globals: "[]".to_string(),
client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT,
client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT,
}
}
pub fn check_valid_password(&self, password: &str) -> bool {
crypto::verify_password_hash(password.as_bytes(),
&self.salt,
&self.password_hash,
self.password_iterations as u32)
crypto::verify_password_hash(
password.as_bytes(),
&self.salt,
&self.password_hash,
self.password_iterations as u32,
)
}
pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool {
@@ -92,38 +93,29 @@ impl User {
}
pub fn set_password(&mut self, password: &str) {
self.password_hash = crypto::hash_password(password.as_bytes(),
&self.salt,
self.password_iterations as u32);
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
}
pub fn reset_security_stamp(&mut self) {
self.security_stamp = crate::util::get_uuid();
}
pub fn is_server_admin(&self) -> bool {
match CONFIG.server_admin_email {
Some(ref server_admin_email) => &self.email == server_admin_email,
None => false
}
}
}
use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization};
use crate::db::schema::{invitations, users};
use crate::db::DbConn;
use diesel;
use diesel::prelude::*;
use crate::db::DbConn;
use crate::db::schema::{users, invitations};
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl User {
pub fn to_json(&self, conn: &DbConn) -> Value {
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
use super::{TwoFactor, UserOrganization};
let mut orgs = UserOrganization::find_by_user(&self.uuid, conn);
if self.is_server_admin() {
orgs.push(UserOrganization::new_virtual(self.uuid.clone(), UserOrgType::Owner, UserOrgStatus::Confirmed));
}
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
@@ -144,22 +136,21 @@ impl User {
})
}
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
diesel::replace_into(users::table) // Insert or update
.values(&*self).execute(&**conn).and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error saving user")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
pub fn delete(self, conn: &DbConn) -> EmptyResult {
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
if user_org.type_ == UserOrgType::Owner {
if UserOrganization::find_by_org_and_type(
&user_org.org_uuid,
UserOrgType::Owner as i32, &conn
).len() <= 1 {
return Err(diesel::result::Error::NotFound);
let owner_type = UserOrgType::Owner as i32;
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 {
err!("Can't delete last owner")
}
}
}
@@ -168,48 +159,44 @@ impl User {
Cipher::delete_all_by_user(&self.uuid, &*conn)?;
Folder::delete_all_by_user(&self.uuid, &*conn)?;
Device::delete_all_by_user(&self.uuid, &*conn)?;
TwoFactor::delete_all_by_user(&self.uuid, &*conn)?;
Invitation::take(&self.email, &*conn); // Delete invitation if any
diesel::delete(users::table.filter(
users::uuid.eq(self.uuid)))
.execute(&**conn).and(Ok(()))
diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
.execute(&**conn)
.map_res("Error deleting user")
}
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
if let Some(mut user) = User::find_by_uuid(&uuid, conn) {
if user.update_revision(conn).is_err(){
if user.update_revision(conn).is_err() {
warn!("Failed to update revision for {}", user.email);
};
};
}
pub fn update_revision(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
diesel::update(
users::table.filter(
users::uuid.eq(&self.uuid)
)
)
.set(users::updated_at.eq(&self.updated_at))
.execute(&**conn).and(Ok(()))
diesel::update(users::table.filter(users::uuid.eq(&self.uuid)))
.set(users::updated_at.eq(&self.updated_at))
.execute(&**conn)
.map_res("Error updating user revision")
}
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase();
users::table
.filter(users::email.eq(lower_mail))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
users::table
.filter(users::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
users::table.filter(users::uuid.eq(uuid)).first::<Self>(&**conn).ok()
}
pub fn get_all(conn: &DbConn) -> Vec<Self> {
users::table
.load::<Self>(&**conn).expect("Error loading users")
users::table.load::<Self>(&**conn).expect("Error loading users")
}
}
@@ -222,37 +209,35 @@ pub struct Invitation {
impl Invitation {
pub fn new(email: String) -> Self {
Self {
email
}
Self { email }
}
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(invitations::table)
.values(&*self)
.execute(&**conn)
.and(Ok(()))
.values(&*self)
.execute(&**conn)
.map_res("Error saving invitation")
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
diesel::delete(invitations::table.filter(
invitations::email.eq(self.email)))
.execute(&**conn)
.and(Ok(()))
pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))
.execute(&**conn)
.map_res("Error deleting invitation")
}
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase();
invitations::table
.filter(invitations::email.eq(lower_mail))
.first::<Self>(&**conn).ok()
.first::<Self>(&**conn)
.ok()
}
pub fn take(mail: &str, conn: &DbConn) -> bool {
CONFIG.invitations_allowed &&
match Self::find_by_mail(mail, &conn) {
Some(invitation) => invitation.delete(&conn).is_ok(),
None => false
}
CONFIG.invitations_allowed
&& match Self::find_by_mail(mail, &conn) {
Some(invitation) => invitation.delete(&conn).is_ok(),
None => false,
}
}
}
}

173
src/error.rs Normal file
View File

@@ -0,0 +1,173 @@
//
// Error generator macro
//
use std::error::Error as StdError;
macro_rules! make_error {
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)* ) => {
#[derive(Display)]
enum ErrorKind { $($name( $ty )),+ }
pub struct Error { message: String, error: ErrorKind }
$(impl From<$ty> for Error {
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
})+
$(impl<S: Into<String>> From<(S, $ty)> for Error {
fn from(val: (S, $ty)) -> Self {
Error { message: val.0.into(), error: ErrorKind::$name(val.1) }
}
})+
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match &self.error {$( ErrorKind::$name(e) => $src_fn(e), )+}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.error {$(
ErrorKind::$name(e) => f.write_str(&$usr_msg_fun(e, &self.message)),
)+}
}
}
};
}
use diesel::result::Error as DieselError;
use jsonwebtoken::errors::Error as JwtError;
use serde_json::{Error as SerError, Value};
use std::io::Error as IOError;
use u2f::u2ferror::U2fError as U2fErr;
// Error struct
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
//
// After the variant itself, there are two expressions. The first one indicates whether the error contains a source error (that we pretty print).
// The second one contains the function used to obtain the response sent to the client
make_error! {
// Used to represent err! calls
SimpleError(String): _no_source, _api_error,
// Used for special return values, like 2FA errors
JsonError(Value): _no_source, _serialize,
DbError(DieselError): _has_source, _api_error,
U2fError(U2fErr): _has_source, _api_error,
SerdeError(SerError): _has_source, _api_error,
JWTError(JwtError): _has_source, _api_error,
IoErrror(IOError): _has_source, _api_error,
//WsError(ws::Error): _has_source, _api_error,
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.source() {
Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e),
None => write!(f, "{}. {}", self.message, self.error),
}
}
}
impl Error {
pub fn new<M: Into<String>, N: Into<String>>(usr_msg: M, log_msg: N) -> Self {
(usr_msg, log_msg.into()).into()
}
pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {
self.message = msg.into();
self
}
}
pub trait MapResult<S> {
fn map_res(self, msg: &str) -> Result<S, Error>;
}
impl<S, E: Into<Error>> MapResult<S> for Result<S, E> {
fn map_res(self, msg: &str) -> Result<S, Error> {
self.map_err(|e| e.into().with_msg(msg))
}
}
impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
fn map_res(self, msg: &str) -> Result<(), Error> {
self.and(Ok(())).map_res(msg)
}
}
fn _has_source<T>(e: T) -> Option<T> {
Some(e)
}
fn _no_source<T, S>(_: T) -> Option<S> {
None
}
fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String {
serde_json::to_string(e).unwrap()
}
fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
let json = json!({
"Message": "",
"error": "",
"error_description": "",
"ValidationErrors": {"": [ msg ]},
"ErrorModel": {
"Message": msg,
"Object": "error"
},
"Object": "error"
});
_serialize(&json, "")
}
//
// Rocket responder impl
//
use std::io::Cursor;
use rocket::http::{ContentType, Status};
use rocket::request::Request;
use rocket::response::{self, Responder, Response};
impl<'r> Responder<'r> for Error {
fn respond_to(self, _: &Request) -> response::Result<'r> {
let usr_msg = format!("{}", self);
error!("{:#?}", self);
Response::build()
.status(Status::BadRequest)
.header(ContentType::JSON)
.sized_body(Cursor::new(usr_msg))
.ok()
}
}
//
// Error return macros
//
#[macro_export]
macro_rules! err {
($msg:expr) => {{
return Err(crate::error::Error::new($msg, $msg));
}};
($usr_msg:expr, $log_value:expr) => {{
return Err(crate::error::Error::new($usr_msg, $log_value));
}};
}
#[macro_export]
macro_rules! err_json {
($expr:expr) => {{
return Err(crate::error::Error::from($expr));
}};
}
#[macro_export]
macro_rules! err_handler {
($expr:expr) => {{
error!("Unauthorized Error: {}", $expr);
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
}};
($usr_msg:expr, $log_value:expr) => {{
error!("Unauthorized Error: {}. {}", $usr_msg, $log_value);
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
}};
}

View File

@@ -1,10 +1,14 @@
use native_tls::{Protocol, TlsConnector};
use lettre::{Transport, SmtpTransport, SmtpClient, ClientTlsParameters, ClientSecurity};
use lettre::smtp::ConnectionReuseParameters;
use lettre::smtp::authentication::Credentials;
use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
use lettre_email::EmailBuilder;
use native_tls::{Protocol, TlsConnector};
use crate::MailConfig;
use crate::CONFIG;
use crate::auth::{generate_invite_claims, encode_jwt};
use crate::api::EmptyResult;
use crate::error::Error;
fn mailer(config: &MailConfig) -> SmtpTransport {
let client_security = if config.smtp_ssl {
@@ -18,10 +22,7 @@ fn mailer(config: &MailConfig) -> SmtpTransport {
ClientSecurity::None
};
let smtp_client = SmtpClient::new(
(config.smtp_host.as_str(), config.smtp_port),
client_security,
).unwrap();
let smtp_client = SmtpClient::new((config.smtp_host.as_str(), config.smtp_port), client_security).unwrap();
let smtp_client = match (&config.smtp_username, &config.smtp_password) {
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
@@ -34,29 +35,105 @@ fn mailer(config: &MailConfig) -> SmtpTransport {
.transport()
}
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> Result<(), String> {
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> EmptyResult {
let (subject, body) = if let Some(hint) = hint {
("Your master password hint",
format!(
"You (or someone) recently requested your master password hint.\n\n\
Your hint is: \"{}\"\n\n\
If you did not request your master password hint you can safely ignore this email.\n",
hint))
(
"Your master password hint",
format!(
"You (or someone) recently requested your master password hint.\n\n\
Your hint is: \"{}\"\n\n\
If you did not request your master password hint you can safely ignore this email.\n",
hint
),
)
} else {
("Sorry, you have no password hint...",
"Sorry, you have not specified any password hint...\n".into())
(
"Sorry, you have no password hint...",
"Sorry, you have not specified any password hint...\n".into(),
)
};
send_email(&address, &subject, &body, &config)
}
pub fn send_invite(
address: &str,
uuid: &str,
org_id: Option<String>,
org_user_id: Option<String>,
org_name: &str,
invited_by_email: Option<String>,
config: &MailConfig,
) -> EmptyResult {
let claims = generate_invite_claims(
uuid.to_string(),
String::from(address),
org_id.clone(),
org_user_id.clone(),
invited_by_email.clone(),
);
let invite_token = encode_jwt(&claims);
let (subject, body) = {
(format!("Join {}", &org_name),
format!(
"<html>
<p>You have been invited to join the <b>{}</b> organization.<br><br>
<a href=\"{}/#/accept-organization/?organizationId={}&organizationUserId={}&email={}&organizationName={}&token={}\">
Click here to join</a></p>
<p>If you do not wish to join this organization, you can safely ignore this email.</p>
</html>",
org_name, CONFIG.domain, org_id.unwrap_or("_".to_string()), org_user_id.unwrap_or("_".to_string()), address, org_name, invite_token
))
};
send_email(&address, &subject, &body, &config)
}
pub fn send_invite_accepted(
new_user_email: &str,
address: &str,
org_name: &str,
config: &MailConfig,
) -> EmptyResult {
let (subject, body) = {
("Invitation accepted",
format!(
"<html>
<p>Your invitation for <b>{}</b> to join <b>{}</b> was accepted. Please <a href=\"{}\">log in</a> to the bitwarden_rs server and confirm them from the organization management page.</p>
</html>", new_user_email, org_name, CONFIG.domain))
};
send_email(&address, &subject, &body, &config)
}
pub fn send_invite_confirmed(
address: &str,
org_name: &str,
config: &MailConfig,
) -> EmptyResult {
let (subject, body) = {
(format!("Invitation to {} confirmed", org_name),
format!(
"<html>
<p>Your invitation to join <b>{}</b> was confirmed. It will now appear under the Organizations the next time you <a href=\"{}\">log in</a> to the web vault.</p>
</html>", org_name, CONFIG.domain))
};
send_email(&address, &subject, &body, &config)
}
fn send_email(address: &str, subject: &str, body: &str, config: &MailConfig) -> EmptyResult {
let email = EmailBuilder::new()
.to(address)
.from((config.smtp_from.clone(), "Bitwarden-rs"))
.subject(subject)
.body(body)
.build()
.map_err(|e| e.to_string())?;
.to(address)
.from((config.smtp_from.clone(), "Bitwarden-rs"))
.subject(subject)
.header(("Content-Type", "text/html"))
.body(body)
.build()
.map_err(|e| Error::new("Error building email", e.to_string()))?;
mailer(config)
.send(email.into())
.map_err(|e| e.to_string())
.map_err(|e| Error::new("Error sending email", e.to_string()))
.and(Ok(()))
}
}

View File

@@ -2,39 +2,51 @@
#![recursion_limit = "128"]
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json;
#[macro_use] extern crate log;
#[macro_use] extern crate diesel;
#[macro_use] extern crate diesel_migrations;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate num_derive;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate log;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate derive_more;
#[macro_use]
extern crate num_derive;
use std::{path::Path, process::{exit, Command}};
use rocket::Rocket;
use std::{
path::Path,
process::{exit, Command},
};
#[macro_use]
mod error;
mod api;
mod auth;
mod crypto;
mod db;
mod mail;
mod util;
mod api;
mod db;
mod crypto;
mod auth;
mod mail;
fn init_rocket() -> Rocket {
// TODO: TO HIDE MOUNTING LOG, call ignite, set logging to disabled, call all the mounts, and then enable it again
rocket::ignite()
.mount("/", api::web_routes())
.mount("/api", api::core_routes())
.mount("/admin", api::admin_routes())
.mount("/identity", api::identity_routes())
.mount("/icons", api::icons_routes())
.mount("/notifications", api::notifications_routes())
.manage(db::init_pool())
.manage(api::start_notification_server())
.attach(util::AppHeaders())
}
// Embed the migrations from the migrations folder into the application
@@ -68,20 +80,20 @@ fn main() {
fn init_logging() -> Result<(), fern::InitError> {
let mut logger = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
))
})
.level(log::LevelFilter::Debug)
.level_for("hyper", log::LevelFilter::Warn)
.level_for("ws", log::LevelFilter::Info)
.level_for("multipart", log::LevelFilter::Info)
.chain(std::io::stdout());
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
record.target(),
record.level(),
message
))
})
.level(log::LevelFilter::Debug)
.level_for("hyper", log::LevelFilter::Warn)
.level_for("ws", log::LevelFilter::Info)
.level_for("multipart", log::LevelFilter::Info)
.chain(std::io::stdout());
if let Some(log_file) = CONFIG.log_file.as_ref() {
logger = logger.chain(fern::log_file(log_file)?);
@@ -94,7 +106,9 @@ fn init_logging() -> Result<(), fern::InitError> {
}
#[cfg(not(feature = "enable_syslog"))]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { logger }
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
logger
}
#[cfg(feature = "enable_syslog")]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
@@ -128,44 +142,60 @@ fn check_db() {
// Turn on WAL in SQLite
use diesel::RunQueryDsl;
let connection = db::get_connection().expect("Can't conect to DB");
diesel::sql_query("PRAGMA journal_mode=wal").execute(&connection).expect("Failed to turn on WAL");
diesel::sql_query("PRAGMA journal_mode=wal")
.execute(&connection)
.expect("Failed to turn on WAL");
}
fn check_rsa_keys() {
// If the RSA keys don't exist, try to create them
if !util::file_exists(&CONFIG.private_rsa_key)
|| !util::file_exists(&CONFIG.public_rsa_key) {
if !util::file_exists(&CONFIG.private_rsa_key) || !util::file_exists(&CONFIG.public_rsa_key) {
info!("JWT keys don't exist, checking if OpenSSL is available...");
Command::new("openssl")
.arg("version")
.output().unwrap_or_else(|_| {
Command::new("openssl").arg("version").output().unwrap_or_else(|_| {
info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
exit(1);
});
info!("OpenSSL detected, creating keys...");
let mut success = Command::new("openssl").arg("genrsa")
.arg("-out").arg(&CONFIG.private_rsa_key_pem)
.output().expect("Failed to create private pem file")
.status.success();
let mut success = Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(&CONFIG.private_rsa_key_pem)
.output()
.expect("Failed to create private pem file")
.status
.success();
success &= Command::new("openssl").arg("rsa")
.arg("-in").arg(&CONFIG.private_rsa_key_pem)
.arg("-outform").arg("DER")
.arg("-out").arg(&CONFIG.private_rsa_key)
.output().expect("Failed to create private der file")
.status.success();
success &= Command::new("openssl")
.arg("rsa")
.arg("-in")
.arg(&CONFIG.private_rsa_key_pem)
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(&CONFIG.private_rsa_key)
.output()
.expect("Failed to create private der file")
.status
.success();
success &= Command::new("openssl").arg("rsa")
.arg("-in").arg(&CONFIG.private_rsa_key)
.arg("-inform").arg("DER")
success &= Command::new("openssl")
.arg("rsa")
.arg("-in")
.arg(&CONFIG.private_rsa_key)
.arg("-inform")
.arg("DER")
.arg("-RSAPublicKey_out")
.arg("-outform").arg("DER")
.arg("-out").arg(&CONFIG.public_rsa_key)
.output().expect("Failed to create public der file")
.status.success();
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(&CONFIG.public_rsa_key)
.output()
.expect("Failed to create public der file")
.status
.success();
if success {
info!("Keys created correctly.");
@@ -220,13 +250,7 @@ impl MailConfig {
});
let smtp_ssl = get_env_or("SMTP_SSL", true);
let smtp_port = get_env("SMTP_PORT").unwrap_or_else(||
if smtp_ssl {
587u16
} else {
25u16
}
);
let smtp_port = get_env("SMTP_PORT").unwrap_or_else(|| if smtp_ssl { 587u16 } else { 25u16 });
let smtp_username = get_env("SMTP_USERNAME");
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
@@ -255,6 +279,9 @@ pub struct Config {
icon_cache_folder: String,
attachments_folder: String,
icon_cache_ttl: u64,
icon_cache_negttl: u64,
private_rsa_key: String,
private_rsa_key_pem: String,
public_rsa_key: String,
@@ -271,7 +298,7 @@ pub struct Config {
local_icon_extractor: bool,
signups_allowed: bool,
invitations_allowed: bool,
server_admin_email: Option<String>,
admin_token: Option<String>,
password_iterations: i32,
show_password_hint: bool,
@@ -304,6 +331,11 @@ impl Config {
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
attachments_folder: get_env_or("ATTACHMENTS_FOLDER", format!("{}/{}", &df, "attachments")),
// icon_cache_ttl defaults to 30 days (30 * 24 * 60 * 60 seconds)
icon_cache_ttl: get_env_or("ICON_CACHE_TTL", 2_592_000),
// icon_cache_negttl defaults to 3 days (3 * 24 * 60 * 60 seconds)
icon_cache_negttl: get_env_or("ICON_CACHE_NEGTTL", 259_200),
private_rsa_key: format!("{}.der", &key),
private_rsa_key_pem: format!("{}.pem", &key),
public_rsa_key: format!("{}.pub.der", &key),
@@ -312,14 +344,18 @@ impl Config {
web_vault_enabled: get_env_or("WEB_VAULT_ENABLED", true),
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
websocket_url: format!("{}:{}", get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()), get_env_or("WEBSOCKET_PORT", 3012)),
websocket_url: format!(
"{}:{}",
get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()),
get_env_or("WEBSOCKET_PORT", 3012)
),
extended_logging: get_env_or("EXTENDED_LOGGING", true),
log_file: get_env("LOG_FILE"),
local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false),
signups_allowed: get_env_or("SIGNUPS_ALLOWED", true),
server_admin_email: get_env("SERVER_ADMIN_EMAIL"),
admin_token: get_env("ADMIN_TOKEN"),
invitations_allowed: get_env_or("INVITATIONS_ALLOWED", true),
password_iterations: get_env_or("PASSWORD_ITERATIONS", 100_000),
show_password_hint: get_env_or("SHOW_PASSWORD_HINT", true),

195
src/static/admin.html Normal file
View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Bitwarden_rs Admin Panel</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.10.0/js/md5.js" integrity="sha256-tCQ/BldMlN2vWe5gAiNoNb5svoOgVUhlUgv7UjONKKQ="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js" integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA="
crossorigin="anonymous"></script>
<style>
body { padding-top: 70px; }
img { width: 48px; height: 48px; }
</style>
<script>
let key = null;
function identicon(email) {
const data = new Identicon(md5(email), {
size: 48, format: 'svg'
}).toString();
return "data:image/svg+xml;base64," + data;
}
function setVis(elem, vis) {
if (vis) { $(elem).removeClass('d-none'); }
else { $(elem).addClass('d-none'); }
}
function updateVis() {
setVis("#no-key-form", !key);
setVis("#users-block", key);
setVis("#invite-form-block", key);
}
function setKey() {
key = $('#key').val() || window.location.hash.slice(1);
updateVis();
if (key) { loadUsers(); }
return false;
}
function resetKey() {
key = null;
updateVis();
}
function fillRow(data) {
for (i in data) {
const user = data[i];
const row = $("#tmp-row").clone();
row.attr("id", "user-row:" + user.Id);
row.find(".tmp-name").text(user.Name);
row.find(".tmp-mail").text(user.Email);
row.find(".tmp-icon").attr("src", identicon(user.Email))
row.find(".tmp-del").on("click", function (e) {
var name = prompt("To delete user '" + user.Name + "', please type the name below")
if (name) {
if (name == user.Name) {
deleteUser(user.Id);
} else {
alert("Wrong name, please try again")
}
}
return false;
});
row.appendTo("#users-list");
setVis(row, true);
}
}
function _headers() { return { "Authorization": "Bearer " + key }; }
function loadUsers() {
$("#users-list").empty();
$.get({ url: "/admin/users", headers: _headers() })
.done(fillRow).fail(resetKey);
return false;
}
function _post(url, successMsg, errMsg, resetOnErr, data) {
$.post({ url: url, headers: _headers(), data: data })
.done(function () {
alert(successMsg);
loadUsers();
}).fail(function (e) {
const r = e.responseJSON;
const msg = r ? r.ErrorModel.Message : "Unknown error";
alert(errMsg + ": " + msg);
if (resetOnErr) { resetKey(); }
});
}
function deleteUser(id) {
_post("/admin/users/" + id + "/delete",
"User deleted correctly",
"Error deleting user", true);
}
function inviteUser() {
inv = $("#email-invite");
data = JSON.stringify({ "Email": inv.val() });
inv.val("");
_post("/admin/invite/", "User invited correctly",
"Error inviting user", false, data);
return false;
}
$(window).on('load', function () {
setKey();
$("#key-form").submit(setKey);
$("#reload-btn").click(loadUsers);
$("#invite-form").submit(inviteUser);
});
</script>
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top shadow">
<a class="navbar-brand" href="#">Bitwarden_rs</a>
<div class="navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="/admin">Admin Panel</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/">Vault</a>
</li>
</ul>
</div>
</nav>
<main class="container">
<div id="no-key-form" class="d-none align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow">
<div>
<h6 class="mb-0 text-white">Authentication key needed to continue</h6>
<small>Please provide it below:</small>
<form class="form-inline" id="key-form">
<input type="password" class="form-control w-50 mr-2" id="key" placeholder="Enter admin key">
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>
<div id="users-block" class="d-none my-3 p-3 bg-white rounded shadow">
<h6 class="border-bottom pb-2 mb-0">Registered Users</h6>
<div id="users-list"></div>
<small class="d-block text-right mt-3">
<a id="reload-btn" href="#">Reload users</a>
</small>
</div>
<div id="invite-form-block" class="d-none align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
<div>
<h6 class="mb-0 text-white">Invite User</h6>
<small>Email:</small>
<form class="form-inline" id="invite-form">
<input type="email" class="form-control w-50 mr-2" id="email-invite" placeholder="Enter email">
<button type="submit" class="btn btn-primary">Invite</button>
</form>
</div>
</div>
<div id="tmp-row" class="d-none media pt-3">
<img class="mr-2 rounded tmp-icon">
<div class="media-body pb-3 mb-0 small border-bottom">
<div class="d-flex justify-content-between">
<strong class="tmp-name">Full Name</strong>
<a class="tmp-del mr-3" href="#">Delete User</a>
</div>
<span class="d-block tmp-mail">Email</span>
</div>
</div>
</main>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

View File

@@ -1,76 +1,56 @@
///
/// Macros
///
#[macro_export]
macro_rules! _err_object {
($msg:expr) => {{
err_json!(json!({
"Message": "",
"error": "",
"error_description": "",
"ValidationErrors": {"": [ $msg ]},
"ErrorModel": {
"Message": $msg,
"Object": "error"
},
"Object": "error"
}))
}};
//
// Web Headers
//
use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Request, Response};
pub struct AppHeaders();
impl Fairing for AppHeaders {
fn info(&self) -> Info {
Info {
name: "Application Headers",
kind: Kind::Response,
}
}
fn on_response(&self, _req: &Request, res: &mut Response) {
res.set_raw_header("Referrer-Policy", "same-origin");
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-XSS-Protection", "1; mode=block");
let csp = "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://*;";
res.set_raw_header("Content-Security-Policy", csp);
// Disable cache unless otherwise specified
if !res.headers().contains("cache-control") {
res.set_raw_header("Cache-Control", "no-cache, no-store, max-age=0");
}
}
}
#[macro_export]
macro_rules! err {
($msg:expr) => {{
error!("{}", $msg);
_err_object!($msg)
}};
($usr_msg:expr, $log_value:expr) => {{
error!("{}: {:#?}", $usr_msg, $log_value);
_err_object!($usr_msg)
}}
}
#[macro_export]
macro_rules! err_json {
($expr:expr) => {{
return Err(rocket::response::status::BadRequest(Some(rocket_contrib::json::Json($expr))));
}}
}
#[macro_export]
macro_rules! err_handler {
($expr:expr) => {{
error!("{}", $expr);
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
}}
}
///
/// File handling
///
use std::path::Path;
use std::io::Read;
//
// File handling
//
use std::fs::{self, File};
use std::io::{Read, Result as IOResult};
use std::path::Path;
pub fn file_exists(path: &str) -> bool {
Path::new(path).exists()
}
pub fn read_file(path: &str) -> Result<Vec<u8>, String> {
let mut file = File::open(Path::new(path))
.map_err(|e| format!("Error opening file: {}", e))?;
pub fn read_file(path: &str) -> IOResult<Vec<u8>> {
let mut contents: Vec<u8> = Vec::new();
file.read_to_end(&mut contents)
.map_err(|e| format!("Error reading file: {}", e))?;
let mut file = File::open(Path::new(path))?;
file.read_to_end(&mut contents)?;
Ok(contents)
}
pub fn delete_file(path: &str) -> bool {
let res = fs::remove_file(path).is_ok();
pub fn delete_file(path: &str) -> IOResult<()> {
let res = fs::remove_file(path);
if let Some(parent) = Path::new(path).parent() {
// If the directory isn't empty, this returns an error, which we ignore
@@ -81,7 +61,6 @@ pub fn delete_file(path: &str) -> bool {
res
}
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
pub fn get_display_size(size: i32) -> String {
@@ -95,7 +74,7 @@ pub fn get_display_size(size: i32) -> String {
} else {
break;
}
};
}
// Round to two decimals
size = (size * 100.).round() / 100.;
@@ -106,13 +85,12 @@ pub fn get_uuid() -> String {
uuid::Uuid::new_v4().to_string()
}
//
// String util methods
//
///
/// String util methods
///
use std::str::FromStr;
use std::ops::Try;
use std::str::FromStr;
pub fn upcase_first(s: &str) -> String {
let mut c = s.chars();
@@ -122,7 +100,11 @@ pub fn upcase_first(s: &str) -> String {
}
}
pub fn try_parse_string<S, T, U>(string: impl Try<Ok = S, Error=U>) -> Option<T> where S: AsRef<str>, T: FromStr {
pub fn try_parse_string<S, T, U>(string: impl Try<Ok = S, Error = U>) -> Option<T>
where
S: AsRef<str>,
T: FromStr,
{
if let Ok(Ok(value)) = string.into_result().map(|s| s.as_ref().parse::<T>()) {
Some(value)
} else {
@@ -130,7 +112,11 @@ pub fn try_parse_string<S, T, U>(string: impl Try<Ok = S, Error=U>) -> Option<T>
}
}
pub fn try_parse_string_or<S, T, U>(string: impl Try<Ok = S, Error=U>, default: T) -> T where S: AsRef<str>, T: FromStr {
pub fn try_parse_string_or<S, T, U>(string: impl Try<Ok = S, Error = U>, default: T) -> T
where
S: AsRef<str>,
T: FromStr,
{
if let Ok(Ok(value)) = string.into_result().map(|s| s.as_ref().parse::<T>()) {
value
} else {
@@ -138,24 +124,29 @@ pub fn try_parse_string_or<S, T, U>(string: impl Try<Ok = S, Error=U>, default:
}
}
///
/// Env methods
///
//
// Env methods
//
use std::env;
pub fn get_env<V>(key: &str) -> Option<V> where V: FromStr {
pub fn get_env<V>(key: &str) -> Option<V>
where
V: FromStr,
{
try_parse_string(env::var(key))
}
pub fn get_env_or<V>(key: &str, default: V) -> V where V: FromStr {
pub fn get_env_or<V>(key: &str, default: V) -> V
where
V: FromStr,
{
try_parse_string_or(env::var(key), default)
}
///
/// Date util methods
///
//
// Date util methods
//
use chrono::NaiveDateTime;
@@ -165,9 +156,9 @@ pub fn format_date(date: &NaiveDateTime) -> String {
date.format(DATETIME_FORMAT).to_string()
}
///
/// Deserialization methods
///
//
// Deserialization methods
//
use std::fmt;
@@ -183,10 +174,11 @@ pub struct UpCase<T: DeserializeOwned> {
pub data: T,
}
/// https://github.com/serde-rs/serde/issues/586
// https://github.com/serde-rs/serde/issues/586
pub fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where T: DeserializeOwned,
D: Deserializer<'de>
where
T: DeserializeOwned,
D: Deserializer<'de>,
{
let d = deserializer.deserialize_any(UpCaseVisitor)?;
T::deserialize(d).map_err(de::Error::custom)
@@ -202,7 +194,8 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where A: MapAccess<'de>
where
A: MapAccess<'de>,
{
let mut result_map = JsonMap::new();
@@ -214,7 +207,9 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where A: SeqAccess<'de> {
where
A: SeqAccess<'de>,
{
let mut result_seq = Vec::<Value>::new();
while let Some(value) = seq.next_element()? {
@@ -228,13 +223,12 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
fn upcase_value(value: &Value) -> Value {
if let Some(map) = value.as_object() {
let mut new_value = json!({});
for (key, val) in map {
let processed_key = _process_key(key);
new_value[processed_key] = upcase_value(val);
}
new_value
} else if let Some(array) = value.as_array() {
// Initialize array with null values
let mut new_value = json!(vec![Value::Null; array.len()]);
@@ -243,7 +237,6 @@ fn upcase_value(value: &Value) -> Value {
new_value[index] = upcase_value(val);
}
new_value
} else {
value.clone()
}