mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ebb30229f1 | ||
|
936af5431a |
@@ -9,6 +9,10 @@ data
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# Git files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
|
||||
|
13
.env
Normal file
13
.env
Normal file
@@ -0,0 +1,13 @@
|
||||
# DATABASE_URL=data/db.sqlite3
|
||||
# PRIVATE_RSA_KEY=data/private_rsa_key.der
|
||||
# PUBLIC_RSA_KEY=data/public_rsa_key.der
|
||||
# ICON_CACHE_FOLDER=data/icon_cache
|
||||
# ATTACHMENTS_FOLDER=data/attachments
|
||||
|
||||
# true for yes, anything else for no
|
||||
SIGNUPS_ALLOWED=true
|
||||
|
||||
# ROCKET_ENV=production
|
||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||
# ROCKET_PORT=8000
|
||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
146
.env.template
146
.env.template
@@ -1,146 +0,0 @@
|
||||
## Bitwarden_RS Configuration File
|
||||
## Uncomment any of the following lines to change the defaults
|
||||
|
||||
## Main data folder
|
||||
# DATA_FOLDER=data
|
||||
|
||||
## Individual folders, these override %DATA_FOLDER%
|
||||
# DATABASE_URL=data/db.sqlite3
|
||||
# RSA_KEY_FILENAME=data/rsa_key
|
||||
# ICON_CACHE_FOLDER=data/icon_cache
|
||||
# ATTACHMENTS_FOLDER=data/attachments
|
||||
|
||||
## Templates data folder, by default uses embedded templates
|
||||
## Check source code to see the format
|
||||
# TEMPLATES_FOLDER=/path/to/templates
|
||||
## Automatically reload the templates for every request, slow, use only for development
|
||||
# RELOAD_TEMPLATES=false
|
||||
|
||||
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
|
||||
# ICON_CACHE_TTL=2592000
|
||||
## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
|
||||
# ICON_CACHE_NEGTTL=259200
|
||||
|
||||
## Web vault settings
|
||||
# WEB_VAULT_FOLDER=web-vault/
|
||||
# WEB_VAULT_ENABLED=true
|
||||
|
||||
## Enables websocket notifications
|
||||
# WEBSOCKET_ENABLED=false
|
||||
|
||||
## Controls the WebSocket server address and port
|
||||
# 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, use the USE_SYSLOG env variable
|
||||
# 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
|
||||
|
||||
## Logging to Syslog
|
||||
## This requires extended logging
|
||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||
# USE_SYSLOG=false
|
||||
|
||||
## Log level
|
||||
## Change the verbosity of the log output
|
||||
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
||||
## This requires extended logging
|
||||
# LOG_LEVEL=Info
|
||||
|
||||
## Enable WAL for the DB
|
||||
## Set to false to avoid enabling WAL during startup.
|
||||
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
||||
## this setting only prevents bitwarden_rs from automatically enabling it on start.
|
||||
## Please read project wiki page about this setting first before changing the value as it can
|
||||
## cause performance degradation or might render the service unable to start.
|
||||
# ENABLE_DB_WAL=true
|
||||
|
||||
## Disable icon downloading
|
||||
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
|
||||
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||
## otherwise it will delete them and they won't be downloaded again.
|
||||
# DISABLE_ICON_DOWNLOAD=false
|
||||
|
||||
## Icon download timeout
|
||||
## Configure the timeout value when downloading the favicons.
|
||||
## The default is 10 seconds, but this could be to low on slower network connections
|
||||
# ICON_DOWNLOAD_TIMEOUT=10
|
||||
|
||||
## Icon blacklist Regex
|
||||
## Any domains or IPs that match this regex won't be fetched by the icon service.
|
||||
## Useful to hide other servers in the local network. Check the WIKI for more details
|
||||
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^
|
||||
|
||||
## Disable 2FA remember
|
||||
## Enabling this would force the users to use a second factor to login every time.
|
||||
## Note that the checkbox would still be present, but ignored.
|
||||
# DISABLE_2FA_REMEMBER=false
|
||||
|
||||
## Controls if new users can register
|
||||
# SIGNUPS_ALLOWED=true
|
||||
|
||||
## Token for the admin interface, preferably use a long random string
|
||||
## One option is to use 'openssl rand -base64 48'
|
||||
## If not set, the admin panel is disabled
|
||||
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
||||
# DISABLE_ADMIN_TOKEN=false
|
||||
|
||||
## Invitations org admins to invite users, even when signups are disabled
|
||||
# INVITATIONS_ALLOWED=true
|
||||
|
||||
## Controls the PBBKDF password iterations to apply on the server
|
||||
## The change only applies when the password is changed
|
||||
# PASSWORD_ITERATIONS=100000
|
||||
|
||||
## Whether password hint should be sent into the error response when the client request it
|
||||
# SHOW_PASSWORD_HINT=true
|
||||
|
||||
## Domain settings
|
||||
## The domain must match the address from where you access the server
|
||||
## It's recommended to configure this value, otherwise certain functionality might not work,
|
||||
## like attachment downloads, email links and U2F.
|
||||
## 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
|
||||
|
||||
## Duo Settings
|
||||
## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves
|
||||
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
|
||||
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
|
||||
## Then set the following options, based on the values obtained from the last step:
|
||||
# DUO_IKEY=<Integration Key>
|
||||
# DUO_SKEY=<Secret Key>
|
||||
# DUO_HOST=<API Hostname>
|
||||
## After that, you should be able to follow the rest of the guide linked above,
|
||||
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||
|
||||
## Rocket specific settings, check Rocket documentation to learn more
|
||||
# ROCKET_ENV=staging
|
||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||
# ROCKET_PORT=8000
|
||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
||||
|
||||
## Mail specific settings, set SMTP_HOST and SMTP_FROM to enable the mail service.
|
||||
## To make sure the email links are pointing to the correct host, set the DOMAIN variable.
|
||||
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
|
||||
# SMTP_HOST=smtp.domain.tld
|
||||
# SMTP_FROM=bitwarden-rs@domain.tld
|
||||
# SMTP_FROM_NAME=Bitwarden_RS
|
||||
# SMTP_PORT=587
|
||||
# SMTP_SSL=true
|
||||
# SMTP_USERNAME=username
|
||||
# SMTP_PASSWORD=password
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,7 +10,7 @@ data
|
||||
*.iml
|
||||
|
||||
# Environment file
|
||||
.env
|
||||
# .env
|
||||
|
||||
# Web vault
|
||||
web-vault
|
@@ -1,9 +0,0 @@
|
||||
dist: xenial
|
||||
|
||||
language: rust
|
||||
rust: nightly
|
||||
cache: cargo
|
||||
|
||||
# Nothing to install
|
||||
install: true
|
||||
script: cargo build --all-features
|
69
BUILD.md
Normal file
69
BUILD.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## How to compile bitwarden_rs
|
||||
Install `rust nightly`, in Windows the recommended way is through `rustup`.
|
||||
|
||||
Install the `openssl` library, in Windows the best option is Microsoft's `vcpkg`,
|
||||
on other systems use their respective package managers.
|
||||
|
||||
Then run:
|
||||
```sh
|
||||
cargo run
|
||||
# or
|
||||
cargo build
|
||||
```
|
||||
|
||||
## How to install the web-vault locally
|
||||
If you're using docker image, you can just update `VAULT_VERSION` variable in Dockerfile and rebuild the image.
|
||||
|
||||
Install `node.js` and either `yarn` or `npm` (usually included with node)
|
||||
|
||||
Clone the web-vault outside the project:
|
||||
```
|
||||
git clone https://github.com/bitwarden/web.git web-vault
|
||||
```
|
||||
|
||||
Modify `web-vault/settings.Production.json` to look like this:
|
||||
```json
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "/api",
|
||||
"identityUri": "/identity",
|
||||
"iconsUri": "/icons",
|
||||
"stripeKey": "",
|
||||
"braintreeKey": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, run the following from the `web-vault` dir:
|
||||
```sh
|
||||
# With yarn (recommended)
|
||||
yarn
|
||||
yarn gulp dist:selfHosted
|
||||
|
||||
# With npm
|
||||
npm install
|
||||
npx gulp dist:selfHosted
|
||||
```
|
||||
|
||||
Finally copy the contents of the `web-vault/dist` folder into the `bitwarden_rs/web-vault` folder.
|
||||
|
||||
## How to recreate database schemas
|
||||
Install diesel-cli with cargo:
|
||||
```sh
|
||||
cargo install diesel_cli --no-default-features --features sqlite-bundled # Or use only sqlite to use the system version
|
||||
```
|
||||
|
||||
Make sure that the correct path to the database is in the `.env` file.
|
||||
|
||||
If you want to modify the schemas, create a new migration with:
|
||||
```
|
||||
diesel migration generate <name>
|
||||
```
|
||||
|
||||
Modify the *.sql files, making sure that any changes are reverted in the down.sql file.
|
||||
|
||||
Apply the migrations and save the generated schemas as follows:
|
||||
```
|
||||
diesel migration redo
|
||||
diesel print-schema > src/db/schema.rs
|
||||
```
|
2712
Cargo.lock
generated
2712
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
91
Cargo.toml
91
Cargo.toml
@@ -1,108 +1,55 @@
|
||||
[package]
|
||||
name = "bitwarden_rs"
|
||||
version = "1.0.0"
|
||||
version = "0.9.0"
|
||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
repository = "https://github.com/dani-garcia/bitwarden_rs"
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0-only"
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
||||
enable_syslog = []
|
||||
|
||||
[target."cfg(not(windows))".dependencies]
|
||||
syslog = "4.0.1"
|
||||
|
||||
[dependencies]
|
||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||
rocket = { version = "0.4.0", features = ["tls"], default-features = false }
|
||||
rocket_contrib = "0.4.0"
|
||||
rocket = { version = "0.3.12", features = ["tls"] }
|
||||
rocket_codegen = "0.3.12"
|
||||
rocket_contrib = "0.3.12"
|
||||
|
||||
# HTTP client
|
||||
reqwest = "0.9.15"
|
||||
reqwest = "0.8.6"
|
||||
|
||||
# multipart/form-data support
|
||||
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
||||
|
||||
# WebSockets library
|
||||
ws = "0.8.0"
|
||||
|
||||
# MessagePack library
|
||||
rmpv = "0.4.0"
|
||||
|
||||
# Concurrent hashmap implementation
|
||||
chashmap = "2.2.2"
|
||||
multipart = "0.14.2"
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
serde_json = "1.0.39"
|
||||
|
||||
# Logging
|
||||
log = "0.4.6"
|
||||
fern = { version = "0.5.8", features = ["syslog-4"] }
|
||||
serde = "1.0.64"
|
||||
serde_derive = "1.0.64"
|
||||
serde_json = "1.0.19"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "1.4.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
|
||||
diesel = { version = "~1.2.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel_migrations = { version = "~1.2.0", features = ["sqlite"] }
|
||||
|
||||
# Bundled SQLite
|
||||
libsqlite3-sys = { version = "0.12.0", features = ["bundled"] }
|
||||
libsqlite3-sys = { version = "0.9.1", features = ["bundled"] }
|
||||
|
||||
# Crypto library
|
||||
ring = { version = "0.13.5", features = ["rsa_signing"] }
|
||||
ring = { version = "= 0.11.0", features = ["rsa_signing"] }
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "0.7.4", features = ["v4"] }
|
||||
uuid = { version = "0.6.5", features = ["v4"] }
|
||||
|
||||
# Date and time library for Rust
|
||||
chrono = "0.4.6"
|
||||
chrono = "0.4.2"
|
||||
|
||||
# TOTP library
|
||||
oath = "0.10.2"
|
||||
|
||||
# Data encoding library
|
||||
data-encoding = "2.1.2"
|
||||
data-encoding = "2.1.1"
|
||||
|
||||
# JWT library
|
||||
jsonwebtoken = "5.0.1"
|
||||
|
||||
# U2F library
|
||||
u2f = "0.1.4"
|
||||
|
||||
# Yubico Library
|
||||
yubico = { version = "0.5.1", features = ["online"], default-features = false }
|
||||
jsonwebtoken = "= 4.0.1"
|
||||
|
||||
# A `dotenv` implementation for Rust
|
||||
dotenv = { version = "0.13.0", default-features = false }
|
||||
|
||||
# Lazy static macro
|
||||
lazy_static = "1.3.0"
|
||||
|
||||
# More derives
|
||||
derive_more = "0.14.0"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.6"
|
||||
num-derive = "0.2.5"
|
||||
|
||||
# Email libraries
|
||||
lettre = "0.9.0"
|
||||
lettre_email = "0.9.0"
|
||||
native-tls = "0.2.2"
|
||||
quoted_printable = "0.4.0"
|
||||
|
||||
# Template library
|
||||
handlebars = "1.1.0"
|
||||
|
||||
# For favicon extraction from main website
|
||||
soup = "0.3.0"
|
||||
regex = "1.1.6"
|
||||
lazy_static = "1.0.1"
|
||||
|
||||
[patch.crates-io]
|
||||
# Add support for Timestamp type
|
||||
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
||||
jsonwebtoken = { path = "libs/jsonwebtoken" } # Make jwt use ring 0.11, to match rocket
|
||||
|
52
Dockerfile
52
Dockerfile
@@ -2,26 +2,36 @@
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
FROM node:9-alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.10.0b"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
ENV VAULT_VERSION "1.26.0"
|
||||
ENV URL "https://github.com/bitwarden/web/archive/v${VAULT_VERSION}.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
tar
|
||||
git \
|
||||
tar \
|
||||
&& npm install -g \
|
||||
gulp-cli \
|
||||
gulp
|
||||
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
RUN mkdir /web-build \
|
||||
&& cd /web-build \
|
||||
&& curl -L "${URL}" | tar -xvz --strip-components=1
|
||||
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
WORKDIR /web-build
|
||||
|
||||
COPY /docker/settings.Production.json /web-build/
|
||||
|
||||
RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/ \
|
||||
&& npm install \
|
||||
&& gulp dist:selfHosted \
|
||||
&& mv dist /web-vault
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rust as build
|
||||
FROM rustlang/rust:nightly as build
|
||||
|
||||
# Using bundled SQLite, no need to install it
|
||||
# RUN apt-get update && apt-get install -y\
|
||||
@@ -33,10 +43,9 @@ FROM rust as build
|
||||
RUN USER=root cargo new --bin app
|
||||
WORKDIR /app
|
||||
|
||||
# Copies over *only* your manifests and build files
|
||||
# Copies over *only* your manifests and vendored dependencies
|
||||
COPY ./Cargo.* ./
|
||||
COPY ./rust-toolchain ./rust-toolchain
|
||||
COPY ./build.rs ./build.rs
|
||||
COPY ./libs ./libs
|
||||
|
||||
# Builds your dependencies and removes the
|
||||
# dummy project, except the target folder
|
||||
@@ -48,9 +57,6 @@ 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
|
||||
@@ -60,27 +66,23 @@ RUN cargo build --release
|
||||
# because we already have a binary built
|
||||
FROM debian:stretch-slim
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /data
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
EXPOSE 3012
|
||||
|
||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||
# Copies the files from the context (env file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY Rocket.toml .
|
||||
COPY .env .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build app/target/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
||||
# Use production to disable Rocket logging
|
||||
#CMD ROCKET_ENV=production ./bitwarden_rs
|
||||
CMD ROCKET_ENV=staging ./bitwarden_rs
|
@@ -1,92 +0,0 @@
|
||||
# Using multistage build:
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.10.0b"
|
||||
|
||||
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 \
|
||||
tar
|
||||
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rust as build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
gcc-aarch64-linux-gnu \
|
||||
&& mkdir -p ~/.cargo \
|
||||
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
|
||||
&& echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config
|
||||
|
||||
ENV CARGO_HOME "/root/.cargo"
|
||||
ENV USER "root"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Prepare openssl arm64 libs
|
||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||
/etc/apt/sources.list.d/deb-src.list \
|
||||
&& dpkg --add-architecture arm64 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
libssl-dev:arm64 \
|
||||
libc6-dev:arm64
|
||||
|
||||
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
||||
ENV CROSS_COMPILE="1"
|
||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
|
||||
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
||||
|
||||
# Copies the complete project
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN rustup target add aarch64-unknown-linux-gnu
|
||||
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/aarch64-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
RUN [ "cross-build-end" ]
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
|
||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
@@ -1,66 +0,0 @@
|
||||
# Using multistage build:
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.10.0b"
|
||||
|
||||
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 \
|
||||
tar
|
||||
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# Musl build image for statically compiled binary
|
||||
FROM clux/muslrust:nightly-2018-12-01 as build
|
||||
|
||||
ENV USER "root"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copies the complete project
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
# Build
|
||||
RUN cargo build --release
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM alpine:3.8
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
ENV SSL_CERT_DIR=/etc/ssl/certs
|
||||
|
||||
# Install needed libraries
|
||||
RUN apk add \
|
||||
openssl\
|
||||
ca-certificates \
|
||||
&& rm /var/cache/apk/*
|
||||
|
||||
RUN mkdir /data
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
EXPOSE 3012
|
||||
|
||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
@@ -1,93 +0,0 @@
|
||||
# Using multistage build:
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.10.0b"
|
||||
|
||||
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 \
|
||||
tar
|
||||
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rust as build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
gcc-arm-linux-gnueabi \
|
||||
&& mkdir -p ~/.cargo \
|
||||
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
|
||||
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.cargo/config
|
||||
|
||||
ENV CARGO_HOME "/root/.cargo"
|
||||
ENV USER "root"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Prepare openssl armel libs
|
||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||
/etc/apt/sources.list.d/deb-src.list \
|
||||
&& dpkg --add-architecture armel \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
libssl-dev:armel \
|
||||
libc6-dev:armel
|
||||
|
||||
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
|
||||
ENV CROSS_COMPILE="1"
|
||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
|
||||
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
||||
|
||||
# Copies the complete project
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN rustup target add arm-unknown-linux-gnueabi
|
||||
RUN cargo build --release --target=arm-unknown-linux-gnueabi -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/rpi-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& ln -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
RUN [ "cross-build-end" ]
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
|
||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
@@ -1,92 +0,0 @@
|
||||
# Using multistage build:
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.10.0b"
|
||||
|
||||
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 \
|
||||
tar
|
||||
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rust as build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
&& mkdir -p ~/.cargo \
|
||||
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
|
||||
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
|
||||
|
||||
ENV CARGO_HOME "/root/.cargo"
|
||||
ENV USER "root"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Prepare openssl armhf libs
|
||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||
/etc/apt/sources.list.d/deb-src.list \
|
||||
&& dpkg --add-architecture armhf \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
libssl-dev:armhf \
|
||||
libc6-dev:armhf
|
||||
|
||||
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
|
||||
ENV CROSS_COMPILE="1"
|
||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
|
||||
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
||||
|
||||
# Copies the complete project
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/armv7hf-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
RUN [ "cross-build-end" ]
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
|
||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
153
README.md
153
README.md
@@ -1,21 +1,8 @@
|
||||
### This is a Bitwarden server API implementation written in Rust compatible with [upstream Bitwarden clients](https://bitwarden.com/#download)*, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.
|
||||
|
||||
---
|
||||
|
||||
[](https://travis-ci.org/dani-garcia/bitwarden_rs)
|
||||
[](https://hub.docker.com/r/mprasil/bitwarden)
|
||||
[](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
|
||||
[](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
|
||||
[](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
|
||||
[](https://matrix.to/#/#bitwarden_rs:matrix.org)
|
||||
This is Bitwarden server API implementation written in rust compatible with [upstream Bitwarden clients](https://bitwarden.com/#download)*, ideal for self-hosted deployment where running official resource-heavy service might not be ideal.
|
||||
|
||||
Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/bitwarden_rs).
|
||||
|
||||
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC.**
|
||||
|
||||
#### ⚠️**IMPORTANT**⚠️: When using this server, please report any Bitwarden related bug-reports or suggestions [here](https://github.com/dani-garcia/bitwarden_rs/issues/new), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official support channels.
|
||||
|
||||
---
|
||||
_*Note, that this project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC._
|
||||
|
||||
## Features
|
||||
|
||||
@@ -27,29 +14,135 @@ Basically full implementation of Bitwarden API is provided including:
|
||||
* Vault API support
|
||||
* Serving the static files for Vault interface
|
||||
* Website icons API
|
||||
* Authenticator and U2F support
|
||||
* YubiKey OTP
|
||||
|
||||
## Installation
|
||||
Pull the docker image and mount a volume from the host for persistent storage:
|
||||
## Docker image usage
|
||||
|
||||
```sh
|
||||
docker pull mprasil/bitwarden:latest
|
||||
### Starting a container
|
||||
|
||||
The persistent data is stored under /data inside the container, so the only requirement for persistent deployment using Docker is to mount persistent volume at the path:
|
||||
|
||||
```
|
||||
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
|
||||
```
|
||||
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
|
||||
|
||||
**IMPORTANT**: Some web browsers, like Chrome, disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault from HTTPS.
|
||||
This will preserve any persistent data under `/bw-data/`, you can adapt the path to whatever suits you.
|
||||
|
||||
This can be configured in [bitwarden_rs directly](https://github.com/dani-garcia/bitwarden_rs/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/bitwarden_rs/wiki/Proxy-examples)).
|
||||
The service will be exposed on port 80.
|
||||
|
||||
If you have an available domain name, you can get HTTPS certificates with [Let's Encrypt](https://letsencrypt.org/), or you can generate self-signed certificates with utilities like [mkcert](https://github.com/FiloSottile/mkcert). Some proxies automatically do this step, like Caddy (see examples linked above).
|
||||
### Updating the bitwarden image
|
||||
|
||||
## Usage
|
||||
See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) for more information on how to configure and run the bitwarden_rs server.
|
||||
Updating is straightforward, you just make sure to preserve the mounted volume. If you used the bind-mounted path as in the example above, you just need to `pull` the latest image, `stop` and `rm` the current container and then start a new one the same way as before:
|
||||
|
||||
## Get in touch
|
||||
```sh
|
||||
# Pull the latest version
|
||||
docker pull mprasil/bitwarden:latest
|
||||
|
||||
To ask an question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine, also please report any bugs spotted here.
|
||||
# Stop and remove the old container
|
||||
docker stop bitwarden
|
||||
docker rm bitwarden
|
||||
|
||||
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!
|
||||
# Start new container with the data mounted
|
||||
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
|
||||
```
|
||||
|
||||
In case you didn't bind mount the volume for persistent data, you need an intermediate step where you preserve the data with an intermediate container:
|
||||
|
||||
```sh
|
||||
# Pull the latest version
|
||||
docker pull mprasil/bitwarden:latest
|
||||
|
||||
# Create intermediate container to preserve data
|
||||
docker run --volumes-from bitwarden --name bitwarden_data busybox true
|
||||
|
||||
# Stop and remove the old container
|
||||
docker stop bitwarden
|
||||
docker rm bitwarden
|
||||
|
||||
# Start new container with the data mounted
|
||||
docker run -d --volumes-from bitwarden_data --name bitwarden -p 80:80 mprasil/bitwarden:latest
|
||||
|
||||
# Optionally remove the intermediate container
|
||||
docker rm bitwarden_data
|
||||
|
||||
# Alternatively you can keep data container around for future updates in which case you can skip last step.
|
||||
```
|
||||
|
||||
## Configuring bitwarden service
|
||||
|
||||
### Changing persistent data location
|
||||
|
||||
#### /data prefix:
|
||||
|
||||
By default all persistent data is saved under `/data`, you can override this path by setting the `DATA_FOLDER` env variable:
|
||||
|
||||
```sh
|
||||
docker run -d --name bitwarden \
|
||||
-e DATA_FOLDER=/persistent \
|
||||
-v /bw-data/:/persistent/ \
|
||||
-p 80:80 \
|
||||
mprasil/bitwarden:latest
|
||||
```
|
||||
|
||||
Notice, that you need to adapt your volume mount accordingly.
|
||||
|
||||
#### database name and location
|
||||
|
||||
Default is `$DATA_FOLDER/db.sqlite3`, you can change the path specifically for database using `DATABASE_URL` variable:
|
||||
|
||||
```sh
|
||||
docker run -d --name bitwarden \
|
||||
-e DATABASE_URL=/database/bitwarden.sqlite3 \
|
||||
-v /bw-data/:/data/ \
|
||||
-v /bw-database/:/database/ \
|
||||
-p 80:80 \
|
||||
mprasil/bitwarden:latest
|
||||
```
|
||||
|
||||
Note, that you need to remember to mount the volume for both database and other persistent data if they are different.
|
||||
|
||||
#### attachments location
|
||||
|
||||
Default is `$DATA_FOLDER/attachments`, you can change the path using `ATTACHMENTS_FOLDER` variable:
|
||||
|
||||
```sh
|
||||
docker run -d --name bitwarden \
|
||||
-e ATTACHMENTS_FOLDER=/attachments \
|
||||
-v /bw-data/:/data/ \
|
||||
-v /bw-attachments/:/attachments/ \
|
||||
-p 80:80 \
|
||||
mprasil/bitwarden:latest
|
||||
```
|
||||
|
||||
Note, that you need to remember to mount the volume for both attachments and other persistent data if they are different.
|
||||
|
||||
#### icons cache
|
||||
|
||||
Default is `$DATA_FOLDER/icon_cache`, you can change the path using `ICON_CACHE_FOLDER` variable:
|
||||
|
||||
```sh
|
||||
docker run -d --name bitwarden \
|
||||
-e ICON_CACHE_FOLDER=/icon_cache \
|
||||
-v /bw-data/:/data/ \
|
||||
-v /icon_cache/ \
|
||||
-p 80:80 \
|
||||
mprasil/bitwarden:latest
|
||||
```
|
||||
|
||||
Note, that in the above example we don't mount the volume locally, which means it won't be persisted during the upgrade unless you use intermediate data container using `--volumes-from`. This will impact performance as bitwarden will have to re-dowload the icons on restart, but might save you from having stale icons in cache as they are not automatically cleaned.
|
||||
|
||||
### Other configuration
|
||||
|
||||
Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables).
|
||||
|
||||
## Building your own image
|
||||
|
||||
Clone the repository, then from the root of the repository run:
|
||||
|
||||
```sh
|
||||
# Build the docker image:
|
||||
docker build -t bitwarden_rs .
|
||||
```
|
||||
|
||||
## Building binary
|
||||
|
||||
For building binary outside the Docker environment and running it locally without docker, please see [build instructions](BUILD.md).
|
@@ -1,2 +0,0 @@
|
||||
[global.limits]
|
||||
json = 10485760 # 10 MiB
|
@@ -1,17 +0,0 @@
|
||||
pool:
|
||||
vmImage: 'Ubuntu-16.04'
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
ls -la
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain)
|
||||
echo "##vso[task.prependpath]$HOME/.cargo/bin"
|
||||
displayName: 'Install Rust'
|
||||
|
||||
- script: |
|
||||
rustc -Vv
|
||||
cargo -V
|
||||
displayName: Query rust and cargo versions
|
||||
|
||||
- script : cargo build --all-features
|
||||
displayName: 'Build project'
|
57
build.rs
57
build.rs
@@ -1,57 +0,0 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
read_git_info().ok();
|
||||
}
|
||||
|
||||
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
||||
let out = Command::new(args[0]).args(&args[1..]).output()?;
|
||||
if !out.status.success() {
|
||||
use std::io::{Error, ErrorKind};
|
||||
return Err(Error::new(ErrorKind::Other, "Command not successful"));
|
||||
}
|
||||
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
|
||||
}
|
||||
|
||||
/// This method reads info from Git, namely tags, branch, and revision
|
||||
fn read_git_info() -> Result<(), std::io::Error> {
|
||||
// The exact tag for the current commit, can be empty when
|
||||
// the current commit doesn't have an associated tag
|
||||
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
|
||||
if let Some(ref exact) = exact_tag {
|
||||
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact);
|
||||
}
|
||||
|
||||
// The last available tag, equal to exact_tag when
|
||||
// the current commit is tagged
|
||||
let last_tag = run(&["git", "describe", "--abbrev=0", "--tags"])?;
|
||||
println!("cargo:rustc-env=GIT_LAST_TAG={}", last_tag);
|
||||
|
||||
// The current branch name
|
||||
let branch = run(&["git", "rev-parse", "--abbrev-ref", "HEAD"])?;
|
||||
println!("cargo:rustc-env=GIT_BRANCH={}", branch);
|
||||
|
||||
// The current git commit hash
|
||||
let rev = run(&["git", "rev-parse", "HEAD"])?;
|
||||
let rev_short = rev.get(..8).unwrap_or_default();
|
||||
println!("cargo:rustc-env=GIT_REV={}", rev_short);
|
||||
|
||||
// Combined version
|
||||
let version = if let Some(exact) = exact_tag {
|
||||
exact
|
||||
} else if &branch != "master" {
|
||||
format!("{}-{} ({})", last_tag, rev_short, branch)
|
||||
} else {
|
||||
format!("{}-{}", last_tag, rev_short)
|
||||
};
|
||||
println!("cargo:rustc-env=GIT_VERSION={}", version);
|
||||
|
||||
// To access these values, use:
|
||||
// env!("GIT_EXACT_TAG")
|
||||
// env!("GIT_LAST_TAG")
|
||||
// env!("GIT_BRANCH")
|
||||
// env!("GIT_REV")
|
||||
// env!("GIT_VERSION")
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/db/schema.rs"
|
9
docker/settings.Production.json
Normal file
9
docker/settings.Production.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "/api",
|
||||
"identityUri": "/identity",
|
||||
"iconsUri": "/icons",
|
||||
"stripeKey": "",
|
||||
"braintreeKey": ""
|
||||
}
|
||||
}
|
20
libs/jsonwebtoken/Cargo.toml
Normal file
20
libs/jsonwebtoken/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[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"
|
21
libs/jsonwebtoken/LICENSE
Normal file
21
libs/jsonwebtoken/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
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.
|
120
libs/jsonwebtoken/src/crypto.rs
Normal file
120
libs/jsonwebtoken/src/crypto.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
}
|
68
libs/jsonwebtoken/src/errors.rs
Normal file
68
libs/jsonwebtoken/src/errors.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
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"];
|
||||
}
|
||||
}
|
64
libs/jsonwebtoken/src/header.rs
Normal file
64
libs/jsonwebtoken/src/header.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
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())
|
||||
}
|
||||
}
|
142
libs/jsonwebtoken/src/lib.rs
Normal file
142
libs/jsonwebtoken/src/lib.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! 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)
|
||||
}
|
42
libs/jsonwebtoken/src/serialization.rs
Normal file
42
libs/jsonwebtoken/src/serialization.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
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))
|
||||
}
|
377
libs/jsonwebtoken/src/validation.rs
Normal file
377
libs/jsonwebtoken/src/validation.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
UPDATE users
|
||||
SET totp_secret = (
|
||||
SELECT twofactor.data FROM twofactor
|
||||
WHERE twofactor.type = 0
|
||||
AND twofactor.user_uuid = users.uuid
|
||||
);
|
||||
|
||||
DROP TABLE twofactor;
|
@@ -1,15 +0,0 @@
|
||||
CREATE TABLE twofactor (
|
||||
uuid TEXT NOT NULL PRIMARY KEY,
|
||||
user_uuid TEXT NOT NULL REFERENCES users (uuid),
|
||||
type INTEGER NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
|
||||
UNIQUE (user_uuid, type)
|
||||
);
|
||||
|
||||
|
||||
INSERT INTO twofactor (uuid, user_uuid, type, enabled, data)
|
||||
SELECT lower(hex(randomblob(16))) , uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL;
|
||||
|
||||
UPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty
|
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE ciphers
|
||||
ADD COLUMN
|
||||
password_history TEXT;
|
@@ -1 +0,0 @@
|
||||
DROP TABLE invitations;
|
@@ -1,3 +0,0 @@
|
||||
CREATE TABLE invitations (
|
||||
email TEXT NOT NULL PRIMARY KEY
|
||||
);
|
@@ -1,7 +0,0 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
client_kdf_type INTEGER NOT NULL DEFAULT 0; -- PBKDF2
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN
|
||||
client_kdf_iter INTEGER NOT NULL DEFAULT 5000;
|
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE attachments
|
||||
ADD COLUMN
|
||||
key TEXT;
|
@@ -1 +0,0 @@
|
||||
nightly-2019-04-26
|
@@ -1 +0,0 @@
|
||||
max_width = 120
|
238
src/api/admin.rs
238
src/api/admin.rs
@@ -1,238 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use rocket::http::{Cookie, Cookies, SameSite};
|
||||
use rocket::request::{self, FlashMessage, Form, FromRequest, Request};
|
||||
use rocket::response::{content::Html, Flash, Redirect};
|
||||
use rocket::{Outcome, Route};
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::db::{models::*, DbConn};
|
||||
use crate::error::Error;
|
||||
use crate::mail;
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
if CONFIG.admin_token().is_none() && !CONFIG.disable_admin_token() {
|
||||
return routes![admin_disabled];
|
||||
}
|
||||
|
||||
routes![
|
||||
admin_login,
|
||||
get_users,
|
||||
post_admin_login,
|
||||
admin_page,
|
||||
invite_user,
|
||||
delete_user,
|
||||
deauth_user,
|
||||
update_revision_users,
|
||||
post_config,
|
||||
delete_config,
|
||||
]
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn admin_disabled() -> &'static str {
|
||||
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
|
||||
}
|
||||
|
||||
const COOKIE_NAME: &str = "BWRS_ADMIN";
|
||||
const ADMIN_PATH: &str = "/admin";
|
||||
|
||||
const BASE_TEMPLATE: &str = "admin/base";
|
||||
const VERSION: Option<&str> = option_env!("GIT_VERSION");
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
||||
// If there is an error, show it
|
||||
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
|
||||
let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg});
|
||||
|
||||
// Return the page
|
||||
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct LoginForm {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -> Result<Redirect, Flash<Redirect>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
// If the token is invalid, redirect to login page
|
||||
if !_validate_token(&data.token) {
|
||||
error!("Invalid admin token. IP: {}", ip.ip);
|
||||
Err(Flash::error(
|
||||
Redirect::to(ADMIN_PATH),
|
||||
"Invalid admin token, please try again.",
|
||||
))
|
||||
} else {
|
||||
// If the token received is valid, generate JWT and save it as a cookie
|
||||
let claims = generate_admin_claims();
|
||||
let jwt = encode_jwt(&claims);
|
||||
|
||||
let cookie = Cookie::build(COOKIE_NAME, jwt)
|
||||
.path(ADMIN_PATH)
|
||||
.max_age(chrono::Duration::minutes(20))
|
||||
.same_site(SameSite::Strict)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
|
||||
cookies.add(cookie);
|
||||
Ok(Redirect::to(ADMIN_PATH))
|
||||
}
|
||||
}
|
||||
|
||||
fn _validate_token(token: &str) -> bool {
|
||||
match CONFIG.admin_token().as_ref() {
|
||||
None => false,
|
||||
Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AdminTemplateData {
|
||||
page_content: String,
|
||||
version: Option<&'static str>,
|
||||
users: Vec<Value>,
|
||||
config: Value,
|
||||
}
|
||||
|
||||
impl AdminTemplateData {
|
||||
fn new(users: Vec<Value>) -> Self {
|
||||
Self {
|
||||
page_content: String::from("admin/page"),
|
||||
version: VERSION,
|
||||
users,
|
||||
config: CONFIG.prepare_json(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(self) -> Result<String, Error> {
|
||||
CONFIG.render_template(BASE_TEMPLATE, &self)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/", rank = 1)]
|
||||
fn admin_page(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||
let users = User::get_all(&conn);
|
||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
||||
|
||||
let text = AdminTemplateData::new(users_json).render()?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct InviteData {
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[post("/invite", data = "<data>")]
|
||||
fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
let data: InviteData = data.into_inner();
|
||||
let email = data.email.clone();
|
||||
if User::find_by_mail(&data.email, &conn).is_some() {
|
||||
err!("User already exists")
|
||||
}
|
||||
|
||||
if !CONFIG.invitations_allowed() {
|
||||
err!("Invitations are not allowed")
|
||||
}
|
||||
|
||||
let mut user = User::new(email);
|
||||
user.save(&conn)?;
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
let org_name = "bitwarden_rs";
|
||||
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
|
||||
} else {
|
||||
let invitation = Invitation::new(data.email);
|
||||
invitation.save(&conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/users")]
|
||||
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
|
||||
let users = User::get_all(&conn);
|
||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
||||
|
||||
Ok(Json(Value::Array(users_json)))
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/delete")]
|
||||
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
|
||||
user.delete(&conn)
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/deauth")]
|
||||
fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
let mut user = match User::find_by_uuid(&uuid, &conn) {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
|
||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
||||
user.reset_security_stamp();
|
||||
|
||||
user.save(&conn)
|
||||
}
|
||||
|
||||
#[post("/users/update_revision")]
|
||||
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
User::update_all_revisions(&conn)
|
||||
}
|
||||
|
||||
#[post("/config", data = "<data>")]
|
||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||
let data: ConfigBuilder = data.into_inner();
|
||||
CONFIG.update_config(data)
|
||||
}
|
||||
|
||||
#[post("/config/delete")]
|
||||
fn delete_config(_token: AdminToken) -> EmptyResult {
|
||||
CONFIG.delete_user_config()
|
||||
}
|
||||
|
||||
pub struct AdminToken {}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
|
||||
type Error = &'static str;
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
if CONFIG.disable_admin_token() {
|
||||
Outcome::Success(AdminToken {})
|
||||
} else {
|
||||
let mut cookies = request.cookies();
|
||||
|
||||
let access_token = match cookies.get(COOKIE_NAME) {
|
||||
Some(cookie) => cookie.value(),
|
||||
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
|
||||
};
|
||||
|
||||
let ip = match request.guard::<ClientIp>() {
|
||||
Outcome::Success(ip) => ip.ip,
|
||||
_ => err_handler!("Error getting Client IP"),
|
||||
};
|
||||
|
||||
if decode_admin(access_token).is_err() {
|
||||
// Remove admin cookie
|
||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||
error!("Invalid or expired admin JWT. IP: {}.", ip);
|
||||
return Outcome::Forward(());
|
||||
}
|
||||
|
||||
Outcome::Success(AdminToken {})
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,51 +1,25 @@
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket_contrib::Json;
|
||||
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
|
||||
use crate::auth::{decode_invite, Headers};
|
||||
use crate::mail;
|
||||
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase};
|
||||
use auth::Headers;
|
||||
|
||||
use crate::CONFIG;
|
||||
use util;
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
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,
|
||||
]
|
||||
}
|
||||
use CONFIG;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RegisterData {
|
||||
Email: String,
|
||||
Kdf: Option<i32>,
|
||||
KdfIterations: Option<i32>,
|
||||
Key: String,
|
||||
#[serde(deserialize_with = "util::upcase_deserialize")]
|
||||
Keys: Option<KeysData>,
|
||||
MasterPasswordHash: String,
|
||||
MasterPasswordHint: Option<String>,
|
||||
Name: Option<String>,
|
||||
Token: Option<String>,
|
||||
OrganizationUserId: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -59,54 +33,15 @@ 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(user) => {
|
||||
if !user.password_hash.is_empty() {
|
||||
err!("User already exists")
|
||||
}
|
||||
|
||||
if let Some(token) = data.Token {
|
||||
let claims = decode_invite(&token)?;
|
||||
if claims.email == data.Email {
|
||||
user
|
||||
} else {
|
||||
err!("Registration email does not match invite email")
|
||||
}
|
||||
} else if Invitation::take(&data.Email, &conn) {
|
||||
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
|
||||
user_org.status = UserOrgStatus::Accepted as i32;
|
||||
user_org.save(&conn)?;
|
||||
}
|
||||
|
||||
user
|
||||
} else if CONFIG.signups_allowed() {
|
||||
err!("Account with this email already exists")
|
||||
} else {
|
||||
err!("Registration not allowed")
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if CONFIG.signups_allowed() || Invitation::take(&data.Email, &conn) {
|
||||
User::new(data.Email.clone())
|
||||
} else {
|
||||
err!("Registration not allowed")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Make sure we don't leave a lingering invitation.
|
||||
Invitation::take(&data.Email, &conn);
|
||||
|
||||
if let Some(client_kdf_iter) = data.KdfIterations {
|
||||
user.client_kdf_iter = client_kdf_iter;
|
||||
if !CONFIG.signups_allowed {
|
||||
err!(format!("Signups not allowed"))
|
||||
}
|
||||
|
||||
if let Some(client_kdf_type) = data.Kdf {
|
||||
user.client_kdf_type = client_kdf_type;
|
||||
if let Some(_) = User::find_by_mail(&data.Email, &conn) {
|
||||
err!("Email already exists")
|
||||
}
|
||||
|
||||
user.set_password(&data.MasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
let mut user = User::new(data.Email, data.Key, data.MasterPasswordHash);
|
||||
|
||||
// Add extra fields if present
|
||||
if let Some(name) = data.Name {
|
||||
@@ -122,7 +57,9 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
user.public_key = Some(keys.PublicKey);
|
||||
}
|
||||
|
||||
user.save(&conn)
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/accounts/profile")]
|
||||
@@ -130,40 +67,11 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
Ok(Json(headers.user.to_json(&conn)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ProfileData {
|
||||
#[serde(rename = "Culture")]
|
||||
_Culture: String, // Ignored, always use en-US
|
||||
MasterPasswordHint: Option<String>,
|
||||
Name: String,
|
||||
}
|
||||
|
||||
#[put("/accounts/profile", data = "<data>")]
|
||||
fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
post_profile(data, headers, conn)
|
||||
}
|
||||
|
||||
#[post("/accounts/profile", data = "<data>")]
|
||||
fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: ProfileData = data.into_inner().data;
|
||||
|
||||
let mut user = headers.user;
|
||||
|
||||
user.name = data.Name;
|
||||
user.password_hint = match data.MasterPasswordHint {
|
||||
Some(ref h) if h.is_empty() => None,
|
||||
_ => data.MasterPasswordHint,
|
||||
};
|
||||
user.save(&conn)?;
|
||||
Ok(Json(user.to_json(&conn)))
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>/public-key")]
|
||||
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!({
|
||||
@@ -182,7 +90,8 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
||||
user.private_key = Some(data.EncryptedPrivateKey);
|
||||
user.public_key = Some(data.PublicKey);
|
||||
|
||||
user.save(&conn)?;
|
||||
user.save(&conn);
|
||||
|
||||
Ok(Json(user.to_json(&conn)))
|
||||
}
|
||||
|
||||
@@ -205,112 +114,9 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
||||
|
||||
user.set_password(&data.NewMasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
user.save(&conn)
|
||||
}
|
||||
user.save(&conn);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ChangeKdfData {
|
||||
Kdf: i32,
|
||||
KdfIterations: i32,
|
||||
|
||||
MasterPasswordHash: String,
|
||||
NewMasterPasswordHash: String,
|
||||
Key: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/kdf", data = "<data>")]
|
||||
fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: ChangeKdfData = data.into_inner().data;
|
||||
let mut user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
user.client_kdf_iter = data.KdfIterations;
|
||||
user.client_kdf_type = data.Kdf;
|
||||
user.set_password(&data.NewMasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
user.save(&conn)
|
||||
}
|
||||
|
||||
#[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, nt: Notify) -> 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;
|
||||
saved_folder.save(&conn)?
|
||||
}
|
||||
|
||||
// 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,
|
||||
&nt,
|
||||
UpdateType::CipherUpdate,
|
||||
)?
|
||||
}
|
||||
|
||||
// Update user data
|
||||
let mut user = headers.user;
|
||||
|
||||
user.key = data.Key;
|
||||
user.private_key = Some(data.PrivateKey);
|
||||
user.reset_security_stamp();
|
||||
|
||||
user.save(&conn)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/accounts/security-stamp", data = "<data>")]
|
||||
@@ -322,29 +128,8 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
||||
user.reset_security_stamp();
|
||||
user.save(&conn)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct EmailTokenData {
|
||||
MasterPasswordHash: String,
|
||||
NewEmail: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/email-token", data = "<data>")]
|
||||
fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: EmailTokenData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
if User::find_by_mail(&data.NewEmail, &conn).is_some() {
|
||||
err!("Email already in use");
|
||||
}
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -354,14 +139,10 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
|
||||
struct ChangeEmailData {
|
||||
MasterPasswordHash: String,
|
||||
NewEmail: String,
|
||||
|
||||
Key: String,
|
||||
NewMasterPasswordHash: String,
|
||||
#[serde(rename = "Token")]
|
||||
_Token: NumberOrString,
|
||||
}
|
||||
|
||||
#[post("/accounts/email", data = "<data>")]
|
||||
|
||||
#[post("/accounts/email-token", data = "<data>")]
|
||||
fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: ChangeEmailData = data.into_inner().data;
|
||||
let mut user = headers.user;
|
||||
@@ -375,19 +156,12 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
||||
}
|
||||
|
||||
user.email = data.NewEmail;
|
||||
user.save(&conn);
|
||||
|
||||
user.set_password(&data.NewMasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
|
||||
user.save(&conn)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/accounts/delete", data = "<data>")]
|
||||
fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_account(data, headers, conn)
|
||||
}
|
||||
|
||||
#[delete("/accounts", data = "<data>")]
|
||||
fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
let user = headers.user;
|
||||
@@ -396,60 +170,33 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
user.delete(&conn)
|
||||
}
|
||||
|
||||
#[get("/accounts/revision-date")]
|
||||
fn revision_date(headers: Headers) -> String {
|
||||
let revision_date = headers.user.updated_at.timestamp_millis();
|
||||
revision_date.to_string()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct PasswordHintData {
|
||||
Email: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/password-hint", data = "<data>")]
|
||||
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordHintData = data.into_inner().data;
|
||||
|
||||
let hint = match User::find_by_mail(&data.Email, &conn) {
|
||||
Some(user) => user.password_hint,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
mail::send_password_hint(&data.Email, hint)?;
|
||||
} else if CONFIG.show_password_hint() {
|
||||
if let Some(hint) = hint {
|
||||
err!(format!("Your password hint is: {}", &hint));
|
||||
} else {
|
||||
err!("Sorry, you have no password hint...");
|
||||
// Delete ciphers and their attachments
|
||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
||||
match cipher.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting cipher")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete folders
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
match f.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting folder")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete devices
|
||||
for d in Device::find_by_user(&user.uuid, &conn) { d.delete(&conn); }
|
||||
|
||||
// Delete user
|
||||
user.delete(&conn);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct PreloginData {
|
||||
Email: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/prelogin", data = "<data>")]
|
||||
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
||||
let data: PreloginData = data.into_inner().data;
|
||||
|
||||
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
||||
Some(user) => (user.client_kdf_type, user.client_kdf_iter),
|
||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"Kdf": kdf_type,
|
||||
"KdfIterations": kdf_iter
|
||||
})))
|
||||
#[get("/accounts/revision-date")]
|
||||
fn revision_date(headers: Headers) -> String {
|
||||
let revision_date = headers.user.updated_at.timestamp();
|
||||
revision_date.to_string()
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,36 +1,20 @@
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use rocket_contrib::{Json, Value};
|
||||
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, 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,
|
||||
]
|
||||
}
|
||||
use api::{JsonResult, EmptyResult, JsonUpcase};
|
||||
use auth::Headers;
|
||||
|
||||
#[get("/folders")]
|
||||
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||
|
||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
|
||||
|
||||
Ok(Json(json!({
|
||||
"Data": folders_json,
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -38,7 +22,7 @@ fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let folder = match Folder::find_by_uuid(&uuid, &conn) {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
_ => err!("Invalid folder")
|
||||
};
|
||||
|
||||
if folder.user_uuid != headers.user.uuid {
|
||||
@@ -52,33 +36,32 @@ fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
pub struct FolderData {
|
||||
pub Name: String,
|
||||
pub Name: String
|
||||
}
|
||||
|
||||
#[post("/folders", data = "<data>")]
|
||||
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||
fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: FolderData = data.into_inner().data;
|
||||
|
||||
let mut folder = Folder::new(headers.user.uuid.clone(), data.Name);
|
||||
|
||||
folder.save(&conn)?;
|
||||
nt.send_folder_update(UpdateType::FolderCreate, &folder);
|
||||
folder.save(&conn);
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>", data = "<data>")]
|
||||
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||
put_folder(uuid, data, headers, conn, nt)
|
||||
fn post_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
put_folder(uuid, data, headers, conn)
|
||||
}
|
||||
|
||||
#[put("/folders/<uuid>", data = "<data>")]
|
||||
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||
fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: FolderData = data.into_inner().data;
|
||||
|
||||
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
_ => err!("Invalid folder")
|
||||
};
|
||||
|
||||
if folder.user_uuid != headers.user.uuid {
|
||||
@@ -87,22 +70,21 @@ fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn
|
||||
|
||||
folder.name = data.Name;
|
||||
|
||||
folder.save(&conn)?;
|
||||
nt.send_folder_update(UpdateType::FolderUpdate, &folder);
|
||||
folder.save(&conn);
|
||||
|
||||
Ok(Json(folder.to_json()))
|
||||
}
|
||||
|
||||
#[post("/folders/<uuid>/delete")]
|
||||
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||
delete_folder(uuid, headers, conn, nt)
|
||||
fn delete_folder_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_folder(uuid, headers, conn)
|
||||
}
|
||||
|
||||
#[delete("/folders/<uuid>")]
|
||||
fn delete_folder(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||
fn delete_folder(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let folder = match Folder::find_by_uuid(&uuid, &conn) {
|
||||
Some(folder) => folder,
|
||||
_ => err!("Invalid folder"),
|
||||
_ => err!("Invalid folder")
|
||||
};
|
||||
|
||||
if folder.user_uuid != headers.user.uuid {
|
||||
@@ -110,8 +92,8 @@ fn delete_folder(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> Em
|
||||
}
|
||||
|
||||
// Delete the actual folder entry
|
||||
folder.delete(&conn)?;
|
||||
|
||||
nt.send_folder_update(UpdateType::FolderDelete, &folder);
|
||||
Ok(())
|
||||
match folder.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed deleting folder")
|
||||
}
|
||||
}
|
||||
|
@@ -260,8 +260,7 @@
|
||||
"Type": 26,
|
||||
"Domains": [
|
||||
"steampowered.com",
|
||||
"steamcommunity.com",
|
||||
"steamgames.com"
|
||||
"steamcommunity.com"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
@@ -647,121 +646,5 @@
|
||||
"wiktionary.org"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 72,
|
||||
"Domains": [
|
||||
"airbnb.at",
|
||||
"airbnb.be",
|
||||
"airbnb.ca",
|
||||
"airbnb.ch",
|
||||
"airbnb.cl",
|
||||
"airbnb.co.cr",
|
||||
"airbnb.co.id",
|
||||
"airbnb.co.in",
|
||||
"airbnb.co.kr",
|
||||
"airbnb.co.nz",
|
||||
"airbnb.co.uk",
|
||||
"airbnb.co.ve",
|
||||
"airbnb.com",
|
||||
"airbnb.com.ar",
|
||||
"airbnb.com.au",
|
||||
"airbnb.com.bo",
|
||||
"airbnb.com.br",
|
||||
"airbnb.com.bz",
|
||||
"airbnb.com.co",
|
||||
"airbnb.com.ec",
|
||||
"airbnb.com.gt",
|
||||
"airbnb.com.hk",
|
||||
"airbnb.com.hn",
|
||||
"airbnb.com.mt",
|
||||
"airbnb.com.my",
|
||||
"airbnb.com.ni",
|
||||
"airbnb.com.pa",
|
||||
"airbnb.com.pe",
|
||||
"airbnb.com.py",
|
||||
"airbnb.com.sg",
|
||||
"airbnb.com.sv",
|
||||
"airbnb.com.tr",
|
||||
"airbnb.com.tw",
|
||||
"airbnb.cz",
|
||||
"airbnb.de",
|
||||
"airbnb.dk",
|
||||
"airbnb.es",
|
||||
"airbnb.fi",
|
||||
"airbnb.fr",
|
||||
"airbnb.gr",
|
||||
"airbnb.gy",
|
||||
"airbnb.hu",
|
||||
"airbnb.ie",
|
||||
"airbnb.is",
|
||||
"airbnb.it",
|
||||
"airbnb.jp",
|
||||
"airbnb.mx",
|
||||
"airbnb.nl",
|
||||
"airbnb.no",
|
||||
"airbnb.pl",
|
||||
"airbnb.pt",
|
||||
"airbnb.ru",
|
||||
"airbnb.se"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 73,
|
||||
"Domains": [
|
||||
"eventbrite.at",
|
||||
"eventbrite.be",
|
||||
"eventbrite.ca",
|
||||
"eventbrite.ch",
|
||||
"eventbrite.cl",
|
||||
"eventbrite.co.id",
|
||||
"eventbrite.co.in",
|
||||
"eventbrite.co.kr",
|
||||
"eventbrite.co.nz",
|
||||
"eventbrite.co.uk",
|
||||
"eventbrite.co.ve",
|
||||
"eventbrite.com",
|
||||
"eventbrite.com.au",
|
||||
"eventbrite.com.bo",
|
||||
"eventbrite.com.br",
|
||||
"eventbrite.com.co",
|
||||
"eventbrite.com.hk",
|
||||
"eventbrite.com.hn",
|
||||
"eventbrite.com.pe",
|
||||
"eventbrite.com.sg",
|
||||
"eventbrite.com.tr",
|
||||
"eventbrite.com.tw",
|
||||
"eventbrite.cz",
|
||||
"eventbrite.de",
|
||||
"eventbrite.dk",
|
||||
"eventbrite.fi",
|
||||
"eventbrite.fr",
|
||||
"eventbrite.gy",
|
||||
"eventbrite.hu",
|
||||
"eventbrite.ie",
|
||||
"eventbrite.is",
|
||||
"eventbrite.it",
|
||||
"eventbrite.jp",
|
||||
"eventbrite.mx",
|
||||
"eventbrite.nl",
|
||||
"eventbrite.no",
|
||||
"eventbrite.pl",
|
||||
"eventbrite.pt",
|
||||
"eventbrite.ru",
|
||||
"eventbrite.se"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 74,
|
||||
"Domains": [
|
||||
"stackexchange.com",
|
||||
"superuser.com",
|
||||
"stackoverflow.com",
|
||||
"serverfault.com",
|
||||
"mathoverflow.net"
|
||||
],
|
||||
"Excluded": false
|
||||
}
|
||||
]
|
@@ -2,71 +2,144 @@ mod accounts;
|
||||
mod ciphers;
|
||||
mod folders;
|
||||
mod organizations;
|
||||
pub(crate) mod two_factor;
|
||||
mod two_factor;
|
||||
|
||||
use self::accounts::*;
|
||||
use self::ciphers::*;
|
||||
use self::folders::*;
|
||||
use self::organizations::*;
|
||||
use self::two_factor::*;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut mod_routes = routes![
|
||||
routes![
|
||||
register,
|
||||
profile,
|
||||
get_public_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
post_sstamp,
|
||||
post_email,
|
||||
delete_account,
|
||||
revision_date,
|
||||
|
||||
sync,
|
||||
|
||||
get_ciphers,
|
||||
get_cipher,
|
||||
get_cipher_admin,
|
||||
get_cipher_details,
|
||||
post_ciphers,
|
||||
post_ciphers_admin,
|
||||
post_ciphers_import,
|
||||
post_attachment,
|
||||
delete_attachment_post,
|
||||
delete_attachment,
|
||||
post_cipher_admin,
|
||||
post_cipher_share,
|
||||
post_cipher,
|
||||
put_cipher,
|
||||
delete_cipher_post,
|
||||
delete_cipher,
|
||||
delete_cipher_selected,
|
||||
delete_all,
|
||||
move_cipher_selected,
|
||||
|
||||
get_folders,
|
||||
get_folder,
|
||||
post_folders,
|
||||
post_folder,
|
||||
put_folder,
|
||||
delete_folder_post,
|
||||
delete_folder,
|
||||
|
||||
get_twofactor,
|
||||
get_recover,
|
||||
recover,
|
||||
generate_authenticator,
|
||||
activate_authenticator,
|
||||
disable_authenticator,
|
||||
|
||||
get_organization,
|
||||
create_organization,
|
||||
delete_organization,
|
||||
get_user_collections,
|
||||
get_org_collections,
|
||||
get_org_collection_detail,
|
||||
get_collection_users,
|
||||
post_organization,
|
||||
post_organization_collections,
|
||||
post_organization_collection_delete_user,
|
||||
post_organization_collection_update,
|
||||
post_organization_collection_delete,
|
||||
post_collections_update,
|
||||
post_collections_admin,
|
||||
get_org_details,
|
||||
get_org_users,
|
||||
send_invite,
|
||||
confirm_invite,
|
||||
get_user,
|
||||
edit_user,
|
||||
delete_user,
|
||||
|
||||
clear_device_token,
|
||||
put_device_token,
|
||||
|
||||
get_eq_domains,
|
||||
post_eq_domains,
|
||||
put_eq_domains,
|
||||
hibp_breach,
|
||||
];
|
||||
|
||||
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
|
||||
//
|
||||
///
|
||||
/// Move this somewhere else
|
||||
///
|
||||
|
||||
use rocket::Route;
|
||||
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use rocket_contrib::{Json, Value};
|
||||
|
||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
|
||||
use crate::auth::Headers;
|
||||
use crate::db::DbConn;
|
||||
use crate::error::Error;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
||||
// This endpoint doesn't have auth header
|
||||
use api::{JsonResult, EmptyResult, JsonUpcase};
|
||||
use auth::Headers;
|
||||
|
||||
let _ = uuid;
|
||||
// uuid is not related to deviceId
|
||||
#[put("/devices/identifier/<uuid>/clear-token", data = "<data>")]
|
||||
fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
println!("UUID: {:#?}", uuid);
|
||||
println!("DATA: {:#?}", data);
|
||||
|
||||
let device = match Device::find_by_uuid(&uuid, &conn) {
|
||||
Some(device) => device,
|
||||
None => err!("Device not found")
|
||||
};
|
||||
|
||||
if device.user_uuid != headers.user.uuid {
|
||||
err!("Device not owned by user")
|
||||
}
|
||||
|
||||
device.delete(&conn);
|
||||
|
||||
// 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: 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
|
||||
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
println!("UUID: {:#?}", uuid);
|
||||
println!("DATA: {:#?}", data);
|
||||
|
||||
// TODO: This should save the push token, but we don't have push functionality
|
||||
let device = match Device::find_by_uuid(&uuid, &conn) {
|
||||
Some(device) => device,
|
||||
None => err!("Device not found")
|
||||
};
|
||||
|
||||
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),
|
||||
})))
|
||||
if device.user_uuid != headers.user.uuid {
|
||||
err!("Device not owned by user")
|
||||
}
|
||||
|
||||
// TODO: What does this do?
|
||||
|
||||
err!("Not implemented")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -77,7 +150,7 @@ struct GlobalDomain {
|
||||
Excluded: bool,
|
||||
}
|
||||
|
||||
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
||||
const GLOBAL_DOMAINS: &'static str = include_str!("global_domains.json");
|
||||
|
||||
#[get("/settings/domains")]
|
||||
fn get_eq_domains(headers: Headers) -> JsonResult {
|
||||
@@ -100,6 +173,7 @@ fn get_eq_domains(headers: Headers) -> JsonResult {
|
||||
})))
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct EquivDomainData {
|
||||
@@ -108,42 +182,19 @@ struct EquivDomainData {
|
||||
}
|
||||
|
||||
#[post("/settings/domains", data = "<data>")]
|
||||
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: EquivDomainData = data.into_inner().data;
|
||||
|
||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
|
||||
let equivalent_domains = data.EquivalentDomains.unwrap_or_default();
|
||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or(Vec::new());
|
||||
let equivalent_domains = data.EquivalentDomains.unwrap_or(Vec::new());
|
||||
|
||||
let mut user = headers.user;
|
||||
use serde_json::to_string;
|
||||
|
||||
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string());
|
||||
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string());
|
||||
user.excluded_globals = to_string(&excluded_globals).unwrap_or("[]".to_string());
|
||||
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or("[]".to_string());
|
||||
|
||||
user.save(&conn)?;
|
||||
user.save(&conn);
|
||||
|
||||
Ok(Json(json!({})))
|
||||
}
|
||||
|
||||
#[put("/settings/domains", data = "<data>")]
|
||||
fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
post_eq_domains(data, headers, conn)
|
||||
}
|
||||
|
||||
#[get("/hibp/breach?<username>")]
|
||||
fn hibp_breach(username: String) -> JsonResult {
|
||||
let url = format!("https://haveibeenpwned.com/api/v2/breachedaccount/{}", username);
|
||||
let user_agent = "Bitwarden_RS";
|
||||
|
||||
use reqwest::{header::USER_AGENT, Client};
|
||||
|
||||
let res = Client::new().get(&url).header(USER_AGENT, user_agent).send()?;
|
||||
|
||||
// If we get a 404, return a 404, it means no breached accounts
|
||||
if res.status() == 404 {
|
||||
return Err(Error::empty().with_code(404));
|
||||
}
|
||||
|
||||
let value: Value = res.error_for_status()?.json()?;
|
||||
Ok(Json(value))
|
||||
Ok(())
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
411
src/api/icons.rs
411
src/api/icons.rs
@@ -1,389 +1,86 @@
|
||||
use std::fs::{create_dir_all, remove_file, symlink_metadata, File};
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::fs::{create_dir_all, File};
|
||||
|
||||
use rocket::http::ContentType;
|
||||
use rocket::response::Content;
|
||||
use rocket::Route;
|
||||
use rocket::response::Content;
|
||||
use rocket::http::ContentType;
|
||||
|
||||
use reqwest::{header::HeaderMap, Client, Response};
|
||||
use reqwest;
|
||||
|
||||
use rocket::http::Cookie;
|
||||
|
||||
use regex::Regex;
|
||||
use soup::prelude::*;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::CONFIG;
|
||||
use CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![icon]
|
||||
}
|
||||
|
||||
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
|
||||
|
||||
const ALLOWED_CHARS: &str = "_-.";
|
||||
|
||||
lazy_static! {
|
||||
// Reuse the client between requests
|
||||
static ref CLIENT: Client = Client::builder()
|
||||
.gzip(true)
|
||||
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
||||
.default_headers(_header_map())
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn is_valid_domain(domain: &str) -> bool {
|
||||
// Don't allow empty or too big domains or path traversal
|
||||
if domain.is_empty() || domain.len() > 255 || domain.contains("..") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only alphanumeric or specific characters
|
||||
for c in domain.chars() {
|
||||
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[get("/<domain>/icon.png")]
|
||||
fn icon(domain: String) -> Content<Vec<u8>> {
|
||||
let icon_type = ContentType::new("image", "x-icon");
|
||||
|
||||
if !is_valid_domain(&domain) {
|
||||
warn!("Invalid domain: {:#?}", domain);
|
||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
||||
// Validate the domain to avoid directory traversal attacks
|
||||
if domain.contains("/") || domain.contains("..") {
|
||||
return Content(icon_type, get_fallback_icon());
|
||||
}
|
||||
|
||||
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||
info!("Icon blacklist enabled: {:#?}", blacklist);
|
||||
let url = format!("https://icons.bitwarden.com/{}/icon.png", domain);
|
||||
|
||||
let regex = Regex::new(&blacklist).expect("Valid Regex");
|
||||
|
||||
if regex.is_match(&domain) {
|
||||
warn!("Blacklisted domain: {:#?}", domain);
|
||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
let icon = get_icon(&domain);
|
||||
// Get the icon, or fallback in case of error
|
||||
let icon = match get_icon_cached(&domain, &url) {
|
||||
Ok(icon) => icon,
|
||||
Err(_) => return Content(icon_type, get_fallback_icon())
|
||||
};
|
||||
|
||||
Content(icon_type, icon)
|
||||
}
|
||||
|
||||
fn get_icon(domain: &str) -> Vec<u8> {
|
||||
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
||||
fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
|
||||
let mut res = reqwest::get(url)?;
|
||||
|
||||
if let Some(icon) = get_cached_icon(&path) {
|
||||
return icon;
|
||||
}
|
||||
res = match res.error_for_status() {
|
||||
Err(e) => return Err(e),
|
||||
Ok(res) => res
|
||||
};
|
||||
|
||||
if CONFIG.disable_icon_download() {
|
||||
return FALLBACK_ICON.to_vec();
|
||||
}
|
||||
|
||||
// Get the icon, or fallback in case of error
|
||||
match download_icon(&domain) {
|
||||
Ok(icon) => {
|
||||
save_icon(&path, &icon);
|
||||
icon
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error downloading icon: {:?}", e);
|
||||
mark_negcache(&path);
|
||||
FALLBACK_ICON.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
||||
// Check for expiration of negatively cached copy
|
||||
if icon_is_negcached(path) {
|
||||
return Some(FALLBACK_ICON.to_vec());
|
||||
}
|
||||
|
||||
// Check for expiration of successfully cached copy
|
||||
if icon_is_expired(path) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try to read the cached icon, and return it if it exists
|
||||
if let Ok(mut f) = File::open(path) {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
if f.read_to_end(&mut buffer).is_ok() {
|
||||
return Some(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {
|
||||
let meta = symlink_metadata(path)?;
|
||||
let modified = meta.modified()?;
|
||||
let age = SystemTime::now().duration_since(modified)?;
|
||||
|
||||
Ok(ttl > 0 && ttl <= age.as_secs())
|
||||
}
|
||||
|
||||
fn icon_is_negcached(path: &str) -> bool {
|
||||
let miss_indicator = path.to_owned() + ".miss";
|
||||
let expired = file_is_expired(&miss_indicator, CONFIG.icon_cache_negttl());
|
||||
|
||||
match expired {
|
||||
// No longer negatively cached, drop the marker
|
||||
Ok(true) => {
|
||||
if let Err(e) = remove_file(&miss_indicator) {
|
||||
error!("Could not remove negative cache indicator for icon {:?}: {:?}", path, e);
|
||||
}
|
||||
false
|
||||
}
|
||||
// The marker hasn't expired yet.
|
||||
Ok(false) => true,
|
||||
// The marker is missing or inaccessible in some way.
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_negcache(path: &str) {
|
||||
let miss_indicator = path.to_owned() + ".miss";
|
||||
File::create(&miss_indicator).expect("Error creating negative cache marker");
|
||||
}
|
||||
|
||||
fn icon_is_expired(path: &str) -> bool {
|
||||
let expired = file_is_expired(path, CONFIG.icon_cache_ttl());
|
||||
expired.unwrap_or(true)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Icon {
|
||||
priority: u8,
|
||||
href: String,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
fn new(priority: u8, href: String) -> Self {
|
||||
Self { href, priority }
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response.
|
||||
/// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies.
|
||||
/// This does not mean that that location does exists, but it is the default location browser use.
|
||||
///
|
||||
/// # Argument
|
||||
/// * `domain` - A string which holds the domain with extension.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
|
||||
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
|
||||
/// ```
|
||||
fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
||||
// Default URL with secure and insecure schemes
|
||||
let ssldomain = format!("https://{}", domain);
|
||||
let httpdomain = format!("http://{}", domain);
|
||||
|
||||
// Create the iconlist
|
||||
let mut iconlist: Vec<Icon> = Vec::new();
|
||||
|
||||
// Create the cookie_str to fill it all the cookies from the response
|
||||
// These cookies can be used to request/download the favicon image.
|
||||
// Some sites have extra security in place with for example XSRF Tokens.
|
||||
let mut cookie_str = String::new();
|
||||
|
||||
let resp = get_page(&ssldomain).or_else(|_| get_page(&httpdomain));
|
||||
if let Ok(content) = resp {
|
||||
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
||||
let url = content.url().clone();
|
||||
let raw_cookies = content.headers().get_all("set-cookie");
|
||||
cookie_str = raw_cookies
|
||||
.iter()
|
||||
.map(|raw_cookie| {
|
||||
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap();
|
||||
format!("{}={}; ", cookie.name(), cookie.value())
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
// Add the default favicon.ico to the list with the domain the content responded from.
|
||||
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
||||
|
||||
let soup = Soup::from_reader(content)?;
|
||||
// Search for and filter
|
||||
let favicons = soup
|
||||
.tag("link")
|
||||
.attr("rel", Regex::new(r"icon$|apple.*icon")?) // Only use icon rels
|
||||
.attr("href", Regex::new(r"(?i)\w+\.(jpg|jpeg|png|ico)(\?.*)?$")?) // Only allow specific extensions
|
||||
.find_all();
|
||||
|
||||
// Loop through all the found icons and determine it's priority
|
||||
for favicon in favicons {
|
||||
let sizes = favicon.get("sizes");
|
||||
let href = favicon.get("href").expect("Missing href");
|
||||
let full_href = url.join(&href).unwrap().into_string();
|
||||
|
||||
let priority = get_icon_priority(&full_href, sizes);
|
||||
|
||||
iconlist.push(Icon::new(priority, full_href))
|
||||
}
|
||||
} else {
|
||||
// Add the default favicon.ico to the list with just the given domain
|
||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
||||
}
|
||||
|
||||
// Sort the iconlist by priority
|
||||
iconlist.sort_by_key(|x| x.priority);
|
||||
|
||||
// There always is an icon in the list, so no need to check if it exists, and just return the first one
|
||||
Ok((iconlist, cookie_str))
|
||||
}
|
||||
|
||||
fn get_page(url: &str) -> Result<Response, Error> {
|
||||
get_page_with_cookies(url, "")
|
||||
}
|
||||
|
||||
fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> {
|
||||
CLIENT
|
||||
.get(url)
|
||||
.header("cookie", cookie_str)
|
||||
.send()?
|
||||
.error_for_status()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns a Integer with the priority of the type of the icon which to prefer.
|
||||
/// The lower the number the better.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `href` - A string which holds the href value or relative path.
|
||||
/// * `sizes` - The size of the icon if available as a <width>x<height> value like 32x32.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
||||
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
||||
/// ```
|
||||
fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
||||
// Check if there is a dimension set
|
||||
let (width, height) = parse_sizes(sizes);
|
||||
|
||||
// Check if there is a size given
|
||||
if width != 0 && height != 0 {
|
||||
// Only allow square dimensions
|
||||
if width == height {
|
||||
// Change priority by given size
|
||||
if width == 32 {
|
||||
1
|
||||
} else if width == 64 {
|
||||
2
|
||||
} else if width >= 24 && width <= 128 {
|
||||
3
|
||||
} else if width == 16 {
|
||||
4
|
||||
} else {
|
||||
5
|
||||
}
|
||||
// There are dimensions available, but the image is not a square
|
||||
} else {
|
||||
200
|
||||
}
|
||||
} else {
|
||||
// Change priority by file extension
|
||||
if href.ends_with(".png") {
|
||||
10
|
||||
} else if href.ends_with(".jpg") || href.ends_with(".jpeg") {
|
||||
20
|
||||
} else {
|
||||
30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Tuple with the width and hight as a seperate value extracted from the sizes attribute
|
||||
/// It will return 0 for both values if no match has been found.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `sizes` - The size of the icon if available as a <width>x<height> value like 32x32.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// let (width, height) = parse_sizes("64x64"); // (64, 64)
|
||||
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
||||
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
||||
/// ```
|
||||
fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
||||
let mut width: u16 = 0;
|
||||
let mut height: u16 = 0;
|
||||
|
||||
if let Some(sizes) = sizes {
|
||||
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
|
||||
None => {}
|
||||
Some(dimensions) => {
|
||||
if dimensions.len() >= 3 {
|
||||
width = dimensions[1].parse::<u16>().unwrap_or_default();
|
||||
height = dimensions[2].parse::<u16>().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(width, height)
|
||||
}
|
||||
|
||||
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
||||
let (iconlist, cookie_str) = get_icon_url(&domain)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
for icon in iconlist.iter().take(5) {
|
||||
match get_page_with_cookies(&icon.href, &cookie_str) {
|
||||
Ok(mut res) => {
|
||||
info!("Downloaded icon from {}", icon.href);
|
||||
res.copy_to(&mut buffer)?;
|
||||
break;
|
||||
}
|
||||
Err(_) => info!("Download failed for {}", icon.href),
|
||||
};
|
||||
}
|
||||
|
||||
if buffer.is_empty() {
|
||||
err!("Empty response")
|
||||
}
|
||||
let mut buffer: Vec<u8> = vec![];
|
||||
res.copy_to(&mut buffer)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn save_icon(path: &str, icon: &[u8]) {
|
||||
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache");
|
||||
fn get_icon_cached(key: &str, url: &str) -> io::Result<Vec<u8>> {
|
||||
create_dir_all(&CONFIG.icon_cache_folder)?;
|
||||
let path = &format!("{}/{}.png", CONFIG.icon_cache_folder, key);
|
||||
|
||||
if let Ok(mut f) = File::create(path) {
|
||||
f.write_all(icon).expect("Error writing icon file");
|
||||
// Try to read the cached icon, and return it if it exists
|
||||
match File::open(path) {
|
||||
Ok(mut f) => {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
if f.read_to_end(&mut buffer).is_ok() {
|
||||
return Ok(buffer);
|
||||
}
|
||||
/* If error reading file continue */
|
||||
}
|
||||
Err(_) => { /* Continue */ }
|
||||
}
|
||||
|
||||
println!("Downloading icon for {}...", key);
|
||||
let icon = match get_icon(url) {
|
||||
Ok(icon) => icon,
|
||||
Err(_) => return Err(io::Error::new(io::ErrorKind::NotFound, ""))
|
||||
};
|
||||
|
||||
// Save the currently downloaded icon
|
||||
match File::create(path) {
|
||||
Ok(mut f) => { f.write_all(&icon).expect("Error writing icon file"); }
|
||||
Err(_) => { /* Continue */ }
|
||||
};
|
||||
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
fn _header_map() -> HeaderMap {
|
||||
// Set some default headers for the request.
|
||||
// Use a browser like user-agent to make sure most websites will return there correct website.
|
||||
use reqwest::header::*;
|
||||
|
||||
macro_rules! headers {
|
||||
($( $name:ident : $value:literal),+ $(,)? ) => {
|
||||
let mut headers = HeaderMap::new();
|
||||
$( headers.insert($name, HeaderValue::from_static($value)); )+
|
||||
headers
|
||||
};
|
||||
}
|
||||
|
||||
headers! {
|
||||
USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299",
|
||||
ACCEPT_LANGUAGE: "en-US,en;q=0.8",
|
||||
CACHE_CONTROL: "no-cache",
|
||||
PRAGMA: "no-cache",
|
||||
ACCEPT: "text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
}
|
||||
fn get_fallback_icon() -> Vec<u8> {
|
||||
let fallback_icon = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
|
||||
get_icon_cached("default", fallback_icon).unwrap()
|
||||
}
|
||||
|
@@ -1,59 +1,40 @@
|
||||
use rocket::request::{Form, FormItems, FromForm};
|
||||
use rocket::Route;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use rocket::{Route, Outcome};
|
||||
use rocket::request::{self, Request, FromRequest, Form, FormItems, FromForm};
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use rocket_contrib::{Json, Value};
|
||||
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use crate::util;
|
||||
use util;
|
||||
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||
|
||||
use crate::auth::ClientIp;
|
||||
|
||||
use crate::CONFIG;
|
||||
use api::JsonResult;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![login]
|
||||
routes![ login]
|
||||
}
|
||||
|
||||
#[post("/connect/token", data = "<data>")]
|
||||
fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
|
||||
let data: ConnectData = data.into_inner();
|
||||
#[post("/connect/token", data = "<connect_data>")]
|
||||
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult {
|
||||
let data = connect_data.get();
|
||||
println!("{:#?}", data);
|
||||
|
||||
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),
|
||||
match data.grant_type {
|
||||
GrantType::RefreshToken =>_refresh_login(data, device_type, conn),
|
||||
GrantType::Password => _password_login(data, device_type, conn)
|
||||
}
|
||||
}
|
||||
|
||||
fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||
fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult {
|
||||
// Extract token
|
||||
let token = data.refresh_token.unwrap();
|
||||
let token = data.get("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"),
|
||||
None => err!("Invalid refresh token")
|
||||
};
|
||||
|
||||
// COMMON
|
||||
@@ -61,8 +42,8 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||
|
||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||
device.save(&conn);
|
||||
|
||||
device.save(&conn)?;
|
||||
Ok(Json(json!({
|
||||
"access_token": access_token,
|
||||
"expires_in": expires_in,
|
||||
@@ -73,59 +54,99 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||
})))
|
||||
}
|
||||
|
||||
fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
|
||||
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) -> JsonResult {
|
||||
// Validate scope
|
||||
let scope = data.scope.as_ref().unwrap();
|
||||
let scope = data.get("scope").unwrap();
|
||||
if scope != "api offline_access" {
|
||||
err!("Scope not supported")
|
||||
}
|
||||
|
||||
// Get the user
|
||||
let username = data.username.as_ref().unwrap();
|
||||
let username = data.get("username").unwrap();
|
||||
let user = match User::find_by_mail(username, &conn) {
|
||||
Some(user) => user,
|
||||
None => err!(
|
||||
"Username or password is incorrect. Try again",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username)
|
||||
),
|
||||
None => err!("Username or password is incorrect. Try again.")
|
||||
};
|
||||
|
||||
// Check password
|
||||
let password = data.password.as_ref().unwrap();
|
||||
let password = data.get("password").unwrap();
|
||||
if !user.check_valid_password(password) {
|
||||
err!(
|
||||
"Username or password is incorrect. Try again",
|
||||
format!("IP: {}. Username: {}.", ip.ip, username)
|
||||
)
|
||||
err!("Username or password is incorrect. Try again.")
|
||||
}
|
||||
|
||||
// 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");
|
||||
// Let's only use the header and ignore the 'devicetype' parameter
|
||||
let device_type_num = device_type.0;
|
||||
|
||||
let (device_id, device_name) = match data.is_device {
|
||||
false => { (format!("web-{}", user.uuid), String::from("web")) }
|
||||
true => {
|
||||
(
|
||||
data.get("deviceidentifier").unwrap().clone(),
|
||||
data.get("devicename").unwrap().clone(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Find device or create new
|
||||
let mut device = match Device::find_by_uuid(&device_id, &conn) {
|
||||
Some(device) => {
|
||||
// Check if owned device, and recreate if not
|
||||
// Check if valid device
|
||||
if device.user_uuid != user.uuid {
|
||||
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
|
||||
device.delete(&conn);
|
||||
err!("Device is not owned by user")
|
||||
}
|
||||
|
||||
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 = if user.requires_twofactor() {
|
||||
let twofactor_provider = util::parse_option_string(data.get("twoFactorProvider")).unwrap_or(0);
|
||||
let twofactor_code = match data.get("twoFactorToken") {
|
||||
Some(code) => code,
|
||||
None => err_json!(_json_err_twofactor())
|
||||
};
|
||||
|
||||
match twofactor_provider {
|
||||
0 /* TOTP */ => {
|
||||
let totp_code: u64 = match twofactor_code.parse() {
|
||||
Ok(code) => code,
|
||||
Err(_) => err!("Invalid Totp code")
|
||||
};
|
||||
|
||||
if !user.check_totp_code(totp_code) {
|
||||
err_json!(_json_err_twofactor())
|
||||
}
|
||||
|
||||
if util::parse_option_string(data.get("twoFactorRemember")).unwrap_or(0) == 1 {
|
||||
device.refresh_twofactor_remember();
|
||||
device.twofactor_remember.clone()
|
||||
} else {
|
||||
device.delete_twofactor_remember();
|
||||
None
|
||||
}
|
||||
},
|
||||
5 /* Remember */ => {
|
||||
match device.twofactor_remember {
|
||||
Some(ref remember) if remember == twofactor_code => (),
|
||||
_ => err_json!(_json_err_twofactor())
|
||||
};
|
||||
None // No twofactor token needed here
|
||||
},
|
||||
_ => err!("Invalid two factor provider"),
|
||||
}
|
||||
} else { None }; // No twofactor token if twofactor is disabled
|
||||
|
||||
// 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);
|
||||
device.save(&conn)?;
|
||||
device.save(&conn);
|
||||
|
||||
let mut result = json!({
|
||||
"access_token": access_token,
|
||||
@@ -141,199 +162,128 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
||||
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>> {
|
||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
||||
|
||||
// No twofactor token if twofactor is disabled
|
||||
if twofactors.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
||||
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
||||
|
||||
let twofactor_code = match data.two_factor_token {
|
||||
Some(ref code) => code,
|
||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||
};
|
||||
|
||||
let selected_twofactor = twofactors.into_iter().filter(|tf| tf.type_ == selected_id).nth(0);
|
||||
|
||||
use crate::api::core::two_factor as _tf;
|
||||
use crate::crypto::ct_eq;
|
||||
|
||||
let selected_data = _selected_data(selected_twofactor);
|
||||
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||
|
||||
match TwoFactorType::from_i32(selected_id) {
|
||||
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
||||
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||
Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
||||
|
||||
Some(TwoFactorType::Remember) => {
|
||||
match device.twofactor_remember {
|
||||
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||
}
|
||||
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||
}
|
||||
}
|
||||
_ => err!("Invalid two factor provider"),
|
||||
}
|
||||
|
||||
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||
Ok(Some(device.refresh_twofactor_remember()))
|
||||
} else {
|
||||
device.delete_twofactor_remember();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
||||
match tf {
|
||||
Some(tf) => Ok(tf.data),
|
||||
None => err!("Two factor doesn't exist"),
|
||||
}
|
||||
}
|
||||
|
||||
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
let mut result = json!({
|
||||
fn _json_err_twofactor() -> Value {
|
||||
json!({
|
||||
"error" : "invalid_grant",
|
||||
"error_description" : "Two factor required.",
|
||||
"TwoFactorProviders" : providers,
|
||||
"TwoFactorProviders2" : {} // { "0" : null }
|
||||
});
|
||||
"TwoFactorProviders" : [ 0 ],
|
||||
"TwoFactorProviders2" : { "0" : null }
|
||||
})
|
||||
}
|
||||
|
||||
for provider in providers {
|
||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
|
||||
|
||||
match TwoFactorType::from_i32(*provider) {
|
||||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||
|
||||
Some(TwoFactorType::U2f) if CONFIG.domain_set() => {
|
||||
let request = two_factor::generate_u2f_login(user_uuid, conn)?;
|
||||
let mut challenge_list = Vec::new();
|
||||
|
||||
for key in request.registered_keys {
|
||||
challenge_list.push(json!({
|
||||
"appId": request.app_id,
|
||||
"challenge": request.challenge,
|
||||
"version": key.version,
|
||||
"keyHandle": key.key_handle,
|
||||
}));
|
||||
}
|
||||
|
||||
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
||||
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Challenges": challenge_list_str,
|
||||
});
|
||||
}
|
||||
|
||||
Some(TwoFactorType::Duo) => {
|
||||
let email = match User::find_by_uuid(user_uuid, &conn) {
|
||||
Some(u) => u.email,
|
||||
None => err!("User does not exist"),
|
||||
};
|
||||
|
||||
let (signature, host) = two_factor::generate_duo_signature(&email, conn)?;
|
||||
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Host": host,
|
||||
"Signature": signature,
|
||||
});
|
||||
}
|
||||
|
||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) {
|
||||
Some(tf) => tf,
|
||||
None => err!("No YubiKey devices registered"),
|
||||
};
|
||||
|
||||
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
|
||||
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Nfc": yubikey_metadata.Nfc,
|
||||
})
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
/*
|
||||
ConnectData {
|
||||
grant_type: Password,
|
||||
is_device: false,
|
||||
data: {
|
||||
"scope": "api offline_access",
|
||||
"client_id": "web",
|
||||
"grant_type": "password",
|
||||
"username": "dani@mail",
|
||||
"password": "8IuV1sJ94tPjyYIK+E+PTjblzjm4W6C4N5wqM0KKsSg="
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(non_snake_case)]
|
||||
RETURNS "TwoFactorToken": "11122233333444555666777888999"
|
||||
|
||||
Next login
|
||||
ConnectData {
|
||||
grant_type: Password,
|
||||
is_device: false,
|
||||
data: {
|
||||
"scope": "api offline_access",
|
||||
"username": "dani@mail",
|
||||
"client_id": "web",
|
||||
"twofactorprovider": "5",
|
||||
"twofactortoken": "11122233333444555666777888999",
|
||||
"grant_type": "password",
|
||||
"twofactorremember": "0",
|
||||
"password": "8IuV1sJ94tPjyYIK+E+PTjblzjm4W6C4N5wqM0KKsSg="
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
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::parse_option_string(type_opt).unwrap_or(0);
|
||||
|
||||
Outcome::Success(DeviceType(type_num))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ConnectData {
|
||||
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>,
|
||||
grant_type: GrantType,
|
||||
is_device: bool,
|
||||
data: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum GrantType { RefreshToken, Password }
|
||||
|
||||
impl ConnectData {
|
||||
fn get(&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 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 '_'
|
||||
let mut data = HashMap::new();
|
||||
|
||||
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),
|
||||
}
|
||||
// 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(format!("Error decoding key or value")),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(form)
|
||||
// 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.get("client_id").unwrap().as_ref() {
|
||||
"browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?,
|
||||
_ => false
|
||||
};
|
||||
(GrantType::Password, is_device)
|
||||
}
|
||||
_ => return Err(format!("Grant type not supported"))
|
||||
};
|
||||
|
||||
Ok(ConnectData { grant_type, is_device, data })
|
||||
}
|
||||
}
|
||||
|
||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||
if value.is_none() {
|
||||
err!(msg)
|
||||
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));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
|
@@ -1,38 +1,31 @@
|
||||
mod admin;
|
||||
pub(crate) mod core;
|
||||
mod core;
|
||||
mod icons;
|
||||
mod identity;
|
||||
mod notifications;
|
||||
mod web;
|
||||
|
||||
pub use self::admin::routes as admin_routes;
|
||||
pub use self::core::routes as core_routes;
|
||||
pub use self::icons::routes as icons_routes;
|
||||
pub use self::identity::routes as identity_routes;
|
||||
pub use self::notifications::routes as notifications_routes;
|
||||
pub use self::notifications::{start_notification_server, Notify, UpdateType};
|
||||
pub use self::web::routes as web_routes;
|
||||
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use rocket::response::status::BadRequest;
|
||||
use rocket_contrib::Json;
|
||||
|
||||
// Type aliases for API methods results
|
||||
type ApiResult<T> = Result<T, crate::error::Error>;
|
||||
pub type JsonResult = ApiResult<Json<Value>>;
|
||||
pub type EmptyResult = ApiResult<()>;
|
||||
type JsonResult = Result<Json, BadRequest<Json>>;
|
||||
type EmptyResult = Result<(), BadRequest<Json>>;
|
||||
|
||||
use crate::util;
|
||||
use util;
|
||||
type JsonUpcase<T> = Json<util::UpCase<T>>;
|
||||
type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>;
|
||||
|
||||
// Common structs representing JSON data received
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct PasswordData {
|
||||
MasterPasswordHash: String,
|
||||
MasterPasswordHash: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
enum NumberOrString {
|
||||
Number(i32),
|
||||
@@ -40,20 +33,17 @@ enum NumberOrString {
|
||||
}
|
||||
|
||||
impl NumberOrString {
|
||||
fn into_string(self) -> String {
|
||||
fn to_string(self) -> String {
|
||||
match self {
|
||||
NumberOrString::Number(n) => n.to_string(),
|
||||
NumberOrString::String(s) => s,
|
||||
NumberOrString::String(s) => s
|
||||
}
|
||||
}
|
||||
|
||||
fn into_i32(self) -> ApiResult<i32> {
|
||||
use std::num::ParseIntError as PIE;
|
||||
fn to_i32(self) -> Option<i32> {
|
||||
match self {
|
||||
NumberOrString::Number(n) => Ok(n),
|
||||
NumberOrString::String(s) => s
|
||||
.parse()
|
||||
.map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())),
|
||||
NumberOrString::Number(n) => Some(n),
|
||||
NumberOrString::String(s) => s.parse().ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,364 +0,0 @@
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::api::JsonResult;
|
||||
use crate::auth::Headers;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![negotiate, websockets_err]
|
||||
}
|
||||
|
||||
#[get("/hub")]
|
||||
fn websockets_err() -> JsonResult {
|
||||
err!("'/notifications/hub' should be proxied to the websocket server or notifications won't work. Go to the README for more info.")
|
||||
}
|
||||
|
||||
#[post("/hub/negotiate")]
|
||||
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
||||
use crate::crypto;
|
||||
use data_encoding::BASE64URL;
|
||||
|
||||
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
|
||||
let mut available_transports: Vec<JsonValue> = Vec::new();
|
||||
|
||||
if CONFIG.websocket_enabled() {
|
||||
available_transports.push(json!({"transport":"WebSockets", "transferFormats":["Text","Binary"]}));
|
||||
}
|
||||
|
||||
// TODO: Implement transports
|
||||
// Rocket WS support: https://github.com/SergioBenitez/Rocket/issues/90
|
||||
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
||||
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
|
||||
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
||||
Ok(Json(json!({
|
||||
"connectionId": conn_id,
|
||||
"availableTransports": available_transports
|
||||
})))
|
||||
}
|
||||
|
||||
//
|
||||
// Websockets server
|
||||
//
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use ws::{self, util::Token, Factory, Handler, Handshake, Message, Sender, WebSocket};
|
||||
|
||||
use chashmap::CHashMap;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde_json::from_str;
|
||||
|
||||
use crate::db::models::{Cipher, Folder, User};
|
||||
|
||||
use rmpv::Value;
|
||||
|
||||
fn serialize(val: Value) -> Vec<u8> {
|
||||
use rmpv::encode::write_value;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
write_value(&mut buf, &val).expect("Error encoding MsgPack");
|
||||
|
||||
// Add size bytes at the start
|
||||
// Extracted from BinaryMessageFormat.js
|
||||
let mut size: usize = buf.len();
|
||||
let mut len_buf: Vec<u8> = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut size_part = size & 0x7f;
|
||||
size >>= 7;
|
||||
|
||||
if size > 0 {
|
||||
size_part |= 0x80;
|
||||
}
|
||||
|
||||
len_buf.push(size_part as u8);
|
||||
|
||||
if size == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
len_buf.append(&mut buf);
|
||||
len_buf
|
||||
}
|
||||
|
||||
fn serialize_date(date: NaiveDateTime) -> Value {
|
||||
let seconds: i64 = date.timestamp();
|
||||
let nanos: i64 = date.timestamp_subsec_nanos().into();
|
||||
let timestamp = nanos << 34 | seconds;
|
||||
|
||||
let bs = timestamp.to_be_bytes();
|
||||
|
||||
// -1 is Timestamp
|
||||
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
|
||||
Value::Ext(-1, bs.to_vec())
|
||||
}
|
||||
|
||||
fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
|
||||
match option {
|
||||
Some(a) => a.into(),
|
||||
None => Value::Nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Server WebSocket handler
|
||||
pub struct WSHandler {
|
||||
out: Sender,
|
||||
user_uuid: Option<String>,
|
||||
users: WebSocketUsers,
|
||||
}
|
||||
|
||||
const RECORD_SEPARATOR: u8 = 0x1e;
|
||||
const INITIAL_RESPONSE: [u8; 3] = [0x7b, 0x7d, RECORD_SEPARATOR]; // {, }, <RS>
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InitialMessage {
|
||||
protocol: String,
|
||||
version: i32,
|
||||
}
|
||||
|
||||
const PING_MS: u64 = 15_000;
|
||||
const PING: Token = Token(1);
|
||||
|
||||
impl Handler for WSHandler {
|
||||
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
||||
// TODO: Improve this split
|
||||
let path = hs.request.resource();
|
||||
let mut query_split: Vec<_> = path.split('?').nth(1).unwrap().split('&').collect();
|
||||
query_split.sort();
|
||||
let access_token = &query_split[0][13..];
|
||||
let _id = &query_split[1][3..];
|
||||
|
||||
// Validate the user
|
||||
use crate::auth;
|
||||
let claims = match auth::decode_login(access_token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => return Err(ws::Error::new(ws::ErrorKind::Internal, "Invalid access token provided")),
|
||||
};
|
||||
|
||||
// Assign the user to the handler
|
||||
let user_uuid = claims.sub;
|
||||
self.user_uuid = Some(user_uuid.clone());
|
||||
|
||||
// Add the current Sender to the user list
|
||||
let handler_insert = self.out.clone();
|
||||
let handler_update = self.out.clone();
|
||||
|
||||
self.users
|
||||
.map
|
||||
.upsert(user_uuid, || vec![handler_insert], |ref mut v| v.push(handler_update));
|
||||
|
||||
// Schedule a ping to keep the connection alive
|
||||
self.out.timeout(PING_MS, PING)
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
|
||||
info!("Server got message '{}'. ", msg);
|
||||
|
||||
if let Message::Text(text) = msg.clone() {
|
||||
let json = &text[..text.len() - 1]; // Remove last char
|
||||
|
||||
if let Ok(InitialMessage { protocol, version }) = from_str::<InitialMessage>(json) {
|
||||
if &protocol == "messagepack" && version == 1 {
|
||||
return self.out.send(&INITIAL_RESPONSE[..]); // Respond to initial message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not the initial message, just echo the message
|
||||
self.out.send(msg)
|
||||
}
|
||||
|
||||
fn on_timeout(&mut self, event: Token) -> ws::Result<()> {
|
||||
if event == PING {
|
||||
// send ping
|
||||
self.out.send(create_ping())?;
|
||||
|
||||
// reschedule the timeout
|
||||
self.out.timeout(PING_MS, PING)
|
||||
} else {
|
||||
Err(ws::Error::new(
|
||||
ws::ErrorKind::Internal,
|
||||
"Invalid timeout token provided",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WSFactory {
|
||||
pub users: WebSocketUsers,
|
||||
}
|
||||
|
||||
impl WSFactory {
|
||||
pub fn init() -> Self {
|
||||
WSFactory {
|
||||
users: WebSocketUsers {
|
||||
map: Arc::new(CHashMap::new()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Factory for WSFactory {
|
||||
type Handler = WSHandler;
|
||||
|
||||
fn connection_made(&mut self, out: Sender) -> Self::Handler {
|
||||
WSHandler {
|
||||
out,
|
||||
user_uuid: None,
|
||||
users: self.users.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn connection_lost(&mut self, handler: Self::Handler) {
|
||||
// Remove handler
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebSocketUsers {
|
||||
map: Arc<CHashMap<String, Vec<Sender>>>,
|
||||
}
|
||||
|
||||
impl WebSocketUsers {
|
||||
fn send_update(&self, user_uuid: &str, data: &[u8]) -> ws::Result<()> {
|
||||
if let Some(user) = self.map.get(user_uuid) {
|
||||
for sender in user.iter() {
|
||||
sender.send(data)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NOTE: The last modified date needs to be updated before calling these methods
|
||||
pub fn send_user_update(&self, ut: UpdateType, user: &User) {
|
||||
let data = create_update(
|
||||
vec![
|
||||
("UserId".into(), user.uuid.clone().into()),
|
||||
("Date".into(), serialize_date(user.updated_at)),
|
||||
],
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid, &data).ok();
|
||||
}
|
||||
|
||||
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), folder.uuid.clone().into()),
|
||||
("UserId".into(), folder.user_uuid.clone().into()),
|
||||
("RevisionDate".into(), serialize_date(folder.updated_at)),
|
||||
],
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&folder.user_uuid, &data).ok();
|
||||
}
|
||||
|
||||
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &[String]) {
|
||||
let user_uuid = convert_option(cipher.user_uuid.clone());
|
||||
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
||||
|
||||
let data = create_update(
|
||||
vec![
|
||||
("Id".into(), cipher.uuid.clone().into()),
|
||||
("UserId".into(), user_uuid),
|
||||
("OrganizationId".into(), org_uuid),
|
||||
("CollectionIds".into(), Value::Nil),
|
||||
("RevisionDate".into(), serialize_date(cipher.updated_at)),
|
||||
],
|
||||
ut,
|
||||
);
|
||||
|
||||
for uuid in user_uuids {
|
||||
self.send_update(&uuid, &data).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Message Structure
|
||||
[
|
||||
1, // MessageType.Invocation
|
||||
{}, // Headers
|
||||
null, // InvocationId
|
||||
"ReceiveMessage", // Target
|
||||
[ // Arguments
|
||||
{
|
||||
"ContextId": "app_id",
|
||||
"Type": ut as i32,
|
||||
"Payload": {}
|
||||
}
|
||||
]
|
||||
]
|
||||
*/
|
||||
fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType) -> Vec<u8> {
|
||||
use rmpv::Value as V;
|
||||
|
||||
let value = V::Array(vec![
|
||||
1.into(),
|
||||
V::Array(vec![]),
|
||||
V::Nil,
|
||||
"ReceiveMessage".into(),
|
||||
V::Array(vec![V::Map(vec![
|
||||
("ContextId".into(), "app_id".into()),
|
||||
("Type".into(), (ut as i32).into()),
|
||||
("Payload".into(), payload.into()),
|
||||
])]),
|
||||
]);
|
||||
|
||||
serialize(value)
|
||||
}
|
||||
|
||||
fn create_ping() -> Vec<u8> {
|
||||
serialize(Value::Array(vec![6.into()]))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum UpdateType {
|
||||
CipherUpdate = 0,
|
||||
CipherCreate = 1,
|
||||
LoginDelete = 2,
|
||||
FolderDelete = 3,
|
||||
Ciphers = 4,
|
||||
|
||||
Vault = 5,
|
||||
OrgKeys = 6,
|
||||
FolderCreate = 7,
|
||||
FolderUpdate = 8,
|
||||
CipherDelete = 9,
|
||||
SyncSettings = 10,
|
||||
|
||||
LogOut = 11,
|
||||
|
||||
None = 100,
|
||||
}
|
||||
|
||||
use rocket::State;
|
||||
pub type Notify<'a> = State<'a, WebSocketUsers>;
|
||||
|
||||
pub fn start_notification_server() -> WebSocketUsers {
|
||||
let factory = WSFactory::init();
|
||||
let users = factory.users.clone();
|
||||
|
||||
if CONFIG.websocket_enabled() {
|
||||
thread::spawn(move || {
|
||||
WebSocket::new(factory)
|
||||
.unwrap()
|
||||
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
users
|
||||
}
|
@@ -1,63 +1,45 @@
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rocket::http::ContentType;
|
||||
use rocket::response::content::Content;
|
||||
use rocket::response::NamedFile;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use rocket::response::NamedFile;
|
||||
use rocket_contrib::Json;
|
||||
|
||||
use crate::util::Cached;
|
||||
use crate::CONFIG;
|
||||
use CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
if CONFIG.web_vault_enabled() {
|
||||
routes![web_index, app_id, web_files, attachments, alive]
|
||||
} else {
|
||||
routes![attachments, alive]
|
||||
}
|
||||
routes![index, files, attachments, alive]
|
||||
}
|
||||
|
||||
// TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache
|
||||
#[get("/")]
|
||||
fn web_index() -> Cached<io::Result<NamedFile>> {
|
||||
Cached::short(NamedFile::open(
|
||||
Path::new(&CONFIG.web_vault_folder()).join("index.html"),
|
||||
))
|
||||
fn index() -> io::Result<NamedFile> {
|
||||
NamedFile::open(
|
||||
Path::new(&CONFIG.web_vault_folder)
|
||||
.join("index.html"))
|
||||
}
|
||||
|
||||
#[get("/app-id.json")]
|
||||
fn app_id() -> Cached<Content<Json<Value>>> {
|
||||
let content_type = ContentType::new("application", "fido.trusted-apps+json");
|
||||
|
||||
Cached::long(Content(
|
||||
content_type,
|
||||
Json(json!({
|
||||
"trustedFacets": [
|
||||
{
|
||||
"version": { "major": 1, "minor": 0 },
|
||||
"ids": [
|
||||
&CONFIG.domain(),
|
||||
"ios:bundle-id:com.8bit.bitwarden",
|
||||
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
|
||||
}]
|
||||
})),
|
||||
))
|
||||
#[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
|
||||
fn files(p: PathBuf) -> io::Result<NamedFile> {
|
||||
NamedFile::open(
|
||||
Path::new(&CONFIG.web_vault_folder)
|
||||
.join(p))
|
||||
}
|
||||
|
||||
#[get("/<p..>", rank = 10)] // Only match this if the other routes don't match
|
||||
fn web_files(p: PathBuf) -> Cached<io::Result<NamedFile>> {
|
||||
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)))
|
||||
}
|
||||
|
||||
#[get("/attachments/<uuid>/<file..>")]
|
||||
fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
|
||||
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file))
|
||||
NamedFile::open(
|
||||
Path::new(&CONFIG.attachments_folder)
|
||||
.join(uuid)
|
||||
.join(file)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#[get("/alive")]
|
||||
fn alive() -> Json<String> {
|
||||
use crate::util::format_date;
|
||||
use util::format_date;
|
||||
use chrono::Utc;
|
||||
|
||||
Json(format_date(&Utc::now().naive_utc()))
|
||||
|
284
src/auth.rs
284
src/auth.rs
@@ -1,74 +1,63 @@
|
||||
//
|
||||
// JWT Handling
|
||||
//
|
||||
use crate::util::read_file;
|
||||
use chrono::{Duration, Utc};
|
||||
///
|
||||
/// JWT Handling
|
||||
///
|
||||
|
||||
use jsonwebtoken::{self, Algorithm, Header};
|
||||
use serde::de::DeserializeOwned;
|
||||
use util::read_file;
|
||||
use chrono::Duration;
|
||||
|
||||
use jwt;
|
||||
use serde::ser::Serialize;
|
||||
|
||||
use crate::error::{Error, MapResult};
|
||||
use crate::CONFIG;
|
||||
use CONFIG;
|
||||
|
||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
|
||||
pub const JWT_ISSUER: &'static str = "localhost:8000/identity";
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
|
||||
static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
|
||||
pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain());
|
||||
pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain());
|
||||
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
|
||||
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
|
||||
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM);
|
||||
|
||||
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
|
||||
Ok(key) => key,
|
||||
Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),
|
||||
Err(e) => panic!("Error loading private RSA Key from {}\n Error: {}", CONFIG.private_rsa_key, e)
|
||||
};
|
||||
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key()) {
|
||||
|
||||
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key) {
|
||||
Ok(key) => key,
|
||||
Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e),
|
||||
Err(e) => panic!("Error loading public RSA Key from {}\n Error: {}", CONFIG.public_rsa_key, e)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
|
||||
match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
|
||||
Ok(token) => token,
|
||||
Err(e) => panic!("Error encoding jwt {}", e),
|
||||
}
|
||||
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
|
||||
Ok(token) => return token,
|
||||
Err(e) => panic!("Error encoding jwt {}", e)
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Error> {
|
||||
let validation = jsonwebtoken::Validation {
|
||||
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
|
||||
let validation = jwt::Validation {
|
||||
leeway: 30, // 30 seconds
|
||||
validate_exp: true,
|
||||
validate_iat: false, // IssuedAt is the same as NotBefore
|
||||
validate_iat: true,
|
||||
validate_nbf: true,
|
||||
aud: None,
|
||||
iss: Some(issuer),
|
||||
iss: Some(JWT_ISSUER.into()),
|
||||
sub: None,
|
||||
algorithms: vec![JWT_ALGORITHM],
|
||||
};
|
||||
|
||||
let token = token.replace(char::is_whitespace, "");
|
||||
|
||||
jsonwebtoken::decode(&token, &PUBLIC_RSA_KEY, &validation)
|
||||
.map(|d| d.claims)
|
||||
.map_res("Error decoding JWT")
|
||||
}
|
||||
|
||||
pub fn decode_login(token: &str) -> Result<LoginJWTClaims, Error> {
|
||||
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
||||
}
|
||||
|
||||
pub fn decode_invite(token: &str) -> Result<InviteJWTClaims, Error> {
|
||||
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
||||
}
|
||||
|
||||
pub fn decode_admin(token: &str) -> Result<AdminJWTClaims, Error> {
|
||||
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
||||
match jwt::decode(token, &PUBLIC_RSA_KEY, &validation) {
|
||||
Ok(decoded) => Ok(decoded.claims),
|
||||
Err(msg) => {
|
||||
println!("Error validating jwt - {:#?}", msg);
|
||||
Err(msg.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginJWTClaims {
|
||||
pub struct JWTClaims {
|
||||
// Not before
|
||||
pub nbf: i64,
|
||||
// Expiration time
|
||||
@@ -86,7 +75,6 @@ pub struct LoginJWTClaims {
|
||||
pub orgowner: Vec<String>,
|
||||
pub orgadmin: Vec<String>,
|
||||
pub orguser: Vec<String>,
|
||||
pub orgmanager: Vec<String>,
|
||||
|
||||
// user security_stamp
|
||||
pub sstamp: String,
|
||||
@@ -98,73 +86,15 @@ pub struct LoginJWTClaims {
|
||||
pub amr: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InviteJWTClaims {
|
||||
// Not before
|
||||
pub nbf: i64,
|
||||
// Expiration time
|
||||
pub exp: i64,
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
///
|
||||
/// Bearer token authentication
|
||||
///
|
||||
|
||||
pub email: String,
|
||||
pub org_id: Option<String>,
|
||||
pub user_org_id: Option<String>,
|
||||
pub invited_by_email: Option<String>,
|
||||
}
|
||||
|
||||
pub fn generate_invite_claims(
|
||||
uuid: String,
|
||||
email: String,
|
||||
org_id: Option<String>,
|
||||
org_user_id: Option<String>,
|
||||
invited_by_email: Option<String>,
|
||||
) -> InviteJWTClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
InviteJWTClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::days(5)).timestamp(),
|
||||
iss: JWT_INVITE_ISSUER.to_string(),
|
||||
sub: uuid.clone(),
|
||||
email: email.clone(),
|
||||
org_id: org_id.clone(),
|
||||
user_org_id: org_user_id.clone(),
|
||||
invited_by_email: invited_by_email.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AdminJWTClaims {
|
||||
// Not before
|
||||
pub nbf: i64,
|
||||
// Expiration time
|
||||
pub exp: i64,
|
||||
// Issuer
|
||||
pub iss: String,
|
||||
// Subject
|
||||
pub sub: String,
|
||||
}
|
||||
|
||||
pub fn generate_admin_claims() -> AdminJWTClaims {
|
||||
let time_now = Utc::now().naive_utc();
|
||||
AdminJWTClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + Duration::minutes(20)).timestamp(),
|
||||
iss: JWT_ADMIN_ISSUER.to_string(),
|
||||
sub: "admin_panel".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Bearer token authentication
|
||||
//
|
||||
use rocket::request::{self, FromRequest, Request};
|
||||
use rocket::Outcome;
|
||||
use rocket::request::{self, Request, FromRequest};
|
||||
|
||||
use crate::db::models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
use crate::db::DbConn;
|
||||
use db::DbConn;
|
||||
use db::models::{User, UserOrganization, UserOrgType, Device};
|
||||
|
||||
pub struct Headers {
|
||||
pub host: String,
|
||||
@@ -179,46 +109,26 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||
let headers = request.headers();
|
||||
|
||||
// Get host
|
||||
let host = if CONFIG.domain_set() {
|
||||
CONFIG.domain()
|
||||
} else if let Some(referer) = headers.get_one("Referer") {
|
||||
referer.to_string()
|
||||
} else {
|
||||
// Try to guess from the headers
|
||||
use std::env;
|
||||
|
||||
let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") {
|
||||
proto
|
||||
} else if env::var("ROCKET_TLS").is_ok() {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
};
|
||||
|
||||
let host = if let Some(host) = headers.get_one("X-Forwarded-Host") {
|
||||
host
|
||||
} else if let Some(host) = headers.get_one("Host") {
|
||||
host
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
format!("{}://{}", protocol, host)
|
||||
let host = match headers.get_one("Host") {
|
||||
Some(host) => format!("http://{}", host), // TODO: Check if HTTPS
|
||||
_ => String::new()
|
||||
};
|
||||
|
||||
// Get access_token
|
||||
let access_token: &str = match headers.get_one("Authorization") {
|
||||
Some(a) => match a.rsplit("Bearer ").next() {
|
||||
Some(split) => split,
|
||||
None => err_handler!("No access token provided"),
|
||||
},
|
||||
None => err_handler!("No access token provided"),
|
||||
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")
|
||||
};
|
||||
|
||||
// Check JWT token is valid and get device and user from it
|
||||
let claims = match decode_login(access_token) {
|
||||
let claims: JWTClaims = match decode_jwt(access_token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err_handler!("Invalid claim"),
|
||||
Err(_) => err_handler!("Invalid claim")
|
||||
};
|
||||
|
||||
let device_uuid = claims.device;
|
||||
@@ -226,17 +136,17 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||
|
||||
let conn = match request.guard::<DbConn>() {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB"),
|
||||
_ => err_handler!("Error getting DB")
|
||||
};
|
||||
|
||||
let device = match Device::find_by_uuid(&device_uuid, &conn) {
|
||||
Some(device) => device,
|
||||
None => err_handler!("Invalid device id"),
|
||||
None => err_handler!("Invalid device id")
|
||||
};
|
||||
|
||||
let user = match User::find_by_uuid(&user_uuid, &conn) {
|
||||
Some(user) => user,
|
||||
None => err_handler!("Device has no user associated"),
|
||||
None => err_handler!("Device has no user associated")
|
||||
};
|
||||
|
||||
if user.security_stamp != claims.sstamp {
|
||||
@@ -251,7 +161,7 @@ pub struct OrgHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
pub org_user_type: i32,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
||||
@@ -259,44 +169,30 @@ 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(_) => Outcome::Forward(()),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Failure(f) => Outcome::Failure(f),
|
||||
Outcome::Success(headers) => {
|
||||
// org_id is expected to be the second param ("/organizations/<org_id>")
|
||||
match request.get_param::<String>(1) {
|
||||
Some(Ok(org_id)) => {
|
||||
// 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) => {
|
||||
let conn = match request.guard::<DbConn>() {
|
||||
Outcome::Success(conn) => conn,
|
||||
_ => err_handler!("Error getting DB"),
|
||||
_ => err_handler!("Error getting DB")
|
||||
};
|
||||
|
||||
let user = headers.user;
|
||||
let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn) {
|
||||
Some(user) => {
|
||||
if user.status == UserOrgStatus::Confirmed as i32 {
|
||||
user
|
||||
} else {
|
||||
err_handler!("The current user isn't confirmed member of the organization")
|
||||
}
|
||||
}
|
||||
None => err_handler!("The current user isn't member of the organization"),
|
||||
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||
Some(user) => user,
|
||||
None => err_handler!("The current user isn't member of the organization")
|
||||
};
|
||||
|
||||
Outcome::Success(Self {
|
||||
Outcome::Success(Self{
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user,
|
||||
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")
|
||||
}
|
||||
},
|
||||
user: headers.user,
|
||||
org_user_type: org_user.type_,
|
||||
})
|
||||
}
|
||||
_ => err_handler!("Error getting the organization id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,7 +203,7 @@ pub struct AdminHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_user_type: UserOrgType,
|
||||
pub org_user_type: i32,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
||||
@@ -315,18 +211,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(_) => Outcome::Forward(()),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Failure(f) => Outcome::Failure(f),
|
||||
Outcome::Success(headers) => {
|
||||
if headers.org_user_type >= UserOrgType::Admin {
|
||||
Outcome::Success(Self {
|
||||
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{
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,41 +240,19 @@ 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(_) => Outcome::Forward(()),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Failure(f) => Outcome::Failure(f),
|
||||
Outcome::Success(headers) => {
|
||||
if headers.org_user_type == UserOrgType::Owner {
|
||||
Outcome::Success(Self {
|
||||
if headers.org_user_type > UserOrgType::Owner as i32 {
|
||||
err_handler!("You need to be Owner to call this endpoint")
|
||||
} else {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
596
src/config.rs
596
src/config.rs
@@ -1,596 +0,0 @@
|
||||
use std::process::exit;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::util::get_env;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config::load().unwrap_or_else(|e| {
|
||||
println!("Error loading config:\n\t{:?}\n", e);
|
||||
exit(12)
|
||||
});
|
||||
pub static ref CONFIG_FILE: String = {
|
||||
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
|
||||
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{}/config.json", data_folder))
|
||||
};
|
||||
}
|
||||
|
||||
pub type Pass = String;
|
||||
|
||||
macro_rules! make_config {
|
||||
($(
|
||||
$(#[doc = $groupdoc:literal])?
|
||||
$group:ident $(: $group_enabled:ident)? {
|
||||
$(
|
||||
$(#[doc = $doc:literal])+
|
||||
$name:ident : $ty:ty, $editable:literal, $none_action:ident $(, $default:expr)?;
|
||||
)+},
|
||||
)+) => {
|
||||
pub struct Config { inner: RwLock<Inner> }
|
||||
|
||||
struct Inner {
|
||||
templates: Handlebars,
|
||||
config: ConfigItems,
|
||||
|
||||
_env: ConfigBuilder,
|
||||
_usr: ConfigBuilder,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct ConfigBuilder {
|
||||
$($(
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
$name: Option<$ty>,
|
||||
)+)+
|
||||
}
|
||||
|
||||
impl ConfigBuilder {
|
||||
fn from_env() -> Self {
|
||||
dotenv::from_path(".env").ok();
|
||||
|
||||
let mut builder = ConfigBuilder::default();
|
||||
$($(
|
||||
builder.$name = get_env(&stringify!($name).to_uppercase());
|
||||
)+)+
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
fn from_file(path: &str) -> Result<Self, Error> {
|
||||
use crate::util::read_file_string;
|
||||
let config_str = read_file_string(path)?;
|
||||
serde_json::from_str(&config_str).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Merges the values of both builders into a new builder.
|
||||
/// If both have the same element, `other` wins.
|
||||
fn merge(&self, other: &Self, show_overrides: bool) -> Self {
|
||||
let mut overrides = Vec::new();
|
||||
let mut builder = self.clone();
|
||||
$($(
|
||||
if let v @Some(_) = &other.$name {
|
||||
builder.$name = v.clone();
|
||||
|
||||
if self.$name.is_some() {
|
||||
overrides.push(stringify!($name).to_uppercase());
|
||||
}
|
||||
}
|
||||
)+)+
|
||||
|
||||
if show_overrides && !overrides.is_empty() {
|
||||
// We can't use warn! here because logging isn't setup yet.
|
||||
println!("[WARNING] The following environment variables are being overriden by the config file,");
|
||||
println!("[WARNING] please use the admin panel to make changes to them:");
|
||||
println!("[WARNING] {}\n", overrides.join(", "));
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Returns a new builder with all the elements from self,
|
||||
/// except those that are equal in both sides
|
||||
fn _remove(&self, other: &Self) -> Self {
|
||||
let mut builder = ConfigBuilder::default();
|
||||
$($(
|
||||
if &self.$name != &other.$name {
|
||||
builder.$name = self.$name.clone();
|
||||
}
|
||||
|
||||
)+)+
|
||||
builder
|
||||
}
|
||||
|
||||
fn build(&self) -> ConfigItems {
|
||||
let mut config = ConfigItems::default();
|
||||
let _domain_set = self.domain.is_some();
|
||||
$($(
|
||||
config.$name = make_config!{ @build self.$name.clone(), &config, $none_action, $($default)? };
|
||||
)+)+
|
||||
config.domain_set = _domain_set;
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ConfigItems { $($(pub $name: make_config!{@type $ty, $none_action}, )+)+ }
|
||||
|
||||
#[allow(unused)]
|
||||
impl Config {
|
||||
$($(
|
||||
pub fn $name(&self) -> make_config!{@type $ty, $none_action} {
|
||||
self.inner.read().unwrap().config.$name.clone()
|
||||
}
|
||||
)+)+
|
||||
|
||||
pub fn prepare_json(&self) -> serde_json::Value {
|
||||
let (def, cfg) = {
|
||||
let inner = &self.inner.read().unwrap();
|
||||
(inner._env.build(), inner.config.clone())
|
||||
};
|
||||
|
||||
|
||||
fn _get_form_type(rust_type: &str) -> &'static str {
|
||||
match rust_type {
|
||||
"Pass" => "password",
|
||||
"String" => "text",
|
||||
"bool" => "checkbox",
|
||||
_ => "number"
|
||||
}
|
||||
}
|
||||
|
||||
fn _get_doc(doc: &str) -> serde_json::Value {
|
||||
let mut split = doc.split("|>").map(str::trim);
|
||||
json!({
|
||||
"name": split.next(),
|
||||
"description": split.next()
|
||||
})
|
||||
}
|
||||
|
||||
json!([ $({
|
||||
"group": stringify!($group),
|
||||
"grouptoggle": stringify!($($group_enabled)?),
|
||||
"groupdoc": make_config!{ @show $($groupdoc)? },
|
||||
"elements": [
|
||||
$( {
|
||||
"editable": $editable,
|
||||
"name": stringify!($name),
|
||||
"value": cfg.$name,
|
||||
"default": def.$name,
|
||||
"type": _get_form_type(stringify!($ty)),
|
||||
"doc": _get_doc(concat!($($doc),+)),
|
||||
}, )+
|
||||
]}, )+ ])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Group or empty string
|
||||
( @show ) => { "" };
|
||||
( @show $lit:literal ) => { $lit };
|
||||
|
||||
// Wrap the optionals in an Option type
|
||||
( @type $ty:ty, option) => { Option<$ty> };
|
||||
( @type $ty:ty, $id:ident) => { $ty };
|
||||
|
||||
// Generate the values depending on none_action
|
||||
( @build $value:expr, $config:expr, option, ) => { $value };
|
||||
( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };
|
||||
( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{
|
||||
match $value {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
let f: &Fn(&ConfigItems) -> _ = &$default_fn;
|
||||
f($config)
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
//STRUCTURE:
|
||||
// /// Short description (without this they won't appear on the list)
|
||||
// group {
|
||||
// /// Friendly Name |> Description (Optional)
|
||||
// name: type, is_editable, none_action, <default_value (Optional)>
|
||||
// }
|
||||
//
|
||||
// Where none_action applied when the value wasn't provided and can be:
|
||||
// def: Use a default value
|
||||
// auto: Value is auto generated based on other values
|
||||
// option: Value is optional
|
||||
make_config! {
|
||||
folders {
|
||||
/// Data folder |> Main data folder
|
||||
data_folder: String, false, def, "data".to_string();
|
||||
|
||||
/// Database URL
|
||||
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
||||
/// Icon chache folder
|
||||
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
||||
/// Attachments folder
|
||||
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
|
||||
/// Templates folder
|
||||
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
||||
/// Session JWT key
|
||||
rsa_key_filename: String, false, auto, |c| format!("{}/{}", c.data_folder, "rsa_key");
|
||||
/// Web vault folder
|
||||
web_vault_folder: String, false, def, "web-vault/".to_string();
|
||||
},
|
||||
ws {
|
||||
/// Enable websocket notifications
|
||||
websocket_enabled: bool, false, def, false;
|
||||
/// Websocket address
|
||||
websocket_address: String, false, def, "0.0.0.0".to_string();
|
||||
/// Websocket port
|
||||
websocket_port: u16, false, def, 3012;
|
||||
},
|
||||
|
||||
/// General settings
|
||||
settings {
|
||||
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
|
||||
/// and port, if it's different than the default. Some server functions don't work correctly without this value
|
||||
domain: String, true, def, "http://localhost".to_string();
|
||||
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
|
||||
domain_set: bool, false, def, false;
|
||||
/// Enable web vault
|
||||
web_vault_enabled: bool, false, def, true;
|
||||
|
||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
||||
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||
/// otherwise it will delete them and they won't be downloaded again.
|
||||
disable_icon_download: bool, true, def, false;
|
||||
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
|
||||
signups_allowed: bool, true, def, true;
|
||||
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
||||
invitations_allowed: bool, true, def, true;
|
||||
/// Password iterations |> Number of server-side passwords hashing iterations.
|
||||
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
||||
password_iterations: i32, true, def, 100_000;
|
||||
/// Show password hints |> Controls if the password hint should be shown directly in the web page.
|
||||
/// Otherwise, if email is disabled, there is no way to see the password hint
|
||||
show_password_hint: bool, true, def, true;
|
||||
|
||||
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
|
||||
admin_token: Pass, true, option;
|
||||
},
|
||||
|
||||
/// Advanced settings
|
||||
advanced {
|
||||
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
|
||||
icon_cache_ttl: u64, true, def, 2_592_000;
|
||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
||||
icon_cache_negttl: u64, true, def, 259_200;
|
||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||
icon_download_timeout: u64, true, def, 10;
|
||||
/// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
|
||||
/// Useful to hide other servers in the local network. Check the WIKI for more details
|
||||
icon_blacklist_regex: String, true, option;
|
||||
|
||||
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
||||
/// Note that the checkbox would still be present, but ignored.
|
||||
disable_2fa_remember: bool, true, def, false;
|
||||
|
||||
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
|
||||
/// ONLY use this during development, as it can slow down the server
|
||||
reload_templates: bool, true, def, false;
|
||||
|
||||
/// Log routes at launch (Dev)
|
||||
log_mounts: bool, true, def, false;
|
||||
/// Enable extended logging
|
||||
extended_logging: bool, false, def, true;
|
||||
/// Enable the log to output to Syslog
|
||||
use_syslog: bool, false, def, false;
|
||||
/// Log file path
|
||||
log_file: String, false, option;
|
||||
/// Log level
|
||||
log_level: String, false, def, "Info".to_string();
|
||||
|
||||
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
|
||||
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||
enable_db_wal: bool, false, def, true;
|
||||
|
||||
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||
disable_admin_token: bool, true, def, false;
|
||||
},
|
||||
|
||||
/// Yubikey settings
|
||||
yubico: _enable_yubico {
|
||||
/// Enabled
|
||||
_enable_yubico: bool, true, def, true;
|
||||
/// Client ID
|
||||
yubico_client_id: String, true, option;
|
||||
/// Secret Key
|
||||
yubico_secret_key: Pass, true, option;
|
||||
/// Server
|
||||
yubico_server: String, true, option;
|
||||
},
|
||||
|
||||
/// Global Duo settings (Note that users can override them)
|
||||
duo: _enable_duo {
|
||||
/// Enabled
|
||||
_enable_duo: bool, true, def, false;
|
||||
/// Integration Key
|
||||
duo_ikey: String, true, option;
|
||||
/// Secret Key
|
||||
duo_skey: Pass, true, option;
|
||||
/// Host
|
||||
duo_host: String, true, option;
|
||||
/// Application Key (generated automatically)
|
||||
_duo_akey: Pass, false, option;
|
||||
},
|
||||
|
||||
/// SMTP Email Settings
|
||||
smtp: _enable_smtp {
|
||||
/// Enabled
|
||||
_enable_smtp: bool, true, def, true;
|
||||
/// Host
|
||||
smtp_host: String, true, option;
|
||||
/// Enable SSL
|
||||
smtp_ssl: bool, true, def, true;
|
||||
/// Use explicit TLS |> Enabling this would force the use of an explicit TLS connection, instead of upgrading an insecure one with STARTTLS
|
||||
smtp_explicit_tls: bool, true, def, false;
|
||||
/// Port
|
||||
smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
|
||||
/// From Address
|
||||
smtp_from: String, true, def, String::new();
|
||||
/// From Name
|
||||
smtp_from_name: String, true, def, "Bitwarden_RS".to_string();
|
||||
/// Username
|
||||
smtp_username: String, true, option;
|
||||
/// Password
|
||||
smtp_password: Pass, true, option;
|
||||
},
|
||||
}
|
||||
|
||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
if let Some(ref token) = cfg.admin_token {
|
||||
if token.trim().is_empty() {
|
||||
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
|
||||
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
|
||||
{
|
||||
err!("All Duo options need to be set for global Duo support")
|
||||
}
|
||||
|
||||
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
||||
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
|
||||
}
|
||||
|
||||
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
|
||||
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support")
|
||||
}
|
||||
|
||||
if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() {
|
||||
err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self, Error> {
|
||||
// Loading from env and file
|
||||
let _env = ConfigBuilder::from_env();
|
||||
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
|
||||
|
||||
// Create merged config, config file overwrites env
|
||||
let builder = _env.merge(&_usr, true);
|
||||
|
||||
// Fill any missing with defaults
|
||||
let config = builder.build();
|
||||
validate_config(&config)?;
|
||||
|
||||
Ok(Config {
|
||||
inner: RwLock::new(Inner {
|
||||
templates: load_templates(&config.templates_folder),
|
||||
config,
|
||||
_env,
|
||||
_usr,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_config(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||
// Remove default values
|
||||
//let builder = other.remove(&self.inner.read().unwrap()._env);
|
||||
|
||||
// TODO: Remove values that are defaults, above only checks those set by env and not the defaults
|
||||
let builder = other;
|
||||
|
||||
// Serialize now before we consume the builder
|
||||
let config_str = serde_json::to_string_pretty(&builder)?;
|
||||
|
||||
// Prepare the combined config
|
||||
let config = {
|
||||
let env = &self.inner.read().unwrap()._env;
|
||||
env.merge(&builder, false).build()
|
||||
};
|
||||
validate_config(&config)?;
|
||||
|
||||
// Save both the user and the combined config
|
||||
{
|
||||
let mut writer = self.inner.write().unwrap();
|
||||
writer.config = config;
|
||||
writer._usr = builder;
|
||||
}
|
||||
|
||||
//Save to file
|
||||
use std::{fs::File, io::Write};
|
||||
let mut file = File::create(&*CONFIG_FILE)?;
|
||||
file.write_all(config_str.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||
let builder = {
|
||||
let usr = &self.inner.read().unwrap()._usr;
|
||||
usr.merge(&other, false)
|
||||
};
|
||||
self.update_config(builder)
|
||||
}
|
||||
|
||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||
crate::util::delete_file(&CONFIG_FILE)?;
|
||||
|
||||
// Empty user config
|
||||
let usr = ConfigBuilder::default();
|
||||
|
||||
// Config now is env + defaults
|
||||
let config = {
|
||||
let env = &self.inner.read().unwrap()._env;
|
||||
env.build()
|
||||
};
|
||||
|
||||
// Save configs
|
||||
{
|
||||
let mut writer = self.inner.write().unwrap();
|
||||
writer.config = config;
|
||||
writer._usr = usr;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn private_rsa_key(&self) -> String {
|
||||
format!("{}.der", CONFIG.rsa_key_filename())
|
||||
}
|
||||
pub fn private_rsa_key_pem(&self) -> String {
|
||||
format!("{}.pem", CONFIG.rsa_key_filename())
|
||||
}
|
||||
pub fn public_rsa_key(&self) -> String {
|
||||
format!("{}.pub.der", CONFIG.rsa_key_filename())
|
||||
}
|
||||
pub fn mail_enabled(&self) -> bool {
|
||||
let inner = &self.inner.read().unwrap().config;
|
||||
inner._enable_smtp && inner.smtp_host.is_some()
|
||||
}
|
||||
|
||||
pub fn get_duo_akey(&self) -> String {
|
||||
if let Some(akey) = self._duo_akey() {
|
||||
akey
|
||||
} else {
|
||||
let akey = crate::crypto::get_random_64();
|
||||
let akey_s = data_encoding::BASE64.encode(&akey);
|
||||
|
||||
// Save the new value
|
||||
let mut builder = ConfigBuilder::default();
|
||||
builder._duo_akey = Some(akey_s.clone());
|
||||
self.update_config_partial(builder).ok();
|
||||
|
||||
akey_s
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_template<T: serde::ser::Serialize>(
|
||||
&self,
|
||||
name: &str,
|
||||
data: &T,
|
||||
) -> Result<String, crate::error::Error> {
|
||||
if CONFIG.reload_templates() {
|
||||
warn!("RELOADING TEMPLATES");
|
||||
let hb = load_templates(CONFIG.templates_folder().as_ref());
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
} else {
|
||||
let hb = &CONFIG.inner.read().unwrap().templates;
|
||||
hb.render(name, data).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError, Renderable,
|
||||
};
|
||||
|
||||
fn load_templates(path: &str) -> Handlebars {
|
||||
let mut hb = Handlebars::new();
|
||||
// Error on missing params
|
||||
hb.set_strict_mode(true);
|
||||
// Register helpers
|
||||
hb.register_helper("case", Box::new(CaseHelper));
|
||||
hb.register_helper("jsesc", Box::new(JsEscapeHelper));
|
||||
|
||||
macro_rules! reg {
|
||||
($name:expr) => {{
|
||||
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
|
||||
hb.register_template_string($name, template).unwrap();
|
||||
}};
|
||||
($name:expr, $ext:expr) => {{
|
||||
reg!($name);
|
||||
reg!(concat!($name, $ext));
|
||||
}};
|
||||
}
|
||||
|
||||
// First register default templates here
|
||||
reg!("email/invite_accepted", ".html");
|
||||
reg!("email/invite_confirmed", ".html");
|
||||
reg!("email/pw_hint_none", ".html");
|
||||
reg!("email/pw_hint_some", ".html");
|
||||
reg!("email/send_org_invite", ".html");
|
||||
|
||||
reg!("admin/base");
|
||||
reg!("admin/login");
|
||||
reg!("admin/page");
|
||||
|
||||
// And then load user templates to overwrite the defaults
|
||||
// Use .hbs extension for the files
|
||||
// Templates get registered with their relative name
|
||||
hb.register_templates_directory(".hbs", path).unwrap();
|
||||
|
||||
hb
|
||||
}
|
||||
|
||||
pub struct CaseHelper;
|
||||
|
||||
impl HelperDef for CaseHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'reg, 'rc>,
|
||||
r: &'reg Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'reg>,
|
||||
out: &mut Output,
|
||||
) -> HelperResult {
|
||||
let param = h
|
||||
.param(0)
|
||||
.ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?;
|
||||
let value = param.value().clone();
|
||||
|
||||
if h.params().iter().skip(1).any(|x| x.value() == &value) {
|
||||
h.template().map(|t| t.render(r, ctx, rc, out)).unwrap_or(Ok(()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JsEscapeHelper;
|
||||
|
||||
impl HelperDef for JsEscapeHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'reg, 'rc>,
|
||||
_: &'reg Handlebars,
|
||||
_: &Context,
|
||||
_: &mut RenderContext<'reg>,
|
||||
out: &mut Output,
|
||||
) -> HelperResult {
|
||||
let param = h
|
||||
.param(0)
|
||||
.ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
|
||||
|
||||
let value = param
|
||||
.value()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
|
||||
|
||||
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
||||
let quoted_value = format!(""{}"", escaped_value);
|
||||
|
||||
out.write("ed_value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// PBKDF2 derivation
|
||||
//
|
||||
///
|
||||
/// PBKDF2 derivation
|
||||
///
|
||||
|
||||
use ring::{digest, hmac, pbkdf2};
|
||||
use ring::{digest, pbkdf2};
|
||||
|
||||
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
|
||||
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||
@@ -19,21 +19,9 @@ pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterati
|
||||
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
|
||||
}
|
||||
|
||||
//
|
||||
// HMAC
|
||||
//
|
||||
pub fn hmac_sign(key: &str, data: &str) -> String {
|
||||
use data_encoding::HEXLOWER;
|
||||
|
||||
let key = hmac::SigningKey::new(&digest::SHA1, key.as_bytes());
|
||||
let signature = hmac::sign(&key, data.as_bytes());
|
||||
|
||||
HEXLOWER.encode(signature.as_ref())
|
||||
}
|
||||
|
||||
//
|
||||
// Random values
|
||||
//
|
||||
///
|
||||
/// Random values
|
||||
///
|
||||
|
||||
pub fn get_random_64() -> Vec<u8> {
|
||||
get_random(vec![0u8; 64])
|
||||
@@ -42,18 +30,7 @@ pub fn get_random_64() -> Vec<u8> {
|
||||
pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
|
||||
SystemRandom::new()
|
||||
.fill(&mut array)
|
||||
.expect("Error generating random values");
|
||||
SystemRandom::new().fill(&mut array).expect("Error generating random values");
|
||||
|
||||
array
|
||||
}
|
||||
|
||||
//
|
||||
// Constant time compare
|
||||
//
|
||||
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
|
||||
use ring::constant_time::verify_slices_are_equal;
|
||||
|
||||
verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok()
|
||||
}
|
||||
|
@@ -1,15 +1,15 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use diesel::{Connection as DieselConnection, ConnectionError};
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::r2d2;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::{Connection as DieselConnection, ConnectionError};
|
||||
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest};
|
||||
use rocket::{Outcome, Request, State};
|
||||
|
||||
use crate::CONFIG;
|
||||
use CONFIG;
|
||||
|
||||
/// An alias to the database connection used
|
||||
type Connection = SqliteConnection;
|
||||
@@ -20,18 +20,20 @@ type Pool = r2d2::Pool<ConnectionManager<Connection>>;
|
||||
/// Connection request guard type: a wrapper around an r2d2 pooled connection.
|
||||
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>);
|
||||
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod models;
|
||||
|
||||
/// Initializes a database pool.
|
||||
pub fn init_pool() -> Pool {
|
||||
let manager = ConnectionManager::new(CONFIG.database_url());
|
||||
let manager = ConnectionManager::new(&*CONFIG.database_url);
|
||||
|
||||
r2d2::Pool::builder().build(manager).expect("Failed to create pool")
|
||||
r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Failed to create pool")
|
||||
}
|
||||
|
||||
pub fn get_connection() -> Result<Connection, ConnectionError> {
|
||||
Connection::establish(&CONFIG.database_url())
|
||||
Connection::establish(&CONFIG.database_url)
|
||||
}
|
||||
|
||||
/// Attempts to retrieve a single connection from the managed database pool. If
|
||||
@@ -44,7 +46,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
|
||||
let pool = request.guard::<State<Pool>>()?;
|
||||
match pool.get() {
|
||||
Ok(conn) => Outcome::Success(DbConn(conn)),
|
||||
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
|
||||
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use serde_json::Value;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::Cipher;
|
||||
use crate::CONFIG;
|
||||
use CONFIG;
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "attachments"]
|
||||
@@ -12,7 +12,6 @@ pub struct Attachment {
|
||||
pub cipher_uuid: String,
|
||||
pub file_name: String,
|
||||
pub file_size: i32,
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
@@ -23,16 +22,15 @@ impl Attachment {
|
||||
cipher_uuid,
|
||||
file_name,
|
||||
file_size,
|
||||
key: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_file_path(&self) -> String {
|
||||
format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id)
|
||||
format!("{}/{}/{}", CONFIG.attachments_folder, self.cipher_uuid, self.id)
|
||||
}
|
||||
|
||||
pub fn to_json(&self, host: &str) -> Value {
|
||||
use crate::util::get_display_size;
|
||||
pub fn to_json(&self, host: &str) -> JsonValue {
|
||||
use util::get_display_size;
|
||||
|
||||
let web_path = format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id);
|
||||
let display_size = get_display_size(self.file_size);
|
||||
@@ -43,67 +41,55 @@ impl Attachment {
|
||||
"FileName": self.file_name,
|
||||
"Size": self.file_size.to_string(),
|
||||
"SizeName": display_size,
|
||||
"Key": self.key,
|
||||
"Object": "attachment"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::attachments;
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::attachments;
|
||||
|
||||
/// Database methods
|
||||
impl Attachment {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
diesel::replace_into(attachments::table)
|
||||
pub fn save(&self, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(attachments::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving attachment")
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
crate::util::retry(
|
||||
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(&**conn),
|
||||
10,
|
||||
)
|
||||
.map_res("Error deleting attachment")?;
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
use util;
|
||||
|
||||
crate::util::delete_file(&self.get_file_path())?;
|
||||
Ok(())
|
||||
util::delete_file(&self.get_file_path());
|
||||
|
||||
diesel::delete(
|
||||
attachments::table.filter(
|
||||
attachments::id.eq(self.id)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
for attachment in Attachment::find_by_cipher(&cipher_uuid, &conn) {
|
||||
attachment.delete(&conn)?;
|
||||
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)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> {
|
||||
let id = id.to_lowercase();
|
||||
|
||||
attachments::table
|
||||
.filter(attachments::id.eq(id))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
attachments::table
|
||||
.filter(attachments::cipher_uuid.eq(cipher_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading attachments")
|
||||
}
|
||||
|
||||
pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> {
|
||||
attachments::table
|
||||
.filter(attachments::cipher_uuid.eq_any(cipher_uuids))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading attachments")
|
||||
.load::<Self>(&**conn).expect("Error loading attachments")
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde_json::Value;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::{
|
||||
Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrgType};
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "ciphers"]
|
||||
@@ -32,7 +32,6 @@ pub struct Cipher {
|
||||
pub data: String,
|
||||
|
||||
pub favorite: bool,
|
||||
pub password_history: Option<String>,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
@@ -41,7 +40,7 @@ impl Cipher {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
|
||||
@@ -56,46 +55,36 @@ impl Cipher {
|
||||
fields: None,
|
||||
|
||||
data: String::new(),
|
||||
password_history: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::*;
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::*;
|
||||
|
||||
/// Database methods
|
||||
impl Cipher {
|
||||
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
|
||||
use crate::util::format_date;
|
||||
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> JsonValue {
|
||||
use serde_json;
|
||||
use util::format_date;
|
||||
use super::Attachment;
|
||||
|
||||
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
|
||||
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
|
||||
let attachments_json: Vec<JsonValue> = attachments.iter().map(|c| c.to_json(host)).collect();
|
||||
|
||||
let fields_json: Value = if let Some(ref fields) = self.fields {
|
||||
let fields_json: JsonValue = if let Some(ref fields) = self.fields {
|
||||
serde_json::from_str(fields).unwrap()
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
} else { JsonValue::Null };
|
||||
|
||||
let password_history_json: Value = if let Some(ref password_history) = self.password_history {
|
||||
serde_json::from_str(password_history).unwrap()
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
|
||||
let mut data_json: Value = serde_json::from_str(&self.data).unwrap();
|
||||
let mut data_json: JsonValue = serde_json::from_str(&self.data).unwrap();
|
||||
|
||||
// TODO: ******* Backwards compat start **********
|
||||
// To remove backwards compatibility, just remove this entire section
|
||||
// and remove the compat code from ciphers::update_cipher_from_data
|
||||
if self.type_ == 1 && data_json["Uris"].is_array() {
|
||||
let uri = data_json["Uris"][0]["Uri"].clone();
|
||||
let uri = data_json["Uris"][0]["uri"].clone();
|
||||
data_json["Uri"] = uri;
|
||||
}
|
||||
// TODO: ******* Backwards compat end **********
|
||||
@@ -108,7 +97,7 @@ impl Cipher {
|
||||
"Favorite": self.favorite,
|
||||
"OrganizationId": self.organization_uuid,
|
||||
"Attachments": attachments_json,
|
||||
"OrganizationUseTotp": true,
|
||||
"OrganizationUseTotp": false,
|
||||
"CollectionIds": self.get_collections(user_uuid, &conn),
|
||||
|
||||
"Name": self.name,
|
||||
@@ -119,8 +108,6 @@ impl Cipher {
|
||||
|
||||
"Object": "cipher",
|
||||
"Edit": true,
|
||||
|
||||
"PasswordHistory": password_history_json,
|
||||
});
|
||||
|
||||
let key = match self.type_ {
|
||||
@@ -135,194 +122,165 @@ impl Cipher {
|
||||
json_object
|
||||
}
|
||||
|
||||
pub fn update_users_revision(&self, conn: &DbConn) -> Vec<String> {
|
||||
let mut user_uuids = Vec::new();
|
||||
match self.user_uuid {
|
||||
Some(ref user_uuid) => {
|
||||
User::update_uuid_revision(&user_uuid, conn);
|
||||
user_uuids.push(user_uuid.clone())
|
||||
}
|
||||
None => {
|
||||
// Belongs to Organization, need to update affected users
|
||||
if let Some(ref org_uuid) = self.organization_uuid {
|
||||
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn)
|
||||
.iter()
|
||||
.for_each(|user_org| {
|
||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||
user_uuids.push(user_org.user_uuid.clone())
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
user_uuids
|
||||
}
|
||||
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
self.update_users_revision(conn);
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
diesel::replace_into(ciphers::table)
|
||||
match diesel::replace_into(ciphers::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving cipher")
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||
self.update_users_revision(conn);
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
||||
CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
||||
Attachment::delete_all_by_cipher(&self.uuid, &conn)?;
|
||||
|
||||
diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting cipher")
|
||||
diesel::delete(
|
||||
ciphers::table.filter(
|
||||
ciphers::uuid.eq(self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for cipher in Self::find_by_org(org_uuid, &conn) {
|
||||
cipher.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
for cipher in Self::find_owned_by_user(user_uuid, &conn) {
|
||||
cipher.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(user_uuid, &conn);
|
||||
|
||||
match (self.get_folder_uuid(&user_uuid, &conn), folder_uuid) {
|
||||
// No changes
|
||||
(None, None) => Ok(()),
|
||||
(Some(ref old), Some(ref new)) if old == new => Ok(()),
|
||||
|
||||
// Add to folder
|
||||
(None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(&conn),
|
||||
|
||||
// Remove from folder
|
||||
(Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) {
|
||||
Some(old) => old.delete(&conn),
|
||||
None => err!("Couldn't move from previous folder"),
|
||||
},
|
||||
|
||||
// Move to another folder
|
||||
(Some(old), Some(new)) => {
|
||||
if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) {
|
||||
old.delete(&conn)?;
|
||||
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> Result<(), &str> {
|
||||
match self.get_folder_uuid(&user_uuid, &conn) {
|
||||
None => {
|
||||
match folder_uuid {
|
||||
Some(new_folder) => {
|
||||
let folder_cipher = FolderCipher::new(&new_folder, &self.uuid);
|
||||
folder_cipher.save(&conn).or(Err("Couldn't save folder setting"))
|
||||
},
|
||||
None => Ok(()) //nothing to do
|
||||
}
|
||||
},
|
||||
Some(current_folder) => {
|
||||
match folder_uuid {
|
||||
Some(new_folder) => {
|
||||
if current_folder == new_folder {
|
||||
Ok(()) //nothing to do
|
||||
} else {
|
||||
match FolderCipher::find_by_folder_and_cipher(¤t_folder, &self.uuid, &conn) {
|
||||
Some(current_folder) => {
|
||||
current_folder.delete(&conn).or(Err("Failed removing old folder mapping"))
|
||||
},
|
||||
None => Ok(()) // Weird, but nothing to do
|
||||
}.and_then(
|
||||
|()| FolderCipher::new(&new_folder, &self.uuid)
|
||||
.save(&conn).or(Err("Couldn't save folder setting"))
|
||||
)
|
||||
}
|
||||
},
|
||||
None => {
|
||||
match FolderCipher::find_by_folder_and_cipher(¤t_folder, &self.uuid, &conn) {
|
||||
Some(current_folder) => {
|
||||
current_folder.delete(&conn).or(Err("Failed removing old folder mapping"))
|
||||
},
|
||||
None => Err("Couldn't move from previous folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
FolderCipher::new(&new, &self.uuid).save(&conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(&self.uuid))
|
||||
.left_join(
|
||||
users_organizations::table.on(ciphers::organization_uuid
|
||||
.eq(users_organizations::org_uuid.nullable())
|
||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
||||
)
|
||||
.left_join(ciphers_collections::table)
|
||||
.left_join(
|
||||
users_collections::table
|
||||
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
|
||||
)
|
||||
.filter(ciphers::user_uuid.eq(user_uuid).or(
|
||||
// Cipher owner
|
||||
users_organizations::access_all.eq(true).or(
|
||||
// access_all in Organization
|
||||
users_organizations::type_.le(UserOrgType::Admin as i32).or(
|
||||
// Org admin or owner
|
||||
users_collections::user_uuid.eq(user_uuid).and(
|
||||
users_collections::read_only.eq(false), //R/W access to collection
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(&self.uuid))
|
||||
.left_join(
|
||||
users_organizations::table.on(ciphers::organization_uuid
|
||||
.eq(users_organizations::org_uuid.nullable())
|
||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
||||
)
|
||||
.left_join(ciphers_collections::table)
|
||||
.left_join(
|
||||
users_collections::table
|
||||
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
|
||||
)
|
||||
.filter(ciphers::user_uuid.eq(user_uuid).or(
|
||||
// Cipher owner
|
||||
users_organizations::access_all.eq(true).or(
|
||||
// access_all in Organization
|
||||
users_organizations::type_.le(UserOrgType::Admin as i32).or(
|
||||
// Org admin or owner
|
||||
users_collections::user_uuid.eq(user_uuid), // Access to Collection
|
||||
),
|
||||
),
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
||||
folders_ciphers::table
|
||||
.inner_join(folders::table)
|
||||
.filter(folders::user_uuid.eq(&user_uuid))
|
||||
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
|
||||
.select(folders_ciphers::folder_uuid)
|
||||
.first::<String>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
// Find all ciphers accessible to user
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
ciphers::table
|
||||
match ciphers::table
|
||||
.filter(ciphers::uuid.eq(&self.uuid))
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
||||
users_organizations::user_uuid.eq(user_uuid).and(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
)
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(ciphers_collections::table.on(
|
||||
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
|
||||
))
|
||||
.left_join(ciphers_collections::table)
|
||||
.left_join(users_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||
))
|
||||
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
|
||||
users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
users_collections::user_uuid.eq(user_uuid).and(
|
||||
users_collections::read_only.eq(false) //R/W access to collection
|
||||
)
|
||||
)
|
||||
)
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.first::<Self>(&**conn).ok() {
|
||||
Some(_) => true,
|
||||
None => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||
match ciphers::table
|
||||
.filter(ciphers::uuid.eq(&self.uuid))
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(ciphers_collections::table)
|
||||
.left_join(users_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||
))
|
||||
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
|
||||
users_collections::user_uuid.eq(user_uuid) // Access to Collection
|
||||
)
|
||||
)
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.first::<Self>(&**conn).ok() {
|
||||
Some(_) => true,
|
||||
None => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
||||
folders_ciphers::table.inner_join(folders::table)
|
||||
.filter(folders::user_uuid.eq(&user_uuid))
|
||||
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
|
||||
.select(folders_ciphers::folder_uuid)
|
||||
.first::<String>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
ciphers::table
|
||||
.filter(ciphers::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
// Find all ciphers accesible to user
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
ciphers::table
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(ciphers_collections::table)
|
||||
.left_join(users_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||
))
|
||||
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
|
||||
users_collections::user_uuid.eq(user_uuid) // Access to Collection
|
||||
)
|
||||
)
|
||||
))
|
||||
.select(ciphers::all_columns)
|
||||
.distinct()
|
||||
.load::<Self>(&**conn).expect("Error loading ciphers")
|
||||
}
|
||||
@@ -358,9 +316,7 @@ impl Cipher {
|
||||
)
|
||||
))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
|
||||
users_collections::user_uuid.eq(user_id)
|
||||
)
|
||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
|
||||
))
|
||||
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
|
||||
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection
|
||||
@@ -369,6 +325,6 @@ impl Cipher {
|
||||
)
|
||||
))
|
||||
.select(ciphers_collections::collection_uuid)
|
||||
.load::<String>(&**conn).unwrap_or_default()
|
||||
.load::<String>(&**conn).unwrap_or(vec![])
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
use serde_json::Value;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{Organization, UserOrganization, UserOrgType};
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "collections"]
|
||||
@@ -16,14 +18,14 @@ pub struct Collection {
|
||||
impl Collection {
|
||||
pub fn new(org_uuid: String, name: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
|
||||
org_uuid,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.org_uuid,
|
||||
@@ -33,101 +35,79 @@ impl Collection {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::*;
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::*;
|
||||
|
||||
/// Database methods
|
||||
impl Collection {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
self.update_users_revision(conn);
|
||||
|
||||
diesel::replace_into(collections::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving collection")
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(collections::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
self.update_users_revision(conn);
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?;
|
||||
CollectionUser::delete_all_by_collection(&self.uuid, &conn)?;
|
||||
|
||||
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting collection")
|
||||
diesel::delete(
|
||||
collections::table.filter(
|
||||
collections::uuid.eq(self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for collection in Self::find_by_organization(org_uuid, &conn) {
|
||||
collection.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_users_revision(&self, conn: &DbConn) {
|
||||
UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn)
|
||||
.iter()
|
||||
.for_each(|user_org| {
|
||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
collections::table
|
||||
.filter(collections::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
collections::table
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||
users_collections::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(users_organizations::table.on(
|
||||
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.filter(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
)
|
||||
.filter(
|
||||
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
|
||||
users_organizations::access_all.eq(true) // access_all in Organization
|
||||
)
|
||||
).select(collections::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading collections")
|
||||
let mut all_access_collections = users_organizations::table
|
||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||
.filter(users_organizations::access_all.eq(true))
|
||||
.inner_join(collections::table.on(collections::org_uuid.eq(users_organizations::org_uuid)))
|
||||
.select(collections::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading collections");
|
||||
|
||||
let mut assigned_collections = users_collections::table.inner_join(collections::table)
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
.select(collections::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading collections");
|
||||
|
||||
all_access_collections.append(&mut assigned_collections);
|
||||
all_access_collections
|
||||
}
|
||||
|
||||
pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
Self::find_by_user_uuid(user_uuid, conn)
|
||||
.into_iter()
|
||||
.filter(|c| c.org_uuid == org_uuid)
|
||||
.collect()
|
||||
Self::find_by_user_uuid(user_uuid, conn).into_iter().filter(|c| c.org_uuid == org_uuid).collect()
|
||||
}
|
||||
|
||||
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
collections::table
|
||||
.filter(collections::org_uuid.eq(org_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading collections")
|
||||
.load::<Self>(&**conn).expect("Error loading collections")
|
||||
}
|
||||
|
||||
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
collections::table
|
||||
.filter(collections::uuid.eq(uuid))
|
||||
.filter(collections::org_uuid.eq(org_uuid))
|
||||
.select(collections::all_columns)
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.filter(collections::uuid.eq(uuid))
|
||||
.filter(collections::org_uuid.eq(org_uuid))
|
||||
.select(collections::all_columns)
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
@@ -157,19 +137,19 @@ impl Collection {
|
||||
match UserOrganization::find_by_user_and_org(&user_uuid, &self.org_uuid, &conn) {
|
||||
None => false, // Not in Org
|
||||
Some(user_org) => {
|
||||
if user_org.access_all {
|
||||
true
|
||||
} else {
|
||||
users_collections::table
|
||||
.inner_join(collections::table)
|
||||
.filter(users_collections::collection_uuid.eq(&self.uuid))
|
||||
.filter(users_collections::user_uuid.eq(&user_uuid))
|
||||
.filter(users_collections::read_only.eq(false))
|
||||
.select(collections::all_columns)
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.is_some() // Read only or no access to collection
|
||||
}
|
||||
if user_org.access_all {
|
||||
true
|
||||
} else {
|
||||
match 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,70 +176,50 @@ impl CollectionUser {
|
||||
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
||||
.filter(collections::org_uuid.eq(org_uuid))
|
||||
.select(users_collections::all_columns)
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading users_collections")
|
||||
.load::<Self>(&**conn).expect("Error loading users_collections")
|
||||
}
|
||||
|
||||
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&user_uuid, conn);
|
||||
|
||||
pub fn save(user_uuid: &str, collection_uuid: &str, read_only:bool, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::replace_into(users_collections::table)
|
||||
.values((
|
||||
users_collections::user_uuid.eq(user_uuid),
|
||||
users_collections::collection_uuid.eq(collection_uuid),
|
||||
users_collections::read_only.eq(read_only),
|
||||
))
|
||||
.execute(&**conn)
|
||||
.map_res("Error adding user to collection")
|
||||
.values((
|
||||
users_collections::user_uuid.eq(user_uuid),
|
||||
users_collections::collection_uuid.eq(collection_uuid),
|
||||
users_collections::read_only.eq(read_only),
|
||||
)).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
|
||||
diesel::delete(
|
||||
users_collections::table
|
||||
.filter(users_collections::user_uuid.eq(&self.user_uuid))
|
||||
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)),
|
||||
)
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing user from collection")
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(users_collections::table
|
||||
.filter(users_collections::user_uuid.eq(&self.user_uuid))
|
||||
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)))
|
||||
.execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||
.select(users_collections::all_columns)
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading users_collections")
|
||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||
.select(users_collections::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading users_collections")
|
||||
}
|
||||
|
||||
pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
.select(users_collections::all_columns)
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(users_collections::table
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
.select(users_collections::all_columns)
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
CollectionUser::find_by_collection(&collection_uuid, conn)
|
||||
.iter()
|
||||
.for_each(|collection| {
|
||||
User::update_uuid_revision(&collection.user_uuid, conn);
|
||||
});
|
||||
|
||||
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting users from collection")
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&user_uuid, conn);
|
||||
|
||||
diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing user from collections")
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,43 +237,36 @@ pub struct CollectionCipher {
|
||||
|
||||
/// Database methods
|
||||
impl CollectionCipher {
|
||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
Self::update_users_revision(&collection_uuid, conn);
|
||||
diesel::replace_into(ciphers_collections::table)
|
||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(ciphers_collections::table)
|
||||
.values((
|
||||
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
||||
ciphers_collections::collection_uuid.eq(collection_uuid),
|
||||
))
|
||||
.execute(&**conn)
|
||||
.map_res("Error adding cipher to collection")
|
||||
}
|
||||
|
||||
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
Self::update_users_revision(&collection_uuid, conn);
|
||||
diesel::delete(
|
||||
ciphers_collections::table
|
||||
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
||||
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)),
|
||||
)
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting cipher from collection")
|
||||
}
|
||||
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing cipher from collections")
|
||||
}
|
||||
|
||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing ciphers from collection")
|
||||
}
|
||||
|
||||
pub fn update_users_revision(collection_uuid: &str, conn: &DbConn) {
|
||||
if let Some(collection) = Collection::find_by_uuid(collection_uuid, conn) {
|
||||
collection.update_users_revision(conn);
|
||||
)).execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
|
||||
match diesel::delete(ciphers_collections::table
|
||||
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
||||
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row deleted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(ciphers_collections::table
|
||||
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(ciphers_collections::table
|
||||
.filter(ciphers_collections::collection_uuid.eq(collection_uuid))
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
}
|
@@ -43,25 +43,23 @@ impl Device {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||
use crate::crypto;
|
||||
pub fn refresh_twofactor_remember(&mut self) {
|
||||
use data_encoding::BASE64;
|
||||
use crypto;
|
||||
|
||||
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
|
||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||
|
||||
twofactor_remember
|
||||
self.twofactor_remember = Some(BASE64.encode(&crypto::get_random(vec![0u8; 180])));
|
||||
}
|
||||
|
||||
pub fn delete_twofactor_remember(&mut self) {
|
||||
self.twofactor_remember = None;
|
||||
}
|
||||
|
||||
|
||||
pub fn refresh_tokens(&mut self, user: &super::User, orgs: Vec<super::UserOrganization>) -> (String, i64) {
|
||||
// If there is no refresh token, we create one
|
||||
if self.refresh_token.is_empty() {
|
||||
use crate::crypto;
|
||||
use data_encoding::BASE64URL;
|
||||
use crypto;
|
||||
|
||||
self.refresh_token = BASE64URL.encode(&crypto::get_random_64());
|
||||
}
|
||||
@@ -70,18 +68,18 @@ 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 crate::auth::{encode_jwt, LoginJWTClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
||||
let claims = LoginJWTClaims {
|
||||
use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
|
||||
let claims = JWTClaims {
|
||||
nbf: time_now.timestamp(),
|
||||
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
||||
iss: JWT_LOGIN_ISSUER.to_string(),
|
||||
iss: JWT_ISSUER.to_string(),
|
||||
sub: user.uuid.to_string(),
|
||||
|
||||
premium: true,
|
||||
@@ -92,7 +90,6 @@ impl Device {
|
||||
orgowner,
|
||||
orgadmin,
|
||||
orguser,
|
||||
orgmanager,
|
||||
|
||||
sstamp: user.security_stamp.to_string(),
|
||||
device: self.uuid.to_string(),
|
||||
@@ -100,61 +97,53 @@ impl Device {
|
||||
amr: vec!["Application".into()],
|
||||
};
|
||||
|
||||
|
||||
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::devices;
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::devices;
|
||||
|
||||
/// Database methods
|
||||
impl Device {
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
crate::util::retry(
|
||||
|| diesel::replace_into(devices::table).values(&*self).execute(&**conn),
|
||||
10,
|
||||
)
|
||||
.map_res("Error saving device")
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing device")
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
for device in Self::find_by_user(user_uuid, &conn) {
|
||||
device.delete(&conn)?;
|
||||
match diesel::replace_into(devices::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> bool {
|
||||
match diesel::delete(devices::table.filter(
|
||||
devices::uuid.eq(self.uuid)))
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row deleted
|
||||
_ => false,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
devices::table
|
||||
.filter(devices::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {
|
||||
devices::table
|
||||
.filter(devices::refresh_token.eq(refresh_token))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
devices::table
|
||||
.filter(devices::user_uuid.eq(user_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading devices")
|
||||
.load::<Self>(&**conn).expect("Error loading devices")
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde_json::Value;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::{Cipher, User};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{User, Cipher};
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "folders"]
|
||||
@@ -31,7 +33,7 @@ impl Folder {
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
|
||||
@@ -40,8 +42,8 @@ impl Folder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
use crate::util::format_date;
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
use util::format_date;
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
@@ -61,99 +63,83 @@ impl FolderCipher {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::{folders, folders_ciphers};
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::{folders, folders_ciphers};
|
||||
|
||||
/// Database methods
|
||||
impl Folder {
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
diesel::replace_into(folders::table)
|
||||
match diesel::replace_into(folders::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving folder")
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
||||
|
||||
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting folder")
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
for folder in Self::find_by_user(user_uuid, &conn) {
|
||||
folder.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
diesel::delete(
|
||||
folders::table.filter(
|
||||
folders::uuid.eq(self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
folders::table
|
||||
.filter(folders::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
folders::table
|
||||
.filter(folders::user_uuid.eq(user_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading folders")
|
||||
.load::<Self>(&**conn).expect("Error loading folders")
|
||||
}
|
||||
}
|
||||
|
||||
impl FolderCipher {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
pub fn save(&self, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::replace_into(folders_ciphers::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error adding cipher to folder")
|
||||
.values(&*self)
|
||||
.execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(
|
||||
folders_ciphers::table
|
||||
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
|
||||
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)),
|
||||
)
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing cipher from folder")
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(folders_ciphers::table
|
||||
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
|
||||
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid))
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing cipher from folders")
|
||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(folders_ciphers::table
|
||||
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing ciphers from folder")
|
||||
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
diesel::delete(folders_ciphers::table
|
||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
folders_ciphers::table
|
||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
folders_ciphers::table
|
||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading folders")
|
||||
.load::<Self>(&**conn).expect("Error loading folders")
|
||||
}
|
||||
}
|
||||
|
@@ -6,14 +6,12 @@ mod user;
|
||||
|
||||
mod collection;
|
||||
mod organization;
|
||||
mod two_factor;
|
||||
|
||||
pub use self::attachment::Attachment;
|
||||
pub use self::cipher::Cipher;
|
||||
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
||||
pub use self::device::Device;
|
||||
pub use self::folder::{Folder, FolderCipher};
|
||||
pub use self::user::User;
|
||||
pub use self::organization::Organization;
|
||||
pub use self::organization::{UserOrgStatus, UserOrgType, UserOrganization};
|
||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||
pub use self::user::{Invitation, User};
|
||||
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
|
||||
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
use serde_json::Value;
|
||||
use std::cmp::Ordering;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::{CollectionUser, User};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
#[table_name = "organizations"]
|
||||
@@ -27,102 +26,15 @@ pub struct UserOrganization {
|
||||
}
|
||||
|
||||
pub enum UserOrgStatus {
|
||||
Invited = 0,
|
||||
_Invited = 0, // Unused, users are accepted automatically
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, 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 {
|
||||
@@ -131,17 +43,6 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -151,25 +52,25 @@ impl UserOrgType {
|
||||
impl Organization {
|
||||
pub fn new(name: String, billing_email: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
|
||||
name,
|
||||
billing_email,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
"Seats": 10,
|
||||
"MaxCollections": 10,
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
"Use2fa": true,
|
||||
|
||||
"Use2fa": false,
|
||||
"UseDirectory": false,
|
||||
"UseEvents": false,
|
||||
"UseGroups": false,
|
||||
"UseTotp": true,
|
||||
"UseTotp": false,
|
||||
|
||||
"BusinessName": null,
|
||||
"BusinessAddress1": null,
|
||||
@@ -179,9 +80,9 @@ impl Organization {
|
||||
"BusinessTaxNumber": null,
|
||||
|
||||
"BillingEmail": self.billing_email,
|
||||
"Plan": "TeamsAnnually",
|
||||
"PlanType": 5, // TeamsAnnually plan
|
||||
"UsersGetPremium": true,
|
||||
"Plan": "Free",
|
||||
"PlanType": 0, // Free plan
|
||||
|
||||
"Object": "organization",
|
||||
})
|
||||
}
|
||||
@@ -190,7 +91,7 @@ impl Organization {
|
||||
impl UserOrganization {
|
||||
pub fn new(user_uuid: String, org_uuid: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
|
||||
user_uuid,
|
||||
org_uuid,
|
||||
@@ -203,51 +104,47 @@ impl UserOrganization {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations};
|
||||
use crate::db::DbConn;
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::organizations;
|
||||
use db::schema::users_organizations;
|
||||
|
||||
/// Database methods
|
||||
impl Organization {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
UserOrganization::find_by_org(&self.uuid, conn)
|
||||
.iter()
|
||||
.for_each(|user_org| {
|
||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||
});
|
||||
|
||||
diesel::replace_into(organizations::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving organization")
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(organizations::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
use super::{Cipher, Collection};
|
||||
|
||||
Cipher::delete_all_by_organization(&self.uuid, &conn)?;
|
||||
Collection::delete_all_by_organization(&self.uuid, &conn)?;
|
||||
UserOrganization::delete_all_by_organization(&self.uuid, &conn)?;
|
||||
|
||||
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving organization")
|
||||
diesel::delete(
|
||||
organizations::table.filter(
|
||||
organizations::uuid.eq(self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
organizations::table
|
||||
.filter(organizations::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl UserOrganization {
|
||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
@@ -255,13 +152,12 @@ impl UserOrganization {
|
||||
"Name": org.name,
|
||||
"Seats": 10,
|
||||
"MaxCollections": 10,
|
||||
"UsersGetPremium": true,
|
||||
|
||||
"Use2fa": true,
|
||||
"Use2fa": false,
|
||||
"UseDirectory": false,
|
||||
"UseEvents": false,
|
||||
"UseGroups": false,
|
||||
"UseTotp": true,
|
||||
"UseTotp": false,
|
||||
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
|
||||
@@ -275,7 +171,8 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_user_details(&self, conn: &DbConn) -> Value {
|
||||
pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue {
|
||||
use super::User;
|
||||
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
@@ -292,22 +189,29 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_collection_user_details(&self, read_only: bool) -> Value {
|
||||
pub fn to_json_collection_user_details(&self, read_only: &bool, conn: &DbConn) -> JsonValue {
|
||||
use super::User;
|
||||
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"ReadOnly": read_only
|
||||
"OrganizationUserId": self.uuid,
|
||||
"AccessAll": self.access_all,
|
||||
"Name": user.name,
|
||||
"Email": user.email,
|
||||
"Type": self.type_,
|
||||
"Status": self.status,
|
||||
"ReadOnly": read_only,
|
||||
"Object": "collectionUser",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_details(&self, conn: &DbConn) -> Value {
|
||||
pub fn to_json_details(&self, conn: &DbConn) -> JsonValue {
|
||||
let coll_uuids = if self.access_all {
|
||||
vec![] // If we have complete access, no need to fill the array
|
||||
} else {
|
||||
use super::CollectionUser;
|
||||
let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn);
|
||||
collections
|
||||
.iter()
|
||||
.map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only}))
|
||||
.collect()
|
||||
collections.iter().map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only})).collect()
|
||||
};
|
||||
|
||||
json!({
|
||||
@@ -323,136 +227,69 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
|
||||
diesel::replace_into(users_organizations::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error adding user to organization")
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
match diesel::replace_into(users_organizations::table)
|
||||
.values(&*self)
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||
use super::CollectionUser;
|
||||
|
||||
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
|
||||
|
||||
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error removing user from organization")
|
||||
diesel::delete(
|
||||
users_organizations::table.filter(
|
||||
users_organizations::uuid.eq(self.uuid)
|
||||
)
|
||||
).execute(&**conn).and(Ok(()))
|
||||
}
|
||||
|
||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> QueryResult<()> {
|
||||
for user_org in Self::find_by_org(&org_uuid, &conn) {
|
||||
user_org.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
for user_org in Self::find_any_state_by_user(&user_uuid, &conn) {
|
||||
user_org.delete(&conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_full_access(self) -> bool {
|
||||
self.access_all || self.type_ >= UserOrgType::Admin
|
||||
self.access_all || self.type_ < UserOrgType::User as i32
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::uuid.eq(uuid))
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
||||
.load::<Self>(&**conn)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
|
||||
.load::<Self>(&**conn)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.unwrap_or_default()
|
||||
.load::<Self>(&**conn).unwrap_or(vec![])
|
||||
}
|
||||
|
||||
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading user organizations")
|
||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||
}
|
||||
|
||||
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.filter(users_organizations::type_.eq(type_))
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading user organizations")
|
||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||
}
|
||||
|
||||
pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
||||
))
|
||||
.left_join(ciphers_collections::table.on(
|
||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and(
|
||||
ciphers_collections::cipher_uuid.eq(&cipher_uuid)
|
||||
)
|
||||
))
|
||||
.filter(
|
||||
users_organizations::access_all.eq(true).or( // AccessAll..
|
||||
ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher
|
||||
)
|
||||
)
|
||||
.select(users_organizations::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||
}
|
||||
|
||||
pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
users_organizations::table
|
||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
||||
))
|
||||
.filter(
|
||||
users_organizations::access_all.eq(true).or( // AccessAll..
|
||||
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher
|
||||
)
|
||||
)
|
||||
.select(users_organizations::all_columns)
|
||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,106 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use super::User;
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "twofactor"]
|
||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||
#[primary_key(uuid)]
|
||||
pub struct TwoFactor {
|
||||
pub uuid: String,
|
||||
pub user_uuid: String,
|
||||
pub type_: i32,
|
||||
pub enabled: bool,
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromPrimitive)]
|
||||
pub enum TwoFactorType {
|
||||
Authenticator = 0,
|
||||
Email = 1,
|
||||
Duo = 2,
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6,
|
||||
|
||||
// These are implementation details
|
||||
U2fRegisterChallenge = 1000,
|
||||
U2fLoginChallenge = 1001,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl TwoFactor {
|
||||
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
user_uuid,
|
||||
type_: type_ as i32,
|
||||
enabled: true,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Key": "", // This key and value vary
|
||||
"Object": "twoFactorAuthenticator" // This value varies
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_list(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Type": self.type_,
|
||||
"Object": "twoFactorProvider"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use crate::db::schema::twofactor;
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
|
||||
/// Database methods
|
||||
impl TwoFactor {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
diesel::replace_into(twofactor::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving twofactor")
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting twofactor")
|
||||
}
|
||||
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
twofactor::table
|
||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||
.filter(twofactor::type_.lt(1000)) // Filter implementation types
|
||||
.load::<Self>(&**conn)
|
||||
.expect("Error loading twofactor")
|
||||
}
|
||||
|
||||
pub fn find_by_user_and_type(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<Self> {
|
||||
twofactor::table
|
||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||
.filter(twofactor::type_.eq(type_))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting twofactors")
|
||||
}
|
||||
}
|
@@ -1,8 +1,11 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use serde_json::Value;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crypto;
|
||||
use CONFIG;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::CONFIG;
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
#[table_name = "users"]
|
||||
@@ -24,114 +27,114 @@ pub struct User {
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
|
||||
#[column_name = "totp_secret"]
|
||||
_totp_secret: Option<String>,
|
||||
pub totp_secret: Option<String>,
|
||||
pub totp_recover: Option<String>,
|
||||
|
||||
pub security_stamp: String,
|
||||
|
||||
pub equivalent_domains: String,
|
||||
pub excluded_globals: String,
|
||||
|
||||
pub client_kdf_type: i32,
|
||||
pub client_kdf_iter: i32,
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
Enabled = 0,
|
||||
Invited = 1,
|
||||
_Disabled = 2,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl User {
|
||||
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
||||
pub const CLIENT_KDF_ITER_DEFAULT: i32 = 5_000;
|
||||
|
||||
pub fn new(mail: String) -> Self {
|
||||
pub fn new(mail: String, key: String, password: String) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
let email = mail.to_lowercase();
|
||||
|
||||
let iterations = CONFIG.password_iterations;
|
||||
let salt = crypto::get_random_64();
|
||||
let password_hash = crypto::hash_password(password.as_bytes(), &salt, iterations as u32);
|
||||
|
||||
Self {
|
||||
uuid: crate::util::get_uuid(),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
name: email.clone(),
|
||||
email,
|
||||
key: String::new(),
|
||||
key,
|
||||
|
||||
password_hash: Vec::new(),
|
||||
salt: crypto::get_random_64(),
|
||||
password_iterations: CONFIG.password_iterations(),
|
||||
password_hash,
|
||||
salt,
|
||||
password_iterations: iterations,
|
||||
|
||||
security_stamp: crate::util::get_uuid(),
|
||||
security_stamp: Uuid::new_v4().to_string(),
|
||||
|
||||
password_hint: None,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
||||
_totp_secret: None,
|
||||
totp_secret: None,
|
||||
totp_recover: None,
|
||||
|
||||
equivalent_domains: "[]".to_string(),
|
||||
excluded_globals: "[]".to_string(),
|
||||
|
||||
client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT,
|
||||
client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_valid_password(&self, password: &str) -> bool {
|
||||
crypto::verify_password_hash(
|
||||
password.as_bytes(),
|
||||
&self.salt,
|
||||
&self.password_hash,
|
||||
self.password_iterations as u32,
|
||||
)
|
||||
crypto::verify_password_hash(password.as_bytes(),
|
||||
&self.salt,
|
||||
&self.password_hash,
|
||||
self.password_iterations as u32)
|
||||
}
|
||||
|
||||
pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool {
|
||||
if let Some(ref totp_recover) = self.totp_recover {
|
||||
crate::crypto::ct_eq(recovery_code, totp_recover.to_lowercase())
|
||||
recovery_code == totp_recover.to_lowercase()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_password(&mut self, password: &str) {
|
||||
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
||||
self.password_hash = crypto::hash_password(password.as_bytes(),
|
||||
&self.salt,
|
||||
self.password_iterations as u32);
|
||||
self.reset_security_stamp();
|
||||
}
|
||||
|
||||
pub fn reset_security_stamp(&mut self) {
|
||||
self.security_stamp = crate::util::get_uuid();
|
||||
self.security_stamp = Uuid::new_v4().to_string();
|
||||
}
|
||||
|
||||
pub fn requires_twofactor(&self) -> bool {
|
||||
self.totp_secret.is_some()
|
||||
}
|
||||
|
||||
pub fn check_totp_code(&self, totp_code: u64) -> bool {
|
||||
if let Some(ref totp_secret) = self.totp_secret {
|
||||
// Validate totp
|
||||
use data_encoding::BASE32;
|
||||
use oath::{totp_raw_now, HashType};
|
||||
|
||||
let decoded_secret = match BASE32.decode(totp_secret.as_bytes()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false
|
||||
};
|
||||
|
||||
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
||||
generated == totp_code
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization};
|
||||
use crate::db::schema::{invitations, users};
|
||||
use crate::db::DbConn;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::error::MapResult;
|
||||
use db::DbConn;
|
||||
use db::schema::users;
|
||||
|
||||
/// Database methods
|
||||
impl User {
|
||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
||||
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||
use super::UserOrganization;
|
||||
|
||||
// TODO: Might want to save the status field in the DB
|
||||
let status = if self.password_hash.is_empty() {
|
||||
UserStatus::Invited
|
||||
} else {
|
||||
UserStatus::Enabled
|
||||
};
|
||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
||||
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||
|
||||
json!({
|
||||
"_Status": status as i32,
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
"Email": self.email,
|
||||
@@ -139,7 +142,7 @@ impl User {
|
||||
"Premium": true,
|
||||
"MasterPasswordHint": self.password_hint,
|
||||
"Culture": "en-US",
|
||||
"TwoFactorEnabled": twofactor_enabled,
|
||||
"TwoFactorEnabled": self.totp_secret.is_some(),
|
||||
"Key": self.key,
|
||||
"PrivateKey": self.private_key,
|
||||
"SecurityStamp": self.security_stamp,
|
||||
@@ -148,138 +151,37 @@ impl User {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
if self.email.trim().is_empty() {
|
||||
err!("User email can't be empty")
|
||||
}
|
||||
|
||||
pub fn save(&mut self, conn: &DbConn) -> bool {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
diesel::replace_into(users::table) // Insert or update
|
||||
match diesel::replace_into(users::table) // Insert or update
|
||||
.values(&*self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving user")
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
|
||||
if user_org.type_ == UserOrgType::Owner {
|
||||
let owner_type = UserOrgType::Owner as i32;
|
||||
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 {
|
||||
err!("Can't delete last owner")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UserOrganization::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Cipher::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Folder::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Device::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
TwoFactor::delete_all_by_user(&self.uuid, &*conn)?;
|
||||
Invitation::take(&self.email, &*conn); // Delete invitation if any
|
||||
|
||||
diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting user")
|
||||
}
|
||||
|
||||
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
||||
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn) {
|
||||
warn!("Failed to update revision for {}: {:#?}", uuid, e);
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row inserted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_all_revisions(conn: &DbConn) -> EmptyResult {
|
||||
let updated_at = Utc::now().naive_utc();
|
||||
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::update(users::table)
|
||||
.set(users::updated_at.eq(updated_at))
|
||||
.execute(&**conn)
|
||||
},
|
||||
10,
|
||||
)
|
||||
.map_res("Error updating revision date for all users")
|
||||
}
|
||||
|
||||
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
self.updated_at = Utc::now().naive_utc();
|
||||
|
||||
Self::_update_revision(&self.uuid, &self.updated_at, conn)
|
||||
}
|
||||
|
||||
fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
|
||||
crate::util::retry(
|
||||
|| {
|
||||
diesel::update(users::table.filter(users::uuid.eq(uuid)))
|
||||
.set(users::updated_at.eq(date))
|
||||
.execute(&**conn)
|
||||
},
|
||||
10,
|
||||
)
|
||||
.map_res("Error updating user revision")
|
||||
pub fn delete(self, conn: &DbConn) -> bool {
|
||||
match diesel::delete(users::table.filter(
|
||||
users::uuid.eq(self.uuid)))
|
||||
.execute(&**conn) {
|
||||
Ok(1) => true, // One row deleted
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
||||
let lower_mail = mail.to_lowercase();
|
||||
users::table
|
||||
.filter(users::email.eq(lower_mail))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||
users::table.filter(users::uuid.eq(uuid)).first::<Self>(&**conn).ok()
|
||||
}
|
||||
|
||||
pub fn get_all(conn: &DbConn) -> Vec<Self> {
|
||||
users::table.load::<Self>(&**conn).expect("Error loading users")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||
#[table_name = "invitations"]
|
||||
#[primary_key(email)]
|
||||
pub struct Invitation {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Invitation {
|
||||
pub fn new(email: String) -> Self {
|
||||
Self { email }
|
||||
}
|
||||
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
if self.email.trim().is_empty() {
|
||||
err!("Invitation email can't be empty")
|
||||
}
|
||||
|
||||
diesel::replace_into(invitations::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving invitation")
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||
diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))
|
||||
.execute(&**conn)
|
||||
.map_res("Error deleting invitation")
|
||||
}
|
||||
|
||||
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
||||
let lower_mail = mail.to_lowercase();
|
||||
invitations::table
|
||||
.filter(invitations::email.eq(lower_mail))
|
||||
.first::<Self>(&**conn)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn take(mail: &str, conn: &DbConn) -> bool {
|
||||
CONFIG.invitations_allowed()
|
||||
&& match Self::find_by_mail(mail, &conn) {
|
||||
Some(invitation) => invitation.delete(&conn).is_ok(),
|
||||
None => false,
|
||||
}
|
||||
users::table
|
||||
.filter(users::uuid.eq(uuid))
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ table! {
|
||||
cipher_uuid -> Text,
|
||||
file_name -> Text,
|
||||
file_size -> Integer,
|
||||
key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +21,6 @@ table! {
|
||||
fields -> Nullable<Text>,
|
||||
data -> Text,
|
||||
favorite -> Bool,
|
||||
password_history -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +71,6 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
invitations (email) {
|
||||
email -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
organizations (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -87,17 +79,6 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
twofactor (uuid) {
|
||||
uuid -> Text,
|
||||
user_uuid -> Text,
|
||||
#[sql_name = "type"]
|
||||
type_ -> Integer,
|
||||
enabled -> Bool,
|
||||
data -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -117,8 +98,6 @@ table! {
|
||||
security_stamp -> Text,
|
||||
equivalent_domains -> Text,
|
||||
excluded_globals -> Text,
|
||||
client_kdf_type -> Integer,
|
||||
client_kdf_iter -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +132,6 @@ joinable!(devices -> users (user_uuid));
|
||||
joinable!(folders -> users (user_uuid));
|
||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||
joinable!(twofactor -> users (user_uuid));
|
||||
joinable!(users_collections -> collections (collection_uuid));
|
||||
joinable!(users_collections -> users (user_uuid));
|
||||
joinable!(users_organizations -> organizations (org_uuid));
|
||||
@@ -167,9 +145,7 @@ allow_tables_to_appear_in_same_query!(
|
||||
devices,
|
||||
folders,
|
||||
folders_ciphers,
|
||||
invitations,
|
||||
organizations,
|
||||
twofactor,
|
||||
users,
|
||||
users_collections,
|
||||
users_organizations,
|
||||
|
216
src/error.rs
216
src/error.rs
@@ -1,216 +0,0 @@
|
||||
//
|
||||
// Error generator macro
|
||||
//
|
||||
use std::error::Error as StdError;
|
||||
|
||||
macro_rules! make_error {
|
||||
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
||||
const BAD_REQUEST: u16 = 400;
|
||||
|
||||
#[derive(Display)]
|
||||
pub enum ErrorKind { $($name( $ty )),+ }
|
||||
pub struct Error { message: String, error: ErrorKind, error_code: u16 }
|
||||
|
||||
$(impl From<$ty> for Error {
|
||||
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
|
||||
})+
|
||||
$(impl<S: Into<String>> From<(S, $ty)> for Error {
|
||||
fn from(val: (S, $ty)) -> Self {
|
||||
Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST }
|
||||
}
|
||||
})+
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match &self.error {$( ErrorKind::$name(e) => $src_fn(e), )+}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match &self.error {$(
|
||||
ErrorKind::$name(e) => f.write_str(&$usr_msg_fun(e, &self.message)),
|
||||
)+}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
use diesel::result::Error as DieselErr;
|
||||
use handlebars::RenderError as HbErr;
|
||||
use jsonwebtoken::errors::Error as JWTErr;
|
||||
use regex::Error as RegexErr;
|
||||
use reqwest::Error as ReqErr;
|
||||
use serde_json::{Error as SerdeErr, Value};
|
||||
use std::io::Error as IOErr;
|
||||
|
||||
use std::option::NoneError as NoneErr;
|
||||
use std::time::SystemTimeError as TimeErr;
|
||||
use u2f::u2ferror::U2fError as U2fErr;
|
||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||
|
||||
#[derive(Display, Serialize)]
|
||||
pub struct Empty {}
|
||||
|
||||
// Error struct
|
||||
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
|
||||
//
|
||||
// After the variant itself, there are two expressions. The first one indicates whether the error contains a source error (that we pretty print).
|
||||
// The second one contains the function used to obtain the response sent to the client
|
||||
make_error! {
|
||||
// Just an empty error
|
||||
EmptyError(Empty): _no_source, _serialize,
|
||||
// Used to represent err! calls
|
||||
SimpleError(String): _no_source, _api_error,
|
||||
// Used for special return values, like 2FA errors
|
||||
JsonError(Value): _no_source, _serialize,
|
||||
DbError(DieselErr): _has_source, _api_error,
|
||||
U2fError(U2fErr): _has_source, _api_error,
|
||||
SerdeError(SerdeErr): _has_source, _api_error,
|
||||
JWTError(JWTErr): _has_source, _api_error,
|
||||
TemplError(HbErr): _has_source, _api_error,
|
||||
//WsError(ws::Error): _has_source, _api_error,
|
||||
IOError(IOErr): _has_source, _api_error,
|
||||
TimeError(TimeErr): _has_source, _api_error,
|
||||
ReqError(ReqErr): _has_source, _api_error,
|
||||
RegexError(RegexErr): _has_source, _api_error,
|
||||
YubiError(YubiErr): _has_source, _api_error,
|
||||
}
|
||||
|
||||
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
|
||||
impl From<NoneErr> for Error {
|
||||
fn from(_: NoneErr) -> Self {
|
||||
Error::from(("NoneError", String::new()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.source() {
|
||||
Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e),
|
||||
None => write!(f, "{}. {}", self.message, self.error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<M: Into<String>, N: Into<String>>(usr_msg: M, log_msg: N) -> Self {
|
||||
(usr_msg, log_msg.into()).into()
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Empty {}.into()
|
||||
}
|
||||
|
||||
pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {
|
||||
self.message = msg.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_code(mut self, code: u16) -> Self {
|
||||
self.error_code = code;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MapResult<S> {
|
||||
fn map_res(self, msg: &str) -> Result<S, Error>;
|
||||
}
|
||||
|
||||
impl<S, E: Into<Error>> MapResult<S> for Result<S, E> {
|
||||
fn map_res(self, msg: &str) -> Result<S, Error> {
|
||||
self.map_err(|e| e.into().with_msg(msg))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
|
||||
fn map_res(self, msg: &str) -> Result<(), Error> {
|
||||
self.and(Ok(())).map_res(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> MapResult<S> for Option<S> {
|
||||
fn map_res(self, msg: &str) -> Result<S, Error> {
|
||||
self.ok_or_else(|| Error::new(msg, ""))
|
||||
}
|
||||
}
|
||||
|
||||
fn _has_source<T>(e: T) -> Option<T> {
|
||||
Some(e)
|
||||
}
|
||||
fn _no_source<T, S>(_: T) -> Option<S> {
|
||||
None
|
||||
}
|
||||
|
||||
fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String {
|
||||
serde_json::to_string(e).unwrap()
|
||||
}
|
||||
|
||||
fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
|
||||
let json = json!({
|
||||
"Message": "",
|
||||
"error": "",
|
||||
"error_description": "",
|
||||
"ValidationErrors": {"": [ msg ]},
|
||||
"ErrorModel": {
|
||||
"Message": msg,
|
||||
"Object": "error"
|
||||
},
|
||||
"Object": "error"
|
||||
});
|
||||
_serialize(&json, "")
|
||||
}
|
||||
|
||||
//
|
||||
// Rocket responder impl
|
||||
//
|
||||
use std::io::Cursor;
|
||||
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, Responder, Response};
|
||||
|
||||
impl<'r> Responder<'r> for Error {
|
||||
fn respond_to(self, _: &Request) -> response::Result<'r> {
|
||||
let usr_msg = format!("{}", self);
|
||||
error!("{:#?}", self);
|
||||
|
||||
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
||||
|
||||
Response::build()
|
||||
.status(code)
|
||||
.header(ContentType::JSON)
|
||||
.sized_body(Cursor::new(usr_msg))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Error return macros
|
||||
//
|
||||
#[macro_export]
|
||||
macro_rules! err {
|
||||
($msg:expr) => {{
|
||||
return Err(crate::error::Error::new($msg, $msg));
|
||||
}};
|
||||
($usr_msg:expr, $log_value:expr) => {{
|
||||
return Err(crate::error::Error::new($usr_msg, $log_value));
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! err_json {
|
||||
($expr:expr) => {{
|
||||
return Err(crate::error::Error::from($expr));
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! err_handler {
|
||||
($expr:expr) => {{
|
||||
error!("Unauthorized Error: {}", $expr);
|
||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
|
||||
}};
|
||||
($usr_msg:expr, $log_value:expr) => {{
|
||||
error!("Unauthorized Error: {}. {}", $usr_msg, $log_value);
|
||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
|
||||
}};
|
||||
}
|
174
src/mail.rs
174
src/mail.rs
@@ -1,174 +0,0 @@
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
|
||||
use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder};
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
use quoted_printable::encode_to_str;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::auth::{encode_jwt, generate_invite_claims};
|
||||
use crate::error::Error;
|
||||
use crate::CONFIG;
|
||||
|
||||
fn mailer() -> SmtpTransport {
|
||||
let host = CONFIG.smtp_host().unwrap();
|
||||
|
||||
let client_security = if CONFIG.smtp_ssl() {
|
||||
let tls = TlsConnector::builder()
|
||||
.min_protocol_version(Some(Protocol::Tlsv11))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let params = ClientTlsParameters::new(host.clone(), tls);
|
||||
|
||||
if CONFIG.smtp_explicit_tls() {
|
||||
ClientSecurity::Wrapper(params)
|
||||
} else {
|
||||
ClientSecurity::Required(params)
|
||||
}
|
||||
} else {
|
||||
ClientSecurity::None
|
||||
};
|
||||
|
||||
let smtp_client = SmtpClient::new((host.as_str(), CONFIG.smtp_port()), client_security).unwrap();
|
||||
|
||||
let smtp_client = match (&CONFIG.smtp_username(), &CONFIG.smtp_password()) {
|
||||
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
|
||||
_ => smtp_client,
|
||||
};
|
||||
|
||||
smtp_client
|
||||
.smtp_utf8(true)
|
||||
.connection_reuse(ConnectionReuseParameters::NoReuse)
|
||||
.transport()
|
||||
}
|
||||
|
||||
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
|
||||
let (subject_html, body_html) = get_template(&format!("{}.html", template_name), &data)?;
|
||||
let (_subject_text, body_text) = get_template(template_name, &data)?;
|
||||
Ok((subject_html, body_html, body_text))
|
||||
}
|
||||
|
||||
fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String, String), Error> {
|
||||
let text = CONFIG.render_template(template_name, data)?;
|
||||
let mut text_split = text.split("<!---------------->");
|
||||
|
||||
let subject = match text_split.next() {
|
||||
Some(s) => s.trim().to_string(),
|
||||
None => err!("Template doesn't contain subject"),
|
||||
};
|
||||
|
||||
let body = match text_split.next() {
|
||||
Some(s) => s.trim().to_string(),
|
||||
None => err!("Template doesn't contain body"),
|
||||
};
|
||||
|
||||
Ok((subject, body))
|
||||
}
|
||||
|
||||
pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
||||
let template_name = if hint.is_some() {
|
||||
"email/pw_hint_some"
|
||||
} else {
|
||||
"email/pw_hint_none"
|
||||
};
|
||||
|
||||
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
||||
|
||||
send_email(&address, &subject, &body_html, &body_text)
|
||||
}
|
||||
|
||||
pub fn send_invite(
|
||||
address: &str,
|
||||
uuid: &str,
|
||||
org_id: Option<String>,
|
||||
org_user_id: Option<String>,
|
||||
org_name: &str,
|
||||
invited_by_email: Option<String>,
|
||||
) -> EmptyResult {
|
||||
let claims = generate_invite_claims(
|
||||
uuid.to_string(),
|
||||
String::from(address),
|
||||
org_id.clone(),
|
||||
org_user_id.clone(),
|
||||
invited_by_email.clone(),
|
||||
);
|
||||
let invite_token = encode_jwt(&claims);
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/send_org_invite",
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
"org_id": org_id.unwrap_or_else(|| "_".to_string()),
|
||||
"org_user_id": org_user_id.unwrap_or_else(|| "_".to_string()),
|
||||
"email": address,
|
||||
"org_name": org_name,
|
||||
"token": invite_token,
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(&address, &subject, &body_html, &body_text)
|
||||
}
|
||||
|
||||
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/invite_accepted",
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
"email": new_user_email,
|
||||
"org_name": org_name,
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(&address, &subject, &body_html, &body_text)
|
||||
}
|
||||
|
||||
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/invite_confirmed",
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
"org_name": org_name,
|
||||
}),
|
||||
)?;
|
||||
|
||||
send_email(&address, &subject, &body_html, &body_text)
|
||||
}
|
||||
|
||||
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
||||
let html = PartBuilder::new()
|
||||
.body(encode_to_str(body_html))
|
||||
.header(("Content-Type", "text/html; charset=utf-8"))
|
||||
.header(("Content-Transfer-Encoding", "quoted-printable"))
|
||||
.build();
|
||||
|
||||
let text = PartBuilder::new()
|
||||
.body(encode_to_str(body_text))
|
||||
.header(("Content-Type", "text/plain; charset=utf-8"))
|
||||
.header(("Content-Transfer-Encoding", "quoted-printable"))
|
||||
.build();
|
||||
|
||||
let alternative = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(text)
|
||||
.child(html);
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to(address)
|
||||
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
|
||||
.subject(subject)
|
||||
.child(alternative.build())
|
||||
.build()
|
||||
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
||||
|
||||
let mut transport = mailer();
|
||||
|
||||
let result = transport
|
||||
.send(email.into())
|
||||
.map_err(|e| Error::new("Error sending email", e.to_string()))
|
||||
.and(Ok(()));
|
||||
|
||||
// Explicitly close the connection, in case of error
|
||||
transport.close();
|
||||
result
|
||||
}
|
293
src/main.rs
293
src/main.rs
@@ -1,251 +1,172 @@
|
||||
#![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[macro_use]
|
||||
#![feature(plugin, custom_derive)]
|
||||
#![plugin(rocket_codegen)]
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
extern crate reqwest;
|
||||
extern crate multipart;
|
||||
extern crate serde;
|
||||
#[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;
|
||||
extern crate ring;
|
||||
extern crate uuid;
|
||||
extern crate chrono;
|
||||
extern crate oath;
|
||||
extern crate data_encoding;
|
||||
extern crate jsonwebtoken as jwt;
|
||||
extern crate dotenv;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate derive_more;
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
|
||||
use std::{
|
||||
path::Path,
|
||||
process::{exit, Command},
|
||||
};
|
||||
use std::{io, env, path::Path, process::{exit, Command}};
|
||||
use rocket::Rocket;
|
||||
|
||||
#[macro_use]
|
||||
mod error;
|
||||
mod api;
|
||||
mod auth;
|
||||
mod config;
|
||||
mod crypto;
|
||||
mod db;
|
||||
mod mail;
|
||||
mod util;
|
||||
|
||||
pub use config::CONFIG;
|
||||
pub use error::{Error, MapResult};
|
||||
mod api;
|
||||
mod db;
|
||||
mod crypto;
|
||||
mod auth;
|
||||
|
||||
fn init_rocket() -> Rocket {
|
||||
rocket::ignite()
|
||||
.mount("/", api::web_routes())
|
||||
.mount("/api", api::core_routes())
|
||||
.mount("/identity", api::identity_routes())
|
||||
.mount("/icons", api::icons_routes())
|
||||
.manage(db::init_pool())
|
||||
}
|
||||
|
||||
// Embed the migrations from the migrations folder into the application
|
||||
// This way, the program automatically migrates the database to the latest version
|
||||
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
|
||||
embed_migrations!();
|
||||
|
||||
fn main() {
|
||||
launch_info();
|
||||
|
||||
if CONFIG.extended_logging() {
|
||||
init_logging().ok();
|
||||
}
|
||||
|
||||
check_db();
|
||||
check_rsa_keys();
|
||||
check_web_vault();
|
||||
migrations::run_migrations();
|
||||
|
||||
launch_rocket();
|
||||
}
|
||||
// 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");
|
||||
embedded_migrations::run_with_output(&connection, &mut io::stdout()).expect("Can't run migrations");
|
||||
|
||||
fn launch_info() {
|
||||
println!("/--------------------------------------------------------------------\\");
|
||||
println!("| Starting Bitwarden_RS |");
|
||||
|
||||
if let Some(version) = option_env!("GIT_VERSION") {
|
||||
println!("|{:^68}|", format!("Version {}", version));
|
||||
}
|
||||
|
||||
println!("|--------------------------------------------------------------------|");
|
||||
println!("| This is an *unofficial* Bitwarden implementation, DO NOT use the |");
|
||||
println!("| official channels to report bugs/features, regardless of client. |");
|
||||
println!("| Report URL: https://github.com/dani-garcia/bitwarden_rs/issues/new |");
|
||||
println!("\\--------------------------------------------------------------------/\n");
|
||||
}
|
||||
|
||||
fn init_logging() -> Result<(), fern::InitError> {
|
||||
use std::str::FromStr;
|
||||
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::from_str(&CONFIG.log_level()).expect("Valid log level"))
|
||||
// Hide unknown certificate errors if using self-signed
|
||||
.level_for("rustls::session", log::LevelFilter::Off)
|
||||
// Hide failed to close stream messages
|
||||
.level_for("hyper::server", log::LevelFilter::Warn)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
if let Some(log_file) = CONFIG.log_file() {
|
||||
logger = logger.chain(fern::log_file(log_file)?);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
|
||||
logger = chain_syslog(logger);
|
||||
}
|
||||
}
|
||||
|
||||
logger.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
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
|
||||
}
|
||||
}
|
||||
init_rocket().launch();
|
||||
}
|
||||
|
||||
fn check_db() {
|
||||
let url = CONFIG.database_url();
|
||||
let path = Path::new(&url);
|
||||
let path = Path::new(&CONFIG.database_url);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
use std::fs;
|
||||
if fs::create_dir_all(parent).is_err() {
|
||||
error!("Error creating database directory");
|
||||
println!("Error creating database directory");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Turn on WAL in SQLite
|
||||
if CONFIG.enable_db_wal() {
|
||||
use diesel::RunQueryDsl;
|
||||
let connection = db::get_connection().expect("Can't conect to DB");
|
||||
diesel::sql_query("PRAGMA journal_mode=wal")
|
||||
.execute(&connection)
|
||||
.expect("Failed to turn on WAL");
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
info!("JWT keys don't exist, checking if OpenSSL is available...");
|
||||
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...");
|
||||
|
||||
Command::new("openssl").arg("version").status().unwrap_or_else(|_| {
|
||||
info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
|
||||
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");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
info!("OpenSSL detected, creating keys...");
|
||||
println!("OpenSSL detected, creating keys...");
|
||||
|
||||
let key = CONFIG.rsa_key_filename();
|
||||
let mut success = Command::new("openssl").arg("genrsa")
|
||||
.arg("-out").arg(&CONFIG.private_rsa_key_pem)
|
||||
.output().expect("Failed to create private pem file")
|
||||
.status.success();
|
||||
|
||||
let pem = format!("{}.pem", key);
|
||||
let priv_der = format!("{}.der", key);
|
||||
let pub_der = format!("{}.pub.der", key);
|
||||
success &= Command::new("openssl").arg("rsa")
|
||||
.arg("-in").arg(&CONFIG.private_rsa_key_pem)
|
||||
.arg("-outform").arg("DER")
|
||||
.arg("-out").arg(&CONFIG.private_rsa_key)
|
||||
.output().expect("Failed to create private der file")
|
||||
.status.success();
|
||||
|
||||
let mut success = Command::new("openssl")
|
||||
.args(&["genrsa", "-out", &pem])
|
||||
.status()
|
||||
.expect("Failed to create private pem file")
|
||||
.success();
|
||||
|
||||
success &= Command::new("openssl")
|
||||
.args(&["rsa", "-in", &pem, "-outform", "DER", "-out", &priv_der])
|
||||
.status()
|
||||
.expect("Failed to create private der file")
|
||||
.success();
|
||||
|
||||
success &= Command::new("openssl")
|
||||
.args(&["rsa", "-in", &priv_der, "-inform", "DER"])
|
||||
.args(&["-RSAPublicKey_out", "-outform", "DER", "-out", &pub_der])
|
||||
.status()
|
||||
.expect("Failed to create public der file")
|
||||
.success();
|
||||
success &= Command::new("openssl").arg("rsa")
|
||||
.arg("-in").arg(&CONFIG.private_rsa_key)
|
||||
.arg("-inform").arg("DER")
|
||||
.arg("-RSAPublicKey_out")
|
||||
.arg("-outform").arg("DER")
|
||||
.arg("-out").arg(&CONFIG.public_rsa_key)
|
||||
.output().expect("Failed to create public der file")
|
||||
.status.success();
|
||||
|
||||
if success {
|
||||
info!("Keys created correctly.");
|
||||
println!("Keys created correctly.");
|
||||
} else {
|
||||
error!("Error creating keys, exiting...");
|
||||
println!("Error creating keys, exiting...");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_web_vault() {
|
||||
if !CONFIG.web_vault_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
|
||||
let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
|
||||
|
||||
if !index_path.exists() {
|
||||
error!("Web vault is not found. To install it, please follow the steps in https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault");
|
||||
println!("Web vault is not found. Please follow the steps in the README to install it");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Embed the migrations from the migrations folder into the application
|
||||
// This way, the program automatically migrates the database to the latest version
|
||||
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
|
||||
#[allow(unused_imports)]
|
||||
mod migrations {
|
||||
embed_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 = crate::db::get_connection().expect("Can't connect to DB");
|
||||
|
||||
use std::io::stdout;
|
||||
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
|
||||
}
|
||||
lazy_static! {
|
||||
// Load the config from .env or from environment variables
|
||||
static ref CONFIG: Config = Config::load();
|
||||
}
|
||||
|
||||
fn launch_rocket() {
|
||||
// Create Rocket object, this stores current log level and sets it's own
|
||||
let rocket = rocket::ignite();
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
database_url: String,
|
||||
icon_cache_folder: String,
|
||||
attachments_folder: String,
|
||||
|
||||
// If we aren't logging the mounts, we force the logging level down
|
||||
if !CONFIG.log_mounts() {
|
||||
log::set_max_level(log::LevelFilter::Warn);
|
||||
}
|
||||
private_rsa_key: String,
|
||||
private_rsa_key_pem: String,
|
||||
public_rsa_key: String,
|
||||
|
||||
let rocket = rocket
|
||||
.mount("/", api::web_routes())
|
||||
.mount("/api", api::core_routes())
|
||||
.mount("/admin", api::admin_routes())
|
||||
.mount("/identity", api::identity_routes())
|
||||
.mount("/icons", api::icons_routes())
|
||||
.mount("/notifications", api::notifications_routes());
|
||||
web_vault_folder: String,
|
||||
|
||||
// Force the level up for the fairings, managed state and lauch
|
||||
if !CONFIG.log_mounts() {
|
||||
log::set_max_level(log::LevelFilter::max());
|
||||
}
|
||||
|
||||
let rocket = rocket
|
||||
.manage(db::init_pool())
|
||||
.manage(api::start_notification_server())
|
||||
.attach(util::AppHeaders());
|
||||
|
||||
// Launch and print error if there is one
|
||||
// The launch will restore the original logging level
|
||||
error!("Launch error {:#?}", rocket.launch());
|
||||
signups_allowed: bool,
|
||||
password_iterations: i32,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn load() -> Self {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let df = env::var("DATA_FOLDER").unwrap_or("data".into());
|
||||
let key = env::var("RSA_KEY_NAME").unwrap_or("rsa_key".into());
|
||||
|
||||
Config {
|
||||
database_url: env::var("DATABASE_URL").unwrap_or(format!("{}/{}", &df, "db.sqlite3")),
|
||||
icon_cache_folder: env::var("ICON_CACHE_FOLDER").unwrap_or(format!("{}/{}", &df, "icon_cache")),
|
||||
attachments_folder: env::var("ATTACHMENTS_FOLDER").unwrap_or(format!("{}/{}", &df, "attachments")),
|
||||
|
||||
private_rsa_key: format!("{}/{}.der", &df, &key),
|
||||
private_rsa_key_pem: format!("{}/{}.pem", &df, &key),
|
||||
public_rsa_key: format!("{}/{}.pub.der", &df, &key),
|
||||
|
||||
web_vault_folder: env::var("WEB_VAULT_FOLDER").unwrap_or("web-vault/".into()),
|
||||
|
||||
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(false),
|
||||
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 344 B |
@@ -1,62 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Bitwarden_rs Admin Panel</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css"
|
||||
integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
|
||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.10.0/js/md5.min.js"
|
||||
integrity="sha256-J9IhvkIJb0diRVJOyu+Ndtg41RibFkF8eaA60jdjtB8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js"
|
||||
integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.min.js"
|
||||
integrity="sha256-fzFFyH01cBVPYzl16KT40wqjhgPtq6FFUB6ckN2+GGw=" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
@media (max-width:768px) {
|
||||
body {
|
||||
padding-top: 190px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top shadow">
|
||||
<a class="navbar-brand" href="#">Bitwarden_rs</a>
|
||||
<div class="navbar-collapse">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/admin">Admin Panel</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Vault</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{#if version}}
|
||||
<div class="navbar-text">Version: {{version}}</div>
|
||||
{{/if}}
|
||||
</nav>
|
||||
|
||||
{{> (page_content) }}
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -1,21 +0,0 @@
|
||||
<main class="container">
|
||||
{{#if error}}
|
||||
<div class="align-items-center p-3 mb-3 text-white-50 bg-warning rounded shadow">
|
||||
<div>
|
||||
<h6 class="mb-0 text-white">{{error}}</h6>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow">
|
||||
<div>
|
||||
<h6 class="mb-0 text-white">Authentication key needed to continue</h6>
|
||||
<small>Please provide it below:</small>
|
||||
|
||||
<form class="form-inline" method="post">
|
||||
<input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@@ -1,310 +0,0 @@
|
||||
<main class="container">
|
||||
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
|
||||
<h6 class="border-bottom pb-2 mb-0">Registered Users</h6>
|
||||
|
||||
<div id="users-list">
|
||||
{{#each users}}
|
||||
<div class="media pt-3">
|
||||
<img class="mr-2 rounded identicon" data-src="{{Email}}">
|
||||
<div class="media-body pb-3 mb-0 small border-bottom">
|
||||
<div class="row justify-content-between">
|
||||
<div class="col">
|
||||
<strong>{{Name}}</strong>
|
||||
{{#if TwoFactorEnabled}}
|
||||
<span class="badge badge-success ml-2">2FA</span>
|
||||
{{/if}}
|
||||
{{#case _Status 1}}
|
||||
<span class="badge badge-warning ml-2">Invited</span>
|
||||
{{/case}}
|
||||
<span class="d-block">{{Email}}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<strong> Organizations:</strong>
|
||||
<span class="d-block">
|
||||
{{#each Organizations}}
|
||||
<span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span>
|
||||
{{/each}}
|
||||
</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 240px;">
|
||||
<a class="mr-3" href="#" onclick='deauthUser({{jsesc Id}})'>Deauthorize sessions</a>
|
||||
<a class="mr-3" href="#" onclick='deleteUser({{jsesc Id}}, {{jsesc Email}})'>Delete User</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-sm btn-link" onclick="updateRevisions();"
|
||||
title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data.">
|
||||
Force clients to resync
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-primary float-right" onclick="reload();">Reload users</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
||||
<div>
|
||||
<h6 class="mb-0 text-white">Invite User</h6>
|
||||
<small>Email:</small>
|
||||
|
||||
<form class="form-inline" id="invite-form">
|
||||
<input type="email" class="form-control w-50 mr-2" id="email-invite" placeholder="Enter email">
|
||||
<button type="submit" class="btn btn-primary">Invite</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
|
||||
<div>
|
||||
<h6 class="text-white mb-3">Configuration</h6>
|
||||
<div class="small text-white mb-3">
|
||||
NOTE: The settings here override the environment variables. Once saved, it's recommended to stop setting
|
||||
them to avoid confusion. This does not apply to the read-only section, which can only be set through the
|
||||
environment.
|
||||
</div>
|
||||
<form class="form accordion" id="config-form">
|
||||
{{#each config}}
|
||||
{{#if groupdoc}}
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
|
||||
data-target="#g_{{group}}">{{groupdoc}}</button></div>
|
||||
<div id="g_{{group}}" class="card-body collapse" data-parent="#config-form">
|
||||
{{#each elements}}
|
||||
{{#if editable}}
|
||||
<div class="form-group row" title="[{{name}}] {{doc.description}}">
|
||||
{{#case type "text" "number" "password"}}
|
||||
<label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label>
|
||||
<div class="col-sm-8 input-group">
|
||||
<input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}"
|
||||
name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"
|
||||
{{/if}}>
|
||||
|
||||
{{#case type "password"}}
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="toggleVis('#input_{{name}}');">Show/hide</button>
|
||||
</div>
|
||||
{{/case}}
|
||||
</div>
|
||||
{{/case}}
|
||||
{{#case type "checkbox"}}
|
||||
<div class="col-sm-3">{{doc.name}}</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input conf-{{type}}" type="checkbox" id="input_{{name}}"
|
||||
name="{{name}}" {{#if value}} checked {{/if}}>
|
||||
|
||||
<label class="form-check-label" for="input_{{name}}"> Default: {{default}} </label>
|
||||
</div>
|
||||
</div>
|
||||
{{/case}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
|
||||
data-target="#g_readonly">Read-Only Config</button></div>
|
||||
<div id="g_readonly" class="card-body collapse" data-parent="#config-form">
|
||||
<div class="small mb-3">
|
||||
NOTE: These options can't be modified in the editor because they would require the server
|
||||
to be restarted. To modify them, you need to set the correct environment variables when
|
||||
launching the server. You can check the variable names in the tooltips of each option.
|
||||
</div>
|
||||
|
||||
{{#each config}}
|
||||
{{#each elements}}
|
||||
{{#unless editable}}
|
||||
<div class="form-group row" title="[{{name}}] {{doc.description}}">
|
||||
{{#case type "text" "number" "password"}}
|
||||
<label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label>
|
||||
<div class="col-sm-8 input-group">
|
||||
<input readonly class="form-control" id="input_{{name}}" type="{{type}}"
|
||||
value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
||||
|
||||
{{#case type "password"}}
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="toggleVis('#input_{{name}}');">Show/hide</button>
|
||||
</div>
|
||||
{{/case}}
|
||||
</div>
|
||||
{{/case}}
|
||||
{{#case type "checkbox"}}
|
||||
<div class="col-sm-3">{{doc.name}}</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
<input disabled class="form-check-input" type="checkbox" id="input_{{name}}"
|
||||
{{#if value}} checked {{/if}}>
|
||||
|
||||
<label class="form-check-label" for="input_{{name}}"> Default: {{default}} </label>
|
||||
</div>
|
||||
</div>
|
||||
{{/case}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-danger float-right" onclick="deleteConf();">Reset defaults</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#config-block ::placeholder {
|
||||
/* Most modern browsers support this now. */
|
||||
color: orangered;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function reload() { window.location.reload(); }
|
||||
function identicon(email) {
|
||||
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
|
||||
return "data:image/svg+xml;base64," + data.toString();
|
||||
}
|
||||
function toggleVis(input_id) {
|
||||
var type = $(input_id).attr("type");
|
||||
if (type === "text") {
|
||||
$(input_id).attr("type", "password");
|
||||
} else {
|
||||
$(input_id).attr("type", "text");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function _post(url, successMsg, errMsg, data) {
|
||||
$.post({
|
||||
url: url,
|
||||
data: data,
|
||||
//async: false,
|
||||
contentType: "application/json",
|
||||
}).done(function () {
|
||||
alert(successMsg);
|
||||
}).fail(function (e) {
|
||||
const r = e.responseJSON;
|
||||
const msg = r ? r.ErrorModel.Message : "Unknown error";
|
||||
alert(errMsg + ": " + msg);
|
||||
}).always(reload);
|
||||
}
|
||||
function deleteUser(id, mail) {
|
||||
var input_mail = prompt("To delete user '" + mail + "', please type the name below")
|
||||
if (input_mail != null) {
|
||||
if (input_mail == mail) {
|
||||
_post("/admin/users/" + id + "/delete",
|
||||
"User deleted correctly",
|
||||
"Error deleting user");
|
||||
} else {
|
||||
alert("Wrong email, please try again")
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function deauthUser(id) {
|
||||
_post("/admin/users/" + id + "/deauth",
|
||||
"Sessions deauthorized correctly",
|
||||
"Error deauthorizing sessions");
|
||||
return false;
|
||||
}
|
||||
function updateRevisions() {
|
||||
_post("/admin/users/update_revision",
|
||||
"Success, clients will sync next time they connect",
|
||||
"Error forcing clients to sync");
|
||||
return false;
|
||||
}
|
||||
function inviteUser() {
|
||||
inv = $("#email-invite");
|
||||
data = JSON.stringify({ "email": inv.val() });
|
||||
inv.val("");
|
||||
_post("/admin/invite/", "User invited correctly",
|
||||
"Error inviting user", data);
|
||||
return false;
|
||||
}
|
||||
function getFormData() {
|
||||
let data = {};
|
||||
|
||||
$(".conf-checkbox").each(function (i, e) {
|
||||
data[e.name] = $(e).is(":checked");
|
||||
});
|
||||
|
||||
$(".conf-number").each(function (i, e) {
|
||||
data[e.name] = +e.value;
|
||||
});
|
||||
|
||||
$(".conf-text, .conf-password").each(function (i, e) {
|
||||
data[e.name] = e.value || null;
|
||||
});
|
||||
return data;
|
||||
}
|
||||
function saveConfig() {
|
||||
data = JSON.stringify(getFormData());
|
||||
_post("/admin/config/", "Config saved correctly",
|
||||
"Error saving config", data);
|
||||
return false;
|
||||
}
|
||||
function deleteConf() {
|
||||
var input = prompt("This will remove all user configurations, and restore the defaults and the " +
|
||||
"values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:");
|
||||
if (input === "DELETE") {
|
||||
_post("/admin/config/delete",
|
||||
"Config deleted correctly",
|
||||
"Error deleting config");
|
||||
} else {
|
||||
alert("Wrong input, please try again")
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function masterCheck(check_id, inputs_query) {
|
||||
function toggleEnabled(check_id, inputs_query, enabled) {
|
||||
$(inputs_query).prop("disabled", !enabled)
|
||||
if (!enabled)
|
||||
$(inputs_query).val("");
|
||||
$(check_id).prop("disabled", false);
|
||||
};
|
||||
function onChanged(check_id, inputs_query) {
|
||||
return function _fn() { toggleEnabled(check_id, inputs_query, this.checked); };
|
||||
};
|
||||
|
||||
toggleEnabled(check_id, inputs_query, $(check_id).is(":checked"));
|
||||
$(check_id).change(onChanged(check_id, inputs_query));
|
||||
|
||||
}
|
||||
let OrgTypes = {
|
||||
"0": { "name": "Owner", "color": "orange" },
|
||||
"1": { "name": "Admin", "color": "blueviolet" },
|
||||
"2": { "name": "User", "color": "blue" },
|
||||
"3": { "name": "Manager", "color": "green" },
|
||||
};
|
||||
$(window).on('load', function () {
|
||||
$("#invite-form").submit(inviteUser);
|
||||
$("#config-form").submit(saveConfig);
|
||||
$("img.identicon").each(function (i, e) {
|
||||
e.src = identicon(e.dataset.src);
|
||||
});
|
||||
$('[data-orgtype]').each(function (i, e) {
|
||||
let orgtype = OrgTypes[e.dataset.orgtype];
|
||||
e.style.backgroundColor = orgtype.color;
|
||||
e.title = orgtype.name;
|
||||
});
|
||||
|
||||
// These are formatted because otherwise the
|
||||
// VSCode formatter breaks But they still work
|
||||
// {{#each config}} {{#if grouptoggle}}
|
||||
masterCheck("#input_{{grouptoggle}}", "#g_{{group}} input");
|
||||
// {{/if}} {{/each}}
|
||||
});
|
||||
</script>
|
@@ -1,8 +0,0 @@
|
||||
Invitation accepted
|
||||
<!---------------->
|
||||
<html>
|
||||
<p>
|
||||
Your invitation for <b>{{email}}</b> to join <b>{{org_name}}</b> was accepted.
|
||||
Please <a href="{{url}}">log in</a> to the bitwarden_rs server and confirm them from the organization management page.
|
||||
</p>
|
||||
</html>
|
@@ -1,138 +0,0 @@
|
||||
Invitation accepted
|
||||
<!---------------->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Bitwarden_rs</title>
|
||||
</head>
|
||||
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body * {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 25px;
|
||||
}
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.container-table {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.content {
|
||||
padding: 0 0 10px 0 !important;
|
||||
}
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
.main {
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.logo {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
.indented {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
This email is to notify you that {{email}} has accepted your invitation to join <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Please <a href="{{url}}">log in</a> to the bitwarden_rs server and confirm them from the organization management page.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
If you do not wish to confirm this user, you can also remove them from the organization on the same page.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@@ -1,8 +0,0 @@
|
||||
Invitation to {{org_name}} confirmed
|
||||
<!---------------->
|
||||
<html>
|
||||
<p>
|
||||
Your invitation to join <b>{{org_name}}</b> was confirmed.
|
||||
It will now appear under the Organizations the next time you <a href="{{url}}">log in</a> to the web vault.
|
||||
</p>
|
||||
</html>
|
@@ -1,134 +0,0 @@
|
||||
Invitation to {{org_name}} confirmed
|
||||
<!---------------->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Bitwarden_rs</title>
|
||||
</head>
|
||||
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body * {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 25px;
|
||||
}
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.container-table {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.content {
|
||||
padding: 0 0 10px 0 !important;
|
||||
}
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
.main {
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.logo {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
.indented {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
This email is to notify you that you have been confirmed as a user of <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
Any collections and logins being shared with you by this organization will now appear in your Bitwarden vault. <br>
|
||||
<a href="{{url}}">Log in</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@@ -1,3 +0,0 @@
|
||||
Sorry, you have no password hint...
|
||||
<!---------------->
|
||||
Sorry, you have not specified any password hint...
|
@@ -1,133 +0,0 @@
|
||||
Sorry, you have no password hint...
|
||||
<!---------------->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Bitwarden_rs</title>
|
||||
</head>
|
||||
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body * {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 25px;
|
||||
}
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.container-table {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.content {
|
||||
padding: 0 0 10px 0 !important;
|
||||
}
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
.main {
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.logo {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
.indented {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
If you did not request your master password hint you can safely ignore this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@@ -1,7 +0,0 @@
|
||||
Your master password hint
|
||||
<!---------------->
|
||||
You (or someone) recently requested your master password hint.
|
||||
|
||||
Your hint is: "{{hint}}"
|
||||
|
||||
If you did not request your master password hint you can safely ignore this email.
|
@@ -1,139 +0,0 @@
|
||||
Your master password hint
|
||||
<!---------------->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Bitwarden_rs</title>
|
||||
</head>
|
||||
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body * {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 25px;
|
||||
}
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.container-table {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.content {
|
||||
padding: 0 0 10px 0 !important;
|
||||
}
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
.main {
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.logo {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
.indented {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
You (or someone) recently requested your master password hint.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Your hint is: "{{hint}}"<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
Log in: <a href="{{url}}">Web Vault</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
If you did not request your master password hint you can safely ignore this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@@ -1,12 +0,0 @@
|
||||
Join {{org_name}}
|
||||
<!---------------->
|
||||
<html>
|
||||
<p>
|
||||
You have been invited to join the <b>{{org_name}}</b> organization.
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{url}}/#/accept-organization/?organizationId={{org_id}}&organizationUserId={{org_user_id}}&email={{email}}&organizationName={{org_name}}&token={{token}}">
|
||||
Click here to join</a>
|
||||
</p>
|
||||
<p>If you do not wish to join this organization, you can safely ignore this email.</p>
|
||||
</html>
|
@@ -1,141 +0,0 @@
|
||||
Join {{org_name}}
|
||||
<!---------------->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Bitwarden_rs</title>
|
||||
</head>
|
||||
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body * {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 25px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 25px;
|
||||
}
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.container-table {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.content {
|
||||
padding: 0 0 10px 0 !important;
|
||||
}
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
.main {
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.logo {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
.indented {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
You have been invited to join the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> organization.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{url}}/#/accept-organization/?organizationId={{org_id}}&organizationUserId={{org_user_id}}&email={{email}}&organizationName={{org_name}}&token={{token}}"
|
||||
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Join Organization Now
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
If you do not wish to join this organization, you can safely ignore this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
285
src/util.rs
285
src/util.rs
@@ -1,93 +1,64 @@
|
||||
//
|
||||
// Web Headers and caching
|
||||
//
|
||||
use rocket::fairing::{Fairing, Info, Kind};
|
||||
use rocket::response::{self, Responder};
|
||||
use rocket::{Request, Response};
|
||||
|
||||
pub struct AppHeaders();
|
||||
|
||||
impl Fairing for AppHeaders {
|
||||
fn info(&self) -> Info {
|
||||
Info {
|
||||
name: "Application Headers",
|
||||
kind: Kind::Response,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_response(&self, _req: &Request, res: &mut Response) {
|
||||
res.set_raw_header("Feature-Policy", "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; sync-xhr 'self' https://haveibeenpwned.com https://twofactorauth.org; usb 'none'; vr 'none'");
|
||||
res.set_raw_header("Referrer-Policy", "same-origin");
|
||||
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
||||
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
||||
res.set_raw_header("X-XSS-Protection", "1; mode=block");
|
||||
let csp = "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://*;";
|
||||
res.set_raw_header("Content-Security-Policy", csp);
|
||||
|
||||
// Disable cache unless otherwise specified
|
||||
if !res.headers().contains("cache-control") {
|
||||
res.set_raw_header("Cache-Control", "no-cache, no-store, max-age=0");
|
||||
}
|
||||
}
|
||||
///
|
||||
/// Macros
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! err {
|
||||
($err:expr, $err_desc:expr, $msg:expr) => {
|
||||
err_json!(json!({
|
||||
"error": $err,
|
||||
"error_description": $err_desc,
|
||||
"ErrorModel": {
|
||||
"Message": $msg,
|
||||
"ValidationErrors": null,
|
||||
"Object": "error"
|
||||
}
|
||||
}))
|
||||
};
|
||||
($msg:expr) => { err!("default_error", "default_error_description", $msg) }
|
||||
}
|
||||
|
||||
pub struct Cached<R>(R, &'static str);
|
||||
|
||||
impl<R> Cached<R> {
|
||||
pub fn long(r: R) -> Cached<R> {
|
||||
// 7 days
|
||||
Cached(r, "public, max-age=604800")
|
||||
}
|
||||
|
||||
pub fn short(r: R) -> Cached<R> {
|
||||
// 10 minutes
|
||||
Cached(r, "public, max-age=600")
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! err_json {
|
||||
($expr:expr) => {{
|
||||
println!("ERROR: {}", $expr);
|
||||
return Err($crate::rocket::response::status::BadRequest(Some($crate::rocket_contrib::Json($expr))));
|
||||
}}
|
||||
}
|
||||
|
||||
impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
|
||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||
match self.0.respond_to(req) {
|
||||
Ok(mut res) => {
|
||||
res.set_raw_header("Cache-Control", self.1);
|
||||
Ok(res)
|
||||
}
|
||||
e @ Err(_) => e,
|
||||
}
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! err_handler {
|
||||
($expr:expr) => {{
|
||||
println!("ERROR: {}", $expr);
|
||||
return $crate::rocket::Outcome::Failure(($crate::rocket::http::Status::Unauthorized, $expr));
|
||||
}}
|
||||
}
|
||||
|
||||
//
|
||||
// File handling
|
||||
//
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Result as IOResult};
|
||||
///
|
||||
/// File handling
|
||||
///
|
||||
|
||||
use std::path::Path;
|
||||
use std::io::Read;
|
||||
use std::fs::{self, File};
|
||||
|
||||
pub fn file_exists(path: &str) -> bool {
|
||||
Path::new(path).exists()
|
||||
}
|
||||
|
||||
pub fn read_file(path: &str) -> IOResult<Vec<u8>> {
|
||||
pub fn read_file(path: &str) -> Result<Vec<u8>, String> {
|
||||
let mut file = File::open(Path::new(path))
|
||||
.map_err(|e| format!("Error opening file: {}", e))?;
|
||||
|
||||
let mut contents: Vec<u8> = Vec::new();
|
||||
|
||||
let mut file = File::open(Path::new(path))?;
|
||||
file.read_to_end(&mut contents)?;
|
||||
file.read_to_end(&mut contents)
|
||||
.map_err(|e| format!("Error reading file: {}", e))?;
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
pub fn read_file_string(path: &str) -> IOResult<String> {
|
||||
let mut contents = String::new();
|
||||
|
||||
let mut file = File::open(Path::new(path))?;
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
pub fn delete_file(path: &str) -> IOResult<()> {
|
||||
let res = fs::remove_file(path);
|
||||
pub fn delete_file(path: &str) -> bool {
|
||||
let res = fs::remove_file(path).is_ok();
|
||||
|
||||
if let Some(parent) = Path::new(path).parent() {
|
||||
// If the directory isn't empty, this returns an error, which we ignore
|
||||
@@ -98,10 +69,11 @@ pub fn delete_file(path: &str) -> IOResult<()> {
|
||||
res
|
||||
}
|
||||
|
||||
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
const UNITS: [&'static str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
pub fn get_display_size(size: i32) -> String {
|
||||
let mut size: f64 = size.into();
|
||||
let mut size = size as f64;
|
||||
let mut unit_counter = 0;
|
||||
|
||||
loop {
|
||||
@@ -111,22 +83,18 @@ pub fn get_display_size(size: i32) -> String {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Round to two decimals
|
||||
size = (size * 100.).round() / 100.;
|
||||
format!("{} {}", size, UNITS[unit_counter])
|
||||
}
|
||||
|
||||
pub fn get_uuid() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
//
|
||||
// String util methods
|
||||
//
|
||||
///
|
||||
/// String util methods
|
||||
///
|
||||
|
||||
use std::ops::Try;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn upcase_first(s: &str) -> String {
|
||||
@@ -137,53 +105,44 @@ pub fn upcase_first(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_parse_string<S, T, U>(string: impl Try<Ok = S, Error = U>) -> Option<T>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
T: FromStr,
|
||||
{
|
||||
if let Ok(Ok(value)) = string.into_result().map(|s| s.as_ref().parse::<T>()) {
|
||||
pub fn parse_option_string<S, T>(string: Option<S>) -> Option<T> where S: AsRef<str>, T: FromStr {
|
||||
if let Some(Ok(value)) = string.map(|s| s.as_ref().parse::<T>()) {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Env methods
|
||||
//
|
||||
|
||||
use std::env;
|
||||
|
||||
pub fn get_env<V>(key: &str) -> Option<V>
|
||||
where
|
||||
V: FromStr,
|
||||
{
|
||||
try_parse_string(env::var(key))
|
||||
}
|
||||
|
||||
//
|
||||
// Date util methods
|
||||
//
|
||||
///
|
||||
/// Date util methods
|
||||
///
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
|
||||
const DATETIME_FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S%.6fZ";
|
||||
|
||||
pub fn format_date(date: &NaiveDateTime) -> String {
|
||||
date.format(DATETIME_FORMAT).to_string()
|
||||
}
|
||||
|
||||
//
|
||||
// Deserialization methods
|
||||
//
|
||||
///
|
||||
/// Deserialization methods
|
||||
///
|
||||
|
||||
use std::fmt;
|
||||
use std::collections::BTreeMap as Map;
|
||||
|
||||
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||
use serde_json::{self, Value};
|
||||
use serde::de::{self, Deserialize, DeserializeOwned, Deserializer};
|
||||
use serde_json::Value;
|
||||
|
||||
pub type JsonMap = serde_json::Map<String, Value>;
|
||||
/// https://github.com/serde-rs/serde/issues/586
|
||||
pub fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where T: DeserializeOwned,
|
||||
D: Deserializer<'de>
|
||||
{
|
||||
let map = Map::<String, Value>::deserialize(deserializer)?;
|
||||
let lower = map.into_iter().map(|(k, v)| (upcase_first(&k), v)).collect();
|
||||
T::deserialize(Value::Object(lower)).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Serialize, Deserialize)]
|
||||
pub struct UpCase<T: DeserializeOwned> {
|
||||
@@ -191,105 +150,3 @@ pub struct UpCase<T: DeserializeOwned> {
|
||||
#[serde(flatten)]
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
// https://github.com/serde-rs/serde/issues/586
|
||||
pub fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let d = deserializer.deserialize_any(UpCaseVisitor)?;
|
||||
T::deserialize(d).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
struct UpCaseVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for UpCaseVisitor {
|
||||
type Value = Value;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an object or an array")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut result_map = JsonMap::new();
|
||||
|
||||
while let Some((key, value)) = map.next_entry()? {
|
||||
result_map.insert(upcase_first(key), upcase_value(value));
|
||||
}
|
||||
|
||||
Ok(Value::Object(result_map))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut result_seq = Vec::<Value>::new();
|
||||
|
||||
while let Some(value) = seq.next_element()? {
|
||||
result_seq.push(upcase_value(value));
|
||||
}
|
||||
|
||||
Ok(Value::Array(result_seq))
|
||||
}
|
||||
}
|
||||
|
||||
fn upcase_value(value: Value) -> Value {
|
||||
if let Value::Object(map) = value {
|
||||
let mut new_value = json!({});
|
||||
|
||||
for (key, val) in map.into_iter() {
|
||||
let processed_key = _process_key(&key);
|
||||
new_value[processed_key] = upcase_value(val);
|
||||
}
|
||||
new_value
|
||||
} else if let Value::Array(array) = value {
|
||||
// Initialize array with null values
|
||||
let mut new_value = json!(vec![Value::Null; array.len()]);
|
||||
|
||||
for (index, val) in array.into_iter().enumerate() {
|
||||
new_value[index] = upcase_value(val);
|
||||
}
|
||||
new_value
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fn _process_key(key: &str) -> String {
|
||||
match key.to_lowercase().as_ref() {
|
||||
"ssn" => "SSN".into(),
|
||||
_ => 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