geforkt von mirrored/vaultwarden
Merge branch 'master' of github.com:Skeen/bitwarden_rs
Dieser Commit ist enthalten in:
Commit
85c8a01f4a
39 geänderte Dateien mit 1687 neuen und 1076 gelöschten Zeilen
|
@ -35,7 +35,7 @@
|
||||||
## Enable extended logging
|
## Enable extended logging
|
||||||
## This shows timestamps and allows logging to file and to syslog
|
## This shows timestamps and allows logging to file and to syslog
|
||||||
### To enable logging to file, use the LOG_FILE env variable
|
### To enable logging to file, use the LOG_FILE env variable
|
||||||
### To enable syslog, you need to compile with `cargo build --features=enable_syslog'
|
### To enable syslog, use the USE_SYSLOG env variable
|
||||||
# EXTENDED_LOGGING=true
|
# EXTENDED_LOGGING=true
|
||||||
|
|
||||||
## Logging to file
|
## Logging to file
|
||||||
|
@ -43,6 +43,17 @@
|
||||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||||
# LOG_FILE=/path/to/log
|
# 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
|
## Enable WAL for the DB
|
||||||
## Set to false to avoid enabling WAL during startup.
|
## 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,
|
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
||||||
|
@ -62,6 +73,16 @@
|
||||||
## The default is 10 seconds, but this could be to low on slower network connections
|
## The default is 10 seconds, but this could be to low on slower network connections
|
||||||
# ICON_DOWNLOAD_TIMEOUT=10
|
# 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
|
## Controls if new users can register
|
||||||
# SIGNUPS_ALLOWED=true
|
# SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
|
@ -96,6 +117,17 @@
|
||||||
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
|
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
# 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 specific settings, check Rocket documentation to learn more
|
||||||
# ROCKET_ENV=staging
|
# ROCKET_ENV=staging
|
||||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||||
|
|
1526
Cargo.lock
generiert
1526
Cargo.lock
generiert
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
60
Cargo.toml
60
Cargo.toml
|
@ -11,50 +11,53 @@ publish = false
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
enable_syslog = ["syslog", "fern/syslog-4"]
|
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
||||||
|
enable_syslog = []
|
||||||
|
|
||||||
|
[target."cfg(not(windows))".dependencies]
|
||||||
|
syslog = "4.0.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||||
rocket = { version = "0.4.0", features = ["tls"], default-features = false }
|
rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
|
||||||
rocket_contrib = "0.4.0"
|
rocket_contrib = "0.5.0-dev"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
reqwest = "0.9.10"
|
reqwest = "0.9.17"
|
||||||
|
|
||||||
# multipart/form-data support
|
# multipart/form-data support
|
||||||
multipart = "0.16.1"
|
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
||||||
|
|
||||||
# WebSockets library
|
# WebSockets library
|
||||||
ws = "0.7.9"
|
ws = "0.8.1"
|
||||||
|
|
||||||
# MessagePack library
|
# MessagePack library
|
||||||
rmpv = "0.4.0"
|
rmpv = "0.4.0"
|
||||||
|
|
||||||
# Concurrent hashmap implementation
|
# Concurrent hashmap implementation
|
||||||
chashmap = "2.2.0"
|
chashmap = "2.2.2"
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = "1.0.88"
|
serde = "1.0.91"
|
||||||
serde_derive = "1.0.88"
|
serde_derive = "1.0.91"
|
||||||
serde_json = "1.0.38"
|
serde_json = "1.0.39"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.6"
|
log = "0.4.6"
|
||||||
fern = "0.5.7"
|
fern = { version = "0.5.8", features = ["syslog-4"] }
|
||||||
syslog = { version = "4.0.1", optional = true }
|
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "1.4.1", features = ["sqlite", "chrono", "r2d2"] }
|
diesel = { version = "1.4.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||||
diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
|
diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled SQLite
|
||||||
libsqlite3-sys = { version = "0.12.0", features = ["bundled"] }
|
libsqlite3-sys = { version = "0.12.0", features = ["bundled"] }
|
||||||
|
|
||||||
# Crypto library
|
# Crypto library
|
||||||
ring = { version = "0.13.5", features = ["rsa_signing"] }
|
ring = "0.14.6"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.7.2", features = ["v4"] }
|
uuid = { version = "0.7.4", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time library for Rust
|
# Date and time library for Rust
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.6"
|
||||||
|
@ -66,43 +69,44 @@ oath = "0.10.2"
|
||||||
data-encoding = "2.1.2"
|
data-encoding = "2.1.2"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "5.0.1"
|
jsonwebtoken = "6.0.1"
|
||||||
|
|
||||||
# U2F library
|
# U2F library
|
||||||
u2f = "0.1.4"
|
u2f = "0.1.6"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.5.1", features = ["online"], default-features = false }
|
yubico = { version = "0.5.1", features = ["online"], default-features = false }
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# A `dotenv` implementation for Rust
|
||||||
dotenv = { version = "0.13.0", default-features = false }
|
dotenv = { version = "0.14.1", default-features = false }
|
||||||
|
|
||||||
# Lazy static macro
|
# Lazy static macro
|
||||||
lazy_static = { version = "1.2.0", features = ["nightly"] }
|
lazy_static = "1.3.0"
|
||||||
|
|
||||||
# More derives
|
# More derives
|
||||||
derive_more = "0.14.0"
|
derive_more = "0.14.0"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
num-derive = "0.2.4"
|
num-derive = "0.2.5"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = "0.9.0"
|
lettre = "0.9.1"
|
||||||
lettre_email = "0.9.0"
|
lettre_email = "0.9.1"
|
||||||
native-tls = "0.2.2"
|
native-tls = "0.2.3"
|
||||||
|
quoted_printable = "0.4.0"
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = "1.1.0"
|
handlebars = "1.1.0"
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
soup = "0.3.0"
|
soup = "0.4.1"
|
||||||
regex = "1.1.0"
|
regex = "1.1.6"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Add support for Timestamp type
|
# Add support for Timestamp type
|
||||||
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
||||||
|
|
||||||
# Use new native_tls version 0.2
|
# Use newest ring
|
||||||
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
|
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
|
||||||
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
|
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM alpine as vault
|
FROM alpine as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.8.0d"
|
ENV VAULT_VERSION "v2.10.1"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM alpine as vault
|
FROM alpine as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.8.0d"
|
ENV VAULT_VERSION "v2.10.1"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM alpine as vault
|
FROM alpine as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.8.0d"
|
ENV VAULT_VERSION "v2.10.1"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ RUN cargo build --release
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.8
|
FROM alpine:3.9
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM alpine as vault
|
FROM alpine as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.8.0d"
|
ENV VAULT_VERSION "v2.10.1"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
FROM alpine as vault
|
FROM alpine as vault
|
||||||
|
|
||||||
ENV VAULT_VERSION "v2.8.0d"
|
ENV VAULT_VERSION "v2.10.1"
|
||||||
|
|
||||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
[![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs)
|
[![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs)
|
||||||
[![Docker Pulls](https://img.shields.io/docker/pulls/mprasil/bitwarden.svg)](https://hub.docker.com/r/mprasil/bitwarden)
|
[![Docker Pulls](https://img.shields.io/docker/pulls/bitwardenrs/server.svg)](https://hub.docker.com/r/bitwardenrs/server)
|
||||||
[![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
|
[![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
|
||||||
[![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
|
[![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
|
||||||
[![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
|
[![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
|
||||||
|
@ -34,8 +34,8 @@ Basically full implementation of Bitwarden API is provided including:
|
||||||
Pull the docker image and mount a volume from the host for persistent storage:
|
Pull the docker image and mount a volume from the host for persistent storage:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull mprasil/bitwarden:latest
|
docker pull bitwardenrs/server:latest
|
||||||
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
|
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 bitwardenrs/server:latest
|
||||||
```
|
```
|
||||||
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
|
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
nightly-2019-01-26
|
nightly-2019-05-11
|
||||||
|
|
|
@ -6,7 +6,7 @@ use rocket::response::{content::Html, Flash, Redirect};
|
||||||
use rocket::{Outcome, Route};
|
use rocket::{Outcome, Route};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult};
|
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||||
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
|
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
|
||||||
use crate::config::ConfigBuilder;
|
use crate::config::ConfigBuilder;
|
||||||
use crate::db::{models::*, DbConn};
|
use crate::db::{models::*, DbConn};
|
||||||
|
@ -21,11 +21,13 @@ pub fn routes() -> Vec<Route> {
|
||||||
|
|
||||||
routes![
|
routes![
|
||||||
admin_login,
|
admin_login,
|
||||||
|
get_users,
|
||||||
post_admin_login,
|
post_admin_login,
|
||||||
admin_page,
|
admin_page,
|
||||||
invite_user,
|
invite_user,
|
||||||
delete_user,
|
delete_user,
|
||||||
deauth_user,
|
deauth_user,
|
||||||
|
update_revision_users,
|
||||||
post_config,
|
post_config,
|
||||||
delete_config,
|
delete_config,
|
||||||
]
|
]
|
||||||
|
@ -89,7 +91,7 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
|
||||||
fn _validate_token(token: &str) -> bool {
|
fn _validate_token(token: &str) -> bool {
|
||||||
match CONFIG.admin_token().as_ref() {
|
match CONFIG.admin_token().as_ref() {
|
||||||
None => false,
|
None => false,
|
||||||
Some(t) => crate::crypto::ct_eq(t, token),
|
Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,9 +145,10 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
||||||
err!("Invitations are not allowed")
|
err!("Invitations are not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut user = User::new(email);
|
||||||
|
user.save(&conn)?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let mut user = User::new(email);
|
|
||||||
user.save(&conn)?;
|
|
||||||
let org_name = "bitwarden_rs";
|
let org_name = "bitwarden_rs";
|
||||||
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
|
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
|
||||||
} else {
|
} else {
|
||||||
|
@ -154,6 +157,14 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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")]
|
#[post("/users/<uuid>/delete")]
|
||||||
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
let user = match User::find_by_uuid(&uuid, &conn) {
|
||||||
|
@ -177,6 +188,11 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
user.save(&conn)
|
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>")]
|
#[post("/config", data = "<data>")]
|
||||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: ConfigBuilder = data.into_inner();
|
let data: ConfigBuilder = data.into_inner();
|
||||||
|
@ -196,8 +212,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||||
if CONFIG.disable_admin_token() {
|
if CONFIG.disable_admin_token() {
|
||||||
Outcome::Success(AdminToken {})
|
Outcome::Success(AdminToken {})
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
let mut cookies = request.cookies();
|
let mut cookies = request.cookies();
|
||||||
|
|
||||||
let access_token = match cookies.get(COOKIE_NAME) {
|
let access_token = match cookies.get(COOKIE_NAME) {
|
||||||
|
|
|
@ -74,10 +74,10 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let user_json = headers.user.to_json(&conn);
|
let user_json = headers.user.to_json(&conn);
|
||||||
|
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||||
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
|
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||||
|
|
||||||
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
|
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
|
||||||
let collections_json: Vec<Value> = collections.iter().map(|c| c.to_json()).collect();
|
let collections_json: Vec<Value> = collections.iter().map(Collection::to_json).collect();
|
||||||
|
|
||||||
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
|
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
|
||||||
let ciphers_json: Vec<Value> = ciphers
|
let ciphers_json: Vec<Value> = ciphers
|
||||||
|
@ -854,11 +854,7 @@ fn move_cipher_selected(data: JsonUpcase<MoveCipherData>, headers: Headers, conn
|
||||||
// Move cipher
|
// Move cipher
|
||||||
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &conn)?;
|
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &conn)?;
|
||||||
|
|
||||||
nt.send_cipher_update(
|
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &[user_uuid.clone()]);
|
||||||
UpdateType::CipherUpdate,
|
|
||||||
&cipher,
|
|
||||||
&[user_uuid.clone()]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -874,8 +870,20 @@ fn move_cipher_selected_put(
|
||||||
move_cipher_selected(data, headers, conn, nt)
|
move_cipher_selected(data, headers, conn, nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/purge", data = "<data>")]
|
#[derive(FromForm)]
|
||||||
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
struct OrganizationId {
|
||||||
|
#[form(field = "organizationId")]
|
||||||
|
org_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/ciphers/purge?<organization..>", data = "<data>")]
|
||||||
|
fn delete_all(
|
||||||
|
organization: Option<Form<OrganizationId>>,
|
||||||
|
data: JsonUpcase<PasswordData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify,
|
||||||
|
) -> EmptyResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
let password_hash = data.MasterPasswordHash;
|
||||||
|
|
||||||
|
@ -885,19 +893,40 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete ciphers and their attachments
|
match organization {
|
||||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
Some(org_data) => {
|
||||||
cipher.delete(&conn)?;
|
// Organization ID in query params, purging organization vault
|
||||||
}
|
match UserOrganization::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn) {
|
||||||
|
None => err!("You don't have permission to purge the organization vault"),
|
||||||
|
Some(user_org) => {
|
||||||
|
if user_org.type_ == UserOrgType::Owner {
|
||||||
|
Cipher::delete_all_by_organization(&org_data.org_id, &conn)?;
|
||||||
|
Collection::delete_all_by_organization(&org_data.org_id, &conn)?;
|
||||||
|
nt.send_user_update(UpdateType::Vault, &user);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
err!("You don't have permission to purge the organization vault");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// No organization ID in query params, purging user vault
|
||||||
|
// Delete ciphers and their attachments
|
||||||
|
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
||||||
|
cipher.delete(&conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete folders
|
// Delete folders
|
||||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||||
f.delete(&conn)?;
|
f.delete(&conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.update_revision(&conn)?;
|
user.update_revision(&conn)?;
|
||||||
nt.send_user_update(UpdateType::Vault, &user);
|
nt.send_user_update(UpdateType::Vault, &user);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||||
|
|
||||||
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
|
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": folders_json,
|
"Data": folders_json,
|
||||||
|
|
|
@ -33,10 +33,10 @@ use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::db::DbConn;
|
|
||||||
|
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
|
use crate::db::DbConn;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
fn clear_device_token(uuid: String) -> EmptyResult {
|
||||||
|
@ -137,12 +137,13 @@ fn hibp_breach(username: String) -> JsonResult {
|
||||||
|
|
||||||
use reqwest::{header::USER_AGENT, Client};
|
use reqwest::{header::USER_AGENT, Client};
|
||||||
|
|
||||||
let value: Value = Client::new()
|
let res = Client::new().get(&url).header(USER_AGENT, user_agent).send()?;
|
||||||
.get(&url)
|
|
||||||
.header(USER_AGENT, user_agent)
|
|
||||||
.send()?
|
|
||||||
.error_for_status()?
|
|
||||||
.json()?;
|
|
||||||
|
|
||||||
|
// 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(Json(value))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use data_encoding::BASE32;
|
use data_encoding::{BASE32, BASE64};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -31,13 +31,16 @@ pub fn routes() -> Vec<Route> {
|
||||||
generate_yubikey,
|
generate_yubikey,
|
||||||
activate_yubikey,
|
activate_yubikey,
|
||||||
activate_yubikey_put,
|
activate_yubikey_put,
|
||||||
|
get_duo,
|
||||||
|
activate_duo,
|
||||||
|
activate_duo_put,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/two-factor")]
|
#[get("/two-factor")]
|
||||||
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
||||||
let twofactors_json: Vec<Value> = twofactors.iter().map(|c| c.to_json_list()).collect();
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": twofactors_json,
|
"Data": twofactors_json,
|
||||||
|
@ -102,6 +105,14 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
||||||
|
if user.totp_recover.is_none() {
|
||||||
|
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
||||||
|
user.totp_recover = Some(totp_recover);
|
||||||
|
user.save(conn).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct DisableTwoFactorData {
|
struct DisableTwoFactorData {
|
||||||
|
@ -196,9 +207,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
||||||
|
|
||||||
// Validate the token provided with the key
|
// Validate the token provided with the key
|
||||||
if !twofactor.check_totp_code(token) {
|
validate_totp_code(token, &twofactor.data)?;
|
||||||
err!("Invalid totp code")
|
|
||||||
}
|
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
@ -215,12 +224,29 @@ fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers
|
||||||
activate_authenticator(data, headers, conn)
|
activate_authenticator(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
|
||||||
if user.totp_recover.is_none() {
|
let totp_code: u64 = match totp_code.parse() {
|
||||||
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
Ok(code) => code,
|
||||||
user.totp_recover = Some(totp_recover);
|
_ => err!("TOTP code is not a number"),
|
||||||
user.save(conn).ok();
|
};
|
||||||
|
|
||||||
|
validate_totp_code(totp_code, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
|
||||||
|
use oath::{totp_raw_now, HashType};
|
||||||
|
|
||||||
|
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => err!("Invalid TOTP secret"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
||||||
|
if generated != totp_code {
|
||||||
|
err!("Invalid TOTP code");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
||||||
|
@ -248,7 +274,7 @@ fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
|
let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
|
||||||
let keys_json: Vec<Value> = keys.iter().map(|r| r.to_json()).collect();
|
let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": enabled,
|
"Enabled": enabled,
|
||||||
|
@ -384,7 +410,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
|
|
||||||
let keys_json: Vec<Value> = regs.iter().map(|r| r.to_json()).collect();
|
let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Keys": keys_json,
|
"Keys": keys_json,
|
||||||
|
@ -671,20 +697,12 @@ fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, c
|
||||||
activate_yubikey(data, headers, conn)
|
activate_yubikey(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
||||||
if response.len() != 44 {
|
if response.len() != 44 {
|
||||||
err!("Invalid Yubikey OTP length");
|
err!("Invalid Yubikey OTP length");
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey_type = TwoFactorType::YubiKey as i32;
|
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
|
||||||
|
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn) {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("No YubiKey devices registered"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let yubikey_metadata: YubikeyMetadata =
|
|
||||||
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
|
|
||||||
let response_id = &response[..12];
|
let response_id = &response[..12];
|
||||||
|
|
||||||
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
|
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
|
||||||
|
@ -698,3 +716,325 @@ pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) ->
|
||||||
Err(_e) => err!("Failed to verify Yubikey against OTP server"),
|
Err(_e) => err!("Failed to verify Yubikey against OTP server"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct DuoData {
|
||||||
|
host: String,
|
||||||
|
ik: String,
|
||||||
|
sk: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DuoData {
|
||||||
|
fn global() -> Option<Self> {
|
||||||
|
match CONFIG.duo_host() {
|
||||||
|
Some(host) => Some(Self {
|
||||||
|
host,
|
||||||
|
ik: CONFIG.duo_ikey().unwrap(),
|
||||||
|
sk: CONFIG.duo_skey().unwrap(),
|
||||||
|
}),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn msg(s: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
host: s.into(),
|
||||||
|
ik: s.into(),
|
||||||
|
sk: s.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn secret() -> Self {
|
||||||
|
Self::msg("<global_secret>")
|
||||||
|
}
|
||||||
|
fn obscure(self) -> Self {
|
||||||
|
let mut host = self.host;
|
||||||
|
let mut ik = self.ik;
|
||||||
|
let mut sk = self.sk;
|
||||||
|
|
||||||
|
let digits = 4;
|
||||||
|
let replaced = "************";
|
||||||
|
|
||||||
|
host.replace_range(digits.., replaced);
|
||||||
|
ik.replace_range(digits.., replaced);
|
||||||
|
sk.replace_range(digits.., replaced);
|
||||||
|
|
||||||
|
Self { host, ik, sk }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DuoStatus {
|
||||||
|
Global(DuoData), // Using the global duo config
|
||||||
|
User(DuoData), // Using the user's config
|
||||||
|
Disabled(bool), // True if there is a global setting
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DuoStatus {
|
||||||
|
fn data(self) -> Option<DuoData> {
|
||||||
|
match self {
|
||||||
|
DuoStatus::Global(data) => Some(data),
|
||||||
|
DuoStatus::User(data) => Some(data),
|
||||||
|
DuoStatus::Disabled(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
|
||||||
|
|
||||||
|
#[post("/two-factor/get-duo", data = "<data>")]
|
||||||
|
fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
|
||||||
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
err!("Invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = get_user_duo_data(&headers.user.uuid, &conn);
|
||||||
|
|
||||||
|
let (enabled, data) = match data {
|
||||||
|
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
|
||||||
|
DuoStatus::User(data) => (true, Some(data.obscure())),
|
||||||
|
DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
|
||||||
|
DuoStatus::Disabled(false) => (false, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = if let Some(data) = data {
|
||||||
|
json!({
|
||||||
|
"Enabled": enabled,
|
||||||
|
"Host": data.host,
|
||||||
|
"SecretKey": data.sk,
|
||||||
|
"IntegrationKey": data.ik,
|
||||||
|
"Object": "twoFactorDuo"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
json!({
|
||||||
|
"Enabled": enabled,
|
||||||
|
"Object": "twoFactorDuo"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case, dead_code)]
|
||||||
|
struct EnableDuoData {
|
||||||
|
MasterPasswordHash: String,
|
||||||
|
Host: String,
|
||||||
|
SecretKey: String,
|
||||||
|
IntegrationKey: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EnableDuoData> for DuoData {
|
||||||
|
fn from(d: EnableDuoData) -> Self {
|
||||||
|
Self {
|
||||||
|
host: d.Host,
|
||||||
|
ik: d.IntegrationKey,
|
||||||
|
sk: d.SecretKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
|
||||||
|
fn empty_or_default(s: &str) -> bool {
|
||||||
|
let st = s.trim();
|
||||||
|
st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
!empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/two-factor/duo", data = "<data>")]
|
||||||
|
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
let data: EnableDuoData = data.into_inner().data;
|
||||||
|
|
||||||
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
err!("Invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
let (data, data_str) = if check_duo_fields_custom(&data) {
|
||||||
|
let data_req: DuoData = data.into();
|
||||||
|
let data_str = serde_json::to_string(&data_req)?;
|
||||||
|
duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
|
||||||
|
(data_req.obscure(), data_str)
|
||||||
|
} else {
|
||||||
|
(DuoData::secret(), String::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
let type_ = TwoFactorType::Duo;
|
||||||
|
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
|
||||||
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Enabled": true,
|
||||||
|
"Host": data.host,
|
||||||
|
"SecretKey": data.sk,
|
||||||
|
"IntegrationKey": data.ik,
|
||||||
|
"Object": "twoFactorDuo"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/two-factor/duo", data = "<data>")]
|
||||||
|
fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
activate_duo(data, headers, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||||
|
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
|
||||||
|
|
||||||
|
use reqwest::{header::*, Client, Method};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
let url = format!("https://{}{}", &data.host, path);
|
||||||
|
let date = Utc::now().to_rfc2822();
|
||||||
|
let username = &data.ik;
|
||||||
|
let fields = [&date, method, &data.host, path, params];
|
||||||
|
let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
|
||||||
|
|
||||||
|
let m = Method::from_str(method).unwrap_or_default();
|
||||||
|
|
||||||
|
Client::new()
|
||||||
|
.request(m, &url)
|
||||||
|
.basic_auth(username, Some(password))
|
||||||
|
.header(USER_AGENT, AGENT)
|
||||||
|
.header(DATE, date)
|
||||||
|
.send()?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const DUO_EXPIRE: i64 = 300;
|
||||||
|
const APP_EXPIRE: i64 = 3600;
|
||||||
|
|
||||||
|
const AUTH_PREFIX: &str = "AUTH";
|
||||||
|
const DUO_PREFIX: &str = "TX";
|
||||||
|
const APP_PREFIX: &str = "APP";
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
|
||||||
|
let type_ = TwoFactorType::Duo as i32;
|
||||||
|
|
||||||
|
// If the user doesn't have an entry, disabled
|
||||||
|
let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, &conn) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return DuoStatus::Disabled(DuoData::global().is_some()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the user has the required values, we use those
|
||||||
|
if let Ok(data) = serde_json::from_str(&twofactor.data) {
|
||||||
|
return DuoStatus::User(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we try to use the globals
|
||||||
|
if let Some(global) = DuoData::global() {
|
||||||
|
return DuoStatus::Global(global);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no globals configured, just disable it
|
||||||
|
DuoStatus::Disabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// let (ik, sk, ak, host) = get_duo_keys();
|
||||||
|
fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
|
||||||
|
let data = User::find_by_mail(email, &conn)
|
||||||
|
.and_then(|u| get_user_duo_data(&u.uuid, &conn).data())
|
||||||
|
.or_else(DuoData::global)
|
||||||
|
.map_res("Can't fetch Duo keys")?;
|
||||||
|
|
||||||
|
Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
|
let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?;
|
||||||
|
|
||||||
|
let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
|
||||||
|
let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
|
||||||
|
|
||||||
|
Ok((format!("{}:{}", duo_sign, app_sign), host))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
||||||
|
let val = format!("{}|{}|{}", email, ikey, expire);
|
||||||
|
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
|
||||||
|
|
||||||
|
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
let split: Vec<&str> = response.split(':').collect();
|
||||||
|
if split.len() != 2 {
|
||||||
|
err!("Invalid response length");
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_sig = split[0];
|
||||||
|
let app_sig = split[1];
|
||||||
|
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
|
let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?;
|
||||||
|
|
||||||
|
let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
|
||||||
|
let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
|
||||||
|
|
||||||
|
if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
|
||||||
|
err!("Error validating duo authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
|
||||||
|
let split: Vec<&str> = val.split('|').collect();
|
||||||
|
if split.len() != 3 {
|
||||||
|
err!("Invalid value length")
|
||||||
|
}
|
||||||
|
|
||||||
|
let u_prefix = split[0];
|
||||||
|
let u_b64 = split[1];
|
||||||
|
let u_sig = split[2];
|
||||||
|
|
||||||
|
let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
|
||||||
|
|
||||||
|
if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
|
||||||
|
err!("Duo signatures don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if u_prefix != prefix {
|
||||||
|
err!("Prefixes don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cookie = match String::from_utf8(cookie_vec) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cookie_split: Vec<&str> = cookie.split('|').collect();
|
||||||
|
if cookie_split.len() != 3 {
|
||||||
|
err!("Invalid cookie length")
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = cookie_split[0];
|
||||||
|
let u_ikey = cookie_split[1];
|
||||||
|
let expire = cookie_split[2];
|
||||||
|
|
||||||
|
if !crypto::ct_eq(ikey, u_ikey) {
|
||||||
|
err!("Invalid ikey")
|
||||||
|
}
|
||||||
|
|
||||||
|
let expire = match expire.parse() {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => err!("Invalid expire time"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if time >= expire {
|
||||||
|
err!("Expired authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(username.into())
|
||||||
|
}
|
||||||
|
|
105
src/api/icons.rs
105
src/api/icons.rs
|
@ -8,7 +8,7 @@ use rocket::Route;
|
||||||
|
|
||||||
use reqwest::{header::HeaderMap, Client, Response};
|
use reqwest::{header::HeaderMap, Client, Response};
|
||||||
|
|
||||||
use rocket::http::{Cookie};
|
use rocket::http::Cookie;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use soup::prelude::*;
|
use soup::prelude::*;
|
||||||
|
@ -22,6 +22,8 @@ pub fn routes() -> Vec<Route> {
|
||||||
|
|
||||||
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
|
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
|
||||||
|
|
||||||
|
const ALLOWED_CHARS: &str = "_-.";
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
// Reuse the client between requests
|
// Reuse the client between requests
|
||||||
static ref CLIENT: Client = Client::builder()
|
static ref CLIENT: Client = Client::builder()
|
||||||
|
@ -32,15 +34,42 @@ lazy_static! {
|
||||||
.unwrap();
|
.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")]
|
#[get("/<domain>/icon.png")]
|
||||||
fn icon(domain: String) -> Content<Vec<u8>> {
|
fn icon(domain: String) -> Content<Vec<u8>> {
|
||||||
let icon_type = ContentType::new("image", "x-icon");
|
let icon_type = ContentType::new("image", "x-icon");
|
||||||
|
|
||||||
// Validate the domain to avoid directory traversal attacks
|
if !is_valid_domain(&domain) {
|
||||||
if domain.contains('/') || domain.contains("..") {
|
warn!("Invalid domain: {:#?}", domain);
|
||||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
return Content(icon_type, FALLBACK_ICON.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||||
|
info!("Icon blacklist enabled: {:#?}", blacklist);
|
||||||
|
|
||||||
|
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);
|
let icon = get_icon(&domain);
|
||||||
|
|
||||||
Content(icon_type, icon)
|
Content(icon_type, icon)
|
||||||
|
@ -132,11 +161,17 @@ fn icon_is_expired(path: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct IconList {
|
struct Icon {
|
||||||
priority: u8,
|
priority: u8,
|
||||||
href: String,
|
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.
|
/// 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.
|
/// 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.
|
/// This does not mean that that location does exists, but it is the default location browser use.
|
||||||
|
@ -149,13 +184,13 @@ struct IconList {
|
||||||
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
|
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
|
||||||
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
|
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
||||||
// Default URL with secure and insecure schemes
|
// Default URL with secure and insecure schemes
|
||||||
let ssldomain = format!("https://{}", domain);
|
let ssldomain = format!("https://{}", domain);
|
||||||
let httpdomain = format!("http://{}", domain);
|
let httpdomain = format!("http://{}", domain);
|
||||||
|
|
||||||
// Create the iconlist
|
// Create the iconlist
|
||||||
let mut iconlist: Vec<IconList> = Vec::new();
|
let mut iconlist: Vec<Icon> = Vec::new();
|
||||||
|
|
||||||
// Create the cookie_str to fill it all the cookies from the response
|
// Create the cookie_str to fill it all the cookies from the response
|
||||||
// These cookies can be used to request/download the favicon image.
|
// These cookies can be used to request/download the favicon image.
|
||||||
|
@ -167,13 +202,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||||
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
||||||
let url = content.url().clone();
|
let url = content.url().clone();
|
||||||
let raw_cookies = content.headers().get_all("set-cookie");
|
let raw_cookies = content.headers().get_all("set-cookie");
|
||||||
cookie_str = raw_cookies.iter().map(|raw_cookie| {
|
cookie_str = raw_cookies
|
||||||
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap();
|
.iter()
|
||||||
format!("{}={}; ", cookie.name(), cookie.value())
|
.map(|raw_cookie| {
|
||||||
}).collect::<String>();
|
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.
|
// Add the default favicon.ico to the list with the domain the content responded from.
|
||||||
iconlist.push(IconList { priority: 35, href: url.join("/favicon.ico").unwrap().into_string() });
|
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
||||||
|
|
||||||
let soup = Soup::from_reader(content)?;
|
let soup = Soup::from_reader(content)?;
|
||||||
// Search for and filter
|
// Search for and filter
|
||||||
|
@ -185,15 +223,17 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||||
|
|
||||||
// Loop through all the found icons and determine it's priority
|
// Loop through all the found icons and determine it's priority
|
||||||
for favicon in favicons {
|
for favicon in favicons {
|
||||||
let sizes = favicon.get("sizes").unwrap_or_default();
|
let sizes = favicon.get("sizes");
|
||||||
let href = url.join(&favicon.get("href").unwrap_or_default()).unwrap().into_string();
|
let href = favicon.get("href").expect("Missing href");
|
||||||
let priority = get_icon_priority(&href, &sizes);
|
let full_href = url.join(&href).unwrap().into_string();
|
||||||
|
|
||||||
iconlist.push(IconList { priority, href })
|
let priority = get_icon_priority(&full_href, sizes);
|
||||||
|
|
||||||
|
iconlist.push(Icon::new(priority, full_href))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add the default favicon.ico to the list with just the given domain
|
// Add the default favicon.ico to the list with just the given domain
|
||||||
iconlist.push(IconList { priority: 35, href: format!("{}/favicon.ico", ssldomain) });
|
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the iconlist by priority
|
// Sort the iconlist by priority
|
||||||
|
@ -204,12 +244,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_page(url: &str) -> Result<Response, Error> {
|
fn get_page(url: &str) -> Result<Response, Error> {
|
||||||
//CLIENT.get(url).send()?.error_for_status().map_err(Into::into)
|
|
||||||
get_page_with_cookies(url, "")
|
get_page_with_cookies(url, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> {
|
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)
|
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.
|
/// Returns a Integer with the priority of the type of the icon which to prefer.
|
||||||
|
@ -224,7 +268,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error>
|
||||||
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
/// 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", "");
|
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_priority(href: &str, sizes: &str) -> u8 {
|
fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
||||||
// Check if there is a dimension set
|
// Check if there is a dimension set
|
||||||
let (width, height) = parse_sizes(sizes);
|
let (width, height) = parse_sizes(sizes);
|
||||||
|
|
||||||
|
@ -272,19 +316,19 @@ fn get_icon_priority(href: &str, sizes: &str) -> u8 {
|
||||||
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
||||||
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
||||||
/// ```
|
/// ```
|
||||||
fn parse_sizes(sizes: &str) -> (u16, u16) {
|
fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
||||||
let mut width: u16 = 0;
|
let mut width: u16 = 0;
|
||||||
let mut height: u16 = 0;
|
let mut height: u16 = 0;
|
||||||
|
|
||||||
if !sizes.is_empty() {
|
if let Some(sizes) = sizes {
|
||||||
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
|
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
|
||||||
None => {},
|
None => {}
|
||||||
Some(dimensions) => {
|
Some(dimensions) => {
|
||||||
if dimensions.len() >= 3 {
|
if dimensions.len() >= 3 {
|
||||||
width = dimensions[1].parse::<u16>().unwrap_or_default();
|
width = dimensions[1].parse::<u16>().unwrap_or_default();
|
||||||
height = dimensions[2].parse::<u16>().unwrap_or_default();
|
height = dimensions[2].parse::<u16>().unwrap_or_default();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,21 +336,18 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
||||||
let (mut iconlist, cookie_str) = get_icon_url(&domain)?;
|
let (iconlist, cookie_str) = get_icon_url(&domain)?;
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
iconlist.truncate(5);
|
for icon in iconlist.iter().take(5) {
|
||||||
for icon in iconlist {
|
match get_page_with_cookies(&icon.href, &cookie_str) {
|
||||||
let url = icon.href;
|
|
||||||
info!("Downloading icon for {} via {}...", domain, url);
|
|
||||||
match get_page_with_cookies(&url, &cookie_str) {
|
|
||||||
Ok(mut res) => {
|
Ok(mut res) => {
|
||||||
info!("Download finished for {}", url);
|
info!("Downloaded icon from {}", icon.href);
|
||||||
res.copy_to(&mut buffer)?;
|
res.copy_to(&mut buffer)?;
|
||||||
break;
|
break;
|
||||||
},
|
}
|
||||||
Err(_) => info!("Download failed for {}", url),
|
Err(_) => info!("Download failed for {}", icon.href),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ use num_traits::FromPrimitive;
|
||||||
use crate::db::models::*;
|
use crate::db::models::*;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::util::{self, JsonMap};
|
use crate::util;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||||
|
|
||||||
|
@ -152,63 +152,46 @@ fn twofactor_auth(
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
||||||
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
|
||||||
|
|
||||||
// No twofactor token if twofactor is disabled
|
// No twofactor token if twofactor is disabled
|
||||||
if twofactors.is_empty() {
|
if twofactors.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one
|
let twofactor_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 {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0);
|
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)?,
|
||||||
|
|
||||||
match TwoFactorType::from_i32(provider) {
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
use crate::crypto::ct_eq;
|
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
Some(ref remember) if ct_eq(remember, twofactor_code) => return Ok(None), // No twofactor token needed here
|
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||||
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
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)?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TwoFactorType::Authenticator) => {
|
|
||||||
let twofactor = match twofactor {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("TOTP not enabled"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let totp_code: u64 = match twofactor_code.parse() {
|
|
||||||
Ok(code) => code,
|
|
||||||
_ => err!("Invalid TOTP code"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !twofactor.check_totp_code(totp_code) {
|
|
||||||
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TwoFactorType::U2f) => {
|
|
||||||
use crate::api::core::two_factor;
|
|
||||||
|
|
||||||
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TwoFactorType::YubiKey) => {
|
|
||||||
use crate::api::core::two_factor;
|
|
||||||
|
|
||||||
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => err!("Invalid two factor provider"),
|
_ => err!("Invalid two factor provider"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.two_factor_remember.unwrap_or(0) == 1 {
|
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||||
Ok(Some(device.refresh_twofactor_remember()))
|
Ok(Some(device.refresh_twofactor_remember()))
|
||||||
} else {
|
} else {
|
||||||
device.delete_twofactor_remember();
|
device.delete_twofactor_remember();
|
||||||
|
@ -216,6 +199,13 @@ fn twofactor_auth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
||||||
use crate::api::core::two_factor;
|
use crate::api::core::two_factor;
|
||||||
|
|
||||||
|
@ -237,22 +227,33 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||||
let mut challenge_list = Vec::new();
|
let mut challenge_list = Vec::new();
|
||||||
|
|
||||||
for key in request.registered_keys {
|
for key in request.registered_keys {
|
||||||
let mut challenge_map = JsonMap::new();
|
challenge_list.push(json!({
|
||||||
|
"appId": request.app_id,
|
||||||
challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
|
"challenge": request.challenge,
|
||||||
challenge_map.insert("challenge".into(), Value::String(request.challenge.clone()));
|
"version": key.version,
|
||||||
challenge_map.insert("version".into(), Value::String(key.version));
|
"keyHandle": key.key_handle,
|
||||||
challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default()));
|
}));
|
||||||
|
|
||||||
challenge_list.push(Value::Object(challenge_map));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut map = JsonMap::new();
|
|
||||||
use serde_json;
|
|
||||||
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
||||||
|
|
||||||
map.insert("Challenges".into(), Value::String(challenge_list_str));
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
"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) => {
|
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||||
|
@ -261,12 +262,11 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||||
None => err!("No YubiKey devices registered"),
|
None => err!("No YubiKey devices registered"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let yubikey_metadata: two_factor::YubikeyMetadata =
|
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
|
||||||
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
|
|
||||||
|
|
||||||
let mut map = JsonMap::new();
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc));
|
"Nfc": yubikey_metadata.Nfc,
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -230,7 +230,7 @@ pub struct WebSocketUsers {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebSocketUsers {
|
impl WebSocketUsers {
|
||||||
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
|
fn send_update(&self, user_uuid: &str, data: &[u8]) -> ws::Result<()> {
|
||||||
if let Some(user) = self.map.get(user_uuid) {
|
if let Some(user) = self.map.get(user_uuid) {
|
||||||
for sender in user.iter() {
|
for sender in user.iter() {
|
||||||
sender.send(data)?;
|
sender.send(data)?;
|
||||||
|
|
|
@ -9,11 +9,12 @@ use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::util::Cached;
|
use crate::util::Cached;
|
||||||
|
use crate::error::Error;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
if CONFIG.web_vault_enabled() {
|
if CONFIG.web_vault_enabled() {
|
||||||
routes![web_index, app_id, web_files, attachments, alive]
|
routes![web_index, app_id, web_files, attachments, alive, images]
|
||||||
} else {
|
} else {
|
||||||
routes![attachments, alive]
|
routes![attachments, alive]
|
||||||
}
|
}
|
||||||
|
@ -62,3 +63,13 @@ fn alive() -> Json<String> {
|
||||||
|
|
||||||
Json(format_date(&Utc::now().naive_utc()))
|
Json(format_date(&Utc::now().naive_utc()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/bwrs_images/<filename>")]
|
||||||
|
fn images(filename: String) -> Result<Content<Vec<u8>>, Error> {
|
||||||
|
let image_type = ContentType::new("image", "png");
|
||||||
|
match filename.as_ref() {
|
||||||
|
"mail-github.png" => Ok(Content(image_type , include_bytes!("../static/images/mail-github.png").to_vec())),
|
||||||
|
"logo-gray.png" => Ok(Content(image_type, include_bytes!("../static/images/logo-gray.png").to_vec())),
|
||||||
|
_ => err!("Image not found")
|
||||||
|
}
|
||||||
|
}
|
11
src/auth.rs
11
src/auth.rs
|
@ -21,17 +21,11 @@ lazy_static! {
|
||||||
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", 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 PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
|
||||||
Ok(key) => key,
|
Ok(key) => key,
|
||||||
Err(e) => panic!(
|
Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),
|
||||||
"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,
|
Ok(key) => key,
|
||||||
Err(e) => panic!(
|
Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e),
|
||||||
"Error loading public RSA Key from {}\n Error: {}",
|
|
||||||
CONFIG.public_rsa_key(), e
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +40,6 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
|
||||||
let validation = jsonwebtoken::Validation {
|
let validation = jsonwebtoken::Validation {
|
||||||
leeway: 30, // 30 seconds
|
leeway: 30, // 30 seconds
|
||||||
validate_exp: true,
|
validate_exp: true,
|
||||||
validate_iat: false, // IssuedAt is the same as NotBefore
|
|
||||||
validate_nbf: true,
|
validate_nbf: true,
|
||||||
aud: None,
|
aud: None,
|
||||||
iss: Some(issuer),
|
iss: Some(issuer),
|
||||||
|
|
101
src/config.rs
101
src/config.rs
|
@ -9,7 +9,10 @@ lazy_static! {
|
||||||
println!("Error loading config:\n\t{:?}\n", e);
|
println!("Error loading config:\n\t{:?}\n", e);
|
||||||
exit(12)
|
exit(12)
|
||||||
});
|
});
|
||||||
pub static ref CONFIG_FILE: String = get_env("CONFIG_FILE").unwrap_or_else(|| "data/config.json".into());
|
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;
|
pub type Pass = String;
|
||||||
|
@ -61,7 +64,7 @@ macro_rules! make_config {
|
||||||
|
|
||||||
/// Merges the values of both builders into a new builder.
|
/// Merges the values of both builders into a new builder.
|
||||||
/// If both have the same element, `other` wins.
|
/// If both have the same element, `other` wins.
|
||||||
fn merge(&self, other: &Self) -> Self {
|
fn merge(&self, other: &Self, show_overrides: bool) -> Self {
|
||||||
let mut overrides = Vec::new();
|
let mut overrides = Vec::new();
|
||||||
let mut builder = self.clone();
|
let mut builder = self.clone();
|
||||||
$($(
|
$($(
|
||||||
|
@ -74,7 +77,7 @@ macro_rules! make_config {
|
||||||
}
|
}
|
||||||
)+)+
|
)+)+
|
||||||
|
|
||||||
if !overrides.is_empty() {
|
if show_overrides && !overrides.is_empty() {
|
||||||
// We can't use warn! here because logging isn't setup yet.
|
// 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] 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] please use the admin panel to make changes to them:");
|
||||||
|
@ -224,24 +227,27 @@ make_config! {
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
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 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: 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 |> Indicates if the domain is set by the admin. Otherwise the default will be used.
|
||||||
domain_set: bool, false, def, false;
|
domain_set: bool, false, def, false;
|
||||||
/// Enable web vault
|
/// Enable web vault
|
||||||
web_vault_enabled: bool, false, def, true;
|
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,
|
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
||||||
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
/// $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.
|
/// otherwise it will delete them and they won't be downloaded again.
|
||||||
disable_icon_download: bool, true, def, false;
|
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
|
/// 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;
|
signups_allowed: bool, true, def, true;
|
||||||
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
||||||
invitations_allowed: bool, true, def, true;
|
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 |> 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;
|
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 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;
|
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 page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
|
||||||
|
@ -255,19 +261,32 @@ make_config! {
|
||||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
/// 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_cache_negttl: u64, true, def, 259_200;
|
||||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||||
icon_download_timeout: u64, true, def, 10;
|
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;
|
||||||
|
|
||||||
/// 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
|
/// 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;
|
reload_templates: bool, true, def, false;
|
||||||
|
|
||||||
/// Log routes at launch (Dev)
|
/// Log routes at launch (Dev)
|
||||||
log_mounts: bool, true, def, false;
|
log_mounts: bool, true, def, false;
|
||||||
/// Enable extended logging
|
/// Enable extended logging
|
||||||
extended_logging: bool, false, def, true;
|
extended_logging: bool, false, def, true;
|
||||||
|
/// Enable the log to output to Syslog
|
||||||
|
use_syslog: bool, false, def, false;
|
||||||
/// Log file path
|
/// Log file path
|
||||||
log_file: String, false, option;
|
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 |> 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;
|
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 (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||||
|
@ -286,6 +305,20 @@ make_config! {
|
||||||
yubico_server: String, true, option;
|
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 Email Settings
|
||||||
smtp: _enable_smtp {
|
smtp: _enable_smtp {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
|
@ -294,8 +327,10 @@ make_config! {
|
||||||
smtp_host: String, true, option;
|
smtp_host: String, true, option;
|
||||||
/// Enable SSL
|
/// Enable SSL
|
||||||
smtp_ssl: bool, true, def, true;
|
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
|
/// Port
|
||||||
smtp_port: u16, true, auto, |c| if c.smtp_ssl {587} else {25};
|
smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
|
||||||
/// From Address
|
/// From Address
|
||||||
smtp_from: String, true, def, String::new();
|
smtp_from: String, true, def, String::new();
|
||||||
/// From Name
|
/// From Name
|
||||||
|
@ -308,6 +343,18 @@ make_config! {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
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() {
|
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")
|
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
|
||||||
}
|
}
|
||||||
|
@ -330,7 +377,7 @@ impl Config {
|
||||||
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
|
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
|
||||||
|
|
||||||
// Create merged config, config file overwrites env
|
// Create merged config, config file overwrites env
|
||||||
let builder = _env.merge(&_usr);
|
let builder = _env.merge(&_usr, true);
|
||||||
|
|
||||||
// Fill any missing with defaults
|
// Fill any missing with defaults
|
||||||
let config = builder.build();
|
let config = builder.build();
|
||||||
|
@ -359,7 +406,7 @@ impl Config {
|
||||||
// Prepare the combined config
|
// Prepare the combined config
|
||||||
let config = {
|
let config = {
|
||||||
let env = &self.inner.read().unwrap()._env;
|
let env = &self.inner.read().unwrap()._env;
|
||||||
env.merge(&builder).build()
|
env.merge(&builder, false).build()
|
||||||
};
|
};
|
||||||
validate_config(&config)?;
|
validate_config(&config)?;
|
||||||
|
|
||||||
|
@ -378,6 +425,14 @@ impl Config {
|
||||||
Ok(())
|
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> {
|
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
crate::util::delete_file(&CONFIG_FILE)?;
|
crate::util::delete_file(&CONFIG_FILE)?;
|
||||||
|
|
||||||
|
@ -413,9 +468,21 @@ impl Config {
|
||||||
let inner = &self.inner.read().unwrap().config;
|
let inner = &self.inner.read().unwrap().config;
|
||||||
inner._enable_smtp && inner.smtp_host.is_some()
|
inner._enable_smtp && inner.smtp_host.is_some()
|
||||||
}
|
}
|
||||||
pub fn yubico_enabled(&self) -> bool {
|
|
||||||
let inner = &self.inner.read().unwrap().config;
|
pub fn get_duo_akey(&self) -> String {
|
||||||
inner._enable_yubico && inner.yubico_client_id.is_some() && inner.yubico_secret_key.is_some()
|
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>(
|
pub fn render_template<T: serde::ser::Serialize>(
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
// PBKDF2 derivation
|
// PBKDF2 derivation
|
||||||
//
|
//
|
||||||
|
|
||||||
use ring::{digest, pbkdf2};
|
use ring::{digest, hmac, pbkdf2};
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
|
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
|
||||||
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||||
|
@ -10,15 +11,29 @@ const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||||
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
|
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
|
||||||
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros
|
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros
|
||||||
|
|
||||||
|
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
|
||||||
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
|
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
|
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
|
||||||
|
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
|
||||||
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
|
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
|
||||||
//
|
//
|
||||||
|
|
|
@ -72,9 +72,7 @@ use crate::error::MapResult;
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Cipher {
|
impl Cipher {
|
||||||
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
|
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
|
||||||
use super::Attachment;
|
|
||||||
use crate::util::format_date;
|
use crate::util::format_date;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
|
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<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
|
||||||
|
|
|
@ -42,21 +42,6 @@ impl TwoFactor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_totp_code(&self, totp_code: u64) -> bool {
|
|
||||||
let totp_secret = self.data.as_bytes();
|
|
||||||
|
|
||||||
use data_encoding::BASE32;
|
|
||||||
use oath::{totp_raw_now, HashType};
|
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(totp_secret) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
|
||||||
generated == totp_code
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Enabled": self.enabled,
|
"Enabled": self.enabled,
|
||||||
|
|
|
@ -37,6 +37,12 @@ pub struct User {
|
||||||
pub client_kdf_iter: i32,
|
pub client_kdf_iter: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UserStatus {
|
||||||
|
Enabled = 0,
|
||||||
|
Invited = 1,
|
||||||
|
_Disabled = 2,
|
||||||
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl User {
|
impl User {
|
||||||
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
||||||
|
@ -113,14 +119,19 @@ use crate::error::MapResult;
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl User {
|
impl User {
|
||||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||||
use super::{TwoFactor, UserOrganization};
|
|
||||||
|
|
||||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
||||||
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||||
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||||
|
|
||||||
|
// TODO: Might want to save the status field in the DB
|
||||||
|
let status = if self.password_hash.is_empty() {
|
||||||
|
UserStatus::Invited
|
||||||
|
} else {
|
||||||
|
UserStatus::Enabled
|
||||||
|
};
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"_Enabled": !self.password_hash.is_empty(),
|
"_Status": status as i32,
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"Email": self.email,
|
"Email": self.email,
|
||||||
|
@ -178,6 +189,20 @@ impl User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
|
39
src/error.rs
39
src/error.rs
|
@ -5,16 +5,18 @@ use std::error::Error as StdError;
|
||||||
|
|
||||||
macro_rules! make_error {
|
macro_rules! make_error {
|
||||||
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
||||||
|
const BAD_REQUEST: u16 = 400;
|
||||||
|
|
||||||
#[derive(Display)]
|
#[derive(Display)]
|
||||||
pub enum ErrorKind { $($name( $ty )),+ }
|
pub enum ErrorKind { $($name( $ty )),+ }
|
||||||
pub struct Error { message: String, error: ErrorKind }
|
pub struct Error { message: String, error: ErrorKind, error_code: u16 }
|
||||||
|
|
||||||
$(impl From<$ty> for Error {
|
$(impl From<$ty> for Error {
|
||||||
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
|
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
|
||||||
})+
|
})+
|
||||||
$(impl<S: Into<String>> From<(S, $ty)> for Error {
|
$(impl<S: Into<String>> From<(S, $ty)> for Error {
|
||||||
fn from(val: (S, $ty)) -> Self {
|
fn from(val: (S, $ty)) -> Self {
|
||||||
Error { message: val.0.into(), error: ErrorKind::$name(val.1) }
|
Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST }
|
||||||
}
|
}
|
||||||
})+
|
})+
|
||||||
impl StdError for Error {
|
impl StdError for Error {
|
||||||
|
@ -39,16 +41,23 @@ use regex::Error as RegexErr;
|
||||||
use reqwest::Error as ReqErr;
|
use reqwest::Error as ReqErr;
|
||||||
use serde_json::{Error as SerdeErr, Value};
|
use serde_json::{Error as SerdeErr, Value};
|
||||||
use std::io::Error as IOErr;
|
use std::io::Error as IOErr;
|
||||||
|
|
||||||
|
use std::option::NoneError as NoneErr;
|
||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use u2f::u2ferror::U2fError as U2fErr;
|
use u2f::u2ferror::U2fError as U2fErr;
|
||||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||||
|
|
||||||
|
#[derive(Display, Serialize)]
|
||||||
|
pub struct Empty {}
|
||||||
|
|
||||||
// Error struct
|
// Error struct
|
||||||
// Contains a String error message, meant for the user and an enum variant, with an error of different types.
|
// 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).
|
// 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
|
// The second one contains the function used to obtain the response sent to the client
|
||||||
make_error! {
|
make_error! {
|
||||||
|
// Just an empty error
|
||||||
|
EmptyError(Empty): _no_source, _serialize,
|
||||||
// Used to represent err! calls
|
// Used to represent err! calls
|
||||||
SimpleError(String): _no_source, _api_error,
|
SimpleError(String): _no_source, _api_error,
|
||||||
// Used for special return values, like 2FA errors
|
// Used for special return values, like 2FA errors
|
||||||
|
@ -66,6 +75,13 @@ make_error! {
|
||||||
YubiError(YubiErr): _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 {
|
impl std::fmt::Debug for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self.source() {
|
match self.source() {
|
||||||
|
@ -80,10 +96,19 @@ impl Error {
|
||||||
(usr_msg, log_msg.into()).into()
|
(usr_msg, log_msg.into()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Empty {}.into()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {
|
pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {
|
||||||
self.message = msg.into();
|
self.message = msg.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_code(mut self, code: u16) -> Self {
|
||||||
|
self.error_code = code;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MapResult<S> {
|
pub trait MapResult<S> {
|
||||||
|
@ -102,6 +127,12 @@ impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn _has_source<T>(e: T) -> Option<T> {
|
||||||
Some(e)
|
Some(e)
|
||||||
}
|
}
|
||||||
|
@ -142,8 +173,10 @@ impl<'r> Responder<'r> for Error {
|
||||||
let usr_msg = format!("{}", self);
|
let usr_msg = format!("{}", self);
|
||||||
error!("{:#?}", self);
|
error!("{:#?}", self);
|
||||||
|
|
||||||
|
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
||||||
|
|
||||||
Response::build()
|
Response::build()
|
||||||
.status(Status::BadRequest)
|
.status(code)
|
||||||
.header(ContentType::JSON)
|
.header(ContentType::JSON)
|
||||||
.sized_body(Cursor::new(usr_msg))
|
.sized_body(Cursor::new(usr_msg))
|
||||||
.ok()
|
.ok()
|
||||||
|
|
40
src/mail.rs
40
src/mail.rs
|
@ -1,8 +1,9 @@
|
||||||
use lettre::smtp::authentication::Credentials;
|
use lettre::smtp::authentication::Credentials;
|
||||||
use lettre::smtp::ConnectionReuseParameters;
|
use lettre::smtp::ConnectionReuseParameters;
|
||||||
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
|
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
|
||||||
use lettre_email::EmailBuilder;
|
use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder};
|
||||||
use native_tls::{Protocol, TlsConnector};
|
use native_tls::{Protocol, TlsConnector};
|
||||||
|
use quoted_printable::encode_to_str;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::auth::{encode_jwt, generate_invite_claims};
|
use crate::auth::{encode_jwt, generate_invite_claims};
|
||||||
|
@ -18,7 +19,13 @@ fn mailer() -> SmtpTransport {
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
ClientSecurity::Required(ClientTlsParameters::new(host.clone(), tls))
|
let params = ClientTlsParameters::new(host.clone(), tls);
|
||||||
|
|
||||||
|
if CONFIG.smtp_explicit_tls() {
|
||||||
|
ClientSecurity::Wrapper(params)
|
||||||
|
} else {
|
||||||
|
ClientSecurity::Required(params)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ClientSecurity::None
|
ClientSecurity::None
|
||||||
};
|
};
|
||||||
|
@ -129,16 +136,39 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
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()
|
let email = EmailBuilder::new()
|
||||||
.to(address)
|
.to(address)
|
||||||
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
|
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.alternative(body_html, body_text)
|
.child(alternative.build())
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
||||||
|
|
||||||
mailer()
|
let mut transport = mailer();
|
||||||
|
|
||||||
|
let result = transport
|
||||||
.send(email.into())
|
.send(email.into())
|
||||||
.map_err(|e| Error::new("Error sending email", e.to_string()))
|
.map_err(|e| Error::new("Error sending email", e.to_string()))
|
||||||
.and(Ok(()))
|
.and(Ok(()));
|
||||||
|
|
||||||
|
// Explicitly close the connection, in case of error
|
||||||
|
transport.close();
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
28
src/main.rs
28
src/main.rs
|
@ -69,6 +69,7 @@ fn launch_info() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_logging() -> Result<(), fern::InitError> {
|
fn init_logging() -> Result<(), fern::InitError> {
|
||||||
|
use std::str::FromStr;
|
||||||
let mut logger = fern::Dispatch::new()
|
let mut logger = fern::Dispatch::new()
|
||||||
.format(|out, message, record| {
|
.format(|out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
|
@ -79,31 +80,30 @@ fn init_logging() -> Result<(), fern::InitError> {
|
||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.level(log::LevelFilter::Debug)
|
.level(log::LevelFilter::from_str(&CONFIG.log_level()).expect("Valid log level"))
|
||||||
.level_for("hyper", log::LevelFilter::Warn)
|
// Hide unknown certificate errors if using self-signed
|
||||||
.level_for("rustls", log::LevelFilter::Warn)
|
.level_for("rustls::session", log::LevelFilter::Off)
|
||||||
.level_for("handlebars", log::LevelFilter::Warn)
|
// Hide failed to close stream messages
|
||||||
.level_for("ws", log::LevelFilter::Info)
|
.level_for("hyper::server", log::LevelFilter::Warn)
|
||||||
.level_for("multipart", log::LevelFilter::Info)
|
|
||||||
.level_for("html5ever", log::LevelFilter::Info)
|
|
||||||
.chain(std::io::stdout());
|
.chain(std::io::stdout());
|
||||||
|
|
||||||
if let Some(log_file) = CONFIG.log_file() {
|
if let Some(log_file) = CONFIG.log_file() {
|
||||||
logger = logger.chain(fern::log_file(log_file)?);
|
logger = logger.chain(fern::log_file(log_file)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger = chain_syslog(logger);
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
|
||||||
|
logger = chain_syslog(logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.apply()?;
|
logger.apply()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "enable_syslog"))]
|
#[cfg(not(windows))]
|
||||||
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
|
||||||
logger
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "enable_syslog")]
|
|
||||||
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
||||||
let syslog_fmt = syslog::Formatter3164 {
|
let syslog_fmt = syslog::Formatter3164 {
|
||||||
facility: syslog::Facility::LOG_USER,
|
facility: syslog::Facility::LOG_USER,
|
||||||
|
|
BIN
src/static/images/logo-gray.png
Normale Datei
BIN
src/static/images/logo-gray.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 7,4 KiB |
BIN
src/static/images/mail-github.png
Normale Datei
BIN
src/static/images/mail-github.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 1,3 KiB |
|
@ -6,16 +6,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<title>Bitwarden_rs Admin Panel</title>
|
<title>Bitwarden_rs Admin Panel</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.2.1/css/bootstrap.min.css"
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css"
|
||||||
integrity="sha256-azvvU9xKluwHFJ0Cpgtf0CYzK7zgtOznnzxV4924X1w=" crossorigin="anonymous" />
|
integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
|
||||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.10.0/js/md5.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.10.0/js/md5.min.js"
|
||||||
integrity="sha256-tCQ/BldMlN2vWe5gAiNoNb5svoOgVUhlUgv7UjONKKQ=" crossorigin="anonymous"></script>
|
integrity="sha256-J9IhvkIJb0diRVJOyu+Ndtg41RibFkF8eaA60jdjtB8=" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js"
|
||||||
integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA=" crossorigin="anonymous"></script>
|
integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA=" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.2.1/js/bootstrap.bundle.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha256-MSYVjWgrr6UL/9eQfQvOyt6/gsxb6dpwI1zqM5DbLCs=" crossorigin="anonymous"></script>
|
integrity="sha256-fzFFyH01cBVPYzl16KT40wqjhgPtq6FFUB6ckN2+GGw=" crossorigin="anonymous"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
{{#if TwoFactorEnabled}}
|
{{#if TwoFactorEnabled}}
|
||||||
<span class="badge badge-success ml-2">2FA</span>
|
<span class="badge badge-success ml-2">2FA</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless _Enabled}}
|
{{#case _Status 1}}
|
||||||
<span class="badge badge-warning ml-2">Disabled</span>
|
<span class="badge badge-warning ml-2">Invited</span>
|
||||||
{{/unless}}
|
{{/case}}
|
||||||
<span class="d-block">{{Email}}</span>
|
<span class="d-block">{{Email}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
@ -37,9 +37,14 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="d-block text-right mt-3">
|
<div class="mt-3">
|
||||||
<a id="reload-btn" href="">Reload users</a>
|
<button type="button" class="btn btn-sm btn-link" onclick="updateRevisions();"
|
||||||
</small>
|
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>
|
||||||
|
|
||||||
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
|
||||||
|
@ -57,6 +62,11 @@
|
||||||
<div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
|
<div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-white mb-3">Configuration</h6>
|
<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">
|
<form class="form accordion" id="config-form">
|
||||||
{{#each config}}
|
{{#each config}}
|
||||||
{{#if groupdoc}}
|
{{#if groupdoc}}
|
||||||
|
@ -105,11 +115,11 @@
|
||||||
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
|
<div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
|
||||||
data-target="#g_readonly">Read-Only Config</button></div>
|
data-target="#g_readonly">Read-Only Config</button></div>
|
||||||
<div id="g_readonly" class="card-body collapse" data-parent="#config-form">
|
<div id="g_readonly" class="card-body collapse" data-parent="#config-form">
|
||||||
<p>
|
<div class="small mb-3">
|
||||||
NOTE: These options can't be modified in the editor because they would require the server
|
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
|
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.
|
launching the server. You can check the variable names in the tooltips of each option.
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
{{#each config}}
|
{{#each config}}
|
||||||
{{#each elements}}
|
{{#each elements}}
|
||||||
|
@ -209,6 +219,12 @@
|
||||||
"Error deauthorizing sessions");
|
"Error deauthorizing sessions");
|
||||||
return false;
|
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() {
|
function inviteUser() {
|
||||||
inv = $("#email-invite");
|
inv = $("#email-invite");
|
||||||
data = JSON.stringify({ "email": inv.val() });
|
data = JSON.stringify({ "email": inv.val() });
|
||||||
|
|
|
@ -82,7 +82,7 @@ Invitation accepted
|
||||||
<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">
|
<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;">
|
<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">
|
<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>
|
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" 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%;" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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;">
|
<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;">
|
||||||
|
@ -118,11 +118,7 @@ Invitation accepted
|
||||||
<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">
|
<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;">
|
<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;">
|
<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">
|
<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;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" 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%;" /></a></td>
|
||||||
<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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -82,7 +82,7 @@ Invitation to {{org_name}} confirmed
|
||||||
<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">
|
<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;">
|
<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">
|
<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>
|
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" 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%;" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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;">
|
<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;">
|
||||||
|
@ -114,11 +114,7 @@ Invitation to {{org_name}} confirmed
|
||||||
<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">
|
<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;">
|
<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;">
|
<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">
|
<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;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" 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%;" /></a></td>
|
||||||
<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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -82,7 +82,7 @@ Sorry, you have no password hint...
|
||||||
<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">
|
<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;">
|
<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">
|
<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>
|
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" 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%;" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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;">
|
<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;">
|
||||||
|
@ -113,11 +113,7 @@ Sorry, you have no password hint...
|
||||||
<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">
|
<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;">
|
<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;">
|
<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">
|
<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;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" 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%;" /></a></td>
|
||||||
<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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -3,5 +3,6 @@ Your master password hint
|
||||||
You (or someone) recently requested your master password hint.
|
You (or someone) recently requested your master password hint.
|
||||||
|
|
||||||
Your hint is: "{{hint}}"
|
Your hint is: "{{hint}}"
|
||||||
|
Log in: <a href="{{url}}">Web Vault</a>
|
||||||
|
|
||||||
If you did not request your master password hint you can safely ignore this email.
|
If you did not request your master password hint you can safely ignore this email.
|
||||||
|
|
|
@ -82,7 +82,7 @@ Your master password hint
|
||||||
<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">
|
<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;">
|
<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">
|
<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>
|
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" 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%;" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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;">
|
<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;">
|
||||||
|
@ -119,11 +119,7 @@ Your master password hint
|
||||||
<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">
|
<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;">
|
<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;">
|
<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">
|
<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;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" 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%;" /></a></td>
|
||||||
<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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -82,7 +82,7 @@ Join {{org_name}}
|
||||||
<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">
|
<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;">
|
<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">
|
<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>
|
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" 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%;" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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;">
|
<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;">
|
||||||
|
@ -101,7 +101,8 @@ Join {{org_name}}
|
||||||
</tr>
|
</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;">
|
<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">
|
<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;">
|
<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
|
Join Organization Now
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -120,11 +121,7 @@ Join {{org_name}}
|
||||||
<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">
|
<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;">
|
<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;">
|
<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">
|
<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;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" 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%;" /></a></td>
|
||||||
<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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
18
src/util.rs
18
src/util.rs
|
@ -218,7 +218,7 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
|
||||||
let mut result_map = JsonMap::new();
|
let mut result_map = JsonMap::new();
|
||||||
|
|
||||||
while let Some((key, value)) = map.next_entry()? {
|
while let Some((key, value)) = map.next_entry()? {
|
||||||
result_map.insert(upcase_first(key), upcase_value(&value));
|
result_map.insert(upcase_first(key), upcase_value(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::Object(result_map))
|
Ok(Value::Object(result_map))
|
||||||
|
@ -231,32 +231,32 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
|
||||||
let mut result_seq = Vec::<Value>::new();
|
let mut result_seq = Vec::<Value>::new();
|
||||||
|
|
||||||
while let Some(value) = seq.next_element()? {
|
while let Some(value) = seq.next_element()? {
|
||||||
result_seq.push(upcase_value(&value));
|
result_seq.push(upcase_value(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::Array(result_seq))
|
Ok(Value::Array(result_seq))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upcase_value(value: &Value) -> Value {
|
fn upcase_value(value: Value) -> Value {
|
||||||
if let Some(map) = value.as_object() {
|
if let Value::Object(map) = value {
|
||||||
let mut new_value = json!({});
|
let mut new_value = json!({});
|
||||||
|
|
||||||
for (key, val) in map {
|
for (key, val) in map.into_iter() {
|
||||||
let processed_key = _process_key(key);
|
let processed_key = _process_key(&key);
|
||||||
new_value[processed_key] = upcase_value(val);
|
new_value[processed_key] = upcase_value(val);
|
||||||
}
|
}
|
||||||
new_value
|
new_value
|
||||||
} else if let Some(array) = value.as_array() {
|
} else if let Value::Array(array) = value {
|
||||||
// Initialize array with null values
|
// Initialize array with null values
|
||||||
let mut new_value = json!(vec![Value::Null; array.len()]);
|
let mut new_value = json!(vec![Value::Null; array.len()]);
|
||||||
|
|
||||||
for (index, val) in array.iter().enumerate() {
|
for (index, val) in array.into_iter().enumerate() {
|
||||||
new_value[index] = upcase_value(val);
|
new_value[index] = upcase_value(val);
|
||||||
}
|
}
|
||||||
new_value
|
new_value
|
||||||
} else {
|
} else {
|
||||||
value.clone()
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Laden …
In neuem Issue referenzieren