mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
Compare commits
93 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dd00591082 | ||
|
1e9dd2fd4e | ||
|
62bc58e145 | ||
|
760e0ab805 | ||
|
8f5bfe7938 | ||
|
b359df7045 | ||
|
f1b1000600 | ||
|
c0e248c457 | ||
|
f510a1b060 | ||
|
fafc3883c6 | ||
|
1bdb98d139 | ||
|
2f5ca88fb1 | ||
|
e7a24159c5 | ||
|
e056cc8178 | ||
|
8ce4c79612 | ||
|
77d9641323 | ||
|
31e4237247 | ||
|
c32c65d367 | ||
|
0a4dbaf307 | ||
|
daa66b08dc | ||
|
d613fa1e68 | ||
|
55fbd8d468 | ||
|
adf40291e8 | ||
|
acfc900997 | ||
|
0a08b1afc8 | ||
|
eb48a3fac2 | ||
|
2e7fa6440b | ||
|
9ecc98c3cc | ||
|
02fd68d63b | ||
|
235bce1ecb | ||
|
e985221b50 | ||
|
77cf63c06d | ||
|
faec050a6d | ||
|
22304f4925 | ||
|
58a78ffa54 | ||
|
64f6c60bfd | ||
|
e0614620ef | ||
|
a28caa33ef | ||
|
ce4fedf191 | ||
|
f2078a3849 | ||
|
5292d38c73 | ||
|
1049646e27 | ||
|
380cf06211 | ||
|
1f35ef2865 | ||
|
c29bc9309a | ||
|
7112c86471 | ||
|
2aabf14372 | ||
|
77ff9c91c5 | ||
|
d9457e929c | ||
|
86b49856a7 | ||
|
54f54ee845 | ||
|
015bd28cc2 | ||
|
990c83a037 | ||
|
c3c74506a7 | ||
|
fb4e6bab14 | ||
|
fe38f95f15 | ||
|
9eaa9c1a17 | ||
|
8ee681c4a3 | ||
|
08aee97c1d | ||
|
2bb6482bec | ||
|
c169095128 | ||
|
b1397c95ca | ||
|
3df31e3464 | ||
|
638a0fd3c3 | ||
|
ebb66c374e | ||
|
89e3c41043 | ||
|
3da410ef71 | ||
|
2dccbd3412 | ||
|
2ff529ed99 | ||
|
4fae1e4298 | ||
|
f7951b44ba | ||
|
ff8eeff995 | ||
|
00019dc356 | ||
|
404fe5321e | ||
|
e7dd239d20 | ||
|
071f3370e3 | ||
|
ee321be579 | ||
|
eb61425da5 | ||
|
b75ba216d1 | ||
|
8651df8c2a | ||
|
948554a20f | ||
|
9cdb605659 | ||
|
928e2424c0 | ||
|
a01fee0b9f | ||
|
67adfee5e5 | ||
|
d66d4fd87f | ||
|
434551e012 | ||
|
69dcbdd3b2 | ||
|
422f7ccfa8 | ||
|
f8ae5013cb | ||
|
d8e5e53273 | ||
|
b6502e9e9d | ||
|
d70864ac73 |
8
.env
8
.env
@@ -14,6 +14,10 @@
|
||||
# WEB_VAULT_FOLDER=web-vault/
|
||||
# WEB_VAULT_ENABLED=true
|
||||
|
||||
## Controls the WebSocket server address and port
|
||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||
# WEBSOCKET_PORT=3012
|
||||
|
||||
## Controls if new users can register
|
||||
# SIGNUPS_ALLOWED=true
|
||||
|
||||
@@ -42,8 +46,10 @@
|
||||
# ROCKET_PORT=8000
|
||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
||||
|
||||
## Mail specific settings, if SMTP_HOST is specified, SMTP_USERNAME and SMTP_PASSWORD are mandatory
|
||||
## Mail specific settings, set SMTP_HOST and SMTP_FROM to enable the mail service.
|
||||
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
|
||||
# SMTP_HOST=smtp.domain.tld
|
||||
# SMTP_FROM=bitwarden-rs@domain.tld
|
||||
# SMTP_PORT=587
|
||||
# SMTP_SSL=true
|
||||
# SMTP_USERNAME=username
|
||||
|
1219
Cargo.lock
generated
1219
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@@ -5,23 +5,32 @@ authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||
|
||||
[dependencies]
|
||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||
rocket = { version = "0.3.16", features = ["tls"] }
|
||||
rocket_codegen = "0.3.16"
|
||||
rocket_contrib = "0.3.16"
|
||||
rocket = { version = "0.3.17", features = ["tls"] }
|
||||
rocket_codegen = "0.3.17"
|
||||
rocket_contrib = "0.3.17"
|
||||
|
||||
# HTTP client
|
||||
reqwest = "0.8.8"
|
||||
reqwest = "0.9.2"
|
||||
|
||||
# multipart/form-data support
|
||||
multipart = "0.15.2"
|
||||
multipart = "0.15.3"
|
||||
|
||||
# WebSockets library
|
||||
ws = "0.7.8"
|
||||
|
||||
# MessagePack library
|
||||
rmpv = "0.4.0"
|
||||
|
||||
# Concurrent hashmap implementation
|
||||
chashmap = "2.2.0"
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = "1.0.74"
|
||||
serde_derive = "1.0.74"
|
||||
serde_json = "1.0.26"
|
||||
serde = "1.0.79"
|
||||
serde_derive = "1.0.79"
|
||||
serde_json = "1.0.31"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "1.3.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel = { version = "1.3.3", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
||||
|
||||
# Bundled SQLite
|
||||
@@ -31,10 +40,10 @@ libsqlite3-sys = { version = "0.9.3", features = ["bundled"] }
|
||||
ring = { version = "= 0.11.0", features = ["rsa_signing"] }
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "0.6.5", features = ["v4"] }
|
||||
uuid = { version = "0.7.1", features = ["v4"] }
|
||||
|
||||
# Date and time library for Rust
|
||||
chrono = "0.4.5"
|
||||
chrono = "0.4.6"
|
||||
|
||||
# TOTP library
|
||||
oath = "0.10.2"
|
||||
@@ -55,17 +64,23 @@ dotenv = { version = "0.13.0", default-features = false }
|
||||
lazy_static = "1.1.0"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.5"
|
||||
num-derive = "0.2.2"
|
||||
num-traits = "0.2.6"
|
||||
num-derive = "0.2.3"
|
||||
|
||||
lettre = "0.8.2"
|
||||
lettre_email = "0.8.2"
|
||||
native-tls = "0.1.5"
|
||||
fast_chemail = "0.9.5"
|
||||
# Email libraries
|
||||
lettre = "0.9.0"
|
||||
lettre_email = "0.9.0"
|
||||
native-tls = "0.2.1"
|
||||
|
||||
# Number encoding library
|
||||
byteorder = "1.2.6"
|
||||
|
||||
[patch.crates-io]
|
||||
# Make jwt use ring 0.11, to match rocket
|
||||
jsonwebtoken = { path = "libs/jsonwebtoken" }
|
||||
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 = '193de35093a44' }
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM node:8-alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.2.0"
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
|
||||
@@ -76,6 +76,7 @@ RUN apt-get update && apt-get install -y\
|
||||
RUN mkdir /data
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
EXPOSE 3012
|
||||
|
||||
# Copies the files from the context (env file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
|
112
Dockerfile.aarch64
Normal file
112
Dockerfile.aarch64
Normal file
@@ -0,0 +1,112 @@
|
||||
# Using multistage build:
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM node:8-alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
git \
|
||||
tar
|
||||
|
||||
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
|
||||
WORKDIR /web-build
|
||||
|
||||
COPY /docker/set-vault-baseurl.patch /web-build/
|
||||
RUN git apply set-vault-baseurl.patch
|
||||
|
||||
RUN npm run sub:init && npm install
|
||||
|
||||
RUN npm run dist \
|
||||
&& mv build /web-vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rust as build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
gcc-aarch64-linux-gnu \
|
||||
&& mkdir -p ~/.cargo \
|
||||
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
|
||||
&& echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config
|
||||
|
||||
ENV CARGO_HOME "/root/.cargo"
|
||||
ENV USER "root"
|
||||
|
||||
# Creates a dummy project used to grab dependencies
|
||||
RUN USER=root cargo new --bin app
|
||||
WORKDIR /app
|
||||
|
||||
# Copies over *only* your manifests and vendored dependencies
|
||||
COPY ./Cargo.* ./
|
||||
COPY ./libs ./libs
|
||||
COPY ./rust-toolchain ./rust-toolchain
|
||||
|
||||
# Prepare openssl arm64 libs
|
||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||
/etc/apt/sources.list.d/deb-src.list \
|
||||
&& dpkg --add-architecture arm64 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
libssl-dev:arm64 \
|
||||
libc6-dev:arm64
|
||||
|
||||
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
||||
ENV CROSS_COMPILE="1"
|
||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
|
||||
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
||||
|
||||
# Builds your dependencies and removes the
|
||||
# dummy project, except the target folder
|
||||
# This folder contains the compiled dependencies
|
||||
RUN rustup target add aarch64-unknown-linux-gnu
|
||||
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
|
||||
RUN find . -not -path "./target*" -delete
|
||||
|
||||
# Copies the complete project
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
# Builds again, this time it'll just be
|
||||
# your actual source files being built
|
||||
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/aarch64-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
RUN [ "cross-build-end" ]
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
|
||||
# Copies the files from the context (env 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 .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM node:8-alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.2.0"
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
|
||||
@@ -68,6 +68,7 @@ RUN apk add \
|
||||
RUN mkdir /data
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
EXPOSE 3012
|
||||
|
||||
# Copies the files from the context (env file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM node:8-alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.2.0"
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
|
||||
@@ -65,7 +65,6 @@ ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
||||
# Builds your dependencies and removes the
|
||||
# dummy project, except the target folder
|
||||
# This folder contains the compiled dependencies
|
||||
COPY . .
|
||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
||||
RUN find . -not -path "./target*" -delete
|
||||
@@ -81,7 +80,7 @@ RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM resin/armv7hf-debian:stretch
|
||||
FROM balenalib/armv7hf-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
94
PROXY.md
Normal file
94
PROXY.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 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'
|
||||
```
|
85
README.md
85
README.md
@@ -24,7 +24,9 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
|
||||
- [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)
|
||||
- [Changing persistent data location](#changing-persistent-data-location)
|
||||
- [/data prefix:](#data-prefix)
|
||||
@@ -41,6 +43,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
|
||||
- [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)
|
||||
@@ -139,7 +142,7 @@ docker run -d --name bitwarden \
|
||||
-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 bellow if you also want to disable that.
|
||||
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
|
||||
|
||||
@@ -153,6 +156,23 @@ docker run -d --name bitwarden \
|
||||
-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`.
|
||||
@@ -175,6 +195,47 @@ docker run -d --name bitwarden \
|
||||
```
|
||||
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.
|
||||
@@ -270,7 +331,7 @@ docker run -d --name bitwarden \
|
||||
|
||||
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 bellow, we're starting with 20 workers:
|
||||
In the example below, we're starting with 20 workers:
|
||||
|
||||
```sh
|
||||
docker run -d --name bitwarden \
|
||||
@@ -287,6 +348,7 @@ 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> \
|
||||
@@ -348,7 +410,7 @@ docker build -t bitwarden_rs .
|
||||
|
||||
## Building binary
|
||||
|
||||
For building binary outside the Docker environment and running it locally without docker, please see [build instructions](BUILD.md).
|
||||
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
|
||||
|
||||
@@ -356,6 +418,11 @@ For building binary outside the Docker environment and running it locally withou
|
||||
|
||||
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
|
||||
@@ -363,10 +430,10 @@ Bitwarden_rs is already packaged for Archlinux thanks to @mqus. There is an [AUR
|
||||
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.
|
||||
|
||||
```
|
||||
sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sq3'"
|
||||
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.sq3 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.
|
||||
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).
|
||||
|
||||
### 2. the attachments folder
|
||||
|
||||
@@ -385,8 +452,8 @@ This is optional, the icon cache can re-download itself however if you have a la
|
||||
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 user name, 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 bellow `1024`. To overcome this, you need to configure server to listen on a different port, you can use `ROCKET_PORT` to do that.
|
||||
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`:
|
||||
|
||||
@@ -411,7 +478,9 @@ We use upstream Vault interface directly without any (significant) changes, this
|
||||
|
||||
### Inviting users into organization
|
||||
|
||||
If you have [invitations disabled](#disable-invitations), the users must already be registered on your server to invite them. The invited users won't get the invitation email, instead they will appear in the interface as if they already accepted the invitation. (if the user has already registered) Organization admin then just needs to confirm them to be proper Organization members and to give them access to the shared secrets.
|
||||
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
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
--- a/src/app/services/services.module.ts
|
||||
+++ b/src/app/services/services.module.ts
|
||||
@@ -120,20 +120,17 @@ const notificationsService = new NotificationsService(userService, syncService,
|
||||
@@ -120,20 +120,16 @@ const notificationsService = new NotificationsService(userService, syncService,
|
||||
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
|
||||
const auditService = new AuditService(cryptoFunctionService, apiService);
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
+ const isDev = false;
|
||||
+ environmentService.baseUrl = window.location.origin;
|
||||
+ environmentService.notificationsUrl = window.location.origin + '/notifications';
|
||||
+
|
||||
await apiService.setUrls({
|
||||
apiService.setUrls({
|
||||
base: isDev ? null : window.location.origin,
|
||||
api: isDev ? 'http://localhost:4000' : null,
|
||||
|
7
migrations/2018-09-19-144557_add_kdf_columns/up.sql
Normal file
7
migrations/2018-09-19-144557_add_kdf_columns/up.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
client_kdf_type INTEGER NOT NULL DEFAULT 0; -- PBKDF2
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
client_kdf_iter INTEGER NOT NULL DEFAULT 5000;
|
@@ -1 +1 @@
|
||||
nightly-2018-08-24
|
||||
nightly-2018-10-03
|
||||
|
@@ -5,7 +5,6 @@ use db::models::*;
|
||||
|
||||
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
|
||||
use auth::Headers;
|
||||
use fast_chemail::is_valid_email;
|
||||
use mail;
|
||||
|
||||
use CONFIG;
|
||||
@@ -14,6 +13,8 @@ use CONFIG;
|
||||
#[allow(non_snake_case)]
|
||||
struct RegisterData {
|
||||
Email: String,
|
||||
Kdf: Option<i32>,
|
||||
KdfIterations: Option<i32>,
|
||||
Key: String,
|
||||
Keys: Option<KeysData>,
|
||||
MasterPasswordHash: String,
|
||||
@@ -38,16 +39,16 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
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;
|
||||
user_org.save(&conn);
|
||||
if user_org.save(&conn).is_err() {
|
||||
err!("Failed to accept user to organization")
|
||||
}
|
||||
};
|
||||
user
|
||||
} else {
|
||||
if CONFIG.signups_allowed {
|
||||
} else if CONFIG.signups_allowed {
|
||||
err!("Account with this email already exists")
|
||||
} else {
|
||||
err!("Registration not allowed")
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
|
||||
@@ -58,6 +59,14 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(client_kdf_iter) = data.KdfIterations {
|
||||
user.client_kdf_iter = client_kdf_iter;
|
||||
}
|
||||
|
||||
if let Some(client_kdf_type) = data.Kdf {
|
||||
user.client_kdf_type = client_kdf_type;
|
||||
}
|
||||
|
||||
user.set_password(&data.MasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
|
||||
@@ -75,9 +84,10 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
user.public_key = Some(keys.PublicKey);
|
||||
}
|
||||
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save user")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/accounts/profile")]
|
||||
@@ -110,9 +120,10 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
|
||||
Some(ref h) if h.is_empty() => None,
|
||||
_ => data.MasterPasswordHint,
|
||||
};
|
||||
user.save(&conn);
|
||||
|
||||
Ok(Json(user.to_json(&conn)))
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(user.to_json(&conn))),
|
||||
Err(_) => err!("Failed to save user profile")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>/public-key")]
|
||||
@@ -138,10 +149,13 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
||||
user.private_key = Some(data.EncryptedPrivateKey);
|
||||
user.public_key = Some(data.PublicKey);
|
||||
|
||||
user.save(&conn);
|
||||
|
||||
Ok(Json(user.to_json(&conn)))
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(user.to_json(&conn))),
|
||||
Err(_) => err!("Failed to save the user's keys")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
@@ -162,9 +176,40 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
||||
|
||||
user.set_password(&data.NewMasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
user.save(&conn);
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save password")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ChangeKdfData {
|
||||
Kdf: i32,
|
||||
KdfIterations: i32,
|
||||
|
||||
MasterPasswordHash: String,
|
||||
NewMasterPasswordHash: String,
|
||||
Key: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/kdf", data = "<data>")]
|
||||
fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: ChangeKdfData = data.into_inner().data;
|
||||
let mut user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
user.client_kdf_iter = data.KdfIterations;
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/accounts/security-stamp", data = "<data>")]
|
||||
@@ -177,9 +222,10 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
|
||||
}
|
||||
|
||||
user.reset_security_stamp();
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to reset security stamp")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -234,12 +280,18 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
||||
user.set_password(&data.NewMasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save email address")
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/accounts/delete", data = "<data>")]
|
||||
fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_account(data, headers, conn)
|
||||
}
|
||||
|
||||
#[delete("/accounts", data = "<data>")]
|
||||
fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
let user = headers.user;
|
||||
@@ -248,29 +300,12 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
// 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")
|
||||
match user.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete folders
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
if f.delete(&conn).is_err() {
|
||||
err!("Failed deleting folder")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete devices
|
||||
for d in Device::find_by_user(&user.uuid, &conn) { d.delete(&conn); }
|
||||
|
||||
// Delete user
|
||||
user.delete(&conn);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/accounts/revision-date")]
|
||||
fn revision_date(headers: Headers) -> String {
|
||||
let revision_date = headers.user.updated_at.timestamp_millis();
|
||||
@@ -287,25 +322,20 @@ struct PasswordHintData {
|
||||
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordHintData = data.into_inner().data;
|
||||
|
||||
if !is_valid_email(&data.Email) {
|
||||
err!("This email address is not valid...");
|
||||
}
|
||||
let hint = match User::find_by_mail(&data.Email, &conn) {
|
||||
Some(user) => user.password_hint,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let user = User::find_by_mail(&data.Email, &conn);
|
||||
if user.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
if let Some(ref mail_config) = CONFIG.mail {
|
||||
if let Err(e) = mail::send_password_hint(&user.email, user.password_hint, mail_config) {
|
||||
if let Err(e) = mail::send_password_hint(&data.Email, hint, mail_config) {
|
||||
err!(format!("There have been a problem sending the email: {}", e));
|
||||
}
|
||||
} else if CONFIG.show_password_hint {
|
||||
if let Some(hint) = user.password_hint {
|
||||
if let Some(hint) = hint {
|
||||
err!(format!("Your password hint is: {}", &hint));
|
||||
} else {
|
||||
err!(format!("Sorry, you have no password hint..."));
|
||||
err!("Sorry, you have no password hint...");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,20 +352,13 @@ struct PreloginData {
|
||||
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
||||
let data: PreloginData = data.into_inner().data;
|
||||
|
||||
match User::find_by_mail(&data.Email, &conn) {
|
||||
Some(user) => {
|
||||
let kdf_type = 0; // PBKDF2: 0
|
||||
|
||||
let _server_iter = user.password_iterations;
|
||||
let client_iter = 5000; // TODO: Make iterations user configurable
|
||||
|
||||
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
||||
Some(user) => (user.client_kdf_type, user.client_kdf_iter),
|
||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"Kdf": kdf_type,
|
||||
"KdfIterations": client_iter
|
||||
"KdfIterations": kdf_iter
|
||||
})))
|
||||
},
|
||||
None => err!("Invalid user"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use rocket::State;
|
||||
use rocket::Data;
|
||||
use rocket::http::ContentType;
|
||||
|
||||
@@ -16,13 +17,19 @@ use db::models::*;
|
||||
|
||||
use crypto;
|
||||
|
||||
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase};
|
||||
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use auth::Headers;
|
||||
|
||||
use CONFIG;
|
||||
|
||||
#[get("/sync")]
|
||||
fn sync(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
#[derive(FromForm)]
|
||||
#[allow(non_snake_case)]
|
||||
struct SyncData {
|
||||
excludeDomains: bool,
|
||||
}
|
||||
|
||||
#[get("/sync?<data>")]
|
||||
fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let user_json = headers.user.to_json(&conn);
|
||||
|
||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||
@@ -34,7 +41,7 @@ fn sync(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 domains_json = api::core::get_eq_domains(headers).unwrap().into_inner();
|
||||
let domains_json = if data.excludeDomains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
|
||||
|
||||
Ok(Json(json!({
|
||||
"Profile": user_json,
|
||||
@@ -46,6 +53,13 @@ fn sync(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/sync")]
|
||||
fn sync_no_query(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let sync_data = SyncData {
|
||||
excludeDomains: false,
|
||||
};
|
||||
sync(sync_data, headers, conn)
|
||||
}
|
||||
|
||||
#[get("/ciphers")]
|
||||
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
@@ -56,6 +70,7 @@ fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
Ok(Json(json!({
|
||||
"Data": ciphers_json,
|
||||
"Object": "list",
|
||||
"ContinuationToken": null
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -117,22 +132,22 @@ pub struct CipherData {
|
||||
}
|
||||
|
||||
#[post("/ciphers/admin", data = "<data>")]
|
||||
fn post_ciphers_admin(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn post_ciphers_admin(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
// TODO: Implement this correctly
|
||||
post_ciphers(data, headers, conn)
|
||||
post_ciphers(data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers", data = "<data>")]
|
||||
fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> 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)?;
|
||||
update_cipher_from_data(&mut cipher, data, &headers, false, &conn, &ws, UpdateType::SyncCipherCreate)?;
|
||||
|
||||
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) -> EmptyResult {
|
||||
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 {
|
||||
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"),
|
||||
@@ -189,7 +204,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());
|
||||
|
||||
cipher.save(&conn);
|
||||
match cipher.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => println!("Error: Failed to save cipher")
|
||||
};
|
||||
ws.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
|
||||
|
||||
if cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn).is_err() {
|
||||
err!("Error saving the folder information")
|
||||
@@ -219,15 +238,19 @@ struct RelationsData {
|
||||
|
||||
|
||||
#[post("/ciphers/import", data = "<data>")]
|
||||
fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data: ImportData = data.into_inner().data;
|
||||
|
||||
// Read and create the folders
|
||||
let folders: Vec<_> = data.Folders.into_iter().map(|folder| {
|
||||
let mut folder = Folder::new(headers.user.uuid.clone(), folder.Name);
|
||||
folder.save(&conn);
|
||||
folder
|
||||
}).collect();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the relations between folders and ciphers
|
||||
use std::collections::HashMap;
|
||||
@@ -243,7 +266,7 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
|
||||
.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)?;
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherCreate)?;
|
||||
|
||||
cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn).ok();
|
||||
}
|
||||
@@ -257,22 +280,22 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
|
||||
|
||||
|
||||
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
||||
fn put_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
put_cipher(uuid, data, headers, conn)
|
||||
fn put_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
put_cipher(uuid, data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/admin", data = "<data>")]
|
||||
fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
post_cipher(uuid, data, headers, conn)
|
||||
fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
post_cipher(uuid, data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>", data = "<data>")]
|
||||
fn post_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
put_cipher(uuid, data, headers, conn)
|
||||
fn post_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
put_cipher(uuid, data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>", data = "<data>")]
|
||||
fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
let data: CipherData = data.into_inner().data;
|
||||
|
||||
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||
@@ -284,7 +307,7 @@ fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn
|
||||
err!("Cipher is not write accessible")
|
||||
}
|
||||
|
||||
update_cipher_from_data(&mut cipher, data, &headers, false, &conn)?;
|
||||
update_cipher_from_data(&mut cipher, data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
||||
}
|
||||
@@ -327,9 +350,15 @@ fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>,
|
||||
Some(collection) => {
|
||||
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
||||
if posted_collections.contains(&collection.uuid) { // Add to collection
|
||||
CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn);
|
||||
match CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed to add cipher to collection")
|
||||
};
|
||||
} else { // Remove from collection
|
||||
CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn);
|
||||
match CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed to remove cipher from collection")
|
||||
};
|
||||
}
|
||||
} else {
|
||||
err!("No rights to modify the collection")
|
||||
@@ -349,17 +378,17 @@ struct ShareCipherData {
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/share", data = "<data>")]
|
||||
fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
let data: ShareCipherData = data.into_inner().data;
|
||||
|
||||
share_cipher_by_uuid(&uuid, data, &headers, &conn)
|
||||
share_cipher_by_uuid(&uuid, data, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[put("/ciphers/<uuid>/share", data = "<data>")]
|
||||
fn put_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn put_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
let data: ShareCipherData = data.into_inner().data;
|
||||
|
||||
share_cipher_by_uuid(&uuid, data, &headers, &conn)
|
||||
share_cipher_by_uuid(&uuid, data, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -370,15 +399,15 @@ struct ShareSelectedCipherData {
|
||||
}
|
||||
|
||||
#[put("/ciphers/share", data = "<data>")]
|
||||
fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let mut data: ShareSelectedCipherData = data.into_inner().data;
|
||||
let mut cipher_ids: Vec<String> = Vec::new();
|
||||
|
||||
if data.Ciphers.len() == 0 {
|
||||
if data.Ciphers.is_empty() {
|
||||
err!("You must select at least one cipher.")
|
||||
}
|
||||
|
||||
if data.CollectionIds.len() == 0 {
|
||||
if data.CollectionIds.is_empty() {
|
||||
err!("You must select at least one collection.")
|
||||
}
|
||||
|
||||
@@ -391,7 +420,7 @@ fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers:
|
||||
|
||||
let attachments = Attachment::find_by_ciphers(cipher_ids, &conn);
|
||||
|
||||
if attachments.len() > 0 {
|
||||
if !attachments.is_empty() {
|
||||
err!("Ciphers should not have any attachments.")
|
||||
}
|
||||
|
||||
@@ -402,15 +431,16 @@ fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers:
|
||||
};
|
||||
|
||||
match shared_cipher_data.Cipher.Id.take() {
|
||||
Some(id) => share_cipher_by_uuid(&id, shared_cipher_data , &headers, &conn)?,
|
||||
Some(id) => share_cipher_by_uuid(&id, shared_cipher_data , &headers, &conn, &ws)?,
|
||||
None => err!("Request missing ids field")
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, conn: &DbConn) -> JsonResult {
|
||||
fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> JsonResult {
|
||||
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||
Some(cipher) => {
|
||||
if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
||||
@@ -427,23 +457,22 @@ fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, co
|
||||
Some(organization_uuid) => {
|
||||
let mut shared_to_collection = false;
|
||||
for uuid in &data.CollectionIds {
|
||||
match Collection::find_by_uuid(uuid, &conn) {
|
||||
match Collection::find_by_uuid_and_org(uuid, &organization_uuid, &conn) {
|
||||
None => err!("Invalid collection ID provided"),
|
||||
Some(collection) => {
|
||||
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
||||
if collection.org_uuid == organization_uuid {
|
||||
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn);
|
||||
match CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed to add cipher to collection")
|
||||
};
|
||||
shared_to_collection = true;
|
||||
} else {
|
||||
err!("Collection does not belong to organization")
|
||||
}
|
||||
} else {
|
||||
err!("No rights to modify the collection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
update_cipher_from_data(&mut cipher, data.Cipher, &headers, shared_to_collection, &conn)?;
|
||||
update_cipher_from_data(&mut cipher, data.Cipher, &headers, shared_to_collection, &conn, &ws, UpdateType::SyncCipherUpdate)?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
||||
}
|
||||
@@ -509,53 +538,53 @@ fn post_attachment_admin(uuid: String, data: Data, content_type: &ContentType, h
|
||||
}
|
||||
|
||||
#[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) -> JsonResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)?;
|
||||
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_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) -> EmptyResult {
|
||||
delete_attachment(uuid, attachment_id, headers, conn)
|
||||
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)
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
|
||||
fn delete_attachment_post(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_attachment(uuid, attachment_id, headers, conn)
|
||||
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)
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
|
||||
fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)
|
||||
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)
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
|
||||
fn delete_attachment_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)
|
||||
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)
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/delete")]
|
||||
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/delete-admin")]
|
||||
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>")]
|
||||
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>/admin")]
|
||||
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[delete("/ciphers", data = "<data>")]
|
||||
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data: Value = data.into_inner().data;
|
||||
|
||||
let uuids = match data.get("Ids") {
|
||||
@@ -567,7 +596,7 @@ fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbCon
|
||||
};
|
||||
|
||||
for uuid in uuids {
|
||||
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn) {
|
||||
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &ws) {
|
||||
return error;
|
||||
};
|
||||
}
|
||||
@@ -576,12 +605,12 @@ 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) -> EmptyResult {
|
||||
delete_cipher_selected(data, headers, conn)
|
||||
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
delete_cipher_selected(data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers/move", data = "<data>")]
|
||||
fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data = data.into_inner().data;
|
||||
|
||||
let folder_id = match data.get("FolderId") {
|
||||
@@ -626,19 +655,23 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn)
|
||||
if cipher.move_to_folder(folder_id.clone(), &headers.user.uuid, &conn).is_err() {
|
||||
err!("Error saving the folder information")
|
||||
}
|
||||
cipher.save(&conn);
|
||||
match cipher.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => println!("Error: Failed to save cipher")
|
||||
};
|
||||
ws.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/ciphers/move", data = "<data>")]
|
||||
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
move_cipher_selected(data, headers, conn)
|
||||
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
move_cipher_selected(data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers/purge", data = "<data>")]
|
||||
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
let password_hash = data.MasterPasswordHash;
|
||||
|
||||
@@ -653,6 +686,9 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
||||
if cipher.delete(&conn).is_err() {
|
||||
err!("Failed deleting cipher")
|
||||
}
|
||||
else {
|
||||
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete folders
|
||||
@@ -660,13 +696,16 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
||||
if f.delete(&conn).is_err() {
|
||||
err!("Failed deleting folder")
|
||||
}
|
||||
else {
|
||||
ws.send_folder_update(UpdateType::SyncFolderCreate, &f);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) -> EmptyResult {
|
||||
let cipher = match Cipher::find_by_uuid(uuid, conn) {
|
||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> EmptyResult {
|
||||
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
@@ -675,13 +714,16 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) -> Empty
|
||||
err!("Cipher can't be deleted by user")
|
||||
}
|
||||
|
||||
match cipher.delete(conn) {
|
||||
Ok(()) => Ok(()),
|
||||
match cipher.delete(&conn) {
|
||||
Ok(()) => {
|
||||
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => err!("Failed deleting cipher")
|
||||
}
|
||||
}
|
||||
|
||||
fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &Headers, conn: &DbConn) -> EmptyResult {
|
||||
fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> EmptyResult {
|
||||
let attachment = match Attachment::find_by_id(&attachment_id, &conn) {
|
||||
Some(attachment) => attachment,
|
||||
None => err!("Attachment doesn't exist")
|
||||
@@ -702,7 +744,10 @@ fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &He
|
||||
|
||||
// Delete attachment
|
||||
match attachment.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Ok(()) => {
|
||||
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => err!("Deleting attachement failed")
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
use rocket::State;
|
||||
use rocket_contrib::{Json, Value};
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use api::{JsonResult, EmptyResult, JsonUpcase};
|
||||
use api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use auth::Headers;
|
||||
|
||||
#[get("/folders")]
|
||||
@@ -15,6 +16,7 @@ fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
Ok(Json(json!({
|
||||
"Data": folders_json,
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -40,23 +42,26 @@ pub struct FolderData {
|
||||
}
|
||||
|
||||
#[post("/folders", data = "<data>")]
|
||||
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
let data: FolderData = data.into_inner().data;
|
||||
|
||||
let mut folder = Folder::new(headers.user.uuid.clone(), data.Name);
|
||||
|
||||
folder.save(&conn);
|
||||
if folder.save(&conn).is_err() {
|
||||
err!("Failed to save folder")
|
||||
}
|
||||
ws.send_folder_update(UpdateType::SyncFolderCreate, &folder);
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>", data = "<data>")]
|
||||
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
put_folder(uuid, data, headers, conn)
|
||||
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
put_folder(uuid, data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[put("/folders/<uuid>", data = "<data>")]
|
||||
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
let data: FolderData = data.into_inner().data;
|
||||
|
||||
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
|
||||
@@ -70,18 +75,21 @@ fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn
|
||||
|
||||
folder.name = data.Name;
|
||||
|
||||
folder.save(&conn);
|
||||
if folder.save(&conn).is_err() {
|
||||
err!("Failed to save folder")
|
||||
}
|
||||
ws.send_folder_update(UpdateType::SyncFolderUpdate, &folder);
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>/delete")]
|
||||
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_folder(uuid, headers, conn)
|
||||
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
delete_folder(uuid, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[delete("/folders/<uuid>")]
|
||||
fn delete_folder(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn delete_folder(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let folder = match Folder::find_by_uuid(&uuid, &conn) {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder")
|
||||
@@ -93,7 +101,10 @@ fn delete_folder(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
|
||||
// Delete the actual folder entry
|
||||
match folder.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Ok(()) => {
|
||||
ws.send_folder_update(UpdateType::SyncFolderDelete, &folder);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => err!("Failed deleting folder")
|
||||
}
|
||||
}
|
||||
|
@@ -260,7 +260,8 @@
|
||||
"Type": 26,
|
||||
"Domains": [
|
||||
"steampowered.com",
|
||||
"steamcommunity.com"
|
||||
"steamcommunity.com",
|
||||
"steamgames.com"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
|
@@ -19,15 +19,18 @@ pub fn routes() -> Vec<Route> {
|
||||
get_public_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
post_kdf,
|
||||
post_sstamp,
|
||||
post_email_token,
|
||||
post_email,
|
||||
delete_account,
|
||||
post_delete_account,
|
||||
revision_date,
|
||||
password_hint,
|
||||
prelogin,
|
||||
|
||||
sync,
|
||||
sync_no_query,
|
||||
|
||||
get_ciphers,
|
||||
get_cipher,
|
||||
@@ -77,6 +80,7 @@ pub fn routes() -> Vec<Route> {
|
||||
activate_authenticator,
|
||||
activate_authenticator_put,
|
||||
generate_u2f,
|
||||
generate_u2f_challenge,
|
||||
activate_u2f,
|
||||
activate_u2f_put,
|
||||
|
||||
@@ -110,6 +114,7 @@ pub fn routes() -> Vec<Route> {
|
||||
put_organization_user,
|
||||
delete_user,
|
||||
post_delete_user,
|
||||
post_reinvite_user,
|
||||
post_org_import,
|
||||
|
||||
clear_device_token,
|
||||
@@ -117,6 +122,7 @@ pub fn routes() -> Vec<Route> {
|
||||
|
||||
get_eq_domains,
|
||||
post_eq_domains,
|
||||
put_eq_domains,
|
||||
|
||||
]
|
||||
}
|
||||
@@ -148,9 +154,10 @@ fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: D
|
||||
err!("Device not owned by user")
|
||||
}
|
||||
|
||||
device.delete(&conn);
|
||||
|
||||
Ok(())
|
||||
match device.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed deleting device")
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
@@ -211,7 +218,7 @@ struct EquivDomainData {
|
||||
}
|
||||
|
||||
#[post("/settings/domains", data = "<data>")]
|
||||
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: EquivDomainData = data.into_inner().data;
|
||||
|
||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
|
||||
@@ -223,7 +230,14 @@ 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());
|
||||
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(json!({}))),
|
||||
Err(_) => err!("Failed to save user")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[put("/settings/domains", data = "<data>")]
|
||||
fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
post_eq_domains(data, headers, conn)
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
use rocket::State;
|
||||
use rocket_contrib::{Json, Value};
|
||||
use CONFIG;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase};
|
||||
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use auth::{Headers, AdminHeaders, OwnerHeaders};
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
@@ -48,9 +49,16 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
|
||||
user_org.type_ = UserOrgType::Owner as i32;
|
||||
user_org.status = UserOrgStatus::Confirmed as i32;
|
||||
|
||||
org.save(&conn);
|
||||
user_org.save(&conn);
|
||||
collection.save(&conn);
|
||||
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");
|
||||
}
|
||||
|
||||
Ok(Json(org.to_json()))
|
||||
}
|
||||
@@ -125,9 +133,11 @@ fn post_organization(org_id: String, _headers: OwnerHeaders, data: JsonUpcase<Or
|
||||
|
||||
org.name = data.Name;
|
||||
org.billing_email = data.BillingEmail;
|
||||
org.save(&conn);
|
||||
|
||||
Ok(Json(org.to_json()))
|
||||
match org.save(&conn) {
|
||||
Ok(()) => Ok(Json(org.to_json())),
|
||||
Err(_) => err!("Failed to modify organization")
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/collections?writeOnly=false
|
||||
@@ -140,7 +150,8 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
.iter()
|
||||
.map(Collection::to_json)
|
||||
.collect::<Value>(),
|
||||
"Object": "list"
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -152,7 +163,8 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
|
||||
.iter()
|
||||
.map(Collection::to_json)
|
||||
.collect::<Value>(),
|
||||
"Object": "list"
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -167,7 +179,9 @@ fn post_organization_collections(org_id: String, _headers: AdminHeaders, data: J
|
||||
|
||||
let mut collection = Collection::new(org.uuid.clone(), data.Name);
|
||||
|
||||
collection.save(&conn);
|
||||
if collection.save(&conn).is_err() {
|
||||
err!("Failed saving Collection");
|
||||
}
|
||||
|
||||
Ok(Json(collection.to_json()))
|
||||
}
|
||||
@@ -196,7 +210,9 @@ fn post_organization_collection_update(org_id: String, col_id: String, _headers:
|
||||
}
|
||||
|
||||
collection.name = data.Name.clone();
|
||||
collection.save(&conn);
|
||||
if collection.save(&conn).is_err() {
|
||||
err!("Failed updating Collection");
|
||||
}
|
||||
|
||||
Ok(Json(collection.to_json()))
|
||||
}
|
||||
@@ -288,12 +304,13 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
|
||||
.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)
|
||||
.to_json_collection_user_details(col_user.read_only, &conn)
|
||||
}).collect();
|
||||
|
||||
Ok(Json(json!({
|
||||
"Data": user_list,
|
||||
"Object": "list"
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -311,22 +328,19 @@ fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResul
|
||||
Ok(Json(json!({
|
||||
"Data": ciphers_json,
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/users")]
|
||||
fn get_org_users(org_id: String, headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||
Some(_) => (),
|
||||
None => err!("User isn't member of organization")
|
||||
}
|
||||
|
||||
fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
let users = UserOrganization::find_by_org(&org_id, &conn);
|
||||
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
|
||||
|
||||
Ok(Json(json!({
|
||||
"Data": users_json,
|
||||
"Object": "list"
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -377,11 +391,11 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
match invitation.save(&conn) {
|
||||
Ok(()) => {
|
||||
let mut user = User::new(email.clone());
|
||||
if user.save(&conn) {
|
||||
if user.save(&conn).is_err() {
|
||||
err!("Failed to create placeholder for invited user")
|
||||
} else {
|
||||
user_org_status = UserOrgStatus::Invited as i32;
|
||||
user
|
||||
} else {
|
||||
err!("Failed to create placeholder for invited user")
|
||||
}
|
||||
}
|
||||
Err(_) => err!(format!("Failed to invite: {}", email))
|
||||
@@ -398,6 +412,8 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -418,7 +434,10 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
}
|
||||
}
|
||||
|
||||
new_user.save(&conn);
|
||||
if new_user.save(&conn).is_err() {
|
||||
err!("Failed to add user to organization")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -448,9 +467,10 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
|
||||
None => err!("Invalid key provided")
|
||||
};
|
||||
|
||||
user_to_confirm.save(&conn);
|
||||
|
||||
Ok(())
|
||||
match user_to_confirm.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to add user to organization")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/users/<org_user_id>")]
|
||||
@@ -541,13 +561,31 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
|
||||
}
|
||||
}
|
||||
|
||||
user_to_edit.save(&conn);
|
||||
|
||||
Ok(())
|
||||
match user_to_edit.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save user data")
|
||||
}
|
||||
}
|
||||
|
||||
#[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")
|
||||
@@ -580,6 +618,11 @@ 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;
|
||||
|
||||
@@ -601,7 +644,7 @@ struct RelationsData {
|
||||
}
|
||||
|
||||
#[post("/ciphers/import-organization?<query>", data = "<data>")]
|
||||
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data: ImportData = data.into_inner().data;
|
||||
let org_id = query.organizationId;
|
||||
|
||||
@@ -617,8 +660,11 @@ fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Head
|
||||
// Read and create the collections
|
||||
let collections: Vec<_> = data.Collections.into_iter().map(|coll| {
|
||||
let mut collection = Collection::new(org_id.clone(), coll.Name);
|
||||
collection.save(&conn);
|
||||
collection
|
||||
if collection.save(&conn).is_err() {
|
||||
err!("Failed to create Collection");
|
||||
}
|
||||
|
||||
Ok(collection)
|
||||
}).collect();
|
||||
|
||||
// Read the relations between collections and ciphers
|
||||
@@ -630,16 +676,23 @@ fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Head
|
||||
// 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).ok();
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherCreate).ok();
|
||||
cipher
|
||||
}).collect();
|
||||
|
||||
// Assign the collections
|
||||
for (cipher_index, coll_index) in relations {
|
||||
let cipher_id = &ciphers[cipher_index].uuid;
|
||||
let coll_id = &collections[coll_index].uuid;
|
||||
let coll = &collections[coll_index];
|
||||
let coll_id = match coll {
|
||||
Ok(coll) => coll.uuid.as_str(),
|
||||
Err(_) => err!("Failed to assign to collection")
|
||||
};
|
||||
|
||||
CollectionCipher::save(cipher_id, coll_id, &conn);
|
||||
match CollectionCipher::save(cipher_id, coll_id, &conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed to add cipher to collection")
|
||||
};
|
||||
}
|
||||
|
||||
let mut user = headers.user;
|
||||
|
@@ -19,7 +19,8 @@ fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
|
||||
Ok(Json(json!({
|
||||
"Data": twofactors_json,
|
||||
"Object": "list"
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -74,9 +75,10 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
|
||||
|
||||
// Remove the recovery code, not needed without twofactors
|
||||
user.totp_recover = None;
|
||||
user.save(&conn);
|
||||
|
||||
Ok(Json(json!({})))
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(json!({}))),
|
||||
Err(_) => err!("Failed to remove the user's two factor recovery code")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -216,7 +218,9 @@ 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);
|
||||
user.save(conn);
|
||||
if user.save(conn).is_err() {
|
||||
println!("Error: Failed to save the user's two factor recovery code")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,24 +252,31 @@ fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
|
||||
let user_uuid = &headers.user.uuid;
|
||||
|
||||
let u2f_type = TwoFactorType::U2f as i32;
|
||||
let register_type = TwoFactorType::U2fRegisterChallenge;
|
||||
let (enabled, challenge) = match TwoFactor::find_by_user_and_type(user_uuid, u2f_type, &conn) {
|
||||
Some(_) => (true, String::new()),
|
||||
None => {
|
||||
let c = _create_u2f_challenge(user_uuid, register_type, &conn);
|
||||
(false, c.challenge)
|
||||
}
|
||||
};
|
||||
let enabled = TwoFactor::find_by_user_and_type(user_uuid, u2f_type, &conn).is_some();
|
||||
|
||||
Ok(Json(json!({
|
||||
"Enabled": enabled,
|
||||
"Challenge": {
|
||||
"Object": "twoFactorU2f"
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/two-factor/get-u2f-challenge", data = "<data>")]
|
||||
fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password");
|
||||
}
|
||||
|
||||
let user_uuid = &headers.user.uuid;
|
||||
|
||||
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fRegisterChallenge, &conn).challenge;
|
||||
|
||||
Ok(Json(json!({
|
||||
"UserId": headers.user.uuid,
|
||||
"AppId": APP_ID.to_string(),
|
||||
"Challenge": challenge,
|
||||
"Version": U2F_VERSION,
|
||||
},
|
||||
"Object": "twoFactorU2f"
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -293,7 +304,7 @@ impl RegisterResponseCopy {
|
||||
RegisterResponse {
|
||||
registration_data: self.registration_data,
|
||||
version: self.version,
|
||||
challenge: challenge,
|
||||
challenge,
|
||||
client_data: self.client_data,
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ 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("..") {
|
||||
if domain.contains('/') || domain.contains("..") {
|
||||
return Content(icon_type, get_fallback_icon());
|
||||
}
|
||||
|
||||
|
@@ -46,16 +46,17 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
|
||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||
device.save(&conn);
|
||||
|
||||
Ok(Json(json!({
|
||||
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(_) => err!("Failed to add device to user")
|
||||
}
|
||||
}
|
||||
|
||||
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, remote: Option<SocketAddr>) -> JsonResult {
|
||||
@@ -107,12 +108,14 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, re
|
||||
Some(device) => {
|
||||
// Check if valid device
|
||||
if device.user_uuid != user.uuid {
|
||||
device.delete(&conn);
|
||||
err!("Device is not owned by user")
|
||||
match device.delete(&conn) {
|
||||
Ok(()) => Device::new(device_id, user.uuid.clone(), device_name, device_type_num),
|
||||
Err(_) => err!("Tried to delete device not owned by user, but failed")
|
||||
}
|
||||
|
||||
} else {
|
||||
device
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Create new device
|
||||
Device::new(device_id, user.uuid.clone(), device_name, device_type_num)
|
||||
@@ -126,7 +129,9 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, re
|
||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||
device.save(&conn);
|
||||
if device.save(&conn).is_err() {
|
||||
err!("Failed to add device to user")
|
||||
}
|
||||
|
||||
let mut result = json!({
|
||||
"access_token": access_token,
|
||||
@@ -158,11 +163,11 @@ fn twofactor_auth(
|
||||
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
||||
|
||||
// No twofactor token if twofactor is disabled
|
||||
if twofactors.len() == 0 {
|
||||
if twofactors.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let provider = match util::parse_option_string(data.get_opt("twoFactorProvider")) {
|
||||
let provider = match util::try_parse_string(data.get_opt("twoFactorProvider")) {
|
||||
Some(provider) => provider,
|
||||
None => providers[0], // If we aren't given a two factor provider, asume the first one
|
||||
};
|
||||
@@ -207,7 +212,7 @@ fn twofactor_auth(
|
||||
_ => err!("Invalid two factor provider"),
|
||||
}
|
||||
|
||||
if util::parse_option_string(data.get_opt("twoFactorRemember")).unwrap_or(0) == 1 {
|
||||
if util::try_parse_string_or(data.get_opt("twoFactorRemember"), 0) == 1 {
|
||||
Ok(Some(device.refresh_twofactor_remember()))
|
||||
} else {
|
||||
device.delete_twofactor_remember();
|
||||
@@ -274,7 +279,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
let headers = request.headers();
|
||||
let type_opt = headers.get_one("Device-Type");
|
||||
let type_num = util::parse_option_string(type_opt).unwrap_or(0);
|
||||
let type_num = util::try_parse_string_or(type_opt, 0);
|
||||
|
||||
Outcome::Success(DeviceType(type_num))
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ 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};
|
||||
|
||||
use rocket::response::status::BadRequest;
|
||||
use rocket_contrib::Json;
|
||||
|
@@ -1,31 +1,373 @@
|
||||
use rocket::Route;
|
||||
use rocket_contrib::Json;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use db::DbConn;
|
||||
use api::JsonResult;
|
||||
use auth::Headers;
|
||||
use db::DbConn;
|
||||
|
||||
use CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![negotiate]
|
||||
routes![negotiate, websockets_err]
|
||||
}
|
||||
|
||||
#[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.")
|
||||
}
|
||||
|
||||
#[post("/hub/negotiate")]
|
||||
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
||||
use data_encoding::BASE64URL;
|
||||
use crypto;
|
||||
use data_encoding::BASE64URL;
|
||||
|
||||
// Store this in db?
|
||||
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
|
||||
let mut available_transports: Vec<JsonValue> = Vec::new();
|
||||
|
||||
if CONFIG.websocket_enabled {
|
||||
available_transports.push(json!({"transport":"WebSockets", "transferFormats":["Text","Binary"]}));
|
||||
}
|
||||
|
||||
// TODO: Implement transports
|
||||
// Rocket WS support: https://github.com/SergioBenitez/Rocket/issues/90
|
||||
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
||||
Ok(Json(json!({
|
||||
"connectionId": conn_id,
|
||||
"availableTransports":[
|
||||
// {"transport":"WebSockets", "transferFormats":["Text","Binary"]},
|
||||
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
|
||||
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
||||
]
|
||||
Ok(Json(json!({
|
||||
"connectionId": conn_id,
|
||||
"availableTransports": available_transports
|
||||
})))
|
||||
}
|
||||
|
||||
///
|
||||
/// Websockets server
|
||||
///
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use ws::{self, util::Token, Factory, Handler, Handshake, Message, Sender, WebSocket};
|
||||
|
||||
use chashmap::CHashMap;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde_json::from_str;
|
||||
|
||||
use db::models::{Cipher, Folder, User};
|
||||
|
||||
use rmpv::Value;
|
||||
|
||||
fn serialize(val: Value) -> Vec<u8> {
|
||||
use rmpv::encode::write_value;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
write_value(&mut buf, &val).expect("Error encoding MsgPack");
|
||||
|
||||
// Add size bytes at the start
|
||||
// Extracted from BinaryMessageFormat.js
|
||||
let mut size: usize = buf.len();
|
||||
let mut len_buf: Vec<u8> = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut size_part = size & 0x7f;
|
||||
size >>= 7;
|
||||
|
||||
if size > 0 {
|
||||
size_part |= 0x80;
|
||||
}
|
||||
|
||||
len_buf.push(size_part as u8);
|
||||
|
||||
if size == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
len_buf.append(&mut buf);
|
||||
len_buf
|
||||
}
|
||||
|
||||
fn serialize_date(date: NaiveDateTime) -> Value {
|
||||
let seconds: i64 = date.timestamp();
|
||||
let nanos: i64 = date.timestamp_subsec_nanos() as i64;
|
||||
let timestamp = nanos << 34 | seconds;
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
let mut bs = [0u8; 8];
|
||||
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
|
||||
Value::Ext(-1, bs.to_vec())
|
||||
}
|
||||
|
||||
fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
|
||||
match option {
|
||||
Some(a) => a.into(),
|
||||
None => Value::Nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Server WebSocket handler
|
||||
pub struct WSHandler {
|
||||
out: Sender,
|
||||
user_uuid: Option<String>,
|
||||
users: WebSocketUsers,
|
||||
}
|
||||
|
||||
const RECORD_SEPARATOR: u8 = 0x1e;
|
||||
const INITIAL_RESPONSE: [u8; 3] = [0x7b, 0x7d, RECORD_SEPARATOR]; // {, }, <RS>
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InitialMessage {
|
||||
protocol: String,
|
||||
version: i32,
|
||||
}
|
||||
|
||||
const PING_MS: u64 = 15_000;
|
||||
const PING: Token = Token(1);
|
||||
|
||||
impl Handler for WSHandler {
|
||||
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
||||
// TODO: Improve this split
|
||||
let path = hs.request.resource();
|
||||
let mut query_split: Vec<_> = path.split('?').nth(1).unwrap().split('&').collect();
|
||||
query_split.sort();
|
||||
let access_token = &query_split[0][13..];
|
||||
let _id = &query_split[1][3..];
|
||||
|
||||
// Validate the user
|
||||
use 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",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Assign the user to the handler
|
||||
let user_uuid = claims.sub;
|
||||
self.user_uuid = Some(user_uuid.clone());
|
||||
|
||||
// Add the current Sender to the user list
|
||||
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),
|
||||
);
|
||||
|
||||
// Schedule a ping to keep the connection alive
|
||||
self.out.timeout(PING_MS, PING)
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
|
||||
println!("Server got message '{}'. ", msg);
|
||||
|
||||
if let Message::Text(text) = msg.clone() {
|
||||
let json = &text[..text.len() - 1]; // Remove last char
|
||||
|
||||
if let Ok(InitialMessage { protocol, version }) = from_str::<InitialMessage>(json) {
|
||||
if &protocol == "messagepack" && version == 1 {
|
||||
return self.out.send(&INITIAL_RESPONSE[..]); // Respond to initial message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not the initial message, just echo the message
|
||||
self.out.send(msg)
|
||||
}
|
||||
|
||||
fn on_timeout(&mut self, event: Token) -> ws::Result<()> {
|
||||
if event == PING {
|
||||
// send ping
|
||||
self.out.send(create_ping())?;
|
||||
|
||||
// reschedule the timeout
|
||||
self.out.timeout(PING_MS, PING)
|
||||
} else {
|
||||
Err(ws::Error::new(
|
||||
ws::ErrorKind::Internal,
|
||||
"Invalid timeout token provided",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WSFactory {
|
||||
pub users: WebSocketUsers,
|
||||
}
|
||||
|
||||
impl WSFactory {
|
||||
pub fn init() -> Self {
|
||||
WSFactory {
|
||||
users: WebSocketUsers {
|
||||
map: Arc::new(CHashMap::new()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Factory for WSFactory {
|
||||
type Handler = WSHandler;
|
||||
|
||||
fn connection_made(&mut self, out: Sender) -> Self::Handler {
|
||||
println!("WS: Connection made");
|
||||
WSHandler {
|
||||
out,
|
||||
user_uuid: None,
|
||||
users: self.users.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn connection_lost(&mut self, handler: Self::Handler) {
|
||||
println!("WS: Connection lost");
|
||||
|
||||
// Remove handler
|
||||
let user_uuid = &handler.user_uuid.unwrap();
|
||||
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
|
||||
user_conn.remove_item(&handler.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebSocketUsers {
|
||||
pub map: Arc<CHashMap<String, Vec<Sender>>>,
|
||||
}
|
||||
|
||||
impl WebSocketUsers {
|
||||
fn send_update(&self, user_uuid: &String, data: Vec<u8>) -> ws::Result<()> {
|
||||
if let Some(user) = self.map.get(user_uuid) {
|
||||
for sender in user.iter() {
|
||||
sender.send(data.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NOTE: The last modified date needs to be updated before calling these methods
|
||||
#[allow(dead_code)]
|
||||
pub fn send_user_update(&self, ut: UpdateType, user: &User) {
|
||||
let data = create_update(
|
||||
vec![
|
||||
("UserId".into(), user.uuid.clone().into()),
|
||||
("Date".into(), serialize_date(user.updated_at)),
|
||||
],
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid.clone(), data).ok();
|
||||
}
|
||||
|
||||
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), folder.uuid.clone().into()),
|
||||
("UserId".into(), folder.user_uuid.clone().into()),
|
||||
("RevisionDate".into(), serialize_date(folder.updated_at)),
|
||||
],
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&folder.user_uuid, data).ok();
|
||||
}
|
||||
|
||||
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &Vec<String>) {
|
||||
let user_uuid = convert_option(cipher.user_uuid.clone());
|
||||
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
||||
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), cipher.uuid.clone().into()),
|
||||
("UserId".into(), user_uuid),
|
||||
("OrganizationId".into(), org_uuid),
|
||||
("CollectionIds".into(), Value::Nil),
|
||||
("RevisionDate".into(), serialize_date(cipher.updated_at)),
|
||||
],
|
||||
ut,
|
||||
);
|
||||
|
||||
for uuid in user_uuids {
|
||||
self.send_update(&uuid, data.clone()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Message Structure
|
||||
[
|
||||
1, // MessageType.Invocation
|
||||
{}, // Headers
|
||||
null, // InvocationId
|
||||
"ReceiveMessage", // Target
|
||||
[ // Arguments
|
||||
{
|
||||
"ContextId": "app_id",
|
||||
"Type": ut as i32,
|
||||
"Payload": {}
|
||||
}
|
||||
]
|
||||
]
|
||||
*/
|
||||
fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType) -> Vec<u8> {
|
||||
use rmpv::Value as V;
|
||||
|
||||
let value = V::Array(vec![
|
||||
1.into(),
|
||||
V::Array(vec![]),
|
||||
V::Nil,
|
||||
"ReceiveMessage".into(),
|
||||
V::Array(vec![V::Map(vec![
|
||||
("ContextId".into(), "app_id".into()),
|
||||
("Type".into(), (ut as i32).into()),
|
||||
("Payload".into(), payload.into()),
|
||||
])]),
|
||||
]);
|
||||
|
||||
serialize(value)
|
||||
}
|
||||
|
||||
fn create_ping() -> Vec<u8> {
|
||||
serialize(Value::Array(vec![6.into()]))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum UpdateType {
|
||||
SyncCipherUpdate = 0,
|
||||
SyncCipherCreate = 1,
|
||||
SyncLoginDelete = 2,
|
||||
SyncFolderDelete = 3,
|
||||
SyncCiphers = 4,
|
||||
|
||||
SyncVault = 5,
|
||||
SyncOrgKeys = 6,
|
||||
SyncFolderCreate = 7,
|
||||
SyncFolderUpdate = 8,
|
||||
SyncCipherDelete = 9,
|
||||
SyncSettings = 10,
|
||||
|
||||
LogOut = 11,
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
users
|
||||
}
|
||||
|
10
src/auth.rs
10
src/auth.rs
@@ -95,7 +95,7 @@ use rocket::Outcome;
|
||||
use rocket::request::{self, Request, FromRequest};
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::{User, UserOrganization, UserOrgType, UserOrgStatus, Device};
|
||||
use db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
|
||||
|
||||
pub struct Headers {
|
||||
pub host: String,
|
||||
@@ -212,7 +212,13 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
||||
err_handler!("The current user isn't confirmed member of the organization")
|
||||
}
|
||||
}
|
||||
None => err_handler!("The current user isn't 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")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Outcome::Success(Self{
|
||||
|
@@ -78,7 +78,7 @@ impl Attachment {
|
||||
println!("ERROR: Failed with 10 retries");
|
||||
return Err(err)
|
||||
} else {
|
||||
retries = retries - 1;
|
||||
retries -= 1;
|
||||
println!("Had to retry! Retries left: {}", retries);
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
continue
|
||||
|
@@ -130,34 +130,38 @@ impl Cipher {
|
||||
json_object
|
||||
}
|
||||
|
||||
pub fn update_users_revision(&self, conn: &DbConn) {
|
||||
pub fn update_users_revision(&self, conn: &DbConn) -> Vec<String> {
|
||||
let mut user_uuids = Vec::new();
|
||||
match self.user_uuid {
|
||||
Some(ref user_uuid) => User::update_uuid_revision(&user_uuid, conn),
|
||||
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
|
||||
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::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) -> bool {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
self.update_users_revision(conn);
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
match diesel::replace_into(ciphers::table)
|
||||
diesel::replace_into(ciphers::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
.execute(&**conn)
|
||||
.and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
|
||||
self.update_users_revision(conn);
|
||||
|
||||
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
||||
@@ -166,7 +170,7 @@ impl Cipher {
|
||||
|
||||
diesel::delete(
|
||||
ciphers::table.filter(
|
||||
ciphers::uuid.eq(self.uuid)
|
||||
ciphers::uuid.eq(&self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
@@ -178,6 +182,13 @@ impl Cipher {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
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) {
|
||||
None => {
|
||||
@@ -356,6 +367,6 @@ impl Cipher {
|
||||
)
|
||||
))
|
||||
.select(ciphers_collections::collection_uuid)
|
||||
.load::<String>(&**conn).unwrap_or(vec![])
|
||||
.load::<String>(&**conn).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
@@ -42,13 +42,18 @@ use db::schema::*;
|
||||
|
||||
/// Database methods
|
||||
impl Collection {
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(collections::table)
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
// 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);
|
||||
});
|
||||
|
||||
diesel::replace_into(collections::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
.execute(&**conn)
|
||||
.and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
@@ -254,25 +259,19 @@ pub struct CollectionCipher {
|
||||
|
||||
/// Database methods
|
||||
impl CollectionCipher {
|
||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(ciphers_collections::table)
|
||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::replace_into(ciphers_collections::table)
|
||||
.values((
|
||||
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
||||
ciphers_collections::collection_uuid.eq(collection_uuid),
|
||||
)).execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
)).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
|
||||
match diesel::delete(ciphers_collections::table
|
||||
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) {
|
||||
Ok(1) => true, // One row deleted
|
||||
_ => false,
|
||||
}
|
||||
.execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
|
@@ -112,24 +112,24 @@ use db::schema::devices;
|
||||
|
||||
/// Database methods
|
||||
impl Device {
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
match diesel::replace_into(devices::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
diesel::replace_into(devices::table)
|
||||
.values(&*self).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> bool {
|
||||
match diesel::delete(devices::table.filter(
|
||||
devices::uuid.eq(self.uuid)))
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row deleted
|
||||
_ => false,
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(devices::table.filter(
|
||||
devices::uuid.eq(self.uuid)
|
||||
)).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for device in Self::find_by_user(user_uuid, &conn) {
|
||||
device.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
|
@@ -70,29 +70,32 @@ use db::schema::{folders, folders_ciphers};
|
||||
|
||||
/// Database methods
|
||||
impl Folder {
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
match diesel::replace_into(folders::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
diesel::replace_into(folders::table)
|
||||
.values(&*self).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
|
||||
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)
|
||||
folders::uuid.eq(&self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for folder in Self::find_by_user(user_uuid, &conn) {
|
||||
folder.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
folders::table
|
||||
.filter(folders::uuid.eq(uuid))
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
use super::{User, CollectionUser};
|
||||
use super::{User, CollectionUser, Invitation};
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
#[table_name = "organizations"]
|
||||
@@ -51,6 +51,8 @@ impl UserOrgType {
|
||||
|
||||
/// 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: Uuid::new_v4().to_string(),
|
||||
@@ -60,13 +62,21 @@ 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) -> JsonValue {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
"Seats": 10,
|
||||
"MaxCollections": 10,
|
||||
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
"Use2fa": true,
|
||||
"UseDirectory": false,
|
||||
"UseEvents": false,
|
||||
@@ -83,7 +93,7 @@ impl Organization {
|
||||
"BillingEmail": self.billing_email,
|
||||
"Plan": "TeamsAnnually",
|
||||
"PlanType": 5, // TeamsAnnually plan
|
||||
|
||||
"UsersGetPremium": true,
|
||||
"Object": "organization",
|
||||
})
|
||||
}
|
||||
@@ -103,6 +113,20 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,24 +137,28 @@ use db::schema::{organizations, users_organizations, users_collections, ciphers_
|
||||
|
||||
/// Database methods
|
||||
impl Organization {
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
if self.uuid == Organization::VIRTUAL_ID {
|
||||
return Err(diesel::result::Error::NotFound)
|
||||
}
|
||||
|
||||
UserOrganization::find_by_org(&self.uuid, conn)
|
||||
.iter()
|
||||
.for_each(|user_org| {
|
||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||
});
|
||||
|
||||
match diesel::replace_into(organizations::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
diesel::replace_into(organizations::table)
|
||||
.values(&*self).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
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)?;
|
||||
@@ -143,6 +171,9 @@ impl 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()
|
||||
@@ -158,6 +189,7 @@ impl UserOrganization {
|
||||
"Name": org.name,
|
||||
"Seats": 10,
|
||||
"MaxCollections": 10,
|
||||
"UsersGetPremium": true,
|
||||
|
||||
"Use2fa": true,
|
||||
"UseDirectory": false,
|
||||
@@ -194,7 +226,7 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_collection_user_details(&self, read_only: &bool, conn: &DbConn) -> JsonValue {
|
||||
pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> JsonValue {
|
||||
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
@@ -230,18 +262,20 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
if self.org_uuid == Organization::VIRTUAL_ID {
|
||||
return Err(diesel::result::Error::NotFound)
|
||||
}
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
|
||||
match diesel::replace_into(users_organizations::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
diesel::replace_into(users_organizations::table)
|
||||
.values(&*self).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
if self.org_uuid == Organization::VIRTUAL_ID {
|
||||
return Err(diesel::result::Error::NotFound)
|
||||
}
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
|
||||
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
|
||||
@@ -260,6 +294,13 @@ impl UserOrganization {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for user_org in Self::find_any_state_by_user(&user_uuid, &conn) {
|
||||
user_org.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_full_access(self) -> bool {
|
||||
self.access_all || self.type_ < UserOrgType::User as i32
|
||||
}
|
||||
@@ -281,21 +322,40 @@ impl UserOrganization {
|
||||
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(vec![])
|
||||
.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(vec![])
|
||||
.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()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
@@ -330,6 +390,22 @@ impl UserOrganization {
|
||||
.select(users_organizations::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||
}
|
||||
|
||||
pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
||||
))
|
||||
.filter(
|
||||
users_organizations::access_all.eq(true).or( // AccessAll..
|
||||
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher
|
||||
)
|
||||
)
|
||||
.select(users_organizations::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -35,17 +35,20 @@ pub struct User {
|
||||
|
||||
pub equivalent_domains: String,
|
||||
pub excluded_globals: String,
|
||||
|
||||
pub client_kdf_type: i32,
|
||||
pub client_kdf_iter: i32,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl User {
|
||||
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
||||
pub const CLIENT_KDF_ITER_DEFAULT: i32 = 5_000;
|
||||
|
||||
pub fn new(mail: String) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
let email = mail.to_lowercase();
|
||||
|
||||
let iterations = CONFIG.password_iterations;
|
||||
let salt = crypto::get_random_64();
|
||||
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
created_at: now,
|
||||
@@ -55,8 +58,8 @@ impl User {
|
||||
key: String::new(),
|
||||
|
||||
password_hash: Vec::new(),
|
||||
salt,
|
||||
password_iterations: iterations,
|
||||
salt: crypto::get_random_64(),
|
||||
password_iterations: CONFIG.password_iterations,
|
||||
|
||||
security_stamp: Uuid::new_v4().to_string(),
|
||||
|
||||
@@ -69,6 +72,9 @@ impl User {
|
||||
|
||||
equivalent_domains: "[]".to_string(),
|
||||
excluded_globals: "[]".to_string(),
|
||||
|
||||
client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT,
|
||||
client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,23 +103,32 @@ impl User {
|
||||
pub fn reset_security_stamp(&mut self) {
|
||||
self.security_stamp = Uuid::new_v4().to_string();
|
||||
}
|
||||
|
||||
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 diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::{users, invitations};
|
||||
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
|
||||
|
||||
/// Database methods
|
||||
impl User {
|
||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||
use super::UserOrganization;
|
||||
use super::TwoFactor;
|
||||
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
|
||||
|
||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
||||
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_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||
|
||||
let twofactor_enabled = TwoFactor::find_by_user(&self.uuid, conn).len() > 0;
|
||||
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
@@ -133,24 +148,34 @@ impl User {
|
||||
}
|
||||
|
||||
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
match diesel::replace_into(users::table) // Insert or update
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
diesel::replace_into(users::table) // Insert or update
|
||||
.values(&*self).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
|
||||
if user_org.type_ == UserOrgType::Owner as i32 {
|
||||
if UserOrganization::find_by_org_and_type(
|
||||
&user_org.org_uuid,
|
||||
UserOrgType::Owner as i32, &conn
|
||||
).len() <= 1 {
|
||||
return Err(diesel::result::Error::NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> bool {
|
||||
match diesel::delete(users::table.filter(
|
||||
UserOrganization::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Cipher::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Folder::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Device::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) {
|
||||
Ok(1) => true, // One row deleted
|
||||
_ => false,
|
||||
}
|
||||
.execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
||||
@@ -184,6 +209,11 @@ impl User {
|
||||
.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")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
|
@@ -72,6 +72,12 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
invitations (email) {
|
||||
email -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
organizations (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -110,12 +116,8 @@ table! {
|
||||
security_stamp -> Text,
|
||||
equivalent_domains -> Text,
|
||||
excluded_globals -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
invitations (email) {
|
||||
email -> Text,
|
||||
client_kdf_type -> Integer,
|
||||
client_kdf_iter -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +166,7 @@ allow_tables_to_appear_in_same_query!(
|
||||
devices,
|
||||
folders,
|
||||
folders_ciphers,
|
||||
invitations,
|
||||
organizations,
|
||||
twofactor,
|
||||
users,
|
||||
|
49
src/mail.rs
49
src/mail.rs
@@ -1,7 +1,6 @@
|
||||
use std::error::Error;
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity};
|
||||
use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder};
|
||||
use lettre::{Transport, SmtpTransport, SmtpClient, ClientTlsParameters, ClientSecurity};
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre_email::EmailBuilder;
|
||||
|
||||
@@ -9,31 +8,30 @@ use MailConfig;
|
||||
|
||||
fn mailer(config: &MailConfig) -> SmtpTransport {
|
||||
let client_security = if config.smtp_ssl {
|
||||
let mut tls_builder = TlsConnector::builder().unwrap();
|
||||
tls_builder.supported_protocols(&[Protocol::Tlsv11, Protocol::Tlsv12]).unwrap();
|
||||
ClientSecurity::Required(
|
||||
ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap())
|
||||
)
|
||||
let tls = TlsConnector::builder()
|
||||
.min_protocol_version(Some(Protocol::Tlsv11))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
ClientSecurity::Required(ClientTlsParameters::new(config.smtp_host.clone(), tls))
|
||||
} else {
|
||||
ClientSecurity::None
|
||||
};
|
||||
|
||||
let smtp_transport = SmtpTransportBuilder::new(
|
||||
(config.smtp_host.to_owned().as_str(), config.smtp_port),
|
||||
client_security
|
||||
let smtp_client = SmtpClient::new(
|
||||
(config.smtp_host.as_str(), config.smtp_port),
|
||||
client_security,
|
||||
).unwrap();
|
||||
|
||||
let smtp_transport = match (&config.smtp_username, &config.smtp_password) {
|
||||
(Some(username), Some(password)) => {
|
||||
smtp_transport.credentials(Credentials::new(username.to_owned(), password.to_owned()))
|
||||
},
|
||||
(_, _) => smtp_transport,
|
||||
let smtp_client = match (&config.smtp_username, &config.smtp_password) {
|
||||
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
|
||||
_ => smtp_client,
|
||||
};
|
||||
|
||||
smtp_transport
|
||||
smtp_client
|
||||
.smtp_utf8(true)
|
||||
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
||||
.build()
|
||||
.transport()
|
||||
}
|
||||
|
||||
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> Result<(), String> {
|
||||
@@ -46,18 +44,19 @@ pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConf
|
||||
hint))
|
||||
} else {
|
||||
("Sorry, you have no password hint...",
|
||||
"Sorry, you have not specified any password hint...\n".to_string())
|
||||
"Sorry, you have not specified any password hint...\n".into())
|
||||
};
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to(address)
|
||||
.from((config.smtp_from.to_owned(), "Bitwarden-rs"))
|
||||
.from((config.smtp_from.clone(), "Bitwarden-rs"))
|
||||
.subject(subject)
|
||||
.body(body)
|
||||
.build().unwrap();
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match mailer(config).send(&email) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.description().to_string()),
|
||||
}
|
||||
mailer(config)
|
||||
.send(email.into())
|
||||
.map_err(|e| e.to_string())
|
||||
.and(Ok(()))
|
||||
}
|
||||
|
89
src/main.rs
89
src/main.rs
@@ -1,10 +1,14 @@
|
||||
#![feature(plugin, custom_derive)]
|
||||
#![feature(plugin, custom_derive, vec_remove_item, try_trait)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![recursion_limit="128"]
|
||||
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
extern crate reqwest;
|
||||
extern crate multipart;
|
||||
extern crate ws;
|
||||
extern crate rmpv;
|
||||
extern crate chashmap;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
@@ -30,9 +34,9 @@ extern crate num_traits;
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate native_tls;
|
||||
extern crate fast_chemail;
|
||||
extern crate byteorder;
|
||||
|
||||
use std::{env, path::Path, process::{exit, Command}};
|
||||
use std::{path::Path, process::{exit, Command}};
|
||||
use rocket::Rocket;
|
||||
|
||||
#[macro_use]
|
||||
@@ -52,6 +56,7 @@ fn init_rocket() -> Rocket {
|
||||
.mount("/icons", api::icons_routes())
|
||||
.mount("/notifications", api::notifications_routes())
|
||||
.manage(db::init_pool())
|
||||
.manage(api::start_notification_server())
|
||||
}
|
||||
|
||||
// Embed the migrations from the migrations folder into the application
|
||||
@@ -76,7 +81,6 @@ fn main() {
|
||||
check_web_vault();
|
||||
migrations::run_migrations();
|
||||
|
||||
|
||||
init_rocket().launch();
|
||||
}
|
||||
|
||||
@@ -172,27 +176,32 @@ pub struct MailConfig {
|
||||
|
||||
impl MailConfig {
|
||||
fn load() -> Option<Self> {
|
||||
let smtp_host = env::var("SMTP_HOST").ok();
|
||||
use util::{get_env, get_env_or};
|
||||
|
||||
// When SMTP_HOST is absent, we assume the user does not want to enable it.
|
||||
if smtp_host.is_none() {
|
||||
return None
|
||||
}
|
||||
let smtp_host = match get_env("SMTP_HOST") {
|
||||
Some(host) => host,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(true);
|
||||
let smtp_port = util::parse_option_string(env::var("SMTP_PORT").ok())
|
||||
.unwrap_or_else(|| {
|
||||
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
|
||||
println!("Please specify SMTP_FROM to enable SMTP support.");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
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_username = env::var("SMTP_USERNAME").ok();
|
||||
let smtp_password = env::var("SMTP_PASSWORD").ok().or_else(|| {
|
||||
let smtp_username = get_env("SMTP_USERNAME");
|
||||
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
|
||||
if smtp_username.as_ref().is_some() {
|
||||
println!("Please specify SMTP_PASSWORD to enable SMTP support.");
|
||||
println!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
|
||||
exit(1);
|
||||
} else {
|
||||
None
|
||||
@@ -200,13 +209,12 @@ impl MailConfig {
|
||||
});
|
||||
|
||||
Some(MailConfig {
|
||||
smtp_host: smtp_host.unwrap(),
|
||||
smtp_port: smtp_port,
|
||||
smtp_ssl: smtp_ssl,
|
||||
smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok())
|
||||
.unwrap_or("bitwarden-rs@localhost".to_string()),
|
||||
smtp_username: smtp_username,
|
||||
smtp_password: smtp_password,
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_ssl,
|
||||
smtp_from,
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -224,9 +232,13 @@ pub struct Config {
|
||||
web_vault_folder: String,
|
||||
web_vault_enabled: bool,
|
||||
|
||||
websocket_enabled: bool,
|
||||
websocket_url: String,
|
||||
|
||||
local_icon_extractor: bool,
|
||||
signups_allowed: bool,
|
||||
invitations_allowed: bool,
|
||||
server_admin_email: Option<String>,
|
||||
password_iterations: i32,
|
||||
show_password_hint: bool,
|
||||
|
||||
@@ -238,32 +250,37 @@ pub struct Config {
|
||||
|
||||
impl Config {
|
||||
fn load() -> Self {
|
||||
use util::{get_env, get_env_or};
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let df = env::var("DATA_FOLDER").unwrap_or("data".into());
|
||||
let key = env::var("RSA_KEY_FILENAME").unwrap_or(format!("{}/{}", &df, "rsa_key"));
|
||||
let df = get_env_or("DATA_FOLDER", "data".to_string());
|
||||
let key = get_env_or("RSA_KEY_FILENAME", format!("{}/{}", &df, "rsa_key"));
|
||||
|
||||
let domain = env::var("DOMAIN");
|
||||
let domain = get_env("DOMAIN");
|
||||
|
||||
Config {
|
||||
database_url: env::var("DATABASE_URL").unwrap_or(format!("{}/{}", &df, "db.sqlite3")),
|
||||
icon_cache_folder: env::var("ICON_CACHE_FOLDER").unwrap_or(format!("{}/{}", &df, "icon_cache")),
|
||||
attachments_folder: env::var("ATTACHMENTS_FOLDER").unwrap_or(format!("{}/{}", &df, "attachments")),
|
||||
database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")),
|
||||
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
|
||||
attachments_folder: get_env_or("ATTACHMENTS_FOLDER", format!("{}/{}", &df, "attachments")),
|
||||
|
||||
private_rsa_key: format!("{}.der", &key),
|
||||
private_rsa_key_pem: format!("{}.pem", &key),
|
||||
public_rsa_key: format!("{}.pub.der", &key),
|
||||
|
||||
web_vault_folder: env::var("WEB_VAULT_FOLDER").unwrap_or("web-vault/".into()),
|
||||
web_vault_enabled: util::parse_option_string(env::var("WEB_VAULT_ENABLED").ok()).unwrap_or(true),
|
||||
web_vault_folder: get_env_or("WEB_VAULT_FOLDER", "web-vault/".into()),
|
||||
web_vault_enabled: get_env_or("WEB_VAULT_ENABLED", true),
|
||||
|
||||
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
|
||||
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
|
||||
invitations_allowed: util::parse_option_string(env::var("INVITATIONS_ALLOWED").ok()).unwrap_or(true),
|
||||
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
|
||||
show_password_hint: util::parse_option_string(env::var("SHOW_PASSWORD_HINT").ok()).unwrap_or(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)),
|
||||
|
||||
domain_set: domain.is_ok(),
|
||||
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"),
|
||||
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),
|
||||
|
||||
domain_set: domain.is_some(),
|
||||
domain: domain.unwrap_or("http://localhost".into()),
|
||||
|
||||
mail: MailConfig::load(),
|
||||
|
43
src/util.rs
43
src/util.rs
@@ -6,17 +6,18 @@ macro_rules! err {
|
||||
($err:expr, $msg:expr) => {{
|
||||
println!("ERROR: {}", $msg);
|
||||
err_json!(json!({
|
||||
"Message": $err,
|
||||
"ValidationErrors": {
|
||||
"": [$msg,],
|
||||
},
|
||||
"error": $err,
|
||||
"error_description": $err,
|
||||
"ErrorModel": {
|
||||
"Message": $msg,
|
||||
"ValidationErrors": null,
|
||||
"ExceptionMessage": null,
|
||||
"ExceptionStackTrace": null,
|
||||
"InnerExceptionMessage": null,
|
||||
"Object": "error",
|
||||
}))
|
||||
"Object": "error"
|
||||
}}))
|
||||
}};
|
||||
($msg:expr) => { err!("The model state is invalid", $msg) }
|
||||
($msg:expr) => { err!("unknown_error", $msg) }
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
@@ -97,6 +98,7 @@ pub fn get_display_size(size: i32) -> String {
|
||||
///
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::ops::Try;
|
||||
|
||||
pub fn upcase_first(s: &str) -> String {
|
||||
let mut c = s.chars();
|
||||
@@ -106,14 +108,37 @@ pub fn upcase_first(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_option_string<S, T>(string: Option<S>) -> Option<T> where S: AsRef<str>, T: FromStr {
|
||||
if let Some(Ok(value)) = string.map(|s| s.as_ref().parse::<T>()) {
|
||||
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 {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Env methods
|
||||
///
|
||||
|
||||
use std::env;
|
||||
|
||||
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 {
|
||||
try_parse_string_or(env::var(key), default)
|
||||
}
|
||||
|
||||
///
|
||||
/// Date util methods
|
||||
///
|
||||
|
Reference in New Issue
Block a user