mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 18:55: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_FOLDER=web-vault/
|
||||||
# WEB_VAULT_ENABLED=true
|
# 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
|
## Controls if new users can register
|
||||||
# SIGNUPS_ALLOWED=true
|
# SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
@@ -42,8 +46,10 @@
|
|||||||
# ROCKET_PORT=8000
|
# ROCKET_PORT=8000
|
||||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
# 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_HOST=smtp.domain.tld
|
||||||
|
# SMTP_FROM=bitwarden-rs@domain.tld
|
||||||
# SMTP_PORT=587
|
# SMTP_PORT=587
|
||||||
# SMTP_SSL=true
|
# SMTP_SSL=true
|
||||||
# SMTP_USERNAME=username
|
# 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]
|
[dependencies]
|
||||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||||
rocket = { version = "0.3.16", features = ["tls"] }
|
rocket = { version = "0.3.17", features = ["tls"] }
|
||||||
rocket_codegen = "0.3.16"
|
rocket_codegen = "0.3.17"
|
||||||
rocket_contrib = "0.3.16"
|
rocket_contrib = "0.3.17"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
reqwest = "0.8.8"
|
reqwest = "0.9.2"
|
||||||
|
|
||||||
# multipart/form-data support
|
# 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
|
# A generic serialization/deserialization framework
|
||||||
serde = "1.0.74"
|
serde = "1.0.79"
|
||||||
serde_derive = "1.0.74"
|
serde_derive = "1.0.79"
|
||||||
serde_json = "1.0.26"
|
serde_json = "1.0.31"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# 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"] }
|
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled SQLite
|
||||||
@@ -31,10 +40,10 @@ libsqlite3-sys = { version = "0.9.3", features = ["bundled"] }
|
|||||||
ring = { version = "= 0.11.0", features = ["rsa_signing"] }
|
ring = { version = "= 0.11.0", features = ["rsa_signing"] }
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.6.5", features = ["v4"] }
|
uuid = { version = "0.7.1", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time library for Rust
|
# Date and time library for Rust
|
||||||
chrono = "0.4.5"
|
chrono = "0.4.6"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
oath = "0.10.2"
|
oath = "0.10.2"
|
||||||
@@ -55,17 +64,23 @@ dotenv = { version = "0.13.0", default-features = false }
|
|||||||
lazy_static = "1.1.0"
|
lazy_static = "1.1.0"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.5"
|
num-traits = "0.2.6"
|
||||||
num-derive = "0.2.2"
|
num-derive = "0.2.3"
|
||||||
|
|
||||||
lettre = "0.8.2"
|
# Email libraries
|
||||||
lettre_email = "0.8.2"
|
lettre = "0.9.0"
|
||||||
native-tls = "0.1.5"
|
lettre_email = "0.9.0"
|
||||||
fast_chemail = "0.9.5"
|
native-tls = "0.2.1"
|
||||||
|
|
||||||
|
# Number encoding library
|
||||||
|
byteorder = "1.2.6"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Make jwt use ring 0.11, to match rocket
|
# Make jwt use ring 0.11, to match rocket
|
||||||
jsonwebtoken = { path = "libs/jsonwebtoken" }
|
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
|
# 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' }
|
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' }
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM node:8-alpine as vault
|
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"
|
ENV URL "https://github.com/bitwarden/web.git"
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ RUN apt-get update && apt-get install -y\
|
|||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
EXPOSE 3012
|
||||||
|
|
||||||
# Copies the files from the context (env file and web-vault)
|
# Copies the files from the context (env file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# 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 #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM node:8-alpine as vault
|
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"
|
ENV URL "https://github.com/bitwarden/web.git"
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ RUN apk add \
|
|||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
EXPOSE 3012
|
||||||
|
|
||||||
# Copies the files from the context (env file and web-vault)
|
# Copies the files from the context (env file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM node:8-alpine as vault
|
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"
|
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
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
COPY . .
|
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||||
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
@@ -81,7 +80,7 @@ RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM resin/armv7hf-debian:stretch
|
FROM balenalib/armv7hf-debian:stretch
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_WORKERS=10
|
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)
|
- [Configuring bitwarden service](#configuring-bitwarden-service)
|
||||||
- [Disable registration of new users](#disable-registration-of-new-users)
|
- [Disable registration of new users](#disable-registration-of-new-users)
|
||||||
- [Disable invitations](#disable-invitations)
|
- [Disable invitations](#disable-invitations)
|
||||||
|
- [Configure server administrator](#configure-server-administrator)
|
||||||
- [Enabling HTTPS](#enabling-https)
|
- [Enabling HTTPS](#enabling-https)
|
||||||
|
- [Enabling WebSocket notifications](#enabling-websocket-notifications)
|
||||||
- [Enabling U2F authentication](#enabling-u2f-authentication)
|
- [Enabling U2F authentication](#enabling-u2f-authentication)
|
||||||
- [Changing persistent data location](#changing-persistent-data-location)
|
- [Changing persistent data location](#changing-persistent-data-location)
|
||||||
- [/data prefix:](#data-prefix)
|
- [/data prefix:](#data-prefix)
|
||||||
@@ -41,6 +43,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
|
|||||||
- [Building binary](#building-binary)
|
- [Building binary](#building-binary)
|
||||||
- [Available packages](#available-packages)
|
- [Available packages](#available-packages)
|
||||||
- [Arch Linux](#arch-linux)
|
- [Arch Linux](#arch-linux)
|
||||||
|
- [Kubernetes deployment](#kubernetes-deployment)
|
||||||
- [Backing up your vault](#backing-up-your-vault)
|
- [Backing up your vault](#backing-up-your-vault)
|
||||||
- [1. the sqlite3 database](#1-the-sqlite3-database)
|
- [1. the sqlite3 database](#1-the-sqlite3-database)
|
||||||
- [2. the attachments folder](#2-the-attachments-folder)
|
- [2. the attachments folder](#2-the-attachments-folder)
|
||||||
@@ -139,7 +142,7 @@ docker run -d --name bitwarden \
|
|||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
mprasil/bitwarden:latest
|
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
|
### Disable invitations
|
||||||
|
|
||||||
@@ -153,6 +156,23 @@ docker run -d --name bitwarden \
|
|||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
mprasil/bitwarden:latest
|
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
|
### Enabling HTTPS
|
||||||
To enable HTTPS, you need to configure the `ROCKET_TLS`.
|
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.
|
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
|
### 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
|
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.
|
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.
|
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
|
```sh
|
||||||
docker run -d --name bitwarden \
|
docker run -d --name bitwarden \
|
||||||
@@ -287,6 +348,7 @@ You can configure bitwarden_rs to send emails via a SMTP agent:
|
|||||||
```sh
|
```sh
|
||||||
docker run -d --name bitwarden \
|
docker run -d --name bitwarden \
|
||||||
-e SMTP_HOST=<smtp.domain.tld> \
|
-e SMTP_HOST=<smtp.domain.tld> \
|
||||||
|
-e SMTP_FROM=<bitwarden@domain.tld> \
|
||||||
-e SMTP_PORT=587 \
|
-e SMTP_PORT=587 \
|
||||||
-e SMTP_SSL=true \
|
-e SMTP_SSL=true \
|
||||||
-e SMTP_USERNAME=<username> \
|
-e SMTP_USERNAME=<username> \
|
||||||
@@ -348,7 +410,7 @@ docker build -t bitwarden_rs .
|
|||||||
|
|
||||||
## Building binary
|
## 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
|
## 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.
|
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
|
## Backing up your vault
|
||||||
|
|
||||||
### 1. the sqlite3 database
|
### 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.
|
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
|
### 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:
|
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.
|
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.
|
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 bellow `1024`. To overcome this, you need to configure server to listen on a different port, you can use `ROCKET_PORT` to do that.
|
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`:
|
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
|
### 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
|
### Running on unencrypted connection
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
--- a/src/app/services/services.module.ts
|
--- a/src/app/services/services.module.ts
|
||||||
+++ b/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 environmentService = new EnvironmentService(apiService, storageService, notificationsService);
|
||||||
const auditService = new AuditService(cryptoFunctionService, apiService);
|
const auditService = new AuditService(cryptoFunctionService, apiService);
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
+ const isDev = false;
|
+ const isDev = false;
|
||||||
+ environmentService.baseUrl = window.location.origin;
|
+ environmentService.baseUrl = window.location.origin;
|
||||||
+ environmentService.notificationsUrl = window.location.origin + '/notifications';
|
+ environmentService.notificationsUrl = window.location.origin + '/notifications';
|
||||||
+
|
apiService.setUrls({
|
||||||
await apiService.setUrls({
|
|
||||||
base: isDev ? null : window.location.origin,
|
base: isDev ? null : window.location.origin,
|
||||||
api: isDev ? 'http://localhost:4000' : null,
|
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 api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
|
||||||
use auth::Headers;
|
use auth::Headers;
|
||||||
use fast_chemail::is_valid_email;
|
|
||||||
use mail;
|
use mail;
|
||||||
|
|
||||||
use CONFIG;
|
use CONFIG;
|
||||||
@@ -14,6 +13,8 @@ use CONFIG;
|
|||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct RegisterData {
|
struct RegisterData {
|
||||||
Email: String,
|
Email: String,
|
||||||
|
Kdf: Option<i32>,
|
||||||
|
KdfIterations: Option<i32>,
|
||||||
Key: String,
|
Key: String,
|
||||||
Keys: Option<KeysData>,
|
Keys: Option<KeysData>,
|
||||||
MasterPasswordHash: String,
|
MasterPasswordHash: String,
|
||||||
@@ -38,16 +39,16 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
if Invitation::take(&data.Email, &conn) {
|
if Invitation::take(&data.Email, &conn) {
|
||||||
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
|
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
|
||||||
user_org.status = UserOrgStatus::Accepted as i32;
|
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
|
user
|
||||||
} else {
|
} else if CONFIG.signups_allowed {
|
||||||
if CONFIG.signups_allowed {
|
|
||||||
err!("Account with this email already exists")
|
err!("Account with this email already exists")
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed")
|
err!("Registration not allowed")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
|
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.set_password(&data.MasterPasswordHash);
|
||||||
user.key = data.Key;
|
user.key = data.Key;
|
||||||
|
|
||||||
@@ -75,9 +84,10 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
user.public_key = Some(keys.PublicKey);
|
user.public_key = Some(keys.PublicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(_) => err!("Failed to save user")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/accounts/profile")]
|
#[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,
|
Some(ref h) if h.is_empty() => None,
|
||||||
_ => data.MasterPasswordHint,
|
_ => data.MasterPasswordHint,
|
||||||
};
|
};
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(Json(user.to_json(&conn))),
|
||||||
Ok(Json(user.to_json(&conn)))
|
Err(_) => err!("Failed to save user profile")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/<uuid>/public-key")]
|
#[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.private_key = Some(data.EncryptedPrivateKey);
|
||||||
user.public_key = Some(data.PublicKey);
|
user.public_key = Some(data.PublicKey);
|
||||||
|
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(Json(user.to_json(&conn))),
|
||||||
Ok(Json(user.to_json(&conn)))
|
Err(_) => err!("Failed to save the user's keys")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
@@ -162,9 +176,40 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
|||||||
|
|
||||||
user.set_password(&data.NewMasterPasswordHash);
|
user.set_password(&data.NewMasterPasswordHash);
|
||||||
user.key = data.Key;
|
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>")]
|
#[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.reset_security_stamp();
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(_) => err!("Failed to reset security stamp")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -234,12 +280,18 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
user.set_password(&data.NewMasterPasswordHash);
|
user.set_password(&data.NewMasterPasswordHash);
|
||||||
user.key = data.Key;
|
user.key = data.Key;
|
||||||
|
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(_) => err!("Failed to save email address")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/delete", data = "<data>")]
|
#[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 {
|
fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
@@ -248,29 +300,12 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete ciphers and their attachments
|
match user.delete(&conn) {
|
||||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
Ok(()) => Ok(()),
|
||||||
if cipher.delete(&conn).is_err() {
|
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?")
|
||||||
err!("Failed deleting cipher")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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")]
|
#[get("/accounts/revision-date")]
|
||||||
fn revision_date(headers: Headers) -> String {
|
fn revision_date(headers: Headers) -> String {
|
||||||
let revision_date = headers.user.updated_at.timestamp_millis();
|
let revision_date = headers.user.updated_at.timestamp_millis();
|
||||||
@@ -287,25 +322,20 @@ struct PasswordHintData {
|
|||||||
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
|
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
|
||||||
let data: PasswordHintData = data.into_inner().data;
|
let data: PasswordHintData = data.into_inner().data;
|
||||||
|
|
||||||
if !is_valid_email(&data.Email) {
|
let hint = match User::find_by_mail(&data.Email, &conn) {
|
||||||
err!("This email address is not valid...");
|
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 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));
|
err!(format!("There have been a problem sending the email: {}", e));
|
||||||
}
|
}
|
||||||
} else if CONFIG.show_password_hint {
|
} 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));
|
err!(format!("Your password hint is: {}", &hint));
|
||||||
} else {
|
} 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 {
|
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
||||||
let data: PreloginData = data.into_inner().data;
|
let data: PreloginData = data.into_inner().data;
|
||||||
|
|
||||||
match User::find_by_mail(&data.Email, &conn) {
|
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
||||||
Some(user) => {
|
Some(user) => (user.client_kdf_type, user.client_kdf_iter),
|
||||||
let kdf_type = 0; // PBKDF2: 0
|
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
||||||
|
};
|
||||||
let _server_iter = user.password_iterations;
|
|
||||||
let client_iter = 5000; // TODO: Make iterations user configurable
|
|
||||||
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Kdf": kdf_type,
|
"Kdf": kdf_type,
|
||||||
"KdfIterations": client_iter
|
"KdfIterations": kdf_iter
|
||||||
})))
|
})))
|
||||||
},
|
|
||||||
None => err!("Invalid user"),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rocket::State;
|
||||||
use rocket::Data;
|
use rocket::Data;
|
||||||
use rocket::http::ContentType;
|
use rocket::http::ContentType;
|
||||||
|
|
||||||
@@ -16,13 +17,19 @@ use db::models::*;
|
|||||||
|
|
||||||
use crypto;
|
use crypto;
|
||||||
|
|
||||||
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase};
|
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||||
use auth::Headers;
|
use auth::Headers;
|
||||||
|
|
||||||
use CONFIG;
|
use CONFIG;
|
||||||
|
|
||||||
#[get("/sync")]
|
#[derive(FromForm)]
|
||||||
fn sync(headers: Headers, conn: DbConn) -> JsonResult {
|
#[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 user_json = headers.user.to_json(&conn);
|
||||||
|
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &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 = Cipher::find_by_user(&headers.user.uuid, &conn);
|
||||||
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||||
|
|
||||||
let domains_json = 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!({
|
Ok(Json(json!({
|
||||||
"Profile": user_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")]
|
#[get("/ciphers")]
|
||||||
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
@@ -56,6 +70,7 @@ fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": ciphers_json,
|
"Data": ciphers_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
|
"ContinuationToken": null
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,22 +132,22 @@ pub struct CipherData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/admin", data = "<data>")]
|
#[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
|
// TODO: Implement this correctly
|
||||||
post_ciphers(data, headers, conn)
|
post_ciphers(data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers", data = "<data>")]
|
#[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 data: CipherData = data.into_inner().data;
|
||||||
|
|
||||||
let mut cipher = Cipher::new(data.Type, data.Name.clone());
|
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)))
|
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 {
|
if let Some(org_id) = data.OrganizationId {
|
||||||
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
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"),
|
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.data = type_data.to_string();
|
||||||
cipher.password_history = data.PasswordHistory.map(|f| f.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() {
|
if cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn).is_err() {
|
||||||
err!("Error saving the folder information")
|
err!("Error saving the folder information")
|
||||||
@@ -219,15 +238,19 @@ struct RelationsData {
|
|||||||
|
|
||||||
|
|
||||||
#[post("/ciphers/import", data = "<data>")]
|
#[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;
|
let data: ImportData = data.into_inner().data;
|
||||||
|
|
||||||
// Read and create the folders
|
// Read and create the folders
|
||||||
let folders: Vec<_> = data.Folders.into_iter().map(|folder| {
|
let mut folders: Vec<_> = Vec::new();
|
||||||
let mut folder = Folder::new(headers.user.uuid.clone(), folder.Name);
|
for folder in data.Folders.into_iter() {
|
||||||
folder.save(&conn);
|
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.Name);
|
||||||
folder
|
if new_folder.save(&conn).is_err() {
|
||||||
}).collect();
|
err!("Failed importing folders")
|
||||||
|
} else {
|
||||||
|
folders.push(new_folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read the relations between folders and ciphers
|
// Read the relations between folders and ciphers
|
||||||
use std::collections::HashMap;
|
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());
|
.map(|i| folders[*i].uuid.clone());
|
||||||
|
|
||||||
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.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();
|
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>")]
|
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
||||||
fn put_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn put_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||||
put_cipher(uuid, data, headers, conn)
|
put_cipher(uuid, data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/admin", data = "<data>")]
|
#[post("/ciphers/<uuid>/admin", data = "<data>")]
|
||||||
fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||||
post_cipher(uuid, data, headers, conn)
|
post_cipher(uuid, data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>", data = "<data>")]
|
#[post("/ciphers/<uuid>", data = "<data>")]
|
||||||
fn post_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn post_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||||
put_cipher(uuid, data, headers, conn)
|
put_cipher(uuid, data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>", data = "<data>")]
|
#[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 data: CipherData = data.into_inner().data;
|
||||||
|
|
||||||
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
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")
|
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)))
|
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) => {
|
Some(collection) => {
|
||||||
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
||||||
if posted_collections.contains(&collection.uuid) { // Add to collection
|
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
|
} 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 {
|
} else {
|
||||||
err!("No rights to modify the collection")
|
err!("No rights to modify the collection")
|
||||||
@@ -349,17 +378,17 @@ struct ShareCipherData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/share", data = "<data>")]
|
#[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;
|
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>")]
|
#[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;
|
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)]
|
#[derive(Deserialize)]
|
||||||
@@ -370,15 +399,15 @@ struct ShareSelectedCipherData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/share", data = "<data>")]
|
#[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 data: ShareSelectedCipherData = data.into_inner().data;
|
||||||
let mut cipher_ids: Vec<String> = Vec::new();
|
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.")
|
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.")
|
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);
|
let attachments = Attachment::find_by_ciphers(cipher_ids, &conn);
|
||||||
|
|
||||||
if attachments.len() > 0 {
|
if !attachments.is_empty() {
|
||||||
err!("Ciphers should not have any attachments.")
|
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() {
|
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")
|
None => err!("Request missing ids field")
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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) {
|
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||||
Some(cipher) => {
|
Some(cipher) => {
|
||||||
if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
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) => {
|
Some(organization_uuid) => {
|
||||||
let mut shared_to_collection = false;
|
let mut shared_to_collection = false;
|
||||||
for uuid in &data.CollectionIds {
|
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"),
|
None => err!("Invalid collection ID provided"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
||||||
if collection.org_uuid == organization_uuid {
|
match CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn) {
|
||||||
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn);
|
Ok(()) => (),
|
||||||
|
Err(_) => err!("Failed to add cipher to collection")
|
||||||
|
};
|
||||||
shared_to_collection = true;
|
shared_to_collection = true;
|
||||||
} else {
|
|
||||||
err!("Collection does not belong to organization")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
err!("No rights to modify the collection")
|
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)))
|
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>")]
|
#[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 {
|
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)?;
|
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &ws)?;
|
||||||
post_attachment(uuid, data, content_type, headers, conn)
|
post_attachment(uuid, data, content_type, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
|
||||||
fn delete_attachment_post_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
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)
|
delete_attachment(uuid, attachment_id, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
|
||||||
fn delete_attachment_post(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_attachment_post(uuid: String, attachment_id: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
delete_attachment(uuid, attachment_id, headers, conn)
|
delete_attachment(uuid, attachment_id, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
|
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
|
||||||
fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
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)
|
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
|
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
|
||||||
fn delete_attachment_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
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)
|
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete")]
|
#[post("/ciphers/<uuid>/delete")]
|
||||||
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete-admin")]
|
#[post("/ciphers/<uuid>/delete-admin")]
|
||||||
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>")]
|
#[delete("/ciphers/<uuid>")]
|
||||||
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>/admin")]
|
#[delete("/ciphers/<uuid>/admin")]
|
||||||
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, &ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers", data = "<data>")]
|
#[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 data: Value = data.into_inner().data;
|
||||||
|
|
||||||
let uuids = match data.get("Ids") {
|
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 {
|
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;
|
return error;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -576,12 +605,12 @@ fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/delete", data = "<data>")]
|
#[post("/ciphers/delete", data = "<data>")]
|
||||||
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
delete_cipher_selected(data, headers, conn)
|
delete_cipher_selected(data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/move", data = "<data>")]
|
#[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 data = data.into_inner().data;
|
||||||
|
|
||||||
let folder_id = match data.get("FolderId") {
|
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() {
|
if cipher.move_to_folder(folder_id.clone(), &headers.user.uuid, &conn).is_err() {
|
||||||
err!("Error saving the folder information")
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/move", data = "<data>")]
|
#[put("/ciphers/move", data = "<data>")]
|
||||||
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
move_cipher_selected(data, headers, conn)
|
move_cipher_selected(data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/purge", data = "<data>")]
|
#[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 data: PasswordData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
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() {
|
if cipher.delete(&conn).is_err() {
|
||||||
err!("Failed deleting cipher")
|
err!("Failed deleting cipher")
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete folders
|
// Delete folders
|
||||||
@@ -660,13 +696,16 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
|||||||
if f.delete(&conn).is_err() {
|
if f.delete(&conn).is_err() {
|
||||||
err!("Failed deleting folder")
|
err!("Failed deleting folder")
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
ws.send_folder_update(UpdateType::SyncFolderCreate, &f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) -> EmptyResult {
|
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, ws: &State<WebSocketUsers>) -> EmptyResult {
|
||||||
let cipher = match Cipher::find_by_uuid(uuid, conn) {
|
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||||
Some(cipher) => cipher,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
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")
|
err!("Cipher can't be deleted by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
match cipher.delete(conn) {
|
match cipher.delete(&conn) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => {
|
||||||
|
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Err(_) => err!("Failed deleting cipher")
|
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) {
|
let attachment = match Attachment::find_by_id(&attachment_id, &conn) {
|
||||||
Some(attachment) => attachment,
|
Some(attachment) => attachment,
|
||||||
None => err!("Attachment doesn't exist")
|
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
|
// Delete attachment
|
||||||
match attachment.delete(&conn) {
|
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")
|
Err(_) => err!("Deleting attachement failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
|
use rocket::State;
|
||||||
use rocket_contrib::{Json, Value};
|
use rocket_contrib::{Json, Value};
|
||||||
|
|
||||||
use db::DbConn;
|
use db::DbConn;
|
||||||
use db::models::*;
|
use db::models::*;
|
||||||
|
|
||||||
use api::{JsonResult, EmptyResult, JsonUpcase};
|
use api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||||
use auth::Headers;
|
use auth::Headers;
|
||||||
|
|
||||||
#[get("/folders")]
|
#[get("/folders")]
|
||||||
@@ -15,6 +16,7 @@ fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": folders_json,
|
"Data": folders_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
|
"ContinuationToken": null,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,23 +42,26 @@ pub struct FolderData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders", data = "<data>")]
|
#[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 data: FolderData = data.into_inner().data;
|
||||||
|
|
||||||
let mut folder = Folder::new(headers.user.uuid.clone(), data.Name);
|
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()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders/<uuid>", data = "<data>")]
|
#[post("/folders/<uuid>", data = "<data>")]
|
||||||
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||||
put_folder(uuid, data, headers, conn)
|
put_folder(uuid, data, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/folders/<uuid>", data = "<data>")]
|
#[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 data: FolderData = data.into_inner().data;
|
||||||
|
|
||||||
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
|
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.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()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/folders/<uuid>/delete")]
|
#[post("/folders/<uuid>/delete")]
|
||||||
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||||
delete_folder(uuid, headers, conn)
|
delete_folder(uuid, headers, conn, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/folders/<uuid>")]
|
#[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) {
|
let folder = match Folder::find_by_uuid(&uuid, &conn) {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
_ => err!("Invalid folder")
|
_ => err!("Invalid folder")
|
||||||
@@ -93,7 +101,10 @@ fn delete_folder(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
|||||||
|
|
||||||
// Delete the actual folder entry
|
// Delete the actual folder entry
|
||||||
match folder.delete(&conn) {
|
match folder.delete(&conn) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => {
|
||||||
|
ws.send_folder_update(UpdateType::SyncFolderDelete, &folder);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Err(_) => err!("Failed deleting folder")
|
Err(_) => err!("Failed deleting folder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -260,7 +260,8 @@
|
|||||||
"Type": 26,
|
"Type": 26,
|
||||||
"Domains": [
|
"Domains": [
|
||||||
"steampowered.com",
|
"steampowered.com",
|
||||||
"steamcommunity.com"
|
"steamcommunity.com",
|
||||||
|
"steamgames.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
|
@@ -19,15 +19,18 @@ pub fn routes() -> Vec<Route> {
|
|||||||
get_public_keys,
|
get_public_keys,
|
||||||
post_keys,
|
post_keys,
|
||||||
post_password,
|
post_password,
|
||||||
|
post_kdf,
|
||||||
post_sstamp,
|
post_sstamp,
|
||||||
post_email_token,
|
post_email_token,
|
||||||
post_email,
|
post_email,
|
||||||
delete_account,
|
delete_account,
|
||||||
|
post_delete_account,
|
||||||
revision_date,
|
revision_date,
|
||||||
password_hint,
|
password_hint,
|
||||||
prelogin,
|
prelogin,
|
||||||
|
|
||||||
sync,
|
sync,
|
||||||
|
sync_no_query,
|
||||||
|
|
||||||
get_ciphers,
|
get_ciphers,
|
||||||
get_cipher,
|
get_cipher,
|
||||||
@@ -77,6 +80,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
activate_authenticator,
|
activate_authenticator,
|
||||||
activate_authenticator_put,
|
activate_authenticator_put,
|
||||||
generate_u2f,
|
generate_u2f,
|
||||||
|
generate_u2f_challenge,
|
||||||
activate_u2f,
|
activate_u2f,
|
||||||
activate_u2f_put,
|
activate_u2f_put,
|
||||||
|
|
||||||
@@ -110,6 +114,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
put_organization_user,
|
put_organization_user,
|
||||||
delete_user,
|
delete_user,
|
||||||
post_delete_user,
|
post_delete_user,
|
||||||
|
post_reinvite_user,
|
||||||
post_org_import,
|
post_org_import,
|
||||||
|
|
||||||
clear_device_token,
|
clear_device_token,
|
||||||
@@ -117,6 +122,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
|
|
||||||
get_eq_domains,
|
get_eq_domains,
|
||||||
post_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")
|
err!("Device not owned by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
device.delete(&conn);
|
match device.delete(&conn) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(_) => err!("Failed deleting device")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||||
@@ -211,7 +218,7 @@ struct EquivDomainData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/settings/domains", data = "<data>")]
|
#[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 data: EquivDomainData = data.into_inner().data;
|
||||||
|
|
||||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
|
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.excluded_globals = to_string(&excluded_globals).unwrap_or("[]".to_string());
|
||||||
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or("[]".to_string());
|
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or("[]".to_string());
|
||||||
|
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(Json(json!({}))),
|
||||||
Ok(())
|
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 rocket_contrib::{Json, Value};
|
||||||
use CONFIG;
|
use CONFIG;
|
||||||
use db::DbConn;
|
use db::DbConn;
|
||||||
use db::models::*;
|
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 auth::{Headers, AdminHeaders, OwnerHeaders};
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer};
|
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.type_ = UserOrgType::Owner as i32;
|
||||||
user_org.status = UserOrgStatus::Confirmed as i32;
|
user_org.status = UserOrgStatus::Confirmed as i32;
|
||||||
|
|
||||||
org.save(&conn);
|
if org.save(&conn).is_err() {
|
||||||
user_org.save(&conn);
|
err!("Failed creating organization")
|
||||||
collection.save(&conn);
|
}
|
||||||
|
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()))
|
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.name = data.Name;
|
||||||
org.billing_email = data.BillingEmail;
|
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
|
// GET /api/collections?writeOnly=false
|
||||||
@@ -140,7 +150,8 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(Collection::to_json)
|
.map(Collection::to_json)
|
||||||
.collect::<Value>(),
|
.collect::<Value>(),
|
||||||
"Object": "list"
|
"Object": "list",
|
||||||
|
"ContinuationToken": null,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +163,8 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
|
|||||||
.iter()
|
.iter()
|
||||||
.map(Collection::to_json)
|
.map(Collection::to_json)
|
||||||
.collect::<Value>(),
|
.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);
|
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()))
|
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.name = data.Name.clone();
|
||||||
collection.save(&conn);
|
if collection.save(&conn).is_err() {
|
||||||
|
err!("Failed updating Collection");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
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| {
|
.iter().map(|col_user| {
|
||||||
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_json_collection_user_details(&col_user.read_only, &conn)
|
.to_json_collection_user_details(col_user.read_only, &conn)
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": user_list,
|
"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!({
|
Ok(Json(json!({
|
||||||
"Data": ciphers_json,
|
"Data": ciphers_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
|
"ContinuationToken": null,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/users")]
|
#[get("/organizations/<org_id>/users")]
|
||||||
fn get_org_users(org_id: String, headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
let users = UserOrganization::find_by_org(&org_id, &conn);
|
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();
|
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": users_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) {
|
match invitation.save(&conn) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let mut user = User::new(email.clone());
|
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_org_status = UserOrgStatus::Invited as i32;
|
||||||
user
|
user
|
||||||
} else {
|
|
||||||
err!("Failed to create placeholder for invited user")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => err!(format!("Failed to invite: {}", email))
|
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 mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||||
let access_all = data.AccessAll.unwrap_or(false);
|
let access_all = data.AccessAll.unwrap_or(false);
|
||||||
new_user.access_all = access_all;
|
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(())
|
Ok(())
|
||||||
@@ -448,9 +467,10 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
|
|||||||
None => err!("Invalid key provided")
|
None => err!("Invalid key provided")
|
||||||
};
|
};
|
||||||
|
|
||||||
user_to_confirm.save(&conn);
|
match user_to_confirm.save(&conn) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(_) => err!("Failed to add user to organization")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/users/<org_user_id>")]
|
#[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);
|
match user_to_edit.save(&conn) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(_) => err!("Failed to save user data")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/organizations/<org_id>/users/<org_user_id>")]
|
#[delete("/organizations/<org_id>/users/<org_user_id>")]
|
||||||
fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
|
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) {
|
let user_to_delete = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => err!("User to delete isn't member of the organization")
|
None => err!("User to delete isn't member of the organization")
|
||||||
@@ -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)
|
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::CipherData;
|
||||||
use super::ciphers::update_cipher_from_data;
|
use super::ciphers::update_cipher_from_data;
|
||||||
|
|
||||||
@@ -601,7 +644,7 @@ struct RelationsData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/import-organization?<query>", data = "<data>")]
|
#[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 data: ImportData = data.into_inner().data;
|
||||||
let org_id = query.organizationId;
|
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
|
// Read and create the collections
|
||||||
let collections: Vec<_> = data.Collections.into_iter().map(|coll| {
|
let collections: Vec<_> = data.Collections.into_iter().map(|coll| {
|
||||||
let mut collection = Collection::new(org_id.clone(), coll.Name);
|
let mut collection = Collection::new(org_id.clone(), coll.Name);
|
||||||
collection.save(&conn);
|
if collection.save(&conn).is_err() {
|
||||||
collection
|
err!("Failed to create Collection");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(collection)
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Read the relations between collections and ciphers
|
// 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
|
// Read and create the ciphers
|
||||||
let ciphers: Vec<_> = data.Ciphers.into_iter().map(|cipher_data| {
|
let ciphers: Vec<_> = data.Ciphers.into_iter().map(|cipher_data| {
|
||||||
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
|
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
|
cipher
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Assign the collections
|
// Assign the collections
|
||||||
for (cipher_index, coll_index) in relations {
|
for (cipher_index, coll_index) in relations {
|
||||||
let cipher_id = &ciphers[cipher_index].uuid;
|
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;
|
let mut user = headers.user;
|
||||||
|
@@ -19,7 +19,8 @@ fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": twofactors_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
|
// Remove the recovery code, not needed without twofactors
|
||||||
user.totp_recover = None;
|
user.totp_recover = None;
|
||||||
user.save(&conn);
|
match user.save(&conn) {
|
||||||
|
Ok(()) => Ok(Json(json!({}))),
|
||||||
Ok(Json(json!({})))
|
Err(_) => err!("Failed to remove the user's two factor recovery code")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -216,7 +218,9 @@ fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
|||||||
if user.totp_recover.is_none() {
|
if user.totp_recover.is_none() {
|
||||||
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
||||||
user.totp_recover = Some(totp_recover);
|
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 user_uuid = &headers.user.uuid;
|
||||||
|
|
||||||
let u2f_type = TwoFactorType::U2f as i32;
|
let u2f_type = TwoFactorType::U2f as i32;
|
||||||
let register_type = TwoFactorType::U2fRegisterChallenge;
|
let enabled = TwoFactor::find_by_user_and_type(user_uuid, u2f_type, &conn).is_some();
|
||||||
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)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": enabled,
|
"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,
|
"UserId": headers.user.uuid,
|
||||||
"AppId": APP_ID.to_string(),
|
"AppId": APP_ID.to_string(),
|
||||||
"Challenge": challenge,
|
"Challenge": challenge,
|
||||||
"Version": U2F_VERSION,
|
"Version": U2F_VERSION,
|
||||||
},
|
|
||||||
"Object": "twoFactorU2f"
|
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +304,7 @@ impl RegisterResponseCopy {
|
|||||||
RegisterResponse {
|
RegisterResponse {
|
||||||
registration_data: self.registration_data,
|
registration_data: self.registration_data,
|
||||||
version: self.version,
|
version: self.version,
|
||||||
challenge: challenge,
|
challenge,
|
||||||
client_data: self.client_data,
|
client_data: self.client_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ fn icon(domain: String) -> Content<Vec<u8>> {
|
|||||||
let icon_type = ContentType::new("image", "x-icon");
|
let icon_type = ContentType::new("image", "x-icon");
|
||||||
|
|
||||||
// Validate the domain to avoid directory traversal attacks
|
// 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());
|
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 orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||||
|
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||||
device.save(&conn);
|
match device.save(&conn) {
|
||||||
|
Ok(()) => Ok(Json(json!({
|
||||||
Ok(Json(json!({
|
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"expires_in": expires_in,
|
"expires_in": expires_in,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"refresh_token": device.refresh_token,
|
"refresh_token": device.refresh_token,
|
||||||
"Key": user.key,
|
"Key": user.key,
|
||||||
"PrivateKey": user.private_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 {
|
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) => {
|
Some(device) => {
|
||||||
// Check if valid device
|
// Check if valid device
|
||||||
if device.user_uuid != user.uuid {
|
if device.user_uuid != user.uuid {
|
||||||
device.delete(&conn);
|
match device.delete(&conn) {
|
||||||
err!("Device is not owned by user")
|
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
|
device
|
||||||
}
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
// Create new device
|
// Create new device
|
||||||
Device::new(device_id, user.uuid.clone(), device_name, device_type_num)
|
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 orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||||
|
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
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!({
|
let mut result = json!({
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
@@ -158,11 +163,11 @@ fn twofactor_auth(
|
|||||||
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
||||||
|
|
||||||
// No twofactor token if twofactor is disabled
|
// No twofactor token if twofactor is disabled
|
||||||
if twofactors.len() == 0 {
|
if twofactors.is_empty() {
|
||||||
return Ok(None);
|
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,
|
Some(provider) => provider,
|
||||||
None => providers[0], // If we aren't given a two factor provider, asume the first one
|
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"),
|
_ => 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()))
|
Ok(Some(device.refresh_twofactor_remember()))
|
||||||
} else {
|
} else {
|
||||||
device.delete_twofactor_remember();
|
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> {
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||||
let headers = request.headers();
|
let headers = request.headers();
|
||||||
let type_opt = headers.get_one("Device-Type");
|
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))
|
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::identity::routes as identity_routes;
|
||||||
pub use self::web::routes as web_routes;
|
pub use self::web::routes as web_routes;
|
||||||
pub use self::notifications::routes as notifications_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::response::status::BadRequest;
|
||||||
use rocket_contrib::Json;
|
use rocket_contrib::Json;
|
||||||
|
@@ -1,31 +1,373 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::Json;
|
use rocket_contrib::Json;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use db::DbConn;
|
|
||||||
use api::JsonResult;
|
use api::JsonResult;
|
||||||
use auth::Headers;
|
use auth::Headers;
|
||||||
|
use db::DbConn;
|
||||||
|
|
||||||
|
use CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
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")]
|
#[post("/hub/negotiate")]
|
||||||
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
||||||
use data_encoding::BASE64URL;
|
|
||||||
use crypto;
|
use crypto;
|
||||||
|
use data_encoding::BASE64URL;
|
||||||
|
|
||||||
// Store this in db?
|
|
||||||
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
|
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
|
// TODO: Implement transports
|
||||||
// Rocket WS support: https://github.com/SergioBenitez/Rocket/issues/90
|
// Rocket WS support: https://github.com/SergioBenitez/Rocket/issues/90
|
||||||
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
// 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":"ServerSentEvents", "transferFormats":["Text"]},
|
||||||
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
// {"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 rocket::request::{self, Request, FromRequest};
|
||||||
|
|
||||||
use db::DbConn;
|
use db::DbConn;
|
||||||
use db::models::{User, UserOrganization, UserOrgType, UserOrgStatus, Device};
|
use db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
|
||||||
|
|
||||||
pub struct Headers {
|
pub struct Headers {
|
||||||
pub host: String,
|
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")
|
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{
|
Outcome::Success(Self{
|
||||||
|
@@ -78,7 +78,7 @@ impl Attachment {
|
|||||||
println!("ERROR: Failed with 10 retries");
|
println!("ERROR: Failed with 10 retries");
|
||||||
return Err(err)
|
return Err(err)
|
||||||
} else {
|
} else {
|
||||||
retries = retries - 1;
|
retries -= 1;
|
||||||
println!("Had to retry! Retries left: {}", retries);
|
println!("Had to retry! Retries left: {}", retries);
|
||||||
thread::sleep(time::Duration::from_millis(500));
|
thread::sleep(time::Duration::from_millis(500));
|
||||||
continue
|
continue
|
||||||
|
@@ -130,34 +130,38 @@ impl Cipher {
|
|||||||
json_object
|
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 {
|
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
|
None => { // Belongs to Organization, need to update affected users
|
||||||
if let Some(ref org_uuid) = self.organization_uuid {
|
if let Some(ref org_uuid) = self.organization_uuid {
|
||||||
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn)
|
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn)
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|user_org| {
|
.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.update_users_revision(conn);
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
match diesel::replace_into(ciphers::table)
|
diesel::replace_into(ciphers::table)
|
||||||
.values(&*self)
|
.values(&*self)
|
||||||
.execute(&**conn) {
|
.execute(&**conn)
|
||||||
Ok(1) => true, // One row inserted
|
.and(Ok(()))
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
|
||||||
self.update_users_revision(conn);
|
self.update_users_revision(conn);
|
||||||
|
|
||||||
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
||||||
@@ -166,7 +170,7 @@ impl Cipher {
|
|||||||
|
|
||||||
diesel::delete(
|
diesel::delete(
|
||||||
ciphers::table.filter(
|
ciphers::table.filter(
|
||||||
ciphers::uuid.eq(self.uuid)
|
ciphers::uuid.eq(&self.uuid)
|
||||||
)
|
)
|
||||||
).execute(&**conn).and(Ok(()))
|
).execute(&**conn).and(Ok(()))
|
||||||
}
|
}
|
||||||
@@ -178,6 +182,13 @@ impl Cipher {
|
|||||||
Ok(())
|
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> {
|
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) {
|
match self.get_folder_uuid(&user_uuid, &conn) {
|
||||||
None => {
|
None => {
|
||||||
@@ -356,6 +367,6 @@ impl Cipher {
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
.select(ciphers_collections::collection_uuid)
|
.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
|
/// Database methods
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||||
match diesel::replace_into(collections::table)
|
// 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)
|
.values(&*self)
|
||||||
.execute(&**conn) {
|
.execute(&**conn)
|
||||||
Ok(1) => true, // One row inserted
|
.and(Ok(()))
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||||
@@ -254,25 +259,19 @@ pub struct CollectionCipher {
|
|||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl CollectionCipher {
|
impl CollectionCipher {
|
||||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
|
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||||
match diesel::replace_into(ciphers_collections::table)
|
diesel::replace_into(ciphers_collections::table)
|
||||||
.values((
|
.values((
|
||||||
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
||||||
ciphers_collections::collection_uuid.eq(collection_uuid),
|
ciphers_collections::collection_uuid.eq(collection_uuid),
|
||||||
)).execute(&**conn) {
|
)).execute(&**conn).and(Ok(()))
|
||||||
Ok(1) => true, // One row inserted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
|
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||||
match diesel::delete(ciphers_collections::table
|
diesel::delete(ciphers_collections::table
|
||||||
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
||||||
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
||||||
.execute(&**conn) {
|
.execute(&**conn).and(Ok(()))
|
||||||
Ok(1) => true, // One row deleted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||||
|
@@ -112,24 +112,24 @@ use db::schema::devices;
|
|||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Device {
|
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();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
match diesel::replace_into(devices::table)
|
diesel::replace_into(devices::table)
|
||||||
.values(&*self)
|
.values(&*self).execute(&**conn).and(Ok(()))
|
||||||
.execute(&**conn) {
|
|
||||||
Ok(1) => true, // One row inserted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> bool {
|
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||||
match diesel::delete(devices::table.filter(
|
diesel::delete(devices::table.filter(
|
||||||
devices::uuid.eq(self.uuid)))
|
devices::uuid.eq(self.uuid)
|
||||||
.execute(&**conn) {
|
)).execute(&**conn).and(Ok(()))
|
||||||
Ok(1) => true, // One row deleted
|
|
||||||
_ => false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
@@ -70,29 +70,32 @@ use db::schema::{folders, folders_ciphers};
|
|||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Folder {
|
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);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
match diesel::replace_into(folders::table)
|
diesel::replace_into(folders::table)
|
||||||
.values(&*self)
|
.values(&*self).execute(&**conn).and(Ok(()))
|
||||||
.execute(&**conn) {
|
|
||||||
Ok(1) => true, // One row inserted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
|
||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
||||||
|
|
||||||
diesel::delete(
|
diesel::delete(
|
||||||
folders::table.filter(
|
folders::table.filter(
|
||||||
folders::uuid.eq(self.uuid)
|
folders::uuid.eq(&self.uuid)
|
||||||
)
|
)
|
||||||
).execute(&**conn).and(Ok(()))
|
).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> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
folders::table
|
folders::table
|
||||||
.filter(folders::uuid.eq(uuid))
|
.filter(folders::uuid.eq(uuid))
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use super::{User, CollectionUser};
|
use super::{User, CollectionUser, Invitation};
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||||
#[table_name = "organizations"]
|
#[table_name = "organizations"]
|
||||||
@@ -51,6 +51,8 @@ impl UserOrgType {
|
|||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl Organization {
|
impl Organization {
|
||||||
|
pub const VIRTUAL_ID: &'static str = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
pub fn new(name: String, billing_email: String) -> Self {
|
pub fn new(name: String, billing_email: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uuid: Uuid::new_v4().to_string(),
|
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 {
|
pub fn to_json(&self) -> JsonValue {
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"Seats": 10,
|
"Seats": 10,
|
||||||
"MaxCollections": 10,
|
"MaxCollections": 10,
|
||||||
|
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||||
"Use2fa": true,
|
"Use2fa": true,
|
||||||
"UseDirectory": false,
|
"UseDirectory": false,
|
||||||
"UseEvents": false,
|
"UseEvents": false,
|
||||||
@@ -83,7 +93,7 @@ impl Organization {
|
|||||||
"BillingEmail": self.billing_email,
|
"BillingEmail": self.billing_email,
|
||||||
"Plan": "TeamsAnnually",
|
"Plan": "TeamsAnnually",
|
||||||
"PlanType": 5, // TeamsAnnually plan
|
"PlanType": 5, // TeamsAnnually plan
|
||||||
|
"UsersGetPremium": true,
|
||||||
"Object": "organization",
|
"Object": "organization",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -103,6 +113,20 @@ impl UserOrganization {
|
|||||||
type_: UserOrgType::User as i32,
|
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
|
/// Database methods
|
||||||
impl Organization {
|
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)
|
UserOrganization::find_by_org(&self.uuid, conn)
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|user_org| {
|
.for_each(|user_org| {
|
||||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||||
});
|
});
|
||||||
|
|
||||||
match diesel::replace_into(organizations::table)
|
diesel::replace_into(organizations::table)
|
||||||
.values(&*self)
|
.values(&*self).execute(&**conn).and(Ok(()))
|
||||||
.execute(&**conn) {
|
|
||||||
Ok(1) => true, // One row inserted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||||
use super::{Cipher, Collection};
|
use super::{Cipher, Collection};
|
||||||
|
|
||||||
|
if self.uuid == Organization::VIRTUAL_ID {
|
||||||
|
return Err(diesel::result::Error::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
Cipher::delete_all_by_organization(&self.uuid, &conn)?;
|
Cipher::delete_all_by_organization(&self.uuid, &conn)?;
|
||||||
Collection::delete_all_by_organization(&self.uuid, &conn)?;
|
Collection::delete_all_by_organization(&self.uuid, &conn)?;
|
||||||
UserOrganization::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> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
if uuid == Organization::VIRTUAL_ID {
|
||||||
|
return Some(Self::new_virtual())
|
||||||
|
};
|
||||||
organizations::table
|
organizations::table
|
||||||
.filter(organizations::uuid.eq(uuid))
|
.filter(organizations::uuid.eq(uuid))
|
||||||
.first::<Self>(&**conn).ok()
|
.first::<Self>(&**conn).ok()
|
||||||
@@ -158,6 +189,7 @@ impl UserOrganization {
|
|||||||
"Name": org.name,
|
"Name": org.name,
|
||||||
"Seats": 10,
|
"Seats": 10,
|
||||||
"MaxCollections": 10,
|
"MaxCollections": 10,
|
||||||
|
"UsersGetPremium": true,
|
||||||
|
|
||||||
"Use2fa": true,
|
"Use2fa": true,
|
||||||
"UseDirectory": false,
|
"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();
|
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
|
||||||
|
|
||||||
json!({
|
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);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
|
|
||||||
match diesel::replace_into(users_organizations::table)
|
diesel::replace_into(users_organizations::table)
|
||||||
.values(&*self)
|
.values(&*self).execute(&**conn).and(Ok(()))
|
||||||
.execute(&**conn) {
|
|
||||||
Ok(1) => true, // One row inserted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
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);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
|
|
||||||
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
|
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
|
||||||
@@ -260,6 +294,13 @@ impl UserOrganization {
|
|||||||
Ok(())
|
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 {
|
pub fn has_full_access(self) -> bool {
|
||||||
self.access_all || self.type_ < UserOrgType::User as i32
|
self.access_all || self.type_ < UserOrgType::User as i32
|
||||||
}
|
}
|
||||||
@@ -281,21 +322,40 @@ impl UserOrganization {
|
|||||||
users_organizations::table
|
users_organizations::table
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
.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> {
|
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
|
.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> {
|
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
|
users_organizations::table
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
@@ -330,6 +390,22 @@ impl UserOrganization {
|
|||||||
.select(users_organizations::all_columns)
|
.select(users_organizations::all_columns)
|
||||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
.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 equivalent_domains: String,
|
||||||
pub excluded_globals: String,
|
pub excluded_globals: String,
|
||||||
|
|
||||||
|
pub client_kdf_type: i32,
|
||||||
|
pub client_kdf_iter: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl User {
|
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 {
|
pub fn new(mail: String) -> Self {
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
let email = mail.to_lowercase();
|
let email = mail.to_lowercase();
|
||||||
|
|
||||||
let iterations = CONFIG.password_iterations;
|
|
||||||
let salt = crypto::get_random_64();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
uuid: Uuid::new_v4().to_string(),
|
uuid: Uuid::new_v4().to_string(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
@@ -55,8 +58,8 @@ impl User {
|
|||||||
key: String::new(),
|
key: String::new(),
|
||||||
|
|
||||||
password_hash: Vec::new(),
|
password_hash: Vec::new(),
|
||||||
salt,
|
salt: crypto::get_random_64(),
|
||||||
password_iterations: iterations,
|
password_iterations: CONFIG.password_iterations,
|
||||||
|
|
||||||
security_stamp: Uuid::new_v4().to_string(),
|
security_stamp: Uuid::new_v4().to_string(),
|
||||||
|
|
||||||
@@ -69,6 +72,9 @@ impl User {
|
|||||||
|
|
||||||
equivalent_domains: "[]".to_string(),
|
equivalent_domains: "[]".to_string(),
|
||||||
excluded_globals: "[]".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) {
|
pub fn reset_security_stamp(&mut self) {
|
||||||
self.security_stamp = Uuid::new_v4().to_string();
|
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;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use db::DbConn;
|
use db::DbConn;
|
||||||
use db::schema::{users, invitations};
|
use db::schema::{users, invitations};
|
||||||
|
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl User {
|
impl User {
|
||||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||||
use super::UserOrganization;
|
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
|
||||||
use super::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 orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||||
|
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||||
let twofactor_enabled = TwoFactor::find_by_user(&self.uuid, conn).len() > 0;
|
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"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();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
match diesel::replace_into(users::table) // Insert or update
|
diesel::replace_into(users::table) // Insert or update
|
||||||
.values(&*self)
|
.values(&*self).execute(&**conn).and(Ok(()))
|
||||||
.execute(&**conn) {
|
}
|
||||||
Ok(1) => true, // One row inserted
|
|
||||||
_ => false,
|
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 {
|
UserOrganization::delete_all_by_user(&self.uuid, &*conn)?;
|
||||||
match diesel::delete(users::table.filter(
|
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)))
|
users::uuid.eq(self.uuid)))
|
||||||
.execute(&**conn) {
|
.execute(&**conn).and(Ok(()))
|
||||||
Ok(1) => true, // One row deleted
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
||||||
@@ -184,6 +209,11 @@ impl User {
|
|||||||
.filter(users::uuid.eq(uuid))
|
.filter(users::uuid.eq(uuid))
|
||||||
.first::<Self>(&**conn).ok()
|
.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)]
|
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||||
|
@@ -72,6 +72,12 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
invitations (email) {
|
||||||
|
email -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
organizations (uuid) {
|
organizations (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -110,12 +116,8 @@ table! {
|
|||||||
security_stamp -> Text,
|
security_stamp -> Text,
|
||||||
equivalent_domains -> Text,
|
equivalent_domains -> Text,
|
||||||
excluded_globals -> Text,
|
excluded_globals -> Text,
|
||||||
}
|
client_kdf_type -> Integer,
|
||||||
}
|
client_kdf_iter -> Integer,
|
||||||
|
|
||||||
table! {
|
|
||||||
invitations (email) {
|
|
||||||
email -> Text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +166,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
devices,
|
devices,
|
||||||
folders,
|
folders,
|
||||||
folders_ciphers,
|
folders_ciphers,
|
||||||
|
invitations,
|
||||||
organizations,
|
organizations,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
|
49
src/mail.rs
49
src/mail.rs
@@ -1,7 +1,6 @@
|
|||||||
use std::error::Error;
|
|
||||||
use native_tls::{Protocol, TlsConnector};
|
use native_tls::{Protocol, TlsConnector};
|
||||||
use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity};
|
use lettre::{Transport, SmtpTransport, SmtpClient, ClientTlsParameters, ClientSecurity};
|
||||||
use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder};
|
use lettre::smtp::ConnectionReuseParameters;
|
||||||
use lettre::smtp::authentication::Credentials;
|
use lettre::smtp::authentication::Credentials;
|
||||||
use lettre_email::EmailBuilder;
|
use lettre_email::EmailBuilder;
|
||||||
|
|
||||||
@@ -9,31 +8,30 @@ use MailConfig;
|
|||||||
|
|
||||||
fn mailer(config: &MailConfig) -> SmtpTransport {
|
fn mailer(config: &MailConfig) -> SmtpTransport {
|
||||||
let client_security = if config.smtp_ssl {
|
let client_security = if config.smtp_ssl {
|
||||||
let mut tls_builder = TlsConnector::builder().unwrap();
|
let tls = TlsConnector::builder()
|
||||||
tls_builder.supported_protocols(&[Protocol::Tlsv11, Protocol::Tlsv12]).unwrap();
|
.min_protocol_version(Some(Protocol::Tlsv11))
|
||||||
ClientSecurity::Required(
|
.build()
|
||||||
ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap())
|
.unwrap();
|
||||||
)
|
|
||||||
|
ClientSecurity::Required(ClientTlsParameters::new(config.smtp_host.clone(), tls))
|
||||||
} else {
|
} else {
|
||||||
ClientSecurity::None
|
ClientSecurity::None
|
||||||
};
|
};
|
||||||
|
|
||||||
let smtp_transport = SmtpTransportBuilder::new(
|
let smtp_client = SmtpClient::new(
|
||||||
(config.smtp_host.to_owned().as_str(), config.smtp_port),
|
(config.smtp_host.as_str(), config.smtp_port),
|
||||||
client_security
|
client_security,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
let smtp_transport = match (&config.smtp_username, &config.smtp_password) {
|
let smtp_client = match (&config.smtp_username, &config.smtp_password) {
|
||||||
(Some(username), Some(password)) => {
|
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
|
||||||
smtp_transport.credentials(Credentials::new(username.to_owned(), password.to_owned()))
|
_ => smtp_client,
|
||||||
},
|
|
||||||
(_, _) => smtp_transport,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
smtp_transport
|
smtp_client
|
||||||
.smtp_utf8(true)
|
.smtp_utf8(true)
|
||||||
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
||||||
.build()
|
.transport()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> Result<(), String> {
|
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> Result<(), String> {
|
||||||
@@ -46,18 +44,19 @@ pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConf
|
|||||||
hint))
|
hint))
|
||||||
} else {
|
} else {
|
||||||
("Sorry, you have no password hint...",
|
("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()
|
let email = EmailBuilder::new()
|
||||||
.to(address)
|
.to(address)
|
||||||
.from((config.smtp_from.to_owned(), "Bitwarden-rs"))
|
.from((config.smtp_from.clone(), "Bitwarden-rs"))
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.body(body)
|
.body(body)
|
||||||
.build().unwrap();
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
match mailer(config).send(&email) {
|
mailer(config)
|
||||||
Ok(_) => Ok(()),
|
.send(email.into())
|
||||||
Err(e) => Err(e.description().to_string()),
|
.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)]
|
#![plugin(rocket_codegen)]
|
||||||
|
#![recursion_limit="128"]
|
||||||
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
|
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
extern crate rocket_contrib;
|
extern crate rocket_contrib;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
extern crate multipart;
|
extern crate multipart;
|
||||||
|
extern crate ws;
|
||||||
|
extern crate rmpv;
|
||||||
|
extern crate chashmap;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
@@ -30,9 +34,9 @@ extern crate num_traits;
|
|||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
extern crate lettre_email;
|
extern crate lettre_email;
|
||||||
extern crate native_tls;
|
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;
|
use rocket::Rocket;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -52,6 +56,7 @@ fn init_rocket() -> Rocket {
|
|||||||
.mount("/icons", api::icons_routes())
|
.mount("/icons", api::icons_routes())
|
||||||
.mount("/notifications", api::notifications_routes())
|
.mount("/notifications", api::notifications_routes())
|
||||||
.manage(db::init_pool())
|
.manage(db::init_pool())
|
||||||
|
.manage(api::start_notification_server())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embed the migrations from the migrations folder into the application
|
// Embed the migrations from the migrations folder into the application
|
||||||
@@ -76,7 +81,6 @@ fn main() {
|
|||||||
check_web_vault();
|
check_web_vault();
|
||||||
migrations::run_migrations();
|
migrations::run_migrations();
|
||||||
|
|
||||||
|
|
||||||
init_rocket().launch();
|
init_rocket().launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,27 +176,32 @@ pub struct MailConfig {
|
|||||||
|
|
||||||
impl MailConfig {
|
impl MailConfig {
|
||||||
fn load() -> Option<Self> {
|
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.
|
// When SMTP_HOST is absent, we assume the user does not want to enable it.
|
||||||
if smtp_host.is_none() {
|
let smtp_host = match get_env("SMTP_HOST") {
|
||||||
return None
|
Some(host) => host,
|
||||||
}
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(true);
|
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
|
||||||
let smtp_port = util::parse_option_string(env::var("SMTP_PORT").ok())
|
println!("Please specify SMTP_FROM to enable SMTP support.");
|
||||||
.unwrap_or_else(|| {
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let smtp_ssl = get_env_or("SMTP_SSL", true);
|
||||||
|
let smtp_port = get_env("SMTP_PORT").unwrap_or_else(||
|
||||||
if smtp_ssl {
|
if smtp_ssl {
|
||||||
587u16
|
587u16
|
||||||
} else {
|
} else {
|
||||||
25u16
|
25u16
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
let smtp_username = env::var("SMTP_USERNAME").ok();
|
let smtp_username = get_env("SMTP_USERNAME");
|
||||||
let smtp_password = env::var("SMTP_PASSWORD").ok().or_else(|| {
|
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
|
||||||
if smtp_username.as_ref().is_some() {
|
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);
|
exit(1);
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -200,13 +209,12 @@ impl MailConfig {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Some(MailConfig {
|
Some(MailConfig {
|
||||||
smtp_host: smtp_host.unwrap(),
|
smtp_host,
|
||||||
smtp_port: smtp_port,
|
smtp_port,
|
||||||
smtp_ssl: smtp_ssl,
|
smtp_ssl,
|
||||||
smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok())
|
smtp_from,
|
||||||
.unwrap_or("bitwarden-rs@localhost".to_string()),
|
smtp_username,
|
||||||
smtp_username: smtp_username,
|
smtp_password,
|
||||||
smtp_password: smtp_password,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,9 +232,13 @@ pub struct Config {
|
|||||||
web_vault_folder: String,
|
web_vault_folder: String,
|
||||||
web_vault_enabled: bool,
|
web_vault_enabled: bool,
|
||||||
|
|
||||||
|
websocket_enabled: bool,
|
||||||
|
websocket_url: String,
|
||||||
|
|
||||||
local_icon_extractor: bool,
|
local_icon_extractor: bool,
|
||||||
signups_allowed: bool,
|
signups_allowed: bool,
|
||||||
invitations_allowed: bool,
|
invitations_allowed: bool,
|
||||||
|
server_admin_email: Option<String>,
|
||||||
password_iterations: i32,
|
password_iterations: i32,
|
||||||
show_password_hint: bool,
|
show_password_hint: bool,
|
||||||
|
|
||||||
@@ -238,32 +250,37 @@ pub struct Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
fn load() -> Self {
|
fn load() -> Self {
|
||||||
|
use util::{get_env, get_env_or};
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
let df = env::var("DATA_FOLDER").unwrap_or("data".into());
|
let df = get_env_or("DATA_FOLDER", "data".to_string());
|
||||||
let key = env::var("RSA_KEY_FILENAME").unwrap_or(format!("{}/{}", &df, "rsa_key"));
|
let key = get_env_or("RSA_KEY_FILENAME", format!("{}/{}", &df, "rsa_key"));
|
||||||
|
|
||||||
let domain = env::var("DOMAIN");
|
let domain = get_env("DOMAIN");
|
||||||
|
|
||||||
Config {
|
Config {
|
||||||
database_url: env::var("DATABASE_URL").unwrap_or(format!("{}/{}", &df, "db.sqlite3")),
|
database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")),
|
||||||
icon_cache_folder: env::var("ICON_CACHE_FOLDER").unwrap_or(format!("{}/{}", &df, "icon_cache")),
|
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
|
||||||
attachments_folder: env::var("ATTACHMENTS_FOLDER").unwrap_or(format!("{}/{}", &df, "attachments")),
|
attachments_folder: get_env_or("ATTACHMENTS_FOLDER", format!("{}/{}", &df, "attachments")),
|
||||||
|
|
||||||
private_rsa_key: format!("{}.der", &key),
|
private_rsa_key: format!("{}.der", &key),
|
||||||
private_rsa_key_pem: format!("{}.pem", &key),
|
private_rsa_key_pem: format!("{}.pem", &key),
|
||||||
public_rsa_key: format!("{}.pub.der", &key),
|
public_rsa_key: format!("{}.pub.der", &key),
|
||||||
|
|
||||||
web_vault_folder: env::var("WEB_VAULT_FOLDER").unwrap_or("web-vault/".into()),
|
web_vault_folder: get_env_or("WEB_VAULT_FOLDER", "web-vault/".into()),
|
||||||
web_vault_enabled: util::parse_option_string(env::var("WEB_VAULT_ENABLED").ok()).unwrap_or(true),
|
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),
|
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
|
||||||
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
|
websocket_url: format!("{}:{}", get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()), get_env_or("WEBSOCKET_PORT", 3012)),
|
||||||
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),
|
|
||||||
|
|
||||||
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()),
|
domain: domain.unwrap_or("http://localhost".into()),
|
||||||
|
|
||||||
mail: MailConfig::load(),
|
mail: MailConfig::load(),
|
||||||
|
43
src/util.rs
43
src/util.rs
@@ -6,17 +6,18 @@ macro_rules! err {
|
|||||||
($err:expr, $msg:expr) => {{
|
($err:expr, $msg:expr) => {{
|
||||||
println!("ERROR: {}", $msg);
|
println!("ERROR: {}", $msg);
|
||||||
err_json!(json!({
|
err_json!(json!({
|
||||||
"Message": $err,
|
"error": $err,
|
||||||
"ValidationErrors": {
|
"error_description": $err,
|
||||||
"": [$msg,],
|
"ErrorModel": {
|
||||||
},
|
"Message": $msg,
|
||||||
|
"ValidationErrors": null,
|
||||||
"ExceptionMessage": null,
|
"ExceptionMessage": null,
|
||||||
"ExceptionStackTrace": null,
|
"ExceptionStackTrace": null,
|
||||||
"InnerExceptionMessage": null,
|
"InnerExceptionMessage": null,
|
||||||
"Object": "error",
|
"Object": "error"
|
||||||
}))
|
}}))
|
||||||
}};
|
}};
|
||||||
($msg:expr) => { err!("The model state is invalid", $msg) }
|
($msg:expr) => { err!("unknown_error", $msg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
@@ -97,6 +98,7 @@ pub fn get_display_size(size: i32) -> String {
|
|||||||
///
|
///
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::ops::Try;
|
||||||
|
|
||||||
pub fn upcase_first(s: &str) -> String {
|
pub fn upcase_first(s: &str) -> String {
|
||||||
let mut c = s.chars();
|
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 {
|
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 Some(Ok(value)) = string.map(|s| s.as_ref().parse::<T>()) {
|
if let Ok(Ok(value)) = string.into_result().map(|s| s.as_ref().parse::<T>()) {
|
||||||
Some(value)
|
Some(value)
|
||||||
} else {
|
} else {
|
||||||
None
|
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
|
/// Date util methods
|
||||||
///
|
///
|
||||||
|
Reference in New Issue
Block a user