mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 18:55:57 +03:00
Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
371017b547 | ||
|
bc20592712 | ||
|
37a6da3443 | ||
|
53bd169462 | ||
|
5a9aab1a32 | ||
|
23eadf2c9a | ||
|
d5dfda8905 | ||
|
ce15c7ffba | ||
|
a335bcd682 | ||
|
76c510c5b6 | ||
|
4635d62e2c | ||
|
65e36e2931 | ||
|
063dfcf487 | ||
|
d3e4fb88ee | ||
|
7ebba736cb | ||
|
a1272c7190 | ||
|
0a6b4e9961 | ||
|
e66edc1ce9 | ||
|
4e827e4f8a | ||
|
295985de7c | ||
|
a48611aa6d | ||
|
f713e2e092 | ||
|
e26e2319da | ||
|
f2ab25085d | ||
|
7adc045b80 | ||
|
19754c967f | ||
|
738ad2127b | ||
|
cb930a0858 | ||
|
94810c106a | ||
|
2fde4e6933 | ||
|
259a2f2982 | ||
|
dc1bb6de20 | ||
|
e34f75c267 | ||
|
2129946d14 | ||
|
bdcdb08fc1 | ||
|
8b5d97790f | ||
|
cac7fb145b | ||
|
6990525e8a | ||
|
55579261b7 | ||
|
70c20f7f52 | ||
|
b56a905322 | ||
|
e177160ee9 | ||
|
6364c05789 | ||
|
f71f10eac6 | ||
|
0f6ab01f77 | ||
|
0935cb90a4 | ||
|
e86cac984a | ||
|
f1acc1e05a | ||
|
3d36ac4601 | ||
|
bf1c96695b | ||
|
9ecb29883c | ||
|
8b3e87cfe0 | ||
|
e5af230315 | ||
|
a1da82c868 | ||
|
981a324027 | ||
|
00abd4c853 | ||
|
a4550e51ea | ||
|
5edbd0e952 | ||
|
1d4944b88e | ||
|
660c8f8d7e | ||
|
478fc0c9dd | ||
|
a2de4ce40e | ||
|
17052b665f | ||
|
f344dbaad4 | ||
|
c5c9e3fd65 | ||
|
aba9c28226 | ||
|
82e2b8a8c0 | ||
|
5a8d5e426d | ||
|
f9ecb7201b | ||
|
044cf19913 | ||
|
2c233cda8b | ||
|
6d735806c0 | ||
|
2433d39df5 | ||
|
9e0e4b13c5 | ||
|
e66436625c | ||
|
24a4478b5c | ||
|
f84cbeaaf8 | ||
|
3cb911a52f | ||
|
dd684753d0 | ||
|
f3e6cc6ffd | ||
|
b94f4db52a | ||
|
66a4c5d48b | ||
|
b4e222d598 | ||
|
4638786507 | ||
|
6eb1c3d638 | ||
|
f408efc927 | ||
|
9b20247fc2 | ||
|
e844f41abc | ||
|
bc532f54d5 | ||
|
c673370103 |
19
.env
19
.env
@@ -18,6 +18,17 @@
|
||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||
# WEBSOCKET_PORT=3012
|
||||
|
||||
## Enable extended logging
|
||||
## This shows timestamps and allows logging to file and to syslog
|
||||
### To enable logging to file, use the LOG_FILE env variable
|
||||
### To enable syslog, you need to compile with `cargo build --features=enable_syslog'
|
||||
# EXTENDED_LOGGING=true
|
||||
|
||||
## Logging to file
|
||||
## This requires extended logging
|
||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||
# LOG_FILE=/path/to/log
|
||||
|
||||
## Controls if new users can register
|
||||
# SIGNUPS_ALLOWED=true
|
||||
|
||||
@@ -40,6 +51,14 @@
|
||||
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
|
||||
# DOMAIN=https://bw.domain.tld:8443
|
||||
|
||||
## Yubico (Yubikey) Settings
|
||||
## Set your Client ID and Secret Key for Yubikey OTP
|
||||
## You can generate it here: https://upgrade.yubico.com/getapikey/
|
||||
## You can optionally specify a custom OTP server
|
||||
# YUBICO_CLIENT_ID=11111
|
||||
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
|
||||
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
||||
|
||||
## Rocket specific settings, check Rocket documentation to learn more
|
||||
# ROCKET_ENV=staging
|
||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||
|
@@ -5,3 +5,5 @@ dist: trusty # so we get a VM with higher specs
|
||||
cache: cargo
|
||||
rust:
|
||||
- nightly
|
||||
script:
|
||||
- cargo build --verbose --all-features
|
||||
|
17
BUILD.md
17
BUILD.md
@@ -3,13 +3,13 @@
|
||||
## Dependencies
|
||||
- `Rust nightly` (strongly recommended to use [rustup](https://rustup.rs/))
|
||||
- `OpenSSL` (should be available in path, install through your system's package manager or use the [prebuilt binaries](https://wiki.openssl.org/index.php/Binaries))
|
||||
- `NodeJS` (required to build the web-vault, (install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/))
|
||||
- `NodeJS` (only when compiling the web-vault, install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/))
|
||||
|
||||
|
||||
## Run/Compile
|
||||
```sh
|
||||
# Compile and run
|
||||
cargo run
|
||||
cargo run --release
|
||||
# or just compile (binary located in target/release/bitwarden_rs)
|
||||
cargo build --release
|
||||
```
|
||||
@@ -17,7 +17,13 @@ cargo build --release
|
||||
When run, the server is accessible in [http://localhost:80](http://localhost:80).
|
||||
|
||||
### Install the web-vault
|
||||
Clone the git repository at [bitwarden/web](https://github.com/bitwarden/web) and checkout the latest release tag (e.g. v2.1.1):
|
||||
A compiled version of the web vault can be downloaded from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
|
||||
|
||||
If you prefer to compile it manually, follow these steps:
|
||||
|
||||
*Note: building the Vault needs ~1.5GB of RAM. On systems like a RaspberryPI with 1GB or less, please [enable swapping](https://www.tecmint.com/create-a-linux-swap-file/) or build it on a more powerful machine and copy the directory from there. This much memory is only needed for building it, running bitwarden_rs with vault needs only about 10MB of RAM.*
|
||||
|
||||
- Clone the git repository at [bitwarden/web](https://github.com/bitwarden/web) and checkout the latest release tag (e.g. v2.1.1):
|
||||
```sh
|
||||
# clone the repository
|
||||
git clone https://github.com/bitwarden/web.git web-vault
|
||||
@@ -26,13 +32,14 @@ cd web-vault
|
||||
git checkout "$(git tag | tail -n1)"
|
||||
```
|
||||
|
||||
Apply the patch file from `docker/set-vault-baseurl.patch`:
|
||||
- Apply the patch file from `docker/set-vault-baseurl.patch`:
|
||||
```sh
|
||||
# In the Vault repository directory
|
||||
git apply /path/to/bitwarden_rs/docker/set-vault-baseurl.patch
|
||||
```
|
||||
|
||||
Then, build the Vault:
|
||||
- Then, build the Vault:
|
||||
|
||||
```sh
|
||||
npm run sub:init
|
||||
npm install
|
||||
|
1398
Cargo.lock
generated
1398
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@@ -2,21 +2,24 @@
|
||||
name = "bitwarden_rs"
|
||||
version = "1.0.0"
|
||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
enable_syslog = ["syslog", "fern/syslog-4"]
|
||||
|
||||
[dependencies]
|
||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||
rocket = { version = "0.3.17", features = ["tls"] }
|
||||
rocket_codegen = "0.3.17"
|
||||
rocket_contrib = "0.3.17"
|
||||
rocket = { version = "0.4.0", features = ["tls"], default-features = false }
|
||||
rocket_contrib = "0.4.0"
|
||||
|
||||
# HTTP client
|
||||
reqwest = "0.9.2"
|
||||
reqwest = "0.9.5"
|
||||
|
||||
# multipart/form-data support
|
||||
multipart = "0.15.3"
|
||||
multipart = "0.15.4"
|
||||
|
||||
# WebSockets library
|
||||
ws = "0.7.8"
|
||||
ws = "0.7.9"
|
||||
|
||||
# MessagePack library
|
||||
rmpv = "0.4.0"
|
||||
@@ -25,9 +28,14 @@ rmpv = "0.4.0"
|
||||
chashmap = "2.2.0"
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = "1.0.79"
|
||||
serde_derive = "1.0.79"
|
||||
serde_json = "1.0.31"
|
||||
serde = "1.0.82"
|
||||
serde_derive = "1.0.82"
|
||||
serde_json = "1.0.33"
|
||||
|
||||
# Logging
|
||||
log = "0.4.6"
|
||||
fern = "0.5.7"
|
||||
syslog = { version = "4.0.1", optional = true }
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "1.3.3", features = ["sqlite", "chrono", "r2d2"] }
|
||||
@@ -37,7 +45,7 @@ diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
||||
libsqlite3-sys = { version = "0.9.3", features = ["bundled"] }
|
||||
|
||||
# Crypto library
|
||||
ring = { version = "= 0.11.0", features = ["rsa_signing"] }
|
||||
ring = { version = "0.13.5", features = ["rsa_signing"] }
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "0.7.1", features = ["v4"] }
|
||||
@@ -52,16 +60,19 @@ oath = "0.10.2"
|
||||
data-encoding = "2.1.1"
|
||||
|
||||
# JWT library
|
||||
jsonwebtoken = "= 4.0.1"
|
||||
jsonwebtoken = "5.0.1"
|
||||
|
||||
# U2F library
|
||||
u2f = "0.1.2"
|
||||
|
||||
# Yubico Library
|
||||
yubico = { version = "=0.4.0", features = ["online"], default-features = false }
|
||||
|
||||
# A `dotenv` implementation for Rust
|
||||
dotenv = { version = "0.13.0", default-features = false }
|
||||
|
||||
# Lazy static macro
|
||||
lazy_static = "1.1.0"
|
||||
lazy_static = { version = "1.2.0", features = ["nightly"] }
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.6"
|
||||
@@ -70,17 +81,21 @@ num-derive = "0.2.3"
|
||||
# Email libraries
|
||||
lettre = "0.9.0"
|
||||
lettre_email = "0.9.0"
|
||||
native-tls = "0.2.1"
|
||||
native-tls = "0.2.2"
|
||||
|
||||
# Number encoding library
|
||||
byteorder = "1.2.6"
|
||||
byteorder = "1.2.7"
|
||||
|
||||
[patch.crates-io]
|
||||
# Make jwt use ring 0.11, to match rocket
|
||||
jsonwebtoken = { path = "libs/jsonwebtoken" }
|
||||
# Add support for Timestamp type
|
||||
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
||||
|
||||
# Use new native_tls version 0.2
|
||||
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
|
||||
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
|
||||
|
||||
# Version 0.1.2 from crates.io lacks a commit that fixes a certificate error
|
||||
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' }
|
||||
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '75b9fa5afb4c5' }
|
||||
|
||||
# Allows optional libusb support
|
||||
yubico = { git = 'https://github.com/dani-garcia/yubico-rs' }
|
||||
|
25
Dockerfile
25
Dockerfile
@@ -2,27 +2,21 @@
|
||||
# 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
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
ENV VAULT_VERSION "v2.7.1"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
git \
|
||||
tar
|
||||
|
||||
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
|
||||
WORKDIR /web-build
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
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
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
@@ -41,7 +35,6 @@ WORKDIR /app
|
||||
|
||||
# Copies over *only* your manifests and vendored dependencies
|
||||
COPY ./Cargo.* ./
|
||||
COPY ./libs ./libs
|
||||
COPY ./rust-toolchain ./rust-toolchain
|
||||
|
||||
# Builds your dependencies and removes the
|
||||
@@ -54,6 +47,9 @@ RUN find . -not -path "./target*" -delete
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
# Make sure that we actually build the project
|
||||
RUN touch src/main.rs
|
||||
|
||||
# Builds again, this time it'll just be
|
||||
# your actual source files being built
|
||||
RUN cargo build --release
|
||||
@@ -64,6 +60,7 @@ RUN cargo build --release
|
||||
FROM debian:stretch-slim
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
# Install needed libraries
|
||||
|
@@ -2,27 +2,21 @@
|
||||
# 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
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
ENV VAULT_VERSION "v2.7.1"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
git \
|
||||
tar
|
||||
|
||||
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
|
||||
WORKDIR /web-build
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
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
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
@@ -39,15 +33,8 @@ RUN apt-get update \
|
||||
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 \
|
||||
@@ -62,19 +49,12 @@ 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
|
||||
# Build
|
||||
RUN rustup target add aarch64-unknown-linux-gnu
|
||||
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
@@ -83,6 +63,7 @@ RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
|
||||
FROM balenalib/aarch64-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
@@ -2,52 +2,37 @@
|
||||
# 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
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
ENV VAULT_VERSION "v2.7.1"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
git \
|
||||
tar
|
||||
|
||||
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
|
||||
WORKDIR /web-build
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
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
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# Musl build image for statically compiled binary
|
||||
FROM clux/muslrust:nightly-2018-08-24 as build
|
||||
FROM clux/muslrust:nightly-2018-12-01 as build
|
||||
|
||||
# Creates a dummy project used to grab dependencies
|
||||
RUN USER=root cargo init --bin
|
||||
ENV USER "root"
|
||||
|
||||
# Copies over *only* your manifests and vendored dependencies
|
||||
COPY ./Cargo.* ./
|
||||
COPY ./libs ./libs
|
||||
COPY ./rust-toolchain ./rust-toolchain
|
||||
|
||||
# Builds your dependencies and removes the
|
||||
# dummy project, except the target folder
|
||||
# This folder contains the compiled dependencies
|
||||
RUN cargo build --release
|
||||
RUN find . -not -path "./target*" -delete
|
||||
WORKDIR /app
|
||||
|
||||
# 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 rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
# Build
|
||||
RUN cargo build --release
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
@@ -56,6 +41,7 @@ RUN cargo build --release
|
||||
FROM alpine:3.8
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
ENV SSL_CERT_DIR=/etc/ssl/certs
|
||||
|
||||
@@ -75,7 +61,7 @@ EXPOSE 3012
|
||||
COPY .env .
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build /volume/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
||||
|
@@ -2,27 +2,21 @@
|
||||
# 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
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.4.0"
|
||||
ENV VAULT_VERSION "v2.7.1"
|
||||
|
||||
ENV URL "https://github.com/bitwarden/web.git"
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
git \
|
||||
tar
|
||||
|
||||
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
|
||||
WORKDIR /web-build
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
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
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
@@ -39,15 +33,8 @@ RUN apt-get update \
|
||||
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 armhf libs
|
||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||
/etc/apt/sources.list.d/deb-src.list \
|
||||
@@ -62,19 +49,12 @@ ENV CROSS_COMPILE="1"
|
||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
|
||||
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
||||
|
||||
# Builds your dependencies and removes the
|
||||
# dummy project, except the target folder
|
||||
# This folder contains the compiled dependencies
|
||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -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
|
||||
# Build
|
||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
@@ -83,6 +63,7 @@ RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
||||
FROM balenalib/armv7hf-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
197
README.md
197
README.md
@@ -28,6 +28,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
|
||||
- [Enabling HTTPS](#enabling-https)
|
||||
- [Enabling WebSocket notifications](#enabling-websocket-notifications)
|
||||
- [Enabling U2F authentication](#enabling-u2f-authentication)
|
||||
- [Enabling YubiKey OTP authentication](#enabling-yubikey-otp-authentication)
|
||||
- [Changing persistent data location](#changing-persistent-data-location)
|
||||
- [/data prefix:](#data-prefix)
|
||||
- [database name and location](#database-name-and-location)
|
||||
@@ -39,6 +40,14 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
|
||||
- [Password hint display](#password-hint-display)
|
||||
- [Disabling or overriding the Vault interface hosting](#disabling-or-overriding-the-vault-interface-hosting)
|
||||
- [Other configuration](#other-configuration)
|
||||
- [Fail2Ban Setup](#fail2ban-setup)
|
||||
- [Logging Failed Login Attempts to Syslog](#logging-failed-login-attempts-to-syslog)
|
||||
- [Fail2Ban Filter](#fail2ban-filter)
|
||||
- [Fail2Ban Jail](#fail2ban-jail)
|
||||
- [Testing Fail2Ban](#testing-fail2ban)
|
||||
- [Running with systemd-docker](#running-with-systemd-docker)
|
||||
- [Setting environment variables](#setting-environment-variables)
|
||||
- [Running the service](#running-the-service)
|
||||
- [Building your own image](#building-your-own-image)
|
||||
- [Building binary](#building-binary)
|
||||
- [Available packages](#available-packages)
|
||||
@@ -68,11 +77,11 @@ Basically full implementation of Bitwarden API is provided including:
|
||||
* Serving the static files for Vault interface
|
||||
* Website icons API
|
||||
* Authenticator and U2F support
|
||||
* YubiKey OTP
|
||||
|
||||
## Missing features
|
||||
* Email confirmation
|
||||
* Other two-factor systems:
|
||||
* YubiKey OTP (if your key supports U2F, you can use that)
|
||||
* Duo
|
||||
* Email codes
|
||||
|
||||
@@ -252,6 +261,22 @@ docker run -d --name bitwarden \
|
||||
|
||||
Note that the value has to include the `https://` and it may include a port at the end (in the format of `https://bw.domain.tld:port`) when not using `443`.
|
||||
|
||||
### Enabling YubiKey OTP authentication
|
||||
To enable YubiKey authentication, you must set the `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` env variables.
|
||||
|
||||
If `YUBICO_SERVER` is not specified, it will use the default YubiCloud servers. You can generate `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` for the default YubiCloud [here](https://upgrade.yubico.com/getapikey/).
|
||||
|
||||
Note: In order to generate API keys or use a YubiKey with an OTP server, it must be registered. After configuring your key in the [YubiKey Personalization Tool](https://www.yubico.com/products/services-software/personalization-tools/use/), you can register it with the default servers [here](https://upload.yubico.com/).
|
||||
|
||||
```sh
|
||||
docker run -d --name bitwarden \
|
||||
-e YUBICO_CLIENT_ID=12345 \
|
||||
-e YUBICO_SECRET_KEY=ABCDEABCDEABCDEABCDE= \
|
||||
-v /bw-data/:/data/ \
|
||||
-p 80:80 \
|
||||
mprasil/bitwarden:latest
|
||||
```
|
||||
|
||||
### Changing persistent data location
|
||||
|
||||
#### /data prefix:
|
||||
@@ -399,6 +424,168 @@ Note that you can also change the path where bitwarden_rs looks for static files
|
||||
|
||||
Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables).
|
||||
|
||||
### Fail2Ban Setup
|
||||
|
||||
Bitwarden_rs logs failed login attempts to stdout. We need to set this so the host OS can see these. Then we can setup Fail2Ban.
|
||||
|
||||
#### Logging Failed Login Attempts to Syslog
|
||||
|
||||
We need to set the logging driver to syslog so the host OS and Fail2Ban can see them.
|
||||
|
||||
If you are using docker commands, you will need to add: `--log-driver syslog --log-opt tag=$TAG` to your command.
|
||||
|
||||
If you are using docker-compose, add this to you yaml file:
|
||||
```
|
||||
bitwarden:
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
tag: "$TAG"
|
||||
```
|
||||
With the above settings in the docker-compose file. Any failed login attempts will look like this in your syslog file:
|
||||
```
|
||||
MMM DD hh:mm:ss server-hostname $TAG[773]: [YYYY-MM-DD][hh:mm:ss][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: XXX.XXX.XXX.XXX. Username: email@domain.com.
|
||||
```
|
||||
You can change the '$TAG' to anything you like. Just remember it because it will be in the Fail2Ban filter.
|
||||
|
||||
#### Fail2Ban Filter
|
||||
|
||||
Create the filter file
|
||||
```
|
||||
sudo nano /etc/fail2ban/filter.d/bitwarden.conf
|
||||
```
|
||||
And add the following
|
||||
```
|
||||
[INCLUDES]
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
_daemon = $TAG
|
||||
failregex = ^%(__prefix_line)s.*Username or password is incorrect\. Try again\. IP: <HOST>\. Username:.*$
|
||||
ignoreregex =
|
||||
```
|
||||
Dont forget to change the '$TAG' to what you set it as from above.
|
||||
|
||||
#### Fail2Ban Jail
|
||||
|
||||
Now we need the jail, create the jail file
|
||||
```
|
||||
sudo nano /etc/fail2ban/jail.d/bitwarden.local
|
||||
```
|
||||
and add:
|
||||
```
|
||||
[bitwarden]
|
||||
enabled = true
|
||||
port = 80,443,8081
|
||||
filter = bitwarden
|
||||
action = iptables-allports[name=bitwarden]
|
||||
logpath = /var/log/syslog
|
||||
maxretry = 3
|
||||
bantime = 14400
|
||||
findtime = 14400
|
||||
```
|
||||
Feel free to change the options as you see fit.
|
||||
|
||||
#### Testing Fail2Ban
|
||||
|
||||
Now just try to login to bitwarden using any email (it doesnt have to be a valid email, just an email format)
|
||||
If it works correctly and your IP is banned, you can unban the ip by running:
|
||||
```
|
||||
sudo fail2ban-client unban XX.XX.XX.XX bitwarden
|
||||
```
|
||||
|
||||
### Running with systemd-docker
|
||||
|
||||
These instructions allow you to have systemd manage the lifecycle of the docker container, if you prefer.
|
||||
|
||||
First, install the `systemd-docker` package using your system package manager.
|
||||
This is a wrapper which improves docker integration with systemd.
|
||||
|
||||
For full instructions and configuration options, see the [GitHub repository](https://github.com/ibuildthecloud/systemd-docker).
|
||||
|
||||
As root, create `/etc/systemd/system/bitwarden.service` using your preferred editor with the following contents:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Bitwarden
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
ExecStartPre=/usr/bin/docker pull mprasil/bitwarden:latest
|
||||
ExecStart=/usr/bin/systemd-docker --cgroups name=systemd --env run \
|
||||
-p 8080:80 \
|
||||
-p 8081:3012 \
|
||||
-v /opt/bw-data:/data/ \
|
||||
--rm --name %n mprasil/bitwarden:latest
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
Type=notify
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Adjust the above example as necessary. In particular, pay attention to the `-p` and `-v` options,
|
||||
as these control the port and volume bindings between the container and the host.
|
||||
|
||||
Explanation of options which may not be self-explanatory:
|
||||
|
||||
- A `TimeoutStartSec` value of 0 stops systemd from considering the service failed
|
||||
after waiting for the default startup time. This is required as it may take a while for the `docker pull` in `ExecStartPre` to finish.
|
||||
- `ExecStartPre`: Pull the docker tag before running.
|
||||
- A `Type` value of `notify` tells systemd to expect a notification from the service that it is ready.
|
||||
- A `NotifyAccess` value of `all` is required by `systemd-docker`.
|
||||
|
||||
#### Setting environment variables
|
||||
|
||||
It's possible to directly specify environment variables in the unit file in two ways:
|
||||
|
||||
- Using an `Environment` directive in the `[Service]` block.
|
||||
- Using the `-e` option of `docker`. In this case, you can omit the `--env` option shown in the example above.
|
||||
|
||||
To verify that your environment variables are set correctly, check the output of `systemctl show bitwarden.service`
|
||||
for an `Environment` line.
|
||||
|
||||
It's also possible to store environment variables in a separate file using the `EnvironmentFile` directive in the unit file.
|
||||
|
||||
Systemd can source a file of the form:
|
||||
|
||||
```shell
|
||||
Key="Value"
|
||||
```
|
||||
|
||||
However, the systemd project does not mandate where this file should be stored. Consult your distribution's documentation for the
|
||||
best location for this file. For example, RedHat based distributions typically place these files in `/etc/sysconfig/`
|
||||
|
||||
If you're unsure, just create a file as root in `/etc/` e.g. `/etc/bitwarden.service.conf`.
|
||||
|
||||
In your unit file, add an `EnvironmentFile` directive in the `[Service]` block, the value being the full path to the
|
||||
file created above. Example:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Bitwarden
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/bitwarden.service.conf
|
||||
TimeoutStartSec=0
|
||||
-snip-
|
||||
```
|
||||
|
||||
#### Running the service
|
||||
|
||||
After the above installation and configuration is complete, reload systemd using `sudo systemctl daemon-reload`.
|
||||
Then, start the Bitwarden service using `sudo systemctl start bitwarden`.
|
||||
|
||||
To have the service start with the system, use `sudo systemctl enable bitwarden`.
|
||||
|
||||
Verify that the container has started using `systemctl status bitwarden`.
|
||||
|
||||
## Building your own image
|
||||
|
||||
Clone the repository, then from the root of the repository run:
|
||||
@@ -430,11 +617,19 @@ It will setup a fully functional and secure `bitwarden_rs` application in Kubern
|
||||
The sqlite3 database should be backed up using the proper sqlite3 backup command. This will ensure the database does not become corrupted if the backup happens during a database write.
|
||||
|
||||
```
|
||||
mkdir $DATA_FOLDER/db-backup
|
||||
sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sqlite3'"
|
||||
```
|
||||
|
||||
This command can be run via a CRON job everyday, however note that it will overwrite the same `backup.sqlite3` file each time. This backup file should therefore be saved via incremental backup either using a CRON job command that appends a timestamp or from another backup app such as Duplicati. To restore simply overwrite `db.sqlite3` with `backup.sqlite3` (while bitwarden_rs is stopped).
|
||||
|
||||
Running the above command requires sqlite3 to be installed on the docker host system. You can achieve the same result with a sqlite3 docker container using the following command.
|
||||
```
|
||||
docker run --rm --volumes-from=bitwarden bruceforce/bw_backup /backup.sh
|
||||
```
|
||||
|
||||
You can also run a container with integrated cron daemon to automatically backup your database. See https://gitlab.com/1O/bitwarden_rs-backup for examples.
|
||||
|
||||
### 2. the attachments folder
|
||||
|
||||
By default, this is located in `$DATA_FOLDER/attachments`
|
||||
|
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "jsonwebtoken"
|
||||
version = "4.0.1"
|
||||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
description = "Create and parse JWT in a strongly typed way."
|
||||
homepage = "https://github.com/Keats/rust-jwt"
|
||||
repository = "https://github.com/Keats/rust-jwt"
|
||||
keywords = ["jwt", "web", "api", "token", "json"]
|
||||
|
||||
[dependencies]
|
||||
error-chain = { version = "0.11", default-features = false }
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
ring = { version = "0.11.0", features = ["rsa_signing", "dev_urandom_fallback"] }
|
||||
base64 = "0.9"
|
||||
untrusted = "0.5"
|
||||
chrono = "0.4"
|
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Vincent Prouillet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@@ -1,120 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64;
|
||||
use ring::{rand, digest, hmac, signature};
|
||||
use ring::constant_time::verify_slices_are_equal;
|
||||
use untrusted;
|
||||
|
||||
use errors::{Result, ErrorKind};
|
||||
|
||||
|
||||
/// The algorithms supported for signing/verifying
|
||||
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Algorithm {
|
||||
/// HMAC using SHA-256
|
||||
HS256,
|
||||
/// HMAC using SHA-384
|
||||
HS384,
|
||||
/// HMAC using SHA-512
|
||||
HS512,
|
||||
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-256
|
||||
RS256,
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-384
|
||||
RS384,
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-512
|
||||
RS512,
|
||||
}
|
||||
|
||||
/// The actual HS signing + encoding
|
||||
fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
|
||||
let signing_key = hmac::SigningKey::new(alg, key);
|
||||
let digest = hmac::sign(&signing_key, signing_input.as_bytes());
|
||||
|
||||
Ok(
|
||||
base64::encode_config::<hmac::Signature>(&digest, base64::URL_SAFE_NO_PAD)
|
||||
)
|
||||
}
|
||||
|
||||
/// The actual RSA signing + encoding
|
||||
/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html
|
||||
fn sign_rsa(alg: Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
|
||||
let ring_alg = match alg {
|
||||
Algorithm::RS256 => &signature::RSA_PKCS1_SHA256,
|
||||
Algorithm::RS384 => &signature::RSA_PKCS1_SHA384,
|
||||
Algorithm::RS512 => &signature::RSA_PKCS1_SHA512,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let key_pair = Arc::new(
|
||||
signature::RSAKeyPair::from_der(untrusted::Input::from(key))
|
||||
.map_err(|_| ErrorKind::InvalidKey)?
|
||||
);
|
||||
let mut signing_state = signature::RSASigningState::new(key_pair)
|
||||
.map_err(|_| ErrorKind::InvalidKey)?;
|
||||
let mut signature = vec![0; signing_state.key_pair().public_modulus_len()];
|
||||
let rng = rand::SystemRandom::new();
|
||||
signing_state.sign(ring_alg, &rng, signing_input.as_bytes(), &mut signature)
|
||||
.map_err(|_| ErrorKind::InvalidKey)?;
|
||||
|
||||
Ok(
|
||||
base64::encode_config::<[u8]>(&signature, base64::URL_SAFE_NO_PAD)
|
||||
)
|
||||
}
|
||||
|
||||
/// Take the payload of a JWT, sign it using the algorithm given and return
|
||||
/// the base64 url safe encoded of the result.
|
||||
///
|
||||
/// Only use this function if you want to do something other than JWT.
|
||||
pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<String> {
|
||||
match algorithm {
|
||||
Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input),
|
||||
Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input),
|
||||
Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input),
|
||||
|
||||
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => sign_rsa(algorithm, key, signing_input),
|
||||
// TODO: if PKCS1 is made prublic, remove the line above and uncomment below
|
||||
// Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input),
|
||||
// Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input),
|
||||
// Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input),
|
||||
}
|
||||
}
|
||||
|
||||
/// See Ring RSA docs for more details
|
||||
fn verify_rsa(alg: &signature::RSAParameters, signature: &str, signing_input: &str, key: &[u8]) -> Result<bool> {
|
||||
let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?;
|
||||
let public_key_der = untrusted::Input::from(key);
|
||||
let message = untrusted::Input::from(signing_input.as_bytes());
|
||||
let expected_signature = untrusted::Input::from(signature_bytes.as_slice());
|
||||
|
||||
let res = signature::verify(alg, public_key_der, message, expected_signature);
|
||||
|
||||
Ok(res.is_ok())
|
||||
}
|
||||
|
||||
/// Compares the signature given with a re-computed signature for HMAC or using the public key
|
||||
/// for RSA.
|
||||
///
|
||||
/// Only use this function if you want to do something other than JWT.
|
||||
///
|
||||
/// `signature` is the signature part of a jwt (text after the second '.')
|
||||
///
|
||||
/// `signing_input` is base64(header) + "." + base64(claims)
|
||||
pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<bool> {
|
||||
match algorithm {
|
||||
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
|
||||
// we just re-sign the data with the key and compare if they are equal
|
||||
let signed = sign(signing_input, key, algorithm)?;
|
||||
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
|
||||
},
|
||||
Algorithm::RS256 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key),
|
||||
Algorithm::RS384 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key),
|
||||
Algorithm::RS512 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key),
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Algorithm {
|
||||
fn default() -> Self {
|
||||
Algorithm::HS256
|
||||
}
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
use base64;
|
||||
use serde_json;
|
||||
use ring;
|
||||
|
||||
error_chain! {
|
||||
errors {
|
||||
/// When a token doesn't have a valid JWT shape
|
||||
InvalidToken {
|
||||
description("invalid token")
|
||||
display("Invalid token")
|
||||
}
|
||||
/// When the signature doesn't match
|
||||
InvalidSignature {
|
||||
description("invalid signature")
|
||||
display("Invalid signature")
|
||||
}
|
||||
/// When the secret given is not a valid RSA key
|
||||
InvalidKey {
|
||||
description("invalid key")
|
||||
display("Invalid Key")
|
||||
}
|
||||
|
||||
// Validation error
|
||||
|
||||
/// When a token’s `exp` claim indicates that it has expired
|
||||
ExpiredSignature {
|
||||
description("expired signature")
|
||||
display("Expired Signature")
|
||||
}
|
||||
/// When a token’s `iss` claim does not match the expected issuer
|
||||
InvalidIssuer {
|
||||
description("invalid issuer")
|
||||
display("Invalid Issuer")
|
||||
}
|
||||
/// When a token’s `aud` claim does not match one of the expected audience values
|
||||
InvalidAudience {
|
||||
description("invalid audience")
|
||||
display("Invalid Audience")
|
||||
}
|
||||
/// When a token’s `aud` claim does not match one of the expected audience values
|
||||
InvalidSubject {
|
||||
description("invalid subject")
|
||||
display("Invalid Subject")
|
||||
}
|
||||
/// When a token’s `iat` claim is in the future
|
||||
InvalidIssuedAt {
|
||||
description("invalid issued at")
|
||||
display("Invalid Issued At")
|
||||
}
|
||||
/// When a token’s nbf claim represents a time in the future
|
||||
ImmatureSignature {
|
||||
description("immature signature")
|
||||
display("Immature Signature")
|
||||
}
|
||||
/// When the algorithm in the header doesn't match the one passed to `decode`
|
||||
InvalidAlgorithm {
|
||||
description("Invalid algorithm")
|
||||
display("Invalid Algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
foreign_links {
|
||||
Unspecified(ring::error::Unspecified) #[doc = "An error happened while signing/verifying a token with RSA"];
|
||||
Base64(base64::DecodeError) #[doc = "An error happened while decoding some base64 text"];
|
||||
Json(serde_json::Error) #[doc = "An error happened while serializing/deserializing JSON"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "An error happened while trying to convert the result of base64 decoding to a String"];
|
||||
}
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
use crypto::Algorithm;
|
||||
|
||||
|
||||
/// A basic JWT header, the alg defaults to HS256 and typ is automatically
|
||||
/// set to `JWT`. All the other fields are optional.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Header {
|
||||
/// The type of JWS: it can only be "JWT" here
|
||||
///
|
||||
/// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub typ: Option<String>,
|
||||
/// The algorithm used
|
||||
///
|
||||
/// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1).
|
||||
pub alg: Algorithm,
|
||||
/// Content type
|
||||
///
|
||||
/// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cty: Option<String>,
|
||||
/// JSON Key URL
|
||||
///
|
||||
/// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jku: Option<String>,
|
||||
/// Key ID
|
||||
///
|
||||
/// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub kid: Option<String>,
|
||||
/// X.509 URL
|
||||
///
|
||||
/// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5u: Option<String>,
|
||||
/// X.509 certificate thumbprint
|
||||
///
|
||||
/// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x5t: Option<String>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
/// Returns a JWT header with the algorithm given
|
||||
pub fn new(algorithm: Algorithm) -> Header {
|
||||
Header {
|
||||
typ: Some("JWT".to_string()),
|
||||
alg: algorithm,
|
||||
cty: None,
|
||||
jku: None,
|
||||
kid: None,
|
||||
x5u: None,
|
||||
x5t: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Header {
|
||||
/// Returns a JWT header using the default Algorithm, HS256
|
||||
fn default() -> Self {
|
||||
Header::new(Algorithm::default())
|
||||
}
|
||||
}
|
@@ -1,142 +0,0 @@
|
||||
//! Create and parses JWT (JSON Web Tokens)
|
||||
//!
|
||||
//! Documentation: [stable](https://docs.rs/jsonwebtoken/)
|
||||
#![recursion_limit = "300"]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(unused_doc_comments)]
|
||||
#![allow(renamed_and_removed_lints)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate serde;
|
||||
extern crate base64;
|
||||
extern crate ring;
|
||||
extern crate untrusted;
|
||||
extern crate chrono;
|
||||
|
||||
/// All the errors, generated using error-chain
|
||||
pub mod errors;
|
||||
mod header;
|
||||
mod crypto;
|
||||
mod serialization;
|
||||
mod validation;
|
||||
|
||||
pub use header::Header;
|
||||
pub use crypto::{
|
||||
Algorithm,
|
||||
sign,
|
||||
verify,
|
||||
};
|
||||
pub use validation::Validation;
|
||||
pub use serialization::TokenData;
|
||||
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::Serialize;
|
||||
|
||||
use errors::{Result, ErrorKind};
|
||||
use serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part};
|
||||
use validation::{validate};
|
||||
|
||||
|
||||
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[macro_use]
|
||||
/// extern crate serde_derive;
|
||||
/// use jsonwebtoken::{encode, Algorithm, Header};
|
||||
///
|
||||
/// /// #[derive(Debug, Serialize, Deserialize)]
|
||||
/// struct Claims {
|
||||
/// sub: String,
|
||||
/// company: String
|
||||
/// }
|
||||
///
|
||||
/// let my_claims = Claims {
|
||||
/// sub: "b@b.com".to_owned(),
|
||||
/// company: "ACME".to_owned()
|
||||
/// };
|
||||
///
|
||||
/// // my_claims is a struct that implements Serialize
|
||||
/// // This will create a JWT using HS256 as algorithm
|
||||
/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
|
||||
/// ```
|
||||
pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &[u8]) -> Result<String> {
|
||||
let encoded_header = to_jwt_part(&header)?;
|
||||
let encoded_claims = to_jwt_part(&claims)?;
|
||||
let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
|
||||
let signature = sign(&*signing_input, key.as_ref(), header.alg)?;
|
||||
|
||||
Ok([signing_input, signature].join("."))
|
||||
}
|
||||
|
||||
/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts
|
||||
/// Errors if we don't
|
||||
macro_rules! expect_two {
|
||||
($iter:expr) => {{
|
||||
let mut i = $iter;
|
||||
match (i.next(), i.next(), i.next()) {
|
||||
(Some(first), Some(second), None) => (first, second),
|
||||
_ => return Err(ErrorKind::InvalidToken.into())
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
/// Decode a token into a struct containing 2 fields: `claims` and `header`.
|
||||
///
|
||||
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[macro_use]
|
||||
/// extern crate serde_derive;
|
||||
/// use jsonwebtoken::{decode, Validation, Algorithm};
|
||||
///
|
||||
/// #[derive(Debug, Serialize, Deserialize)]
|
||||
/// struct Claims {
|
||||
/// sub: String,
|
||||
/// company: String
|
||||
/// }
|
||||
///
|
||||
/// let token = "a.jwt.token".to_string();
|
||||
/// // Claims is a struct that implements Deserialize
|
||||
/// let token_data = decode::<Claims>(&token, "secret", &Validation::new(Algorithm::HS256));
|
||||
/// ```
|
||||
pub fn decode<T: DeserializeOwned>(token: &str, key: &[u8], validation: &Validation) -> Result<TokenData<T>> {
|
||||
let (signature, signing_input) = expect_two!(token.rsplitn(2, '.'));
|
||||
let (claims, header) = expect_two!(signing_input.rsplitn(2, '.'));
|
||||
let header: Header = from_jwt_part(header)?;
|
||||
|
||||
if !verify(signature, signing_input, key, header.alg)? {
|
||||
return Err(ErrorKind::InvalidSignature.into());
|
||||
}
|
||||
|
||||
if !validation.algorithms.contains(&header.alg) {
|
||||
return Err(ErrorKind::InvalidAlgorithm.into());
|
||||
}
|
||||
|
||||
let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?;
|
||||
|
||||
validate(&claims_map, validation)?;
|
||||
|
||||
Ok(TokenData { header: header, claims: decoded_claims })
|
||||
}
|
||||
|
||||
/// Decode a token and return the Header. This is not doing any kind of validation: it is meant to be
|
||||
/// used when you don't know which `alg` the token is using and want to find out.
|
||||
///
|
||||
/// If the token has an invalid format, it will return an error.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use jsonwebtoken::decode_header;
|
||||
///
|
||||
/// let token = "a.jwt.token".to_string();
|
||||
/// let header = decode_header(&token);
|
||||
/// ```
|
||||
pub fn decode_header(token: &str) -> Result<Header> {
|
||||
let (_, signing_input) = expect_two!(token.rsplitn(2, '.'));
|
||||
let (_, header) = expect_two!(signing_input.rsplitn(2, '.'));
|
||||
from_jwt_part(header)
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
use base64;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::Serialize;
|
||||
use serde_json::{from_str, to_string, Value};
|
||||
use serde_json::map::Map;
|
||||
|
||||
use errors::{Result};
|
||||
use header::Header;
|
||||
|
||||
|
||||
/// The return type of a successful call to decode
|
||||
#[derive(Debug)]
|
||||
pub struct TokenData<T> {
|
||||
/// The decoded JWT header
|
||||
pub header: Header,
|
||||
/// The decoded JWT claims
|
||||
pub claims: T
|
||||
}
|
||||
|
||||
/// Serializes to JSON and encodes to base64
|
||||
pub fn to_jwt_part<T: Serialize>(input: &T) -> Result<String> {
|
||||
let encoded = to_string(input)?;
|
||||
Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD))
|
||||
}
|
||||
|
||||
/// Decodes from base64 and deserializes from JSON to a struct
|
||||
pub fn from_jwt_part<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<T> {
|
||||
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
|
||||
let s = String::from_utf8(decoded)?;
|
||||
|
||||
Ok(from_str(&s)?)
|
||||
}
|
||||
|
||||
/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap
|
||||
pub fn from_jwt_part_claims<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<(T, Map<String, Value>)> {
|
||||
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
|
||||
let s = String::from_utf8(decoded)?;
|
||||
|
||||
let claims: T = from_str(&s)?;
|
||||
let map: Map<_,_> = from_str(&s)?;
|
||||
Ok((claims, map))
|
||||
}
|
@@ -1,377 +0,0 @@
|
||||
use chrono::Utc;
|
||||
use serde::ser::Serialize;
|
||||
use serde_json::{Value, from_value, to_value};
|
||||
use serde_json::map::Map;
|
||||
|
||||
use errors::{Result, ErrorKind};
|
||||
use crypto::Algorithm;
|
||||
|
||||
|
||||
/// Contains the various validations that are applied after decoding a token.
|
||||
///
|
||||
/// All time validation happen on UTC timestamps.
|
||||
///
|
||||
/// ```rust
|
||||
/// use jsonwebtoken::Validation;
|
||||
///
|
||||
/// // Default value
|
||||
/// let validation = Validation::default();
|
||||
///
|
||||
/// // Changing one parameter
|
||||
/// let mut validation = Validation {leeway: 60, ..Default::default()};
|
||||
///
|
||||
/// // Setting audience
|
||||
/// let mut validation = Validation::default();
|
||||
/// validation.set_audience(&"Me"); // string
|
||||
/// validation.set_audience(&["Me", "You"]); // array of strings
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Validation {
|
||||
/// Add some leeway (in seconds) to the `exp`, `iat` and `nbf` validation to
|
||||
/// account for clock skew.
|
||||
///
|
||||
/// Defaults to `0`.
|
||||
pub leeway: i64,
|
||||
/// Whether to validate the `exp` field.
|
||||
///
|
||||
/// It will return an error if the time in the `exp` field is past.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
pub validate_exp: bool,
|
||||
/// Whether to validate the `iat` field.
|
||||
///
|
||||
/// It will return an error if the time in the `iat` field is in the future.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
pub validate_iat: bool,
|
||||
/// Whether to validate the `nbf` field.
|
||||
///
|
||||
/// It will return an error if the current timestamp is before the time in the `nbf` field.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
pub validate_nbf: bool,
|
||||
/// If it contains a value, the validation will check that the `aud` field is the same as the
|
||||
/// one provided and will error otherwise.
|
||||
/// Since `aud` can be either a String or a Vec<String> in the JWT spec, you will need to use
|
||||
/// the [set_audience](struct.Validation.html#method.set_audience) method to set it.
|
||||
///
|
||||
/// Defaults to `None`.
|
||||
pub aud: Option<Value>,
|
||||
/// If it contains a value, the validation will check that the `iss` field is the same as the
|
||||
/// one provided and will error otherwise.
|
||||
///
|
||||
/// Defaults to `None`.
|
||||
pub iss: Option<String>,
|
||||
/// If it contains a value, the validation will check that the `sub` field is the same as the
|
||||
/// one provided and will error otherwise.
|
||||
///
|
||||
/// Defaults to `None`.
|
||||
pub sub: Option<String>,
|
||||
/// If it contains a value, the validation will check that the `alg` of the header is contained
|
||||
/// in the ones provided and will error otherwise.
|
||||
///
|
||||
/// Defaults to `vec![Algorithm::HS256]`.
|
||||
pub algorithms: Vec<Algorithm>,
|
||||
}
|
||||
|
||||
impl Validation {
|
||||
/// Create a default validation setup allowing the given alg
|
||||
pub fn new(alg: Algorithm) -> Validation {
|
||||
let mut validation = Validation::default();
|
||||
validation.algorithms = vec![alg];
|
||||
validation
|
||||
}
|
||||
|
||||
/// Since `aud` can be either a String or an array of String in the JWT spec, this method will take
|
||||
/// care of serializing the value.
|
||||
pub fn set_audience<T: Serialize>(&mut self, audience: &T) {
|
||||
self.aud = Some(to_value(audience).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Validation {
|
||||
fn default() -> Validation {
|
||||
Validation {
|
||||
leeway: 0,
|
||||
|
||||
validate_exp: true,
|
||||
validate_iat: true,
|
||||
validate_nbf: true,
|
||||
|
||||
iss: None,
|
||||
sub: None,
|
||||
aud: None,
|
||||
|
||||
algorithms: vec![Algorithm::HS256],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()> {
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
if let Some(iat) = claims.get("iat") {
|
||||
if options.validate_iat && from_value::<i64>(iat.clone())? > now + options.leeway {
|
||||
return Err(ErrorKind::InvalidIssuedAt.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(exp) = claims.get("exp") {
|
||||
if options.validate_exp && from_value::<i64>(exp.clone())? < now - options.leeway {
|
||||
return Err(ErrorKind::ExpiredSignature.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nbf) = claims.get("nbf") {
|
||||
if options.validate_nbf && from_value::<i64>(nbf.clone())? > now + options.leeway {
|
||||
return Err(ErrorKind::ImmatureSignature.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(iss) = claims.get("iss") {
|
||||
if let Some(ref correct_iss) = options.iss {
|
||||
if from_value::<String>(iss.clone())? != *correct_iss {
|
||||
return Err(ErrorKind::InvalidIssuer.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sub) = claims.get("sub") {
|
||||
if let Some(ref correct_sub) = options.sub {
|
||||
if from_value::<String>(sub.clone())? != *correct_sub {
|
||||
return Err(ErrorKind::InvalidSubject.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(aud) = claims.get("aud") {
|
||||
if let Some(ref correct_aud) = options.aud {
|
||||
if aud != correct_aud {
|
||||
return Err(ErrorKind::InvalidAudience.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{to_value};
|
||||
use serde_json::map::Map;
|
||||
use chrono::Utc;
|
||||
|
||||
use super::{validate, Validation};
|
||||
|
||||
use errors::ErrorKind;
|
||||
|
||||
#[test]
|
||||
fn iat_in_past_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
|
||||
let res = validate(&claims, &Validation::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iat_in_future_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
|
||||
let res = validate(&claims, &Validation::default());
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::InvalidIssuedAt => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iat_in_future_but_in_leeway_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 50).unwrap());
|
||||
let validation = Validation {
|
||||
leeway: 1000 * 60,
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_in_future_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap());
|
||||
let res = validate(&claims, &Validation::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_in_past_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 100000).unwrap());
|
||||
let res = validate(&claims, &Validation::default());
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::ExpiredSignature => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_in_past_but_in_leeway_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 500).unwrap());
|
||||
let validation = Validation {
|
||||
leeway: 1000 * 60,
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nbf_in_past_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
|
||||
let res = validate(&claims, &Validation::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nbf_in_future_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
|
||||
let res = validate(&claims, &Validation::default());
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::ImmatureSignature => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nbf_in_future_but_in_leeway_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 500).unwrap());
|
||||
let validation = Validation {
|
||||
leeway: 1000 * 60,
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iss_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("iss".to_string(), to_value("Keats").unwrap());
|
||||
let validation = Validation {
|
||||
iss: Some("Keats".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iss_not_matching_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("iss".to_string(), to_value("Hacked").unwrap());
|
||||
let validation = Validation {
|
||||
iss: Some("Keats".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::InvalidIssuer => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("sub".to_string(), to_value("Keats").unwrap());
|
||||
let validation = Validation {
|
||||
sub: Some("Keats".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_not_matching_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("sub".to_string(), to_value("Hacked").unwrap());
|
||||
let validation = Validation {
|
||||
sub: Some("Keats".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::InvalidSubject => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aud_string_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
|
||||
let mut validation = Validation::default();
|
||||
validation.set_audience(&"Everyone");
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aud_array_of_string_ok() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("aud".to_string(), to_value(["UserA", "UserB"]).unwrap());
|
||||
let mut validation = Validation::default();
|
||||
validation.set_audience(&["UserA", "UserB"]);
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aud_type_mismatch_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
|
||||
let mut validation = Validation::default();
|
||||
validation.set_audience(&["UserA", "UserB"]);
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::InvalidAudience => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aud_correct_type_not_matching_fails() {
|
||||
let mut claims = Map::new();
|
||||
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
|
||||
let mut validation = Validation::default();
|
||||
validation.set_audience(&"None");
|
||||
let res = validate(&claims, &validation);
|
||||
assert!(res.is_err());
|
||||
|
||||
match res.unwrap_err().kind() {
|
||||
&ErrorKind::InvalidAudience => (),
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
}
|
3
migrations/2018-11-27-152651_add_att_key_columns/up.sql
Normal file
3
migrations/2018-11-27-152651_add_att_key_columns/up.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE attachments
|
||||
ADD COLUMN
|
||||
key TEXT;
|
@@ -1 +1 @@
|
||||
nightly-2018-10-03
|
||||
nightly-2018-12-01
|
||||
|
@@ -1,13 +1,37 @@
|
||||
use rocket_contrib::Json;
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
|
||||
use auth::Headers;
|
||||
use mail;
|
||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, UpdateType, WebSocketUsers};
|
||||
use crate::auth::Headers;
|
||||
use crate::mail;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
use rocket::{Route, State};
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
register,
|
||||
profile,
|
||||
put_profile,
|
||||
post_profile,
|
||||
get_public_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
post_kdf,
|
||||
post_rotatekey,
|
||||
post_sstamp,
|
||||
post_email_token,
|
||||
post_email,
|
||||
delete_account,
|
||||
post_delete_account,
|
||||
revision_date,
|
||||
password_hint,
|
||||
prelogin,
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
@@ -33,23 +57,22 @@ struct KeysData {
|
||||
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
let data: RegisterData = data.into_inner().data;
|
||||
|
||||
|
||||
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
||||
Some(mut user) => {
|
||||
Some(user) => {
|
||||
if Invitation::take(&data.Email, &conn) {
|
||||
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
|
||||
user_org.status = UserOrgStatus::Accepted as i32;
|
||||
if user_org.save(&conn).is_err() {
|
||||
err!("Failed to accept user to organization")
|
||||
}
|
||||
};
|
||||
}
|
||||
user
|
||||
} else if CONFIG.signups_allowed {
|
||||
err!("Account with this email already exists")
|
||||
err!("Account with this email already exists")
|
||||
} else {
|
||||
err!("Registration not allowed")
|
||||
err!("Registration not allowed")
|
||||
}
|
||||
},
|
||||
}
|
||||
None => {
|
||||
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
|
||||
User::new(data.Email)
|
||||
@@ -86,7 +109,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save user")
|
||||
Err(_) => err!("Failed to save user"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +122,7 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
#[allow(non_snake_case)]
|
||||
struct ProfileData {
|
||||
#[serde(rename = "Culture")]
|
||||
_Culture: String, // Ignored, always use en-US
|
||||
_Culture: String, // Ignored, always use en-US
|
||||
MasterPasswordHint: Option<String>,
|
||||
Name: String,
|
||||
}
|
||||
@@ -122,7 +145,7 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
|
||||
};
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(user.to_json(&conn))),
|
||||
Err(_) => err!("Failed to save user profile")
|
||||
Err(_) => err!("Failed to save user profile"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +153,7 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
|
||||
fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist")
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
@@ -151,12 +174,10 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
||||
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(user.to_json(&conn))),
|
||||
Err(_) => err!("Failed to save the user's keys")
|
||||
Err(_) => err!("Failed to save the user's keys"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ChangePassData {
|
||||
@@ -178,7 +199,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
||||
user.key = data.Key;
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save password")
|
||||
Err(_) => err!("Failed to save password"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,10 +229,86 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
|
||||
user.key = data.Key;
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save password settings")
|
||||
Err(_) => err!("Failed to save password settings"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct UpdateFolderData {
|
||||
Id: String,
|
||||
Name: String,
|
||||
}
|
||||
|
||||
use super::ciphers::CipherData;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct KeyData {
|
||||
Ciphers: Vec<CipherData>,
|
||||
Folders: Vec<UpdateFolderData>,
|
||||
Key: String,
|
||||
PrivateKey: String,
|
||||
MasterPasswordHash: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/key", data = "<data>")]
|
||||
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data: KeyData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
let user_uuid = &headers.user.uuid;
|
||||
|
||||
// Update folder data
|
||||
for folder_data in data.Folders {
|
||||
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &conn) {
|
||||
Some(folder) => folder,
|
||||
None => err!("Folder doesn't exist"),
|
||||
};
|
||||
|
||||
if &saved_folder.user_uuid != user_uuid {
|
||||
err!("The folder is not owned by the user")
|
||||
}
|
||||
|
||||
saved_folder.name = folder_data.Name;
|
||||
if saved_folder.save(&conn).is_err() {
|
||||
err!("Failed to save folder")
|
||||
}
|
||||
}
|
||||
|
||||
// Update cipher data
|
||||
use super::ciphers::update_cipher_from_data;
|
||||
|
||||
for cipher_data in data.Ciphers {
|
||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn) {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
|
||||
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
|
||||
err!("The cipher is not owned by the user")
|
||||
}
|
||||
|
||||
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?
|
||||
}
|
||||
|
||||
// Update user data
|
||||
let mut user = headers.user;
|
||||
|
||||
user.key = data.Key;
|
||||
user.private_key = Some(data.PrivateKey);
|
||||
user.reset_security_stamp();
|
||||
|
||||
if user.save(&conn).is_err() {
|
||||
err!("Failed modify user key");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/accounts/security-stamp", data = "<data>")]
|
||||
fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
@@ -224,7 +321,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
|
||||
user.reset_security_stamp();
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to reset security stamp")
|
||||
Err(_) => err!("Failed to reset security stamp"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +379,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
||||
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed to save email address")
|
||||
Err(_) => err!("Failed to save email address"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +399,7 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
|
||||
|
||||
match user.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?")
|
||||
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,35 +1,76 @@
|
||||
use std::collections::{HashSet, HashMap};
|
||||
use std::path::Path;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use rocket::State;
|
||||
use rocket::Data;
|
||||
use rocket::http::ContentType;
|
||||
use rocket::{request::Form, Data, Route, State};
|
||||
|
||||
use rocket_contrib::{Json, Value};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use multipart::server::{Multipart, SaveResult};
|
||||
use multipart::server::save::SavedData;
|
||||
use multipart::server::{Multipart, SaveResult};
|
||||
|
||||
use data_encoding::HEXLOWER;
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crypto;
|
||||
use crate::crypto;
|
||||
|
||||
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use auth::Headers;
|
||||
use crate::api::{self, EmptyResult, JsonResult, JsonUpcase, PasswordData, UpdateType, WebSocketUsers};
|
||||
use crate::auth::Headers;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
#[derive(FromForm)]
|
||||
#[allow(non_snake_case)]
|
||||
struct SyncData {
|
||||
excludeDomains: bool,
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
sync,
|
||||
get_ciphers,
|
||||
get_cipher,
|
||||
get_cipher_admin,
|
||||
get_cipher_details,
|
||||
post_ciphers,
|
||||
put_cipher_admin,
|
||||
post_ciphers_admin,
|
||||
post_ciphers_create,
|
||||
post_ciphers_import,
|
||||
post_attachment,
|
||||
post_attachment_admin,
|
||||
post_attachment_share,
|
||||
delete_attachment_post,
|
||||
delete_attachment_post_admin,
|
||||
delete_attachment,
|
||||
delete_attachment_admin,
|
||||
post_cipher_admin,
|
||||
post_cipher_share,
|
||||
put_cipher_share,
|
||||
put_cipher_share_seleted,
|
||||
post_cipher,
|
||||
put_cipher,
|
||||
delete_cipher_post,
|
||||
delete_cipher_post_admin,
|
||||
delete_cipher,
|
||||
delete_cipher_admin,
|
||||
delete_cipher_selected,
|
||||
delete_cipher_selected_post,
|
||||
delete_all,
|
||||
move_cipher_selected,
|
||||
move_cipher_selected_put,
|
||||
|
||||
post_collections_update,
|
||||
post_collections_admin,
|
||||
put_collections_admin,
|
||||
]
|
||||
}
|
||||
|
||||
#[get("/sync?<data>")]
|
||||
fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
#[derive(FromForm, Default)]
|
||||
struct SyncData {
|
||||
#[form(field = "excludeDomains")]
|
||||
exclude_domains: bool, // Default: 'false'
|
||||
}
|
||||
|
||||
#[get("/sync?<data..>")]
|
||||
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let user_json = headers.user.to_json(&conn);
|
||||
|
||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||
@@ -41,7 +82,7 @@ fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
|
||||
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||
|
||||
let domains_json = if data.excludeDomains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
|
||||
let domains_json = if data.exclude_domains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
|
||||
|
||||
Ok(Json(json!({
|
||||
"Profile": user_json,
|
||||
@@ -53,14 +94,6 @@ fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/sync")]
|
||||
fn sync_no_query(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let sync_data = SyncData {
|
||||
excludeDomains: false,
|
||||
};
|
||||
sync(sync_data, headers, conn)
|
||||
}
|
||||
|
||||
#[get("/ciphers")]
|
||||
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
|
||||
@@ -103,7 +136,7 @@ fn get_cipher_details(uuid: String, headers: Headers, conn: DbConn) -> JsonResul
|
||||
#[allow(non_snake_case)]
|
||||
pub struct CipherData {
|
||||
// Id is optional as it is included only in bulk share
|
||||
Id: Option<String>,
|
||||
pub Id: Option<String>,
|
||||
// Folder id is not included in import
|
||||
FolderId: Option<String>,
|
||||
// TODO: Some of these might appear all the time, no need for Option
|
||||
@@ -129,12 +162,37 @@ pub struct CipherData {
|
||||
Favorite: Option<bool>,
|
||||
|
||||
PasswordHistory: Option<Value>,
|
||||
|
||||
// These are used during key rotation
|
||||
#[serde(rename = "Attachments")]
|
||||
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
|
||||
Attachments2: Option<HashMap<String, Attachments2Data>>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct Attachments2Data {
|
||||
FileName: String,
|
||||
Key: String,
|
||||
}
|
||||
|
||||
#[post("/ciphers/admin", data = "<data>")]
|
||||
fn post_ciphers_admin(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
// TODO: Implement this correctly
|
||||
post_ciphers(data, headers, conn, ws)
|
||||
fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
let data: ShareCipherData = data.into_inner().data;
|
||||
|
||||
let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone());
|
||||
cipher.user_uuid = Some(headers.user.uuid.clone());
|
||||
match cipher.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed saving cipher")
|
||||
};
|
||||
|
||||
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers/create", data = "<data>")]
|
||||
fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
|
||||
post_ciphers_admin(data, headers, conn, ws)
|
||||
}
|
||||
|
||||
#[post("/ciphers", data = "<data>")]
|
||||
@@ -175,6 +233,28 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
|
||||
}
|
||||
}
|
||||
|
||||
// Modify attachments name and keys when rotating
|
||||
if let Some(attachments) = data.Attachments2 {
|
||||
for (id, attachment) in attachments {
|
||||
let mut saved_att = match Attachment::find_by_id(&id, &conn) {
|
||||
Some(att) => att,
|
||||
None => err!("Attachment doesn't exist")
|
||||
};
|
||||
|
||||
if saved_att.cipher_uuid != cipher.uuid {
|
||||
err!("Attachment is not owned by the cipher")
|
||||
}
|
||||
|
||||
saved_att.key = Some(attachment.Key);
|
||||
saved_att.file_name = attachment.FileName;
|
||||
|
||||
match saved_att.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed to save attachment")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let type_data_opt = match data.Type {
|
||||
1 => data.Login,
|
||||
2 => data.SecureNote,
|
||||
@@ -206,7 +286,7 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
|
||||
|
||||
match cipher.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => println!("Error: Failed to save cipher")
|
||||
Err(_) => err!("Failed to save cipher")
|
||||
};
|
||||
ws.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
|
||||
|
||||
@@ -253,7 +333,6 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
|
||||
}
|
||||
|
||||
// Read the relations between folders and ciphers
|
||||
use std::collections::HashMap;
|
||||
let mut relations_map = HashMap::new();
|
||||
|
||||
for relation in data.FolderRelationships {
|
||||
@@ -496,37 +575,52 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
|
||||
|
||||
let base_path = Path::new(&CONFIG.attachments_folder).join(&cipher.uuid);
|
||||
|
||||
let mut attachment_key = None;
|
||||
|
||||
Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| {
|
||||
// This is provided by the client, don't trust it
|
||||
let name = field.headers.filename.expect("No filename provided");
|
||||
|
||||
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
|
||||
let path = base_path.join(&file_name);
|
||||
|
||||
let size = match field.data.save()
|
||||
.memory_threshold(0)
|
||||
.size_limit(None)
|
||||
.with_path(path) {
|
||||
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||
SaveResult::Full(other) => {
|
||||
println!("Attachment is not a file: {:?}", other);
|
||||
return;
|
||||
match field.headers.name.as_str() {
|
||||
"key" => {
|
||||
use std::io::Read;
|
||||
let mut key_buffer = String::new();
|
||||
if field.data.read_to_string(&mut key_buffer).is_ok() {
|
||||
attachment_key = Some(key_buffer);
|
||||
}
|
||||
},
|
||||
SaveResult::Partial(_, reason) => {
|
||||
println!("Partial result: {:?}", reason);
|
||||
return;
|
||||
},
|
||||
SaveResult::Error(e) => {
|
||||
println!("Error: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
"data" => {
|
||||
// This is provided by the client, don't trust it
|
||||
let name = field.headers.filename.expect("No filename provided");
|
||||
|
||||
let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
|
||||
match attachment.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => println!("Error: failed to save attachment")
|
||||
};
|
||||
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
|
||||
let path = base_path.join(&file_name);
|
||||
|
||||
let size = match field.data.save()
|
||||
.memory_threshold(0)
|
||||
.size_limit(None)
|
||||
.with_path(path) {
|
||||
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||
SaveResult::Full(other) => {
|
||||
error!("Attachment is not a file: {:?}", other);
|
||||
return;
|
||||
},
|
||||
SaveResult::Partial(_, reason) => {
|
||||
error!("Partial result: {:?}", reason);
|
||||
return;
|
||||
},
|
||||
SaveResult::Error(e) => {
|
||||
error!("Error: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
|
||||
attachment.key = attachment_key.clone();
|
||||
match attachment.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => error!("Failed to save attachment")
|
||||
};
|
||||
},
|
||||
_ => error!("Invalid multipart name")
|
||||
}
|
||||
}).expect("Error processing multipart data");
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
||||
@@ -657,7 +751,7 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn,
|
||||
}
|
||||
match cipher.save(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => println!("Error: Failed to save cipher")
|
||||
Err(_) => err!("Failed to save cipher")
|
||||
};
|
||||
ws.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||
}
|
||||
@@ -695,8 +789,7 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, ws
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
if f.delete(&conn).is_err() {
|
||||
err!("Failed deleting folder")
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
ws.send_folder_update(UpdateType::SyncFolderCreate, &f);
|
||||
}
|
||||
}
|
||||
@@ -748,6 +841,6 @@ fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &He
|
||||
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => err!("Deleting attachement failed")
|
||||
Err(_) => err!("Deleting attachment failed")
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,26 @@
|
||||
use rocket::State;
|
||||
use rocket_contrib::{Json, Value};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::models::*;
|
||||
|
||||
use api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use auth::Headers;
|
||||
use crate::api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use crate::auth::Headers;
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
get_folders,
|
||||
get_folder,
|
||||
post_folders,
|
||||
post_folder,
|
||||
put_folder,
|
||||
delete_folder_post,
|
||||
delete_folder,
|
||||
]
|
||||
}
|
||||
|
||||
#[get("/folders")]
|
||||
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
|
@@ -4,178 +4,69 @@ mod folders;
|
||||
mod organizations;
|
||||
pub(crate) mod two_factor;
|
||||
|
||||
use self::accounts::*;
|
||||
use self::ciphers::*;
|
||||
use self::folders::*;
|
||||
use self::organizations::*;
|
||||
use self::two_factor::*;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
register,
|
||||
profile,
|
||||
put_profile,
|
||||
post_profile,
|
||||
get_public_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
post_kdf,
|
||||
post_sstamp,
|
||||
post_email_token,
|
||||
post_email,
|
||||
delete_account,
|
||||
post_delete_account,
|
||||
revision_date,
|
||||
password_hint,
|
||||
prelogin,
|
||||
|
||||
sync,
|
||||
sync_no_query,
|
||||
|
||||
get_ciphers,
|
||||
get_cipher,
|
||||
get_cipher_admin,
|
||||
get_cipher_details,
|
||||
post_ciphers,
|
||||
put_cipher_admin,
|
||||
post_ciphers_admin,
|
||||
post_ciphers_import,
|
||||
post_attachment,
|
||||
post_attachment_admin,
|
||||
post_attachment_share,
|
||||
delete_attachment_post,
|
||||
delete_attachment_post_admin,
|
||||
delete_attachment,
|
||||
delete_attachment_admin,
|
||||
post_cipher_admin,
|
||||
post_cipher_share,
|
||||
put_cipher_share,
|
||||
put_cipher_share_seleted,
|
||||
post_cipher,
|
||||
put_cipher,
|
||||
delete_cipher_post,
|
||||
delete_cipher_post_admin,
|
||||
delete_cipher,
|
||||
delete_cipher_admin,
|
||||
delete_cipher_selected,
|
||||
delete_cipher_selected_post,
|
||||
delete_all,
|
||||
move_cipher_selected,
|
||||
move_cipher_selected_put,
|
||||
|
||||
get_folders,
|
||||
get_folder,
|
||||
post_folders,
|
||||
post_folder,
|
||||
put_folder,
|
||||
delete_folder_post,
|
||||
delete_folder,
|
||||
|
||||
get_twofactor,
|
||||
get_recover,
|
||||
recover,
|
||||
disable_twofactor,
|
||||
disable_twofactor_put,
|
||||
generate_authenticator,
|
||||
activate_authenticator,
|
||||
activate_authenticator_put,
|
||||
generate_u2f,
|
||||
generate_u2f_challenge,
|
||||
activate_u2f,
|
||||
activate_u2f_put,
|
||||
|
||||
get_organization,
|
||||
create_organization,
|
||||
delete_organization,
|
||||
post_delete_organization,
|
||||
leave_organization,
|
||||
get_user_collections,
|
||||
get_org_collections,
|
||||
get_org_collection_detail,
|
||||
get_collection_users,
|
||||
put_organization,
|
||||
post_organization,
|
||||
post_organization_collections,
|
||||
delete_organization_collection_user,
|
||||
post_organization_collection_delete_user,
|
||||
post_organization_collection_update,
|
||||
put_organization_collection_update,
|
||||
delete_organization_collection,
|
||||
post_organization_collection_delete,
|
||||
post_collections_update,
|
||||
post_collections_admin,
|
||||
put_collections_admin,
|
||||
get_org_details,
|
||||
get_org_users,
|
||||
send_invite,
|
||||
confirm_invite,
|
||||
get_user,
|
||||
edit_user,
|
||||
put_organization_user,
|
||||
delete_user,
|
||||
post_delete_user,
|
||||
post_reinvite_user,
|
||||
post_org_import,
|
||||
|
||||
let mut mod_routes = routes![
|
||||
clear_device_token,
|
||||
put_device_token,
|
||||
|
||||
get_eq_domains,
|
||||
post_eq_domains,
|
||||
put_eq_domains,
|
||||
];
|
||||
|
||||
]
|
||||
let mut routes = Vec::new();
|
||||
routes.append(&mut accounts::routes());
|
||||
routes.append(&mut ciphers::routes());
|
||||
routes.append(&mut folders::routes());
|
||||
routes.append(&mut organizations::routes());
|
||||
routes.append(&mut two_factor::routes());
|
||||
routes.append(&mut mod_routes);
|
||||
|
||||
routes
|
||||
}
|
||||
|
||||
///
|
||||
/// Move this somewhere else
|
||||
///
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
use rocket_contrib::{Json, Value};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use api::{JsonResult, EmptyResult, JsonUpcase};
|
||||
use auth::Headers;
|
||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
|
||||
use crate::auth::Headers;
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token", data = "<data>")]
|
||||
fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let _data: Value = data.into_inner();
|
||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
||||
// This endpoint doesn't have auth header
|
||||
|
||||
let device = match Device::find_by_uuid(&uuid, &conn) {
|
||||
Some(device) => device,
|
||||
None => err!("Device not found")
|
||||
};
|
||||
let _ = uuid;
|
||||
// uuid is not related to deviceId
|
||||
|
||||
if device.user_uuid != headers.user.uuid {
|
||||
err!("Device not owned by user")
|
||||
}
|
||||
|
||||
match device.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed deleting device")
|
||||
}
|
||||
// This only clears push token
|
||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let _data: Value = data.into_inner();
|
||||
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> JsonResult {
|
||||
let _data: Value = data.into_inner().data;
|
||||
// Data has a single string value "PushToken"
|
||||
let _ = uuid;
|
||||
// uuid is not related to deviceId
|
||||
|
||||
let device = match Device::find_by_uuid(&uuid, &conn) {
|
||||
Some(device) => device,
|
||||
None => err!("Device not found")
|
||||
};
|
||||
// TODO: This should save the push token, but we don't have push functionality
|
||||
|
||||
if device.user_uuid != headers.user.uuid {
|
||||
err!("Device not owned by user")
|
||||
}
|
||||
|
||||
// TODO: What does this do?
|
||||
|
||||
err!("Not implemented")
|
||||
Ok(Json(json!({
|
||||
"Id": headers.device.uuid,
|
||||
"Name": headers.device.name,
|
||||
"Type": headers.device.type_,
|
||||
"Identifier": headers.device.uuid,
|
||||
"CreationDate": crate::util::format_date(&headers.device.created_at),
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -209,7 +100,6 @@ fn get_eq_domains(headers: Headers) -> JsonResult {
|
||||
})))
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct EquivDomainData {
|
||||
@@ -232,9 +122,8 @@ fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: Db
|
||||
|
||||
match user.save(&conn) {
|
||||
Ok(()) => Ok(Json(json!({}))),
|
||||
Err(_) => err!("Failed to save user")
|
||||
Err(_) => err!("Failed to save user"),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[put("/settings/domains", data = "<data>")]
|
||||
|
@@ -1,14 +1,53 @@
|
||||
use rocket::State;
|
||||
use rocket_contrib::{Json, Value};
|
||||
use CONFIG;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
use rocket::request::Form;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use auth::{Headers, AdminHeaders, OwnerHeaders};
|
||||
use crate::CONFIG;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::models::*;
|
||||
|
||||
use crate::api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
|
||||
use crate::auth::{Headers, AdminHeaders, OwnerHeaders};
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
get_organization,
|
||||
create_organization,
|
||||
delete_organization,
|
||||
post_delete_organization,
|
||||
leave_organization,
|
||||
get_user_collections,
|
||||
get_org_collections,
|
||||
get_org_collection_detail,
|
||||
get_collection_users,
|
||||
put_organization,
|
||||
post_organization,
|
||||
post_organization_collections,
|
||||
delete_organization_collection_user,
|
||||
post_organization_collection_delete_user,
|
||||
post_organization_collection_update,
|
||||
put_organization_collection_update,
|
||||
delete_organization_collection,
|
||||
post_organization_collection_delete,
|
||||
get_org_details,
|
||||
get_org_users,
|
||||
send_invite,
|
||||
confirm_invite,
|
||||
get_user,
|
||||
edit_user,
|
||||
put_organization_user,
|
||||
delete_user,
|
||||
post_delete_user,
|
||||
post_reinvite_user,
|
||||
post_org_import,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
@@ -91,7 +130,7 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
|
||||
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||
None => err!("User not part of organization"),
|
||||
Some(user_org) => {
|
||||
if user_org.type_ == UserOrgType::Owner as i32 {
|
||||
if user_org.type_ == UserOrgType::Owner {
|
||||
let num_owners = UserOrganization::find_by_org_and_type(
|
||||
&org_id, UserOrgType::Owner as i32, &conn)
|
||||
.len();
|
||||
@@ -315,14 +354,14 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
#[allow(non_snake_case)]
|
||||
struct OrgIdData {
|
||||
organizationId: String
|
||||
#[form(field = "organizationId")]
|
||||
organization_id: String
|
||||
}
|
||||
|
||||
#[get("/ciphers/organization-details?<data>")]
|
||||
fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let ciphers = Cipher::find_by_org(&data.organizationId, &conn);
|
||||
#[get("/ciphers/organization-details?<data..>")]
|
||||
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
|
||||
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||
|
||||
Ok(Json(json!({
|
||||
@@ -378,9 +417,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
None => err!("Invalid type")
|
||||
};
|
||||
|
||||
if new_type != UserOrgType::User as i32 &&
|
||||
headers.org_user_type != UserOrgType::Owner as i32 {
|
||||
err!("Only Owners can invite Admins or Owners")
|
||||
if new_type != UserOrgType::User &&
|
||||
headers.org_user_type != UserOrgType::Owner {
|
||||
err!("Only Owners can invite Managers, Admins or Owners")
|
||||
}
|
||||
|
||||
for email in data.Emails.iter() {
|
||||
@@ -452,9 +491,9 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
|
||||
None => err!("The specified user isn't a member of the organization")
|
||||
};
|
||||
|
||||
if user_to_confirm.type_ != UserOrgType::User as i32 &&
|
||||
headers.org_user_type != UserOrgType::Owner as i32 {
|
||||
err!("Only Owners can confirm Admins or Owners")
|
||||
if user_to_confirm.type_ != UserOrgType::User &&
|
||||
headers.org_user_type != UserOrgType::Owner {
|
||||
err!("Only Owners can confirm Managers, Admins or Owners")
|
||||
}
|
||||
|
||||
if user_to_confirm.status != UserOrgStatus::Accepted as i32 {
|
||||
@@ -502,7 +541,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
|
||||
let data: EditUserData = data.into_inner().data;
|
||||
|
||||
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
|
||||
Some(new_type) => new_type as i32,
|
||||
Some(new_type) => new_type,
|
||||
None => err!("Invalid type")
|
||||
};
|
||||
|
||||
@@ -511,21 +550,21 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
|
||||
None => err!("The specified user isn't member of the organization")
|
||||
};
|
||||
|
||||
if new_type != user_to_edit.type_ as i32 && (
|
||||
user_to_edit.type_ <= UserOrgType::Admin as i32 ||
|
||||
new_type <= UserOrgType::Admin as i32
|
||||
if new_type != user_to_edit.type_ && (
|
||||
user_to_edit.type_ >= UserOrgType::Admin ||
|
||||
new_type >= UserOrgType::Admin
|
||||
) &&
|
||||
headers.org_user_type != UserOrgType::Owner as i32 {
|
||||
headers.org_user_type != UserOrgType::Owner {
|
||||
err!("Only Owners can grant and remove Admin or Owner privileges")
|
||||
}
|
||||
|
||||
if user_to_edit.type_ == UserOrgType::Owner as i32 &&
|
||||
headers.org_user_type != UserOrgType::Owner as i32 {
|
||||
if user_to_edit.type_ == UserOrgType::Owner &&
|
||||
headers.org_user_type != UserOrgType::Owner {
|
||||
err!("Only Owners can edit Owner users")
|
||||
}
|
||||
|
||||
if user_to_edit.type_ == UserOrgType::Owner as i32 &&
|
||||
new_type != UserOrgType::Owner as i32 {
|
||||
if user_to_edit.type_ == UserOrgType::Owner &&
|
||||
new_type != UserOrgType::Owner {
|
||||
|
||||
// Removing owner permmission, check that there are at least another owner
|
||||
let num_owners = UserOrganization::find_by_org_and_type(
|
||||
@@ -538,7 +577,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
|
||||
}
|
||||
|
||||
user_to_edit.access_all = data.AccessAll;
|
||||
user_to_edit.type_ = new_type;
|
||||
user_to_edit.type_ = new_type as i32;
|
||||
|
||||
// Delete all the odd collections
|
||||
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
|
||||
@@ -591,12 +630,12 @@ fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn:
|
||||
None => err!("User to delete isn't member of the organization")
|
||||
};
|
||||
|
||||
if user_to_delete.type_ != UserOrgType::User as i32 &&
|
||||
headers.org_user_type != UserOrgType::Owner as i32 {
|
||||
if user_to_delete.type_ != UserOrgType::User &&
|
||||
headers.org_user_type != UserOrgType::Owner {
|
||||
err!("Only Owners can delete Admins or Owners")
|
||||
}
|
||||
|
||||
if user_to_delete.type_ == UserOrgType::Owner as i32 {
|
||||
if user_to_delete.type_ == UserOrgType::Owner {
|
||||
// Removing owner, check that there are at least another owner
|
||||
let num_owners = UserOrganization::find_by_org_and_type(
|
||||
&org_id, UserOrgType::Owner as i32, &conn)
|
||||
@@ -643,17 +682,17 @@ struct RelationsData {
|
||||
Value: usize,
|
||||
}
|
||||
|
||||
#[post("/ciphers/import-organization?<query>", data = "<data>")]
|
||||
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
|
||||
fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
|
||||
let data: ImportData = data.into_inner().data;
|
||||
let org_id = query.organizationId;
|
||||
let org_id = query.into_inner().organization_id;
|
||||
|
||||
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||
Some(user) => user,
|
||||
None => err!("User is not part of the organization")
|
||||
};
|
||||
|
||||
if org_user.type_ > UserOrgType::Admin as i32 {
|
||||
if org_user.type_ < UserOrgType::Admin {
|
||||
err!("Only admins or owners can import into an organization")
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,40 @@
|
||||
use data_encoding::BASE32;
|
||||
use rocket_contrib::{Json, Value};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
|
||||
use db::{
|
||||
|
||||
use crate::db::{
|
||||
models::{TwoFactor, TwoFactorType, User},
|
||||
DbConn,
|
||||
};
|
||||
|
||||
use crypto;
|
||||
use crate::crypto;
|
||||
|
||||
use api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||
use auth::Headers;
|
||||
use crate::api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||
use crate::auth::Headers;
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
get_twofactor,
|
||||
get_recover,
|
||||
recover,
|
||||
disable_twofactor,
|
||||
disable_twofactor_put,
|
||||
generate_authenticator,
|
||||
activate_authenticator,
|
||||
activate_authenticator_put,
|
||||
generate_u2f,
|
||||
generate_u2f_challenge,
|
||||
activate_u2f,
|
||||
activate_u2f_put,
|
||||
generate_yubikey,
|
||||
activate_yubikey,
|
||||
activate_yubikey_put,
|
||||
]
|
||||
}
|
||||
|
||||
#[get("/two-factor")]
|
||||
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
@@ -50,7 +74,7 @@ struct RecoverTwoFactor {
|
||||
fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
|
||||
let data: RecoverTwoFactor = data.into_inner().data;
|
||||
|
||||
use db::models::User;
|
||||
use crate::db::models::User;
|
||||
|
||||
// Get the user
|
||||
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
||||
@@ -219,7 +243,7 @@ fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
||||
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
||||
user.totp_recover = Some(totp_recover);
|
||||
if user.save(conn).is_err() {
|
||||
println!("Error: Failed to save the user's two factor recovery code")
|
||||
error!("Failed to save the user's two factor recovery code")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +252,7 @@ use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
||||
use u2f::protocol::{Challenge, U2f};
|
||||
use u2f::register::Registration;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
const U2F_VERSION: &str = "U2F_V2";
|
||||
|
||||
@@ -376,7 +400,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
|
||||
})))
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {:#?}", e);
|
||||
error!("{:#?}", e);
|
||||
err!("Error activating u2f")
|
||||
}
|
||||
}
|
||||
@@ -480,14 +504,228 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
|
||||
match response {
|
||||
Ok(new_counter) => {
|
||||
_counter = new_counter;
|
||||
println!("O {:#}", new_counter);
|
||||
info!("O {:#}", new_counter);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("E {:#}", e);
|
||||
info!("E {:#}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
err!("error verifying response")
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct EnableYubikeyData {
|
||||
MasterPasswordHash: String,
|
||||
Key1: Option<String>,
|
||||
Key2: Option<String>,
|
||||
Key3: Option<String>,
|
||||
Key4: Option<String>,
|
||||
Key5: Option<String>,
|
||||
Nfc: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct YubikeyMetadata {
|
||||
Keys: Vec<String>,
|
||||
pub Nfc: bool,
|
||||
}
|
||||
|
||||
use yubico::Yubico;
|
||||
use yubico::config::Config;
|
||||
|
||||
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
|
||||
let mut yubikeys: Vec<String> = Vec::new();
|
||||
|
||||
if data.Key1.is_some() {
|
||||
yubikeys.push(data.Key1.as_ref().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if data.Key2.is_some() {
|
||||
yubikeys.push(data.Key2.as_ref().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if data.Key3.is_some() {
|
||||
yubikeys.push(data.Key3.as_ref().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if data.Key4.is_some() {
|
||||
yubikeys.push(data.Key4.as_ref().unwrap().to_owned());
|
||||
}
|
||||
|
||||
if data.Key5.is_some() {
|
||||
yubikeys.push(data.Key5.as_ref().unwrap().to_owned());
|
||||
}
|
||||
|
||||
yubikeys
|
||||
}
|
||||
|
||||
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
|
||||
let mut result = json!({});
|
||||
|
||||
for (i, key) in yubikeys.into_iter().enumerate() {
|
||||
result[format!("Key{}", i+1)] = Value::String(key);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn verify_yubikey_otp(otp: String) -> JsonResult {
|
||||
if !CONFIG.yubico_cred_set {
|
||||
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
|
||||
Yubikey OTP Disabled")
|
||||
}
|
||||
|
||||
let yubico = Yubico::new();
|
||||
let config = Config::default().set_client_id(CONFIG.yubico_client_id.to_owned()).set_key(CONFIG.yubico_secret_key.to_owned());
|
||||
|
||||
let result = match CONFIG.yubico_server {
|
||||
Some(ref server) => yubico.verify(otp, config.set_api_hosts(vec![server.to_owned()])),
|
||||
None => yubico.verify(otp, config)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_answer) => Ok(Json(json!({}))),
|
||||
Err(_e) => err!("Failed to verify OTP"),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/two-factor/get-yubikey", data = "<data>")]
|
||||
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
if !CONFIG.yubico_cred_set {
|
||||
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
|
||||
Yubikey OTP Disabled")
|
||||
}
|
||||
|
||||
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 yubikey_type = TwoFactorType::YubiKey as i32;
|
||||
|
||||
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
|
||||
|
||||
if let Some(r) = r {
|
||||
let yubikey_metadata: YubikeyMetadata =
|
||||
serde_json::from_str(&r.data).expect("Can't parse YubikeyMetadata data");
|
||||
|
||||
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
|
||||
|
||||
result["Enabled"] = Value::Bool(true);
|
||||
result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
|
||||
result["Object"] = Value::String("twoFactorU2f".to_owned());
|
||||
|
||||
Ok(Json(result))
|
||||
} else {
|
||||
Ok(Json(json!({
|
||||
"Enabled": false,
|
||||
"Object": "twoFactorU2f",
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/two-factor/yubikey", data = "<data>")]
|
||||
fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: EnableYubikeyData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password");
|
||||
}
|
||||
|
||||
// Check if we already have some data
|
||||
let yubikey_data = TwoFactor::find_by_user_and_type(
|
||||
&headers.user.uuid,
|
||||
TwoFactorType::YubiKey as i32,
|
||||
&conn,
|
||||
);
|
||||
|
||||
if let Some(yubikey_data) = yubikey_data {
|
||||
yubikey_data.delete(&conn).expect("Error deleting current Yubikeys");
|
||||
}
|
||||
|
||||
let yubikeys = parse_yubikeys(&data);
|
||||
|
||||
if yubikeys.is_empty() {
|
||||
return Ok(Json(json!({
|
||||
"Enabled": false,
|
||||
"Object": "twoFactorU2f",
|
||||
})));
|
||||
}
|
||||
|
||||
// Ensure they are valid OTPs
|
||||
for yubikey in &yubikeys {
|
||||
if yubikey.len() == 12 {
|
||||
// YubiKey ID
|
||||
continue
|
||||
}
|
||||
|
||||
let result = verify_yubikey_otp(yubikey.to_owned());
|
||||
|
||||
if let Err(_e) = result {
|
||||
err!("Invalid Yubikey OTP provided");
|
||||
}
|
||||
}
|
||||
|
||||
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
|
||||
|
||||
let yubikey_metadata = YubikeyMetadata {
|
||||
Keys: yubikey_ids,
|
||||
Nfc: data.Nfc,
|
||||
};
|
||||
|
||||
let yubikey_registration = TwoFactor::new(
|
||||
headers.user.uuid.clone(),
|
||||
TwoFactorType::YubiKey,
|
||||
serde_json::to_string(&yubikey_metadata).unwrap(),
|
||||
);
|
||||
yubikey_registration
|
||||
.save(&conn).expect("Failed to save Yubikey info");
|
||||
|
||||
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
|
||||
|
||||
result["Enabled"] = Value::Bool(true);
|
||||
result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
|
||||
result["Object"] = Value::String("twoFactorU2f".to_owned());
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[put("/two-factor/yubikey", data = "<data>")]
|
||||
fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
activate_yubikey(data, headers, conn)
|
||||
}
|
||||
|
||||
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> {
|
||||
if response.len() != 44 {
|
||||
err!("Invalid Yubikey OTP length");
|
||||
}
|
||||
|
||||
let yubikey_type = TwoFactorType::YubiKey as i32;
|
||||
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn) {
|
||||
Some(tf) => tf,
|
||||
None => err!("No YubiKey devices registered"),
|
||||
};
|
||||
|
||||
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
|
||||
let response_id = &response[..12];
|
||||
|
||||
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
|
||||
err!("Given Yubikey is not registered");
|
||||
}
|
||||
|
||||
let result = verify_yubikey_otp(response.to_owned());
|
||||
|
||||
match result {
|
||||
Ok(_answer) => Ok(()),
|
||||
Err(_e) => err!("Failed to verify Yubikey against OTP server"),
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ use rocket::http::ContentType;
|
||||
|
||||
use reqwest;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![icon]
|
||||
@@ -43,7 +43,7 @@ fn get_icon (domain: &str) -> Vec<u8> {
|
||||
icon
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error downloading icon: {:?}", e);
|
||||
error!("Error downloading icon: {:?}", e);
|
||||
get_fallback_icon()
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ fn get_icon_url(domain: &str) -> String {
|
||||
}
|
||||
|
||||
fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
|
||||
println!("Downloading icon for {}...", url);
|
||||
info!("Downloading icon for {}...", url);
|
||||
let mut res = reqwest::get(url)?;
|
||||
|
||||
res = res.error_for_status()?;
|
||||
@@ -105,7 +105,7 @@ fn get_fallback_icon() -> Vec<u8> {
|
||||
icon
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error downloading fallback icon: {:?}", e);
|
||||
error!("Error downloading fallback icon: {:?}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
@@ -1,42 +1,57 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use rocket::request::{Form, FormItems, FromForm};
|
||||
use rocket::Route;
|
||||
|
||||
use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request};
|
||||
use rocket::{Outcome, Route};
|
||||
|
||||
use rocket_contrib::{Json, Value};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use db::models::*;
|
||||
use db::DbConn;
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use util::{self, JsonMap};
|
||||
use crate::util::{self, JsonMap};
|
||||
|
||||
use api::{ApiResult, JsonResult};
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||
|
||||
use CONFIG;
|
||||
use crate::auth::ClientIp;
|
||||
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![login]
|
||||
}
|
||||
|
||||
#[post("/connect/token", data = "<connect_data>")]
|
||||
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn, socket: Option<SocketAddr>) -> JsonResult {
|
||||
let data = connect_data.get();
|
||||
#[post("/connect/token", data = "<data>")]
|
||||
fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
|
||||
let data: ConnectData = data.into_inner();
|
||||
|
||||
match data.grant_type {
|
||||
GrantType::RefreshToken => _refresh_login(data, device_type, conn),
|
||||
GrantType::Password => _password_login(data, device_type, conn, socket),
|
||||
match data.grant_type.as_ref() {
|
||||
"refresh_token" => {
|
||||
_check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
|
||||
_refresh_login(data, conn)
|
||||
}
|
||||
"password" => {
|
||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||
_check_is_some(&data.password, "password cannot be blank")?;
|
||||
_check_is_some(&data.scope, "scope cannot be blank")?;
|
||||
_check_is_some(&data.username, "username cannot be blank")?;
|
||||
|
||||
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||
|
||||
_password_login(data, conn, ip)
|
||||
}
|
||||
t => err!("Invalid type", t),
|
||||
}
|
||||
}
|
||||
|
||||
fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult {
|
||||
fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||
// Extract token
|
||||
let token = data.get("refresh_token");
|
||||
let token = data.refresh_token.unwrap();
|
||||
|
||||
// Get device by refresh token
|
||||
let mut device = match Device::find_by_refresh_token(token, &conn) {
|
||||
let mut device = match Device::find_by_refresh_token(&token, &conn) {
|
||||
Some(device) => device,
|
||||
None => err!("Invalid refresh token"),
|
||||
};
|
||||
@@ -47,90 +62,72 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
|
||||
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||
match device.save(&conn) {
|
||||
Ok(()) => Ok(Json(json!({
|
||||
"access_token": access_token,
|
||||
"expires_in": expires_in,
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": device.refresh_token,
|
||||
"Key": user.key,
|
||||
"PrivateKey": user.private_key,
|
||||
}))),
|
||||
Err(_) => err!("Failed to add device to user")
|
||||
Ok(()) => Ok(Json(json!({
|
||||
"access_token": access_token,
|
||||
"expires_in": expires_in,
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": device.refresh_token,
|
||||
"Key": user.key,
|
||||
"PrivateKey": user.private_key,
|
||||
}))),
|
||||
Err(e) => err!("Failed to add device to user", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, remote: Option<SocketAddr>) -> JsonResult {
|
||||
// Get the ip for error reporting
|
||||
let ip = match remote {
|
||||
Some(ip) => ip.ip(),
|
||||
None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
};
|
||||
|
||||
fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
|
||||
// Validate scope
|
||||
let scope = data.get("scope");
|
||||
let scope = data.scope.as_ref().unwrap();
|
||||
if scope != "api offline_access" {
|
||||
err!("Scope not supported")
|
||||
}
|
||||
|
||||
// Get the user
|
||||
let username = data.get("username");
|
||||
let username = data.username.as_ref().unwrap();
|
||||
let user = match User::find_by_mail(username, &conn) {
|
||||
Some(user) => user,
|
||||
None => err!(format!(
|
||||
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
|
||||
ip, username
|
||||
ip.ip, username
|
||||
)),
|
||||
};
|
||||
|
||||
// Check password
|
||||
let password = data.get("password");
|
||||
let password = data.password.as_ref().unwrap();
|
||||
if !user.check_valid_password(password) {
|
||||
err!(format!(
|
||||
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
|
||||
ip, username
|
||||
ip.ip, username
|
||||
))
|
||||
}
|
||||
|
||||
// Let's only use the header and ignore the 'devicetype' parameter
|
||||
let device_type_num = device_type.0;
|
||||
|
||||
let (device_id, device_name) = if data.is_device {
|
||||
(
|
||||
data.get("deviceidentifier").clone(),
|
||||
data.get("devicename").clone(),
|
||||
)
|
||||
} else {
|
||||
(format!("web-{}", user.uuid), String::from("web"))
|
||||
};
|
||||
// On iOS, device_type sends "iOS", on others it sends a number
|
||||
let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0);
|
||||
let device_id = data.device_identifier.clone().expect("No device id provided");
|
||||
let device_name = data.device_name.clone().expect("No device name provided");
|
||||
|
||||
// Find device or create new
|
||||
let mut device = match Device::find_by_uuid(&device_id, &conn) {
|
||||
Some(device) => {
|
||||
// Check if valid device
|
||||
// Check if owned device, and recreate if not
|
||||
if device.user_uuid != user.uuid {
|
||||
match device.delete(&conn) {
|
||||
Ok(()) => Device::new(device_id, user.uuid.clone(), device_name, device_type_num),
|
||||
Err(_) => err!("Tried to delete device not owned by user, but failed")
|
||||
}
|
||||
info!("Device exists but is owned by another user. The old device will be discarded");
|
||||
Device::new(device_id, user.uuid.clone(), device_name, device_type)
|
||||
} else {
|
||||
device
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Create new device
|
||||
Device::new(device_id, user.uuid.clone(), device_name, device_type_num)
|
||||
}
|
||||
None => Device::new(device_id, user.uuid.clone(), device_name, device_type),
|
||||
};
|
||||
|
||||
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
|
||||
let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?;
|
||||
|
||||
// Common
|
||||
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||
if device.save(&conn).is_err() {
|
||||
err!("Failed to add device to user")
|
||||
if let Err(e) = device.save(&conn) {
|
||||
err!("Failed to add device to user", e)
|
||||
}
|
||||
|
||||
let mut result = json!({
|
||||
@@ -147,15 +144,11 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, re
|
||||
result["TwoFactorToken"] = Value::String(token);
|
||||
}
|
||||
|
||||
info!("User {} logged in successfully. IP: {}", username, ip.ip);
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
fn twofactor_auth(
|
||||
user_uuid: &str,
|
||||
data: &ConnectData,
|
||||
device: &mut Device,
|
||||
conn: &DbConn,
|
||||
) -> ApiResult<Option<String>> {
|
||||
fn twofactor_auth(user_uuid: &str, data: &ConnectData, device: &mut Device, conn: &DbConn) -> ApiResult<Option<String>> {
|
||||
let twofactors_raw = TwoFactor::find_by_user(user_uuid, conn);
|
||||
// Remove u2f challenge twofactors (impl detail)
|
||||
let twofactors: Vec<_> = twofactors_raw.iter().filter(|tf| tf.type_ < 1000).collect();
|
||||
@@ -167,13 +160,10 @@ fn twofactor_auth(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let provider = match util::try_parse_string(data.get_opt("twoFactorProvider")) {
|
||||
Some(provider) => provider,
|
||||
None => providers[0], // If we aren't given a two factor provider, asume the first one
|
||||
};
|
||||
let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one
|
||||
|
||||
let twofactor_code = match data.get_opt("twoFactorToken") {
|
||||
Some(code) => code,
|
||||
let twofactor_code = match data.two_factor_token {
|
||||
Some(ref code) => code,
|
||||
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
||||
};
|
||||
|
||||
@@ -181,8 +171,8 @@ fn twofactor_auth(
|
||||
|
||||
match TwoFactorType::from_i32(provider) {
|
||||
Some(TwoFactorType::Remember) => {
|
||||
match &device.twofactor_remember {
|
||||
Some(remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
|
||||
match device.twofactor_remember {
|
||||
Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
|
||||
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
||||
}
|
||||
}
|
||||
@@ -204,15 +194,21 @@ fn twofactor_auth(
|
||||
}
|
||||
|
||||
Some(TwoFactorType::U2f) => {
|
||||
use api::core::two_factor;
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?;
|
||||
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
|
||||
}
|
||||
|
||||
Some(TwoFactorType::YubiKey) => {
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
|
||||
}
|
||||
|
||||
_ => err!("Invalid two factor provider"),
|
||||
}
|
||||
|
||||
if util::try_parse_string_or(data.get_opt("twoFactorRemember"), 0) == 1 {
|
||||
if data.two_factor_remember.unwrap_or(0) == 1 {
|
||||
Ok(Some(device.refresh_twofactor_remember()))
|
||||
} else {
|
||||
device.delete_twofactor_remember();
|
||||
@@ -221,7 +217,7 @@ fn twofactor_auth(
|
||||
}
|
||||
|
||||
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
||||
use api::core::two_factor;
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
let mut result = json!({
|
||||
"error" : "invalid_grant",
|
||||
@@ -244,13 +240,9 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||
let mut challenge_map = JsonMap::new();
|
||||
|
||||
challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
|
||||
challenge_map
|
||||
.insert("challenge".into(), Value::String(request.challenge.clone()));
|
||||
challenge_map.insert("challenge".into(), Value::String(request.challenge.clone()));
|
||||
challenge_map.insert("version".into(), Value::String(key.version));
|
||||
challenge_map.insert(
|
||||
"keyHandle".into(),
|
||||
Value::String(key.key_handle.unwrap_or_default()),
|
||||
);
|
||||
challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default()));
|
||||
|
||||
challenge_list.push(Value::Object(challenge_map));
|
||||
}
|
||||
@@ -263,6 +255,19 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
||||
}
|
||||
|
||||
Some(TwoFactorType::YubiKey) => {
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::YubiKey as i32, &conn) {
|
||||
Some(tf) => tf,
|
||||
None => err!("No YubiKey devices registered"),
|
||||
};
|
||||
|
||||
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
|
||||
|
||||
let mut map = JsonMap::new();
|
||||
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc));
|
||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -270,93 +275,64 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct DeviceType(i32);
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
|
||||
type Error = &'static str;
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
let headers = request.headers();
|
||||
let type_opt = headers.get_one("Device-Type");
|
||||
let type_num = util::try_parse_string_or(type_opt, 0);
|
||||
|
||||
Outcome::Success(DeviceType(type_num))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ConnectData {
|
||||
grant_type: GrantType,
|
||||
is_device: bool,
|
||||
data: HashMap<String, String>,
|
||||
grant_type: String, // refresh_token, password
|
||||
|
||||
// Needed for grant_type="refresh_token"
|
||||
refresh_token: Option<String>,
|
||||
|
||||
// Needed for grant_type="password"
|
||||
client_id: Option<String>, // web, cli, desktop, browser, mobile
|
||||
password: Option<String>,
|
||||
scope: Option<String>,
|
||||
username: Option<String>,
|
||||
|
||||
device_identifier: Option<String>,
|
||||
device_name: Option<String>,
|
||||
device_type: Option<String>,
|
||||
|
||||
// Needed for two-factor auth
|
||||
two_factor_provider: Option<i32>,
|
||||
two_factor_token: Option<String>,
|
||||
two_factor_remember: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum GrantType {
|
||||
RefreshToken,
|
||||
Password,
|
||||
}
|
||||
|
||||
impl ConnectData {
|
||||
fn get(&self, key: &str) -> &String {
|
||||
&self.data[&key.to_lowercase()]
|
||||
}
|
||||
|
||||
fn get_opt(&self, key: &str) -> Option<&String> {
|
||||
self.data.get(&key.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
const VALUES_REFRESH: [&str; 1] = ["refresh_token"];
|
||||
const VALUES_PASSWORD: [&str; 5] = ["client_id", "grant_type", "password", "scope", "username"];
|
||||
const VALUES_DEVICE: [&str; 3] = ["deviceidentifier", "devicename", "devicetype"];
|
||||
|
||||
impl<'f> FromForm<'f> for ConnectData {
|
||||
type Error = String;
|
||||
|
||||
fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<Self, Self::Error> {
|
||||
let mut data = HashMap::new();
|
||||
let mut form = Self::default();
|
||||
for item in items {
|
||||
let (key, value) = item.key_value_decoded();
|
||||
let mut normalized_key = key.to_lowercase();
|
||||
normalized_key.retain(|c| c != '_'); // Remove '_'
|
||||
|
||||
// Insert data into map
|
||||
for (key, value) in items {
|
||||
match (key.url_decode(), value.url_decode()) {
|
||||
(Ok(key), Ok(value)) => data.insert(key.to_lowercase(), value),
|
||||
_ => return Err("Error decoding key or value".to_string()),
|
||||
};
|
||||
match normalized_key.as_ref() {
|
||||
"granttype" => form.grant_type = value,
|
||||
"refreshtoken" => form.refresh_token = Some(value),
|
||||
"clientid" => form.client_id = Some(value),
|
||||
"password" => form.password = Some(value),
|
||||
"scope" => form.scope = Some(value),
|
||||
"username" => form.username = Some(value),
|
||||
"deviceidentifier" => form.device_identifier = Some(value),
|
||||
"devicename" => form.device_name = Some(value),
|
||||
"devicetype" => form.device_type = Some(value),
|
||||
"twofactorprovider" => form.two_factor_provider = value.parse().ok(),
|
||||
"twofactortoken" => form.two_factor_token = Some(value),
|
||||
"twofactorremember" => form.two_factor_remember = value.parse().ok(),
|
||||
key => warn!("Detected unexpected parameter during login: {}", key),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate needed values
|
||||
let (grant_type, is_device) = match data.get("grant_type").map(String::as_ref) {
|
||||
Some("refresh_token") => {
|
||||
check_values(&data, &VALUES_REFRESH)?;
|
||||
(GrantType::RefreshToken, false) // Device doesn't matter here
|
||||
}
|
||||
Some("password") => {
|
||||
check_values(&data, &VALUES_PASSWORD)?;
|
||||
|
||||
let is_device = match data["client_id"].as_ref() {
|
||||
"browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?,
|
||||
_ => false,
|
||||
};
|
||||
(GrantType::Password, is_device)
|
||||
}
|
||||
_ => return Err("Grant type not supported".to_string()),
|
||||
};
|
||||
|
||||
Ok(ConnectData {
|
||||
grant_type,
|
||||
is_device,
|
||||
data,
|
||||
})
|
||||
Ok(form)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_values(map: &HashMap<String, String>, values: &[&str]) -> Result<bool, String> {
|
||||
for value in values {
|
||||
if !map.contains_key(*value) {
|
||||
return Err(format!("{} cannot be blank", value));
|
||||
}
|
||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||
if value.is_none() {
|
||||
err!(msg)
|
||||
}
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -12,14 +12,15 @@ pub use self::notifications::routes as notifications_routes;
|
||||
pub use self::notifications::{start_notification_server, WebSocketUsers, UpdateType};
|
||||
|
||||
use rocket::response::status::BadRequest;
|
||||
use rocket_contrib::Json;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
// Type aliases for API methods results
|
||||
type ApiResult<T> = Result<T, BadRequest<Json>>;
|
||||
type JsonResult = ApiResult<Json>;
|
||||
type ApiResult<T> = Result<T, BadRequest<Json<Value>>>;
|
||||
type JsonResult = ApiResult<Json<Value>>;
|
||||
type EmptyResult = ApiResult<()>;
|
||||
|
||||
use util;
|
||||
use crate::util;
|
||||
type JsonUpcase<T> = Json<util::UpCase<T>>;
|
||||
|
||||
// Common structs representing JSON data received
|
||||
|
@@ -1,12 +1,12 @@
|
||||
use rocket::Route;
|
||||
use rocket_contrib::Json;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use api::JsonResult;
|
||||
use auth::Headers;
|
||||
use db::DbConn;
|
||||
use crate::api::JsonResult;
|
||||
use crate::auth::Headers;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![negotiate, websockets_err]
|
||||
@@ -19,7 +19,7 @@ fn websockets_err() -> JsonResult {
|
||||
|
||||
#[post("/hub/negotiate")]
|
||||
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
||||
use crypto;
|
||||
use crate::crypto;
|
||||
use data_encoding::BASE64URL;
|
||||
|
||||
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
|
||||
@@ -52,7 +52,7 @@ use chashmap::CHashMap;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde_json::from_str;
|
||||
|
||||
use db::models::{Cipher, Folder, User};
|
||||
use crate::db::models::{Cipher, Folder, User};
|
||||
|
||||
use rmpv::Value;
|
||||
|
||||
@@ -139,7 +139,7 @@ impl Handler for WSHandler {
|
||||
let _id = &query_split[1][3..];
|
||||
|
||||
// Validate the user
|
||||
use auth;
|
||||
use crate::auth;
|
||||
let claims = match auth::decode_jwt(access_token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => {
|
||||
@@ -169,7 +169,7 @@ impl Handler for WSHandler {
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
|
||||
println!("Server got message '{}'. ", msg);
|
||||
info!("Server got message '{}'. ", msg);
|
||||
|
||||
if let Message::Text(text) = msg.clone() {
|
||||
let json = &text[..text.len() - 1]; // Remove last char
|
||||
@@ -219,7 +219,6 @@ 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,
|
||||
@@ -228,12 +227,11 @@ impl Factory for WSFactory {
|
||||
}
|
||||
|
||||
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);
|
||||
if let Some(user_uuid) = &handler.user_uuid {
|
||||
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
|
||||
user_conn.remove_item(&handler.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,10 +242,10 @@ pub struct WebSocketUsers {
|
||||
}
|
||||
|
||||
impl WebSocketUsers {
|
||||
fn send_update(&self, user_uuid: &String, data: Vec<u8>) -> ws::Result<()> {
|
||||
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
|
||||
if let Some(user) = self.map.get(user_uuid) {
|
||||
for sender in user.iter() {
|
||||
sender.send(data.clone())?;
|
||||
sender.send(data)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -264,7 +262,7 @@ impl WebSocketUsers {
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid.clone(), data).ok();
|
||||
self.send_update(&user.uuid.clone(), &data).ok();
|
||||
}
|
||||
|
||||
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
|
||||
@@ -277,10 +275,10 @@ impl WebSocketUsers {
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&folder.user_uuid, data).ok();
|
||||
self.send_update(&folder.user_uuid, &data).ok();
|
||||
}
|
||||
|
||||
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &Vec<String>) {
|
||||
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &[String]) {
|
||||
let user_uuid = convert_option(cipher.user_uuid.clone());
|
||||
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
||||
|
||||
@@ -296,7 +294,7 @@ impl WebSocketUsers {
|
||||
);
|
||||
|
||||
for uuid in user_uuids {
|
||||
self.send_update(&uuid, data.clone()).ok();
|
||||
self.send_update(&uuid, &data).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,10 @@ use rocket::response::{self, NamedFile, Responder};
|
||||
use rocket::response::content::Content;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::Route;
|
||||
use rocket_contrib::{Json, Value};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
if CONFIG.web_vault_enabled {
|
||||
@@ -73,7 +74,7 @@ fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
|
||||
|
||||
#[get("/alive")]
|
||||
fn alive() -> Json<String> {
|
||||
use util::format_date;
|
||||
use crate::util::format_date;
|
||||
use chrono::Utc;
|
||||
|
||||
Json(format_date(&Utc::now().naive_utc()))
|
||||
|
105
src/auth.rs
105
src/auth.rs
@@ -1,22 +1,21 @@
|
||||
///
|
||||
/// JWT Handling
|
||||
///
|
||||
|
||||
use util::read_file;
|
||||
use crate::util::read_file;
|
||||
use chrono::Duration;
|
||||
|
||||
use jwt;
|
||||
use jsonwebtoken::{self, Algorithm, Header};
|
||||
use serde::ser::Serialize;
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
|
||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
|
||||
pub static ref JWT_ISSUER: String = CONFIG.domain.clone();
|
||||
|
||||
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM);
|
||||
static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
|
||||
|
||||
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
|
||||
Ok(key) => key,
|
||||
@@ -30,17 +29,17 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
|
||||
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
|
||||
match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
|
||||
Ok(token) => token,
|
||||
Err(e) => panic!("Error encoding jwt {}", e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
|
||||
let validation = jwt::Validation {
|
||||
let validation = jsonwebtoken::Validation {
|
||||
leeway: 30, // 30 seconds
|
||||
validate_exp: true,
|
||||
validate_iat: true,
|
||||
validate_iat: false, // IssuedAt is the same as NotBefore
|
||||
validate_nbf: true,
|
||||
aud: None,
|
||||
iss: Some(JWT_ISSUER.clone()),
|
||||
@@ -48,10 +47,10 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
|
||||
algorithms: vec![JWT_ALGORITHM],
|
||||
};
|
||||
|
||||
match jwt::decode(token, &PUBLIC_RSA_KEY, &validation) {
|
||||
match jsonwebtoken::decode(token, &PUBLIC_RSA_KEY, &validation) {
|
||||
Ok(decoded) => Ok(decoded.claims),
|
||||
Err(msg) => {
|
||||
println!("Error validating jwt - {:#?}", msg);
|
||||
error!("Error validating jwt - {:#?}", msg);
|
||||
Err(msg.to_string())
|
||||
}
|
||||
}
|
||||
@@ -76,6 +75,7 @@ pub struct JWTClaims {
|
||||
pub orgowner: Vec<String>,
|
||||
pub orgadmin: Vec<String>,
|
||||
pub orguser: Vec<String>,
|
||||
pub orgmanager: Vec<String>,
|
||||
|
||||
// user security_stamp
|
||||
pub sstamp: String,
|
||||
@@ -90,12 +90,11 @@ pub struct JWTClaims {
|
||||
///
|
||||
/// Bearer token authentication
|
||||
///
|
||||
|
||||
use rocket::Outcome;
|
||||
use rocket::request::{self, Request, FromRequest};
|
||||
|
||||
use db::DbConn;
|
||||
use db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
|
||||
use crate::db::DbConn;
|
||||
use crate::db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
|
||||
|
||||
pub struct Headers {
|
||||
pub host: String,
|
||||
@@ -139,13 +138,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||
|
||||
// Get access_token
|
||||
let access_token: &str = match request.headers().get_one("Authorization") {
|
||||
Some(a) => {
|
||||
match a.rsplit("Bearer ").next() {
|
||||
Some(split) => split,
|
||||
None => err_handler!("No access token provided")
|
||||
}
|
||||
}
|
||||
None => err_handler!("No access token provided")
|
||||
Some(a) => match a.rsplit("Bearer ").next() {
|
||||
Some(split) => split,
|
||||
None => err_handler!("No access token provided"),
|
||||
},
|
||||
None => err_handler!("No access token provided"),
|
||||
};
|
||||
|
||||
// Check JWT token is valid and get device and user from it
|
||||
@@ -184,7 +181,7 @@ pub struct OrgHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: i32,
|
||||
pub org_user_type: UserOrgType,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
||||
@@ -192,13 +189,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
match request.guard::<Headers>() {
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Forward(_) => Outcome::Forward(()),
|
||||
Outcome::Failure(f) => Outcome::Failure(f),
|
||||
Outcome::Success(headers) => {
|
||||
// org_id is expected to be the first dynamic param
|
||||
match request.get_param::<String>(0) {
|
||||
Err(_) => err_handler!("Error getting the organization id"),
|
||||
Ok(org_id) => {
|
||||
// org_id is expected to be the second param ("/organizations/<org_id>")
|
||||
match request.get_param::<String>(1) {
|
||||
Some(Ok(org_id)) => {
|
||||
let conn = match request.guard::<DbConn>() {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB")
|
||||
@@ -225,9 +221,16 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user_type: org_user.type_,
|
||||
org_user_type: {
|
||||
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.type_) {
|
||||
org_usr_type
|
||||
} else { // This should only happen if the DB is corrupted
|
||||
err_handler!("Unknown user type in the database")
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
_ => err_handler!("Error getting the organization id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +241,7 @@ pub struct AdminHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: i32,
|
||||
pub org_user_type: UserOrgType,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
||||
@@ -246,18 +249,18 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
match request.guard::<OrgHeaders>() {
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Forward(_) => Outcome::Forward(()),
|
||||
Outcome::Failure(f) => Outcome::Failure(f),
|
||||
Outcome::Success(headers) => {
|
||||
if headers.org_user_type > UserOrgType::Admin as i32 {
|
||||
err_handler!("You need to be Admin or Owner to call this endpoint")
|
||||
} else {
|
||||
Outcome::Success(Self{
|
||||
if headers.org_user_type >= UserOrgType::Admin {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_user_type: headers.org_user_type,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be Admin or Owner to call this endpoint")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,19 +278,41 @@ impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
match request.guard::<OrgHeaders>() {
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Forward(_) => Outcome::Forward(()),
|
||||
Outcome::Failure(f) => Outcome::Failure(f),
|
||||
Outcome::Success(headers) => {
|
||||
if headers.org_user_type > UserOrgType::Owner as i32 {
|
||||
err_handler!("You need to be Owner to call this endpoint")
|
||||
} else {
|
||||
Outcome::Success(Self{
|
||||
if headers.org_user_type == UserOrgType::Owner {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be Owner to call this endpoint")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Client IP address detection
|
||||
///
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub struct ClientIp {
|
||||
pub ip: IpAddr,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
let ip = match request.client_ip() {
|
||||
Some(addr) => addr,
|
||||
None => "0.0.0.0".parse().unwrap(),
|
||||
};
|
||||
|
||||
Outcome::Success(ClientIp { ip })
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest};
|
||||
use rocket::{Outcome, Request, State};
|
||||
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
/// An alias to the database connection used
|
||||
type Connection = SqliteConnection;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::Cipher;
|
||||
use CONFIG;
|
||||
use crate::CONFIG;
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "attachments"]
|
||||
@@ -12,6 +12,7 @@ pub struct Attachment {
|
||||
pub cipher_uuid: String,
|
||||
pub file_name: String,
|
||||
pub file_size: i32,
|
||||
pub key: Option<String>
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
@@ -22,6 +23,7 @@ impl Attachment {
|
||||
cipher_uuid,
|
||||
file_name,
|
||||
file_size,
|
||||
key: None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +31,8 @@ impl Attachment {
|
||||
format!("{}/{}/{}", CONFIG.attachments_folder, self.cipher_uuid, self.id)
|
||||
}
|
||||
|
||||
pub fn to_json(&self, host: &str) -> JsonValue {
|
||||
use util::get_display_size;
|
||||
pub fn to_json(&self, host: &str) -> Value {
|
||||
use crate::util::get_display_size;
|
||||
|
||||
let web_path = format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id);
|
||||
let display_size = get_display_size(self.file_size);
|
||||
@@ -41,6 +43,7 @@ impl Attachment {
|
||||
"FileName": self.file_name,
|
||||
"Size": self.file_size.to_string(),
|
||||
"SizeName": display_size,
|
||||
"Key": self.key,
|
||||
"Object": "attachment"
|
||||
})
|
||||
}
|
||||
@@ -48,8 +51,8 @@ impl Attachment {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::attachments;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::attachments;
|
||||
|
||||
/// Database methods
|
||||
impl Attachment {
|
||||
@@ -61,39 +64,21 @@ impl Attachment {
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
use util;
|
||||
use std::{thread, time};
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::delete(attachments::table.filter(attachments::id.eq(&self.id)))
|
||||
.execute(&**conn)
|
||||
},
|
||||
10,
|
||||
)?;
|
||||
|
||||
let mut retries = 10;
|
||||
|
||||
loop {
|
||||
match diesel::delete(
|
||||
attachments::table.filter(
|
||||
attachments::id.eq(&self.id)
|
||||
)
|
||||
).execute(&**conn) {
|
||||
Ok(_) => break,
|
||||
Err(err) => {
|
||||
if retries < 1 {
|
||||
println!("ERROR: Failed with 10 retries");
|
||||
return Err(err)
|
||||
} else {
|
||||
retries -= 1;
|
||||
println!("Had to retry! Retries left: {}", retries);
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util::delete_file(&self.get_file_path());
|
||||
crate::util::delete_file(&self.get_file_path());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for attachement in Attachment::find_by_cipher(&cipher_uuid, &conn) {
|
||||
attachement.delete(&conn)?;
|
||||
for attachment in Attachment::find_by_cipher(&cipher_uuid, &conn) {
|
||||
attachment.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrganization, UserOrgType, UserOrgStatus};
|
||||
|
||||
@@ -41,7 +39,7 @@ impl Cipher {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
|
||||
@@ -63,28 +61,28 @@ impl Cipher {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::*;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::*;
|
||||
|
||||
/// Database methods
|
||||
impl Cipher {
|
||||
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> JsonValue {
|
||||
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
|
||||
use serde_json;
|
||||
use util::format_date;
|
||||
use crate::util::format_date;
|
||||
use super::Attachment;
|
||||
|
||||
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
|
||||
let attachments_json: Vec<JsonValue> = attachments.iter().map(|c| c.to_json(host)).collect();
|
||||
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
|
||||
|
||||
let fields_json: JsonValue = if let Some(ref fields) = self.fields {
|
||||
let fields_json: Value = if let Some(ref fields) = self.fields {
|
||||
serde_json::from_str(fields).unwrap()
|
||||
} else { JsonValue::Null };
|
||||
} else { Value::Null };
|
||||
|
||||
let password_history_json: JsonValue = if let Some(ref password_history) = self.password_history {
|
||||
let password_history_json: Value = if let Some(ref password_history) = self.password_history {
|
||||
serde_json::from_str(password_history).unwrap()
|
||||
} else { JsonValue::Null };
|
||||
} else { Value::Null };
|
||||
|
||||
let mut data_json: JsonValue = serde_json::from_str(&self.data).unwrap();
|
||||
let mut data_json: Value = serde_json::from_str(&self.data).unwrap();
|
||||
|
||||
// TODO: ******* Backwards compat start **********
|
||||
// To remove backwards compatibility, just remove this entire section
|
||||
@@ -234,7 +232,7 @@ impl Cipher {
|
||||
}
|
||||
|
||||
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||
match ciphers::table
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(&self.uuid))
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
||||
@@ -255,14 +253,11 @@ impl Cipher {
|
||||
)
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.first::<Self>(&**conn).ok() {
|
||||
Some(_) => true,
|
||||
None => false
|
||||
}
|
||||
.first::<Self>(&**conn).ok().is_some()
|
||||
}
|
||||
|
||||
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||
match ciphers::table
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(&self.uuid))
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
||||
@@ -281,10 +276,7 @@ impl Cipher {
|
||||
)
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.first::<Self>(&**conn).ok() {
|
||||
Some(_) => true,
|
||||
None => false
|
||||
}
|
||||
.first::<Self>(&**conn).ok().is_some()
|
||||
}
|
||||
|
||||
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
||||
@@ -318,7 +310,9 @@ impl Cipher {
|
||||
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
|
||||
users_collections::user_uuid.eq(user_uuid) // Access to Collection
|
||||
users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
)
|
||||
)
|
||||
)
|
||||
))
|
||||
|
@@ -1,6 +1,4 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus};
|
||||
|
||||
@@ -18,14 +16,14 @@ pub struct Collection {
|
||||
impl Collection {
|
||||
pub fn new(org_uuid: String, name: String) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
|
||||
org_uuid,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.org_uuid,
|
||||
@@ -37,8 +35,8 @@ impl Collection {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::*;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::*;
|
||||
|
||||
/// Database methods
|
||||
impl Collection {
|
||||
@@ -150,15 +148,12 @@ impl Collection {
|
||||
if user_org.access_all {
|
||||
true
|
||||
} else {
|
||||
match users_collections::table.inner_join(collections::table)
|
||||
users_collections::table.inner_join(collections::table)
|
||||
.filter(users_collections::collection_uuid.eq(&self.uuid))
|
||||
.filter(users_collections::user_uuid.eq(&user_uuid))
|
||||
.filter(users_collections::read_only.eq(false))
|
||||
.select(collections::all_columns)
|
||||
.first::<Self>(&**conn).ok() {
|
||||
None => false, // Read only or no access to collection
|
||||
Some(_) => true,
|
||||
}
|
||||
.first::<Self>(&**conn).ok().is_some() // Read only or no access to collection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ impl Device {
|
||||
|
||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||
use data_encoding::BASE64;
|
||||
use crypto;
|
||||
use crate::crypto;
|
||||
|
||||
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
|
||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||
@@ -62,7 +62,7 @@ impl Device {
|
||||
// If there is no refresh token, we create one
|
||||
if self.refresh_token.is_empty() {
|
||||
use data_encoding::BASE64URL;
|
||||
use crypto;
|
||||
use crate::crypto;
|
||||
|
||||
self.refresh_token = BASE64URL.encode(&crypto::get_random_64());
|
||||
}
|
||||
@@ -71,14 +71,14 @@ impl Device {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
self.updated_at = time_now;
|
||||
|
||||
|
||||
let orgowner: Vec<_> = orgs.iter().filter(|o| o.type_ == 0).map(|o| o.org_uuid.clone()).collect();
|
||||
let orgadmin: Vec<_> = orgs.iter().filter(|o| o.type_ == 1).map(|o| o.org_uuid.clone()).collect();
|
||||
let orguser: Vec<_> = orgs.iter().filter(|o| o.type_ == 2).map(|o| o.org_uuid.clone()).collect();
|
||||
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.type_ == 3).map(|o| o.org_uuid.clone()).collect();
|
||||
|
||||
|
||||
// Create the JWT claims struct, to send to the client
|
||||
use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
|
||||
use crate::auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
|
||||
let claims = JWTClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
||||
@@ -93,6 +93,7 @@ impl Device {
|
||||
orgowner,
|
||||
orgadmin,
|
||||
orguser,
|
||||
orgmanager,
|
||||
|
||||
sstamp: user.security_stamp.to_string(),
|
||||
device: self.uuid.to_string(),
|
||||
@@ -100,23 +101,29 @@ impl Device {
|
||||
amr: vec!["Application".into()],
|
||||
};
|
||||
|
||||
|
||||
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
|
||||
}
|
||||
}
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::devices;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::devices;
|
||||
|
||||
/// Database methods
|
||||
impl Device {
|
||||
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
diesel::replace_into(devices::table)
|
||||
.values(&*self).execute(&**conn).and(Ok(()))
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::replace_into(devices::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn)
|
||||
},
|
||||
10,
|
||||
)
|
||||
.and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{User, Cipher};
|
||||
|
||||
@@ -33,7 +31,7 @@ impl Folder {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
|
||||
@@ -42,8 +40,8 @@ impl Folder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
use util::format_date;
|
||||
pub fn to_json(&self) -> Value {
|
||||
use crate::util::format_date;
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
@@ -65,8 +63,8 @@ impl FolderCipher {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::{folders, folders_ciphers};
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::{folders, folders_ciphers};
|
||||
|
||||
/// Database methods
|
||||
impl Folder {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::cmp::Ordering;
|
||||
use serde_json::Value;
|
||||
|
||||
use uuid::Uuid;
|
||||
use super::{User, CollectionUser, Invitation};
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
@@ -32,10 +32,101 @@ pub enum UserOrgStatus {
|
||||
Confirmed = 2,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Eq)]
|
||||
pub enum UserOrgType {
|
||||
Owner = 0,
|
||||
Admin = 1,
|
||||
User = 2,
|
||||
Manager = 3,
|
||||
}
|
||||
|
||||
impl Ord for UserOrgType {
|
||||
fn cmp(&self, other: &UserOrgType) -> Ordering {
|
||||
if self == other {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
match self {
|
||||
UserOrgType::Owner => Ordering::Greater,
|
||||
UserOrgType::Admin => match other {
|
||||
UserOrgType::Owner => Ordering::Less,
|
||||
_ => Ordering::Greater
|
||||
},
|
||||
UserOrgType::Manager => match other {
|
||||
UserOrgType::Owner | UserOrgType::Admin => Ordering::Less,
|
||||
_ => Ordering::Greater
|
||||
},
|
||||
UserOrgType::User => Ordering::Less
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for UserOrgType {
|
||||
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<i32> for UserOrgType {
|
||||
fn eq(&self, other: &i32) -> bool {
|
||||
*other == *self as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<i32> for UserOrgType {
|
||||
fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
|
||||
if let Some(other) = Self::from_i32(*other) {
|
||||
return Some(self.cmp(&other))
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn gt(&self, other: &i32) -> bool {
|
||||
match self.partial_cmp(other) {
|
||||
Some(Ordering::Less) | Some(Ordering::Equal) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn ge(&self, other: &i32) -> bool {
|
||||
match self.partial_cmp(other) {
|
||||
Some(Ordering::Less) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl PartialEq<UserOrgType> for i32 {
|
||||
fn eq(&self, other: &UserOrgType) -> bool {
|
||||
*self == *other as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<UserOrgType> for i32 {
|
||||
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
|
||||
if let Some(self_type) = UserOrgType::from_i32(*self) {
|
||||
return Some(self_type.cmp(other))
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn lt(&self, other: &UserOrgType) -> bool {
|
||||
match self.partial_cmp(other) {
|
||||
Some(Ordering::Less) | None => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn le(&self, other: &UserOrgType) -> bool {
|
||||
match self.partial_cmp(other) {
|
||||
Some(Ordering::Less) | Some(Ordering::Equal) | None => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl UserOrgType {
|
||||
@@ -44,9 +135,21 @@ impl UserOrgType {
|
||||
"0" | "Owner" => Some(UserOrgType::Owner),
|
||||
"1" | "Admin" => Some(UserOrgType::Admin),
|
||||
"2" | "User" => Some(UserOrgType::User),
|
||||
"3" | "Manager" => Some(UserOrgType::Manager),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_i32(i: i32) -> Option<Self> {
|
||||
match i {
|
||||
0 => Some(UserOrgType::Owner),
|
||||
1 => Some(UserOrgType::Admin),
|
||||
2 => Some(UserOrgType::User),
|
||||
3 => Some(UserOrgType::Manager),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
@@ -55,7 +158,7 @@ impl Organization {
|
||||
|
||||
pub fn new(name: String, billing_email: String) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
|
||||
name,
|
||||
billing_email,
|
||||
@@ -70,7 +173,7 @@ impl Organization {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
@@ -102,7 +205,7 @@ impl Organization {
|
||||
impl UserOrganization {
|
||||
pub fn new(user_uuid: String, org_uuid: String) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
|
||||
user_uuid,
|
||||
org_uuid,
|
||||
@@ -132,8 +235,8 @@ impl UserOrganization {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
|
||||
|
||||
/// Database methods
|
||||
impl Organization {
|
||||
@@ -181,7 +284,7 @@ impl Organization {
|
||||
}
|
||||
|
||||
impl UserOrganization {
|
||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
@@ -209,7 +312,7 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue {
|
||||
pub fn to_json_user_details(&self, conn: &DbConn) -> Value {
|
||||
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
@@ -226,7 +329,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) -> Value {
|
||||
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
@@ -241,7 +344,7 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_details(&self, conn: &DbConn) -> JsonValue {
|
||||
pub fn to_json_details(&self, conn: &DbConn) -> Value {
|
||||
let coll_uuids = if self.access_all {
|
||||
vec![] // If we have complete access, no need to fill the array
|
||||
} else {
|
||||
@@ -302,7 +405,7 @@ impl UserOrganization {
|
||||
}
|
||||
|
||||
pub fn has_full_access(self) -> bool {
|
||||
self.access_all || self.type_ < UserOrgType::User as i32
|
||||
self.access_all || self.type_ >= UserOrgType::Admin
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
|
@@ -1,6 +1,4 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::User;
|
||||
|
||||
@@ -36,7 +34,7 @@ pub enum TwoFactorType {
|
||||
impl TwoFactor {
|
||||
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
user_uuid,
|
||||
type_: type_ as i32,
|
||||
enabled: true,
|
||||
@@ -59,7 +57,7 @@ impl TwoFactor {
|
||||
generated == totp_code
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Key": "", // This key and value vary
|
||||
@@ -67,7 +65,7 @@ impl TwoFactor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_list(&self) -> JsonValue {
|
||||
pub fn to_json_list(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Type": self.type_,
|
||||
@@ -78,8 +76,8 @@ impl TwoFactor {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::twofactor;
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::twofactor;
|
||||
|
||||
/// Database methods
|
||||
impl TwoFactor {
|
||||
|
@@ -1,10 +1,8 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::Value;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crypto;
|
||||
use CONFIG;
|
||||
use crate::crypto;
|
||||
use crate::CONFIG;
|
||||
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
@@ -50,7 +48,7 @@ impl User {
|
||||
let email = mail.to_lowercase();
|
||||
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
uuid: crate::util::get_uuid(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
name: email.clone(),
|
||||
@@ -61,7 +59,7 @@ impl User {
|
||||
salt: crypto::get_random_64(),
|
||||
password_iterations: CONFIG.password_iterations,
|
||||
|
||||
security_stamp: Uuid::new_v4().to_string(),
|
||||
security_stamp: crate::util::get_uuid(),
|
||||
|
||||
password_hint: None,
|
||||
private_key: None,
|
||||
@@ -97,11 +95,10 @@ impl User {
|
||||
self.password_hash = crypto::hash_password(password.as_bytes(),
|
||||
&self.salt,
|
||||
self.password_iterations as u32);
|
||||
self.reset_security_stamp();
|
||||
}
|
||||
|
||||
pub fn reset_security_stamp(&mut self) {
|
||||
self.security_stamp = Uuid::new_v4().to_string();
|
||||
self.security_stamp = crate::util::get_uuid();
|
||||
}
|
||||
|
||||
pub fn is_server_admin(&self) -> bool {
|
||||
@@ -114,20 +111,20 @@ impl User {
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::{users, invitations};
|
||||
use crate::db::DbConn;
|
||||
use crate::db::schema::{users, invitations};
|
||||
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
|
||||
|
||||
/// Database methods
|
||||
impl User {
|
||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
|
||||
|
||||
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<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||
|
||||
json!({
|
||||
@@ -157,7 +154,7 @@ impl User {
|
||||
|
||||
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 user_org.type_ == UserOrgType::Owner {
|
||||
if UserOrganization::find_by_org_and_type(
|
||||
&user_org.org_uuid,
|
||||
UserOrgType::Owner as i32, &conn
|
||||
@@ -181,7 +178,7 @@ impl User {
|
||||
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
||||
if let Some(mut user) = User::find_by_uuid(&uuid, conn) {
|
||||
if user.update_revision(conn).is_err(){
|
||||
println!("Warning: Failed to update revision for {}", user.email);
|
||||
warn!("Failed to update revision for {}", user.email);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ table! {
|
||||
cipher_uuid -> Text,
|
||||
file_name -> Text,
|
||||
file_size -> Integer,
|
||||
key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre_email::EmailBuilder;
|
||||
|
||||
use MailConfig;
|
||||
use crate::MailConfig;
|
||||
|
||||
fn mailer(config: &MailConfig) -> SmtpTransport {
|
||||
let client_security = if config.smtp_ssl {
|
||||
|
145
src/main.rs
145
src/main.rs
@@ -1,40 +1,15 @@
|
||||
#![feature(plugin, custom_derive, vec_remove_item, try_trait)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![recursion_limit="128"]
|
||||
#![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait)]
|
||||
#![recursion_limit = "128"]
|
||||
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
extern crate reqwest;
|
||||
extern crate multipart;
|
||||
extern crate ws;
|
||||
extern crate rmpv;
|
||||
extern crate chashmap;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
extern crate ring;
|
||||
extern crate uuid;
|
||||
extern crate chrono;
|
||||
extern crate oath;
|
||||
extern crate data_encoding;
|
||||
extern crate jsonwebtoken as jwt;
|
||||
extern crate u2f;
|
||||
extern crate dotenv;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
extern crate num_traits;
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate native_tls;
|
||||
extern crate byteorder;
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use] extern crate serde_json;
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate diesel;
|
||||
#[macro_use] extern crate diesel_migrations;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
#[macro_use] extern crate num_derive;
|
||||
|
||||
use std::{path::Path, process::{exit, Command}};
|
||||
use rocket::Rocket;
|
||||
@@ -49,6 +24,9 @@ mod auth;
|
||||
mod mail;
|
||||
|
||||
fn init_rocket() -> Rocket {
|
||||
|
||||
// TODO: TO HIDE MOUNTING LOG, call ignite, set logging to disabled, call all the mounts, and then enable it again
|
||||
|
||||
rocket::ignite()
|
||||
.mount("/", api::web_routes())
|
||||
.mount("/api", api::core_routes())
|
||||
@@ -68,7 +46,7 @@ mod migrations {
|
||||
|
||||
pub fn run_migrations() {
|
||||
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||
let connection = ::db::get_connection().expect("Can't conect to DB");
|
||||
let connection = crate::db::get_connection().expect("Can't conect to DB");
|
||||
|
||||
use std::io::stdout;
|
||||
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
|
||||
@@ -76,6 +54,10 @@ mod migrations {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if CONFIG.extended_logging {
|
||||
init_logging().ok();
|
||||
}
|
||||
|
||||
check_db();
|
||||
check_rsa_keys();
|
||||
check_web_vault();
|
||||
@@ -84,13 +66,61 @@ fn main() {
|
||||
init_rocket().launch();
|
||||
}
|
||||
|
||||
fn init_logging() -> Result<(), fern::InitError> {
|
||||
let mut logger = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::Debug)
|
||||
.level_for("hyper", log::LevelFilter::Warn)
|
||||
.level_for("ws", log::LevelFilter::Info)
|
||||
.level_for("multipart", log::LevelFilter::Info)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
if let Some(log_file) = CONFIG.log_file.as_ref() {
|
||||
logger = logger.chain(fern::log_file(log_file)?);
|
||||
}
|
||||
|
||||
logger = chain_syslog(logger);
|
||||
logger.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enable_syslog"))]
|
||||
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { logger }
|
||||
|
||||
#[cfg(feature = "enable_syslog")]
|
||||
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
||||
let syslog_fmt = syslog::Formatter3164 {
|
||||
facility: syslog::Facility::LOG_USER,
|
||||
hostname: None,
|
||||
process: "bitwarden_rs".into(),
|
||||
pid: 0,
|
||||
};
|
||||
|
||||
match syslog::unix(syslog_fmt) {
|
||||
Ok(sl) => logger.chain(sl),
|
||||
Err(e) => {
|
||||
error!("Unable to connect to syslog: {:?}", e);
|
||||
logger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_db() {
|
||||
let path = Path::new(&CONFIG.database_url);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
use std::fs;
|
||||
if fs::create_dir_all(parent).is_err() {
|
||||
println!("Error creating database directory");
|
||||
error!("Error creating database directory");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -105,16 +135,16 @@ fn check_rsa_keys() {
|
||||
// If the RSA keys don't exist, try to create them
|
||||
if !util::file_exists(&CONFIG.private_rsa_key)
|
||||
|| !util::file_exists(&CONFIG.public_rsa_key) {
|
||||
println!("JWT keys don't exist, checking if OpenSSL is available...");
|
||||
info!("JWT keys don't exist, checking if OpenSSL is available...");
|
||||
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
.output().unwrap_or_else(|_| {
|
||||
println!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
|
||||
info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
println!("OpenSSL detected, creating keys...");
|
||||
info!("OpenSSL detected, creating keys...");
|
||||
|
||||
let mut success = Command::new("openssl").arg("genrsa")
|
||||
.arg("-out").arg(&CONFIG.private_rsa_key_pem)
|
||||
@@ -138,9 +168,9 @@ fn check_rsa_keys() {
|
||||
.status.success();
|
||||
|
||||
if success {
|
||||
println!("Keys created correctly.");
|
||||
info!("Keys created correctly.");
|
||||
} else {
|
||||
println!("Error creating keys, exiting...");
|
||||
error!("Error creating keys, exiting...");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -154,7 +184,7 @@ fn check_web_vault() {
|
||||
let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
|
||||
|
||||
if !index_path.exists() {
|
||||
println!("Web vault is not found. Please follow the steps in the README to install it");
|
||||
error!("Web vault is not found. Please follow the steps in the README to install it");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -176,7 +206,7 @@ pub struct MailConfig {
|
||||
|
||||
impl MailConfig {
|
||||
fn load() -> Option<Self> {
|
||||
use util::{get_env, get_env_or};
|
||||
use crate::util::{get_env, get_env_or};
|
||||
|
||||
// When SMTP_HOST is absent, we assume the user does not want to enable it.
|
||||
let smtp_host = match get_env("SMTP_HOST") {
|
||||
@@ -185,7 +215,7 @@ impl MailConfig {
|
||||
};
|
||||
|
||||
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
|
||||
println!("Please specify SMTP_FROM to enable SMTP support.");
|
||||
error!("Please specify SMTP_FROM to enable SMTP support.");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
@@ -201,7 +231,7 @@ impl MailConfig {
|
||||
let smtp_username = get_env("SMTP_USERNAME");
|
||||
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
|
||||
if smtp_username.as_ref().is_some() {
|
||||
println!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
|
||||
error!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
|
||||
exit(1);
|
||||
} else {
|
||||
None
|
||||
@@ -235,6 +265,9 @@ pub struct Config {
|
||||
websocket_enabled: bool,
|
||||
websocket_url: String,
|
||||
|
||||
extended_logging: bool,
|
||||
log_file: Option<String>,
|
||||
|
||||
local_icon_extractor: bool,
|
||||
signups_allowed: bool,
|
||||
invitations_allowed: bool,
|
||||
@@ -245,12 +278,17 @@ pub struct Config {
|
||||
domain: String,
|
||||
domain_set: bool,
|
||||
|
||||
yubico_cred_set: bool,
|
||||
yubico_client_id: String,
|
||||
yubico_secret_key: String,
|
||||
yubico_server: Option<String>,
|
||||
|
||||
mail: Option<MailConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn load() -> Self {
|
||||
use util::{get_env, get_env_or};
|
||||
use crate::util::{get_env, get_env_or};
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let df = get_env_or("DATA_FOLDER", "data".to_string());
|
||||
@@ -258,6 +296,9 @@ impl Config {
|
||||
|
||||
let domain = get_env("DOMAIN");
|
||||
|
||||
let yubico_client_id = get_env("YUBICO_CLIENT_ID");
|
||||
let yubico_secret_key = get_env("YUBICO_SECRET_KEY");
|
||||
|
||||
Config {
|
||||
database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")),
|
||||
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
|
||||
@@ -273,6 +314,9 @@ impl Config {
|
||||
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
|
||||
websocket_url: format!("{}:{}", get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()), get_env_or("WEBSOCKET_PORT", 3012)),
|
||||
|
||||
extended_logging: get_env_or("EXTENDED_LOGGING", true),
|
||||
log_file: get_env("LOG_FILE"),
|
||||
|
||||
local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false),
|
||||
signups_allowed: get_env_or("SIGNUPS_ALLOWED", true),
|
||||
server_admin_email: get_env("SERVER_ADMIN_EMAIL"),
|
||||
@@ -283,6 +327,11 @@ impl Config {
|
||||
domain_set: domain.is_some(),
|
||||
domain: domain.unwrap_or("http://localhost".into()),
|
||||
|
||||
yubico_cred_set: yubico_client_id.is_some() && yubico_secret_key.is_some(),
|
||||
yubico_client_id: yubico_client_id.unwrap_or("00000".into()),
|
||||
yubico_secret_key: yubico_secret_key.unwrap_or("AAAAAAA".into()),
|
||||
yubico_server: get_env("YUBICO_SERVER"),
|
||||
|
||||
mail: MailConfig::load(),
|
||||
}
|
||||
}
|
||||
|
77
src/util.rs
77
src/util.rs
@@ -2,36 +2,46 @@
|
||||
/// Macros
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! err {
|
||||
($err:expr, $msg:expr) => {{
|
||||
println!("ERROR: {}", $msg);
|
||||
macro_rules! _err_object {
|
||||
($msg:expr) => {{
|
||||
err_json!(json!({
|
||||
"error": $err,
|
||||
"error_description": $err,
|
||||
"ErrorModel": {
|
||||
"Message": $msg,
|
||||
"ValidationErrors": null,
|
||||
"ExceptionMessage": null,
|
||||
"ExceptionStackTrace": null,
|
||||
"InnerExceptionMessage": null,
|
||||
"Object": "error"
|
||||
}}))
|
||||
"Message": "",
|
||||
"error": "",
|
||||
"error_description": "",
|
||||
"ValidationErrors": {"": [ $msg ]},
|
||||
"ErrorModel": {
|
||||
"Message": $msg,
|
||||
"Object": "error"
|
||||
},
|
||||
"Object": "error"
|
||||
}))
|
||||
}};
|
||||
($msg:expr) => { err!("unknown_error", $msg) }
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! err {
|
||||
($msg:expr) => {{
|
||||
error!("{}", $msg);
|
||||
_err_object!($msg)
|
||||
}};
|
||||
($usr_msg:expr, $log_value:expr) => {{
|
||||
error!("{}: {:#?}", $usr_msg, $log_value);
|
||||
_err_object!($usr_msg)
|
||||
}}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! err_json {
|
||||
($expr:expr) => {{
|
||||
return Err($crate::rocket::response::status::BadRequest(Some($crate::rocket_contrib::Json($expr))));
|
||||
return Err(rocket::response::status::BadRequest(Some(rocket_contrib::json::Json($expr))));
|
||||
}}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! err_handler {
|
||||
($expr:expr) => {{
|
||||
println!("ERROR: {}", $expr);
|
||||
return $crate::rocket::Outcome::Failure(($crate::rocket::http::Status::Unauthorized, $expr));
|
||||
error!("{}", $expr);
|
||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -92,6 +102,10 @@ pub fn get_display_size(size: i32) -> String {
|
||||
format!("{} {}", size, UNITS[unit_counter])
|
||||
}
|
||||
|
||||
pub fn get_uuid() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// String util methods
|
||||
@@ -238,6 +252,33 @@ fn upcase_value(value: &Value) -> Value {
|
||||
fn _process_key(key: &str) -> String {
|
||||
match key.to_lowercase().as_ref() {
|
||||
"ssn" => "SSN".into(),
|
||||
_ => self::upcase_first(key)
|
||||
_ => self::upcase_first(key),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Retry methods
|
||||
//
|
||||
|
||||
pub fn retry<F, T, E>(func: F, max_tries: i32) -> Result<T, E>
|
||||
where
|
||||
F: Fn() -> Result<T, E>,
|
||||
{
|
||||
use std::{thread::sleep, time::Duration};
|
||||
let mut tries = 0;
|
||||
|
||||
loop {
|
||||
match func() {
|
||||
ok @ Ok(_) => return ok,
|
||||
err @ Err(_) => {
|
||||
tries += 1;
|
||||
|
||||
if tries >= max_tries {
|
||||
return err;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user