Compare commits

...

124 Commits

Author SHA1 Message Date
Daniel García
924e4a17e5 Merge pull request #175 from stammw/master
Documentation for SMTP and password hint configuration
2018-09-13 15:46:52 +02:00
Daniel García
fdbd73c716 Merge branch 'master' into master 2018-09-13 15:39:28 +02:00
Daniel García
f397f0cbd0 Implement organization import for admins and owners (Fixes #178) 2018-09-13 15:16:24 +02:00
Daniel García
4d2c6e39b2 Merge pull request #177 from mprasil/raspberry
Add Dockerfile for Raspberry Pi
2018-09-13 00:19:15 +02:00
Daniel García
3e1afb139c Remove unnecessary return 2018-09-12 23:58:02 +02:00
Jean-Christophe BEGUE
af69606bea Documentation for SMTP and password hint configuration 2018-09-12 21:19:29 +02:00
Miroslav Prasil
bc8ff14695 Fix the binary path 2018-09-12 13:51:43 +01:00
Miroslav Prasil
5f7b220eb4 Initial shot as cross compilation 2018-09-12 12:15:26 +01:00
Daniel García
1b20a25514 Merge pull request #173 from mprasil/poormans_invites
Implement poor man's invitation via Organization invitation
2018-09-11 16:48:56 +02:00
Miroslav Prasil
c1cd4d9a6b Modify User::new to be keyless and paswordless 2018-09-11 14:25:12 +01:00
Daniel García
b63693aefb Merge pull request #137 from stammw/master
SMTP implementation, along with password HINT email
2018-09-11 14:58:09 +02:00
Miroslav Prasil
ec05f14f5a Implement poor man's invitation via Organization invitation 2018-09-11 13:09:59 +01:00
Jean-Christophe BEGUE
37d88be2be return an error when email adress for password hint is not valid 2018-09-11 13:12:24 +02:00
Jean-Christophe BEGUE
1c641d7635 Special messages when user has no password hint 2018-09-11 13:04:34 +02:00
Jean-Christophe BEGUE
e2ab2f7306 Save None instead of empty password hint 2018-09-11 13:00:59 +02:00
Daniel García
8df6f79f19 Merge pull request #170 from mprasil/org-user-edit
Fix editing users in Organization
2018-09-04 17:32:16 +02:00
Miroslav Prasil
c58682e3fb Fix the logic in user edditing 2018-09-04 16:10:26 +01:00
Miroslav Prasil
db111ae2a0 Check properly the user membership in Organization 2018-09-04 13:37:44 +01:00
Miroslav Prasil
049aa33f17 Fix editing users in Organization 2018-09-04 12:15:46 +01:00
Daniel García
b1ac37609f Merge pull request #169 from mprasil/http_warning
Add info on running over HTTP (documentation for #153)
2018-09-03 13:47:05 +02:00
Miroslav Prasil
53e8f78af6 Link to the https setup 2018-09-03 10:59:59 +01:00
Miroslav Prasil
1bced97e04 Add info on running over HTTP (documentation for #153) 2018-09-03 10:53:52 +01:00
Daniel García
f94e626021 Merge pull request #166 from mprasil/alpine
Alpine
2018-08-30 16:47:58 +02:00
Daniel García
0a3b84b815 Merge pull request #165 from mprasil/shared_edit_fix
Fix editing shared cipher (fixes #164)
2018-08-30 16:47:08 +02:00
Miroslav Prasil
d336d89b83 Fix editing shared cipher (fixes #164) 2018-08-30 11:12:29 +01:00
Miroslav Prasil
1a5c1979e3 Move Alpine Dockerfile to separate file 2018-08-30 10:38:38 +01:00
Miroslav Prasil
cec9566d2a Merge branch 'master' into alpine 2018-08-29 15:06:50 +01:00
Baelyk
fe473b9e75 Attachment::save() returns Result instead of bool (#161)
Returning a result instead of a bool as per #6
2018-08-29 15:22:19 +02:00
mprasil
062ae4dd59 Allow non-Admin user to share to collection (fixes #157) (#159)
* Allow non-Admin user to share to collection (fixes #157)

* Better handling of collection sharing
2018-08-29 15:22:03 +02:00
Miroslav Prasil
45d676eb10 Merge branch 'master' into alpine 2018-08-29 10:07:09 +01:00
mprasil
3cfdf9b585 Add DELETE handlers fo cipher and attachment deletion (fixes #158) (#160) 2018-08-29 00:48:53 +02:00
Miroslav Prasil
08b551624c Merge branch 'master' into alpine 2018-08-28 14:06:54 +01:00
Daniel García
761a0a3393 Removed accidental change to Dockerfile 2018-08-28 12:54:57 +02:00
Daniel García
6660b0aef3 Updated web vault to version 2.2 2018-08-28 03:22:13 +02:00
Kumar Ankur
781056152a Support password history #155 (#156)
* Password History Support (#155)

* down.sql logic not required as per review comments
2018-08-27 23:08:58 +02:00
Miroslav Prasil
6822bb28a0 Merge branch 'master' into alpine 2018-08-26 16:58:46 +01:00
Daniel García
b82710eecf Merge pull request #152 from Baelyk/master
Add ip and username to failed login attempts
2018-08-26 17:43:50 +02:00
Baelyk
c386b3bcf7 Add IP and Username to failed login attempts
Resolves #119
2018-08-25 17:07:59 -05:00
Miroslav Prasil
ffec0b065b Updated build image version 2018-08-25 09:29:50 +01:00
Miroslav Prasil
5b7fe9f155 Merge branch 'master' into alpine 2018-08-24 23:17:52 +01:00
Daniel García
8d1ee859f2 Implemented basic support for prelogin and notification negotiation 2018-08-24 19:02:34 +02:00
Daniel García
c91f80c456 Fixed rust toolchain date 2018-08-24 17:12:04 +02:00
Daniel García
39891e86a0 Updated dependencies, added Travis CI integration and some badges 2018-08-24 17:07:11 +02:00
Miroslav Prasil
575f701390 Merge branch 'master' into alpine 2018-08-23 21:59:23 +01:00
Daniel García
335099cd30 Merge pull request #150 from mprasil/build_instructions
Update the build instruction for new Vault
2018-08-23 16:05:24 +02:00
Miroslav Prasil
9fad541c87 Clone repository instead of downloading as suggested by @mqus 2018-08-23 12:08:54 +01:00
Miroslav Prasil
007e053e2f Update the build instruction for new Vault 2018-08-23 11:06:32 +01:00
Miroslav Prasil
ef2413a5aa Fix SSL issue, rm cache 2018-08-21 22:08:16 +01:00
Miroslav Prasil
ca8e1c646d Update build image 2018-08-21 22:08:16 +01:00
Miroslav Prasil
346c7630c9 Initial implementation of musl build on top of Alpine 2018-08-21 22:08:16 +01:00
Daniel García
1c57c9d8e0 Merge pull request #148 from mprasil/beta
Merge Beta to master
2018-08-21 22:41:50 +02:00
Daniel García
bd20d8724b Merge pull request #147 from mprasil/master
Bump version to 0.13.0 - latest Vault v1
2018-08-21 22:32:54 +02:00
Miroslav Prasil
69a18255c6 Bump up version to 1.0.0 2018-08-21 21:21:54 +01:00
Miroslav Prasil
c40baf5e17 Merge branch 'master' into beta 2018-08-21 21:17:12 +01:00
Miroslav Prasil
df041108f6 Bump version to 0.13.0 - latest Vault v1 2018-08-21 21:13:56 +01:00
Daniel García
ee10d278a7 Merge pull request #146 from mprasil/cipher_folder_revision
Update affected users revision on cipher and folder change
2018-08-21 21:44:33 +02:00
Miroslav Prasil
2b2401be19 Update affected users revision on cipher and folder change 2018-08-21 17:32:00 +01:00
Daniel García
4f58d07c83 Merge pull request #145 from mprasil/org_user_revision
Organization update improvements
2018-08-21 16:27:19 +02:00
Miroslav Prasil
9eea0151ba Update user revision timestamp on Organization changes 2018-08-21 13:26:22 +01:00
Miroslav Prasil
40d09ddd2a Add PUT alias for Organization updates 2018-08-21 13:25:52 +01:00
Daniel García
d332e87655 Merge pull request #144 from mprasil/collection_revision
Update affected users revision when there are collection changes
2018-08-21 13:47:19 +02:00
Daniel García
0fa48a749f Merge pull request #143 from mprasil/update_revision_fix
Actually update the revision date for user struct, not just in DB
2018-08-21 13:46:05 +02:00
Miroslav Prasil
a5ef8aef0f Update affected users revision when there are collection changes 2018-08-21 12:20:55 +01:00
Miroslav Prasil
4fb09c5b4d Actually update the revision date for user struct, not just in DB 2018-08-21 10:36:04 +01:00
Jean-Christophe BEGUE
9e63985b28 Check email validity before using it for password hint sending 2018-08-16 21:25:28 +02:00
Daniel García
6fdeeb56ce Merge pull request #140 from mprasil/error_format
Update the error format to show message in new Vault
2018-08-16 00:52:46 +02:00
Daniel García
b002d34cd4 Merge pull request #139 from mprasil/edit_shared_fix
Add PUT alias for editing cipher
2018-08-15 23:02:59 +02:00
Daniel García
e46fc62b78 Merge pull request #141 from mprasil/profile_update
Add PUT alias for profile update
2018-08-15 23:02:34 +02:00
Jean-Christophe BEGUE
401aa7c699 make SMTP authentication optionnal, let lettre pick the better auth mechanism 2018-08-15 17:21:19 +02:00
Miroslav Prasil
12a2dc0901 Add PUT alias for profile update 2018-08-15 16:10:40 +01:00
Miroslav Prasil
b3f3fd81ac Update theerror format to show message in new Vault 2018-08-15 15:50:07 +01:00
Miroslav Prasil
f2fec345ec Add PUT alias for editing cipher 2018-08-15 14:27:37 +01:00
Daniel García
b6312340b6 Merge pull request #138 from mprasil/readme_updates
Cleaned up HTTPS example
2018-08-15 15:12:01 +02:00
Daniel García
3d1fc0f2e8 Merge pull request #136 from mprasil/disable_analytics
Remove analytics from Vault
2018-08-15 15:11:45 +02:00
Jean-Christophe BEGUE
d68f57cbba Fix password hint showing logic 2018-08-15 14:08:00 +02:00
Miroslav Prasil
80bad9f66d Cleaned up HTTPS example 2018-08-15 11:18:34 +01:00
Jean-Christophe BEGUE
19e0605d30 Better message into the password hint email 2018-08-15 10:17:05 +02:00
Jean-Christophe BEGUE
812387e586 SMTP integration, send password hint by email. 2018-08-15 08:45:18 +02:00
Miroslav Prasil
5ecafb157d Disable analytics via patch to Vault 2018-08-14 21:48:56 +01:00
Daniel García
f1ade62638 Merge pull request #135 from mprasil/empty_collection
Deserialize "null" to empty Vec for Collections
2018-08-14 16:43:03 +02:00
Miroslav Prasil
00b882935f Deserialize "null" to empty Vec for Collections 2018-08-14 11:06:42 +01:00
Daniel García
eb5641b863 Merge pull request #134 from mprasil/put_collections_admin
Add aliases for PUTs and DELETEs on collections, organizations and org users
2018-08-13 20:16:34 +02:00
Miroslav Prasil
0dfd9c7670 Add couple more aliases for PUTs and DELETEs 2018-08-13 16:45:30 +01:00
Miroslav Prasil
6ede1743ac add alias for PUT collections-admin 2018-08-13 16:00:10 +01:00
Daniel García
d3f357b708 Implemented PUT for u2f registration 2018-08-13 15:26:01 +02:00
Daniel García
5a55dd1d4b Merge pull request #133 from mprasil/document_differences
Extend documentation
2018-08-13 14:38:54 +02:00
Daniel García
16056626b0 Merge pull request #131 from mprasil/revision_date
Implement update_revision trigger
2018-08-13 14:38:30 +02:00
Jean-Christophe BEGUE
f7ffb81d9e SMTP configuration parsing and checking 2018-08-13 13:46:32 +02:00
Miroslav Prasil
626a3c93ba Revert "Merge branch 'beta' of https://github.com/krankur/bitwarden_rs into beta"
This reverts commit 3fd3d8d5e9.
2018-08-13 12:35:41 +01:00
Miroslav Prasil
c0f554311b Extend documentation 2018-08-13 12:01:52 +01:00
Miroslav Prasil
3f5a99916a Implement update_revision trigger 2018-08-13 10:58:39 +01:00
Miroslav Prasil
b5a057f063 Merge branch 'master' into beta 2018-08-10 21:43:16 +01:00
Daniel García
e7e0717f5b Merge pull request #129 from krankur/beta
Implemented PUT for /two-factor/authenticator and /two-factor/disable #124
2018-08-10 22:20:44 +02:00
Kumar Ankur
3fd3d8d5e9 Merge branch 'beta' of https://github.com/krankur/bitwarden_rs into beta 2018-08-10 23:49:34 +05:30
Kumar Ankur
7b2de40beb Merge branch 'beta' of https://github.com/krankur/bitwarden_rs into beta 2018-08-10 23:26:55 +05:30
Kumar Ankur
5f6d721c09 Implemented PUT for /two-factor/authenticator and /two-factor/disable 2018-08-10 23:20:19 +05:30
Kumar Ankur
ddda86b90d Implemented bulk cipher share (share selected) #100 2018-08-10 23:20:19 +05:30
Daniel García
c6256e1455 Merge pull request #128 from mprasil/revision_date
Return revision date in miliseconds (fixes #127)
2018-08-10 19:40:56 +02:00
Daniel García
0cd3053fcb Merge pull request #125 from stammw/master
Make password hints available in the error message #85
2018-08-10 19:40:31 +02:00
Miroslav Prasil
58c1545707 Return revision date in miliseconds (fixes #127) 2018-08-10 17:18:59 +01:00
Jean-Christophe BEGUE
d3b4b10d18 Add a explaination to the password hint message #85 2018-08-10 16:59:23 +02:00
Jean-Christophe BEGUE
c031ae9f2f Make password hints available in the error message #85 2018-08-10 15:52:06 +02:00
Daniel García
672e3273cd Merge pull request #123 from mprasil/beta_patch
Fix patch file for v2.1.1
2018-08-09 16:40:29 +02:00
Miroslav Prasil
039860f87e Fix patch file for v2.1.1 2018-08-09 13:38:40 +01:00
Daniel García
9511456ded Merge pull request #116 from krankur/beta
Implemented bulk cipher share (share selected) #100
2018-08-09 01:44:16 +02:00
Daniel García
04b198a7e2 Merge pull request #120 from mprasil/vault_2.1.1
Update vault to latest version
2018-08-09 01:43:22 +02:00
Miroslav Prasil
73a1abed10 Update vault to latest version 2018-08-08 23:15:01 +02:00
Kumar Ankur
fb7b1c8c18 Implemented bulk cipher share (share selected) #100 2018-08-06 03:29:44 +05:30
Daniel García
8ffa7ebb6a Merge pull request #115 from krankur/beta
Implmeneted DELETE on 'api/ciphers' to delete selected ciphers (#98)
2018-08-03 16:29:23 +02:00
Kumar Ankur
aac1304b46 clean up 2018-08-03 19:31:01 +05:30
Kumar Ankur
7dfc759691 Implmeneted DELETE on 'api/ciphers' to delete selected ciphers (#98) 2018-08-03 19:23:38 +05:30
Daniel García
54afe0671e Merge pull request #111 from krankur/beta
Implemented PUT for single cipher sharing (#97)
2018-08-01 21:12:48 +02:00
Kumar Ankur
74e2ca81ae Implemented PUT for single cipher sharing (#97) 2018-08-02 00:07:14 +05:30
Daniel García
d6fadb52ff Merge pull request #109 from mprasil/beta_merge
Merge changes from master to beta
2018-08-01 12:57:32 +02:00
Miroslav Prasil
b163aeb8ca Merge changes in master to beta branch (concurrency fixes) 2018-08-01 11:37:42 +01:00
Daniel García
fcb479a457 Merge pull request #108 from krankur/beta
Implementing PUT for 'api/ciphers/move' (#99)
2018-08-01 11:49:14 +02:00
Kumar Ankur
0e095a9fa4 change to reuse the logic for POST in PUT as well 2018-08-01 13:50:52 +05:30
Kumar Ankur
2f6aa3c363 Reverting removal of 'api/ciphers/move' POST as it is required for backward compatibility 2018-08-01 11:21:05 +05:30
Kumar Ankur
fcc485384f clean up 2018-08-01 04:12:46 +05:30
Kumar Ankur
91a2319325 Implementing PUT for ciphers/move (#99) 2018-08-01 03:58:47 +05:30
Daniel García
07a30c8334 Merge pull request #106 from mprasil/beta_stable
Use stable release of v2.0.0
2018-07-30 15:43:41 +02:00
Miroslav Prasil
ceb3d0314d Use stable release of v2.0.0 2018-07-27 10:01:33 +01:00
Daniel García
659f677897 Add missing slash, to put it like it was at first 2018-07-21 18:50:54 +02:00
Daniel García
a291dea16f Updated dependencies and Docker image to new web-vault 2018-07-21 17:27:00 +02:00
35 changed files with 1561 additions and 582 deletions

10
.env
View File

@@ -27,6 +27,9 @@
## The change only applies when the password is changed ## The change only applies when the password is changed
# PASSWORD_ITERATIONS=100000 # PASSWORD_ITERATIONS=100000
## Whether password hint should be sent into the error response when the client request it
# SHOW_PASSWORD_HINT=true
## Domain settings ## Domain settings
## The domain must match the address from where you access the server ## The domain must match the address from where you access the server
## Unless you are using U2F, or having problems with attachments not downloading, there is no need to change this ## Unless you are using U2F, or having problems with attachments not downloading, there is no need to change this
@@ -38,3 +41,10 @@
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
# ROCKET_PORT=8000 # ROCKET_PORT=8000
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
## Mail specific settings, if SMTP_HOST is specified, SMTP_USERNAME and SMTP_PASSWORD are mandatory
# SMTP_HOST=smtp.domain.tld
# SMTP_PORT=587
# SMTP_SSL=true
# SMTP_USERNAME=username
# SMTP_PASSWORD=password

7
.travis.yml Normal file
View File

@@ -0,0 +1,7 @@
# Copied from Rocket's .travis.yml
language: rust
sudo: required # so we get a VM with higher specs
dist: trusty # so we get a VM with higher specs
cache: cargo
rust:
- nightly

View File

@@ -17,28 +17,29 @@ cargo build --release
When run, the server is accessible in [http://localhost:80](http://localhost:80). When run, the server is accessible in [http://localhost:80](http://localhost:80).
### Install the web-vault ### Install the web-vault
Download the latest official release from the [releases page](https://github.com/bitwarden/web/releases) and extract it. Clone the git repository at [bitwarden/web](https://github.com/bitwarden/web) and checkout the latest release tag (e.g. v2.1.1):
Modify `web-vault/settings.Production.json` to look like this:
```json
{
"appSettings": {
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "/icons",
"stripeKey": "",
"braintreeKey": ""
}
}
```
Then, run the following from the `web-vault` directory:
```sh ```sh
npm install # clone the repository
npx gulp dist:selfHosted git clone https://github.com/bitwarden/web.git web-vault
cd web-vault
# switch to the latest tag
git checkout "$(git tag | tail -n1)"
``` ```
Finally copy the contents of the `web-vault/dist` folder into the `bitwarden_rs/web-vault` folder. Apply the patch file from `docker/set-vault-baseurl.patch`:
```sh
# In the Vault repository directory
git apply /path/to/bitwarden_rs/docker/set-vault-baseurl.patch
```
Then, build the Vault:
```sh
npm run sub:init
npm install
npm run dist
```
Finally copy the contents of the `build` folder into the `bitwarden_rs/web-vault` folder.
# Configuration # Configuration
The available configuration options are documented in the default `.env` file, and they can be modified by uncommenting the desired options in that file or by setting their respective environment variables. Look at the README file for the main configuration options available. The available configuration options are documented in the default `.env` file, and they can be modified by uncommenting the desired options in that file or by setting their respective environment variables. Look at the README file for the main configuration options available.

685
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,31 @@
[package] [package]
name = "bitwarden_rs" name = "bitwarden_rs"
version = "0.12.0" version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"] authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
[dependencies] [dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed. # Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
rocket = { version = "0.3.14", features = ["tls"] } rocket = { version = "0.3.16", features = ["tls"] }
rocket_codegen = "0.3.14" rocket_codegen = "0.3.16"
rocket_contrib = "0.3.14" rocket_contrib = "0.3.16"
# HTTP client # HTTP client
reqwest = "0.8.6" reqwest = "0.8.8"
# multipart/form-data support # multipart/form-data support
multipart = "0.14.2" multipart = "0.15.2"
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = "1.0.70" serde = "1.0.74"
serde_derive = "1.0.70" serde_derive = "1.0.74"
serde_json = "1.0.22" serde_json = "1.0.26"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "1.3.2", features = ["sqlite", "chrono", "r2d2"] } diesel = { version = "1.3.2", features = ["sqlite", "chrono", "r2d2"] }
diesel_migrations = { version = "1.3.0", features = ["sqlite"] } diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
# Bundled SQLite # Bundled SQLite
libsqlite3-sys = { version = "0.9.1", features = ["bundled"] } libsqlite3-sys = { version = "0.9.3", features = ["bundled"] }
# Crypto library # Crypto library
ring = { version = "= 0.11.0", features = ["rsa_signing"] } ring = { version = "= 0.11.0", features = ["rsa_signing"] }
@@ -34,7 +34,7 @@ ring = { version = "= 0.11.0", features = ["rsa_signing"] }
uuid = { version = "0.6.5", features = ["v4"] } uuid = { version = "0.6.5", features = ["v4"] }
# Date and time library for Rust # Date and time library for Rust
chrono = "0.4.4" chrono = "0.4.5"
# TOTP library # TOTP library
oath = "0.10.2" oath = "0.10.2"
@@ -52,15 +52,20 @@ u2f = "0.1.2"
dotenv = { version = "0.13.0", default-features = false } dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro # Lazy static macro
lazy_static = "1.0.1" lazy_static = "1.1.0"
# Numerical libraries # Numerical libraries
num-traits = "0.2.5" num-traits = "0.2.5"
num-derive = "0.2.2" num-derive = "0.2.2"
lettre = "0.8.2"
lettre_email = "0.8.2"
native-tls = "0.1.5"
fast_chemail = "0.9.5"
[patch.crates-io] [patch.crates-io]
# Make jwt use ring 0.11, to match rocket # Make jwt use ring 0.11, to match rocket
jsonwebtoken = { path = "libs/jsonwebtoken" } jsonwebtoken = { path = "libs/jsonwebtoken" }
# Version 0.1.2 from crates.io lacks a commit that fixes a certificate error # Version 0.1.2 from crates.io lacks a commit that fixes a certificate error
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' } u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' }

View File

@@ -2,31 +2,27 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM node:9-alpine as vault FROM node:8-alpine as vault
ENV VAULT_VERSION "1.27.0" ENV VAULT_VERSION "v2.2.0"
ENV URL "https://github.com/bitwarden/web/archive/v${VAULT_VERSION}.tar.gz"
ENV URL "https://github.com/bitwarden/web.git"
RUN apk add --update-cache --upgrade \ RUN apk add --update-cache --upgrade \
curl \ curl \
git \ git \
tar \ tar
&& npm install -g \
gulp-cli \
gulp
RUN mkdir /web-build \
&& cd /web-build \
&& curl -L "${URL}" | tar -xvz --strip-components=1
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
WORKDIR /web-build WORKDIR /web-build
COPY /docker/settings.Production.json /web-build/ COPY /docker/set-vault-baseurl.patch /web-build/
RUN git apply set-vault-baseurl.patch
RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/ \ RUN npm run sub:init && npm install
&& npm install \
&& gulp dist:selfHosted \ RUN npm run dist \
&& mv dist /web-vault && mv build /web-vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because

80
Dockerfile.alpine Normal file
View File

@@ -0,0 +1,80 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault
ENV VAULT_VERSION "v2.2.0"
ENV URL "https://github.com/bitwarden/web.git"
RUN apk add --update-cache --upgrade \
curl \
git \
tar
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
WORKDIR /web-build
COPY /docker/set-vault-baseurl.patch /web-build/
RUN git apply set-vault-baseurl.patch
RUN npm run sub:init && npm install
RUN npm run dist \
&& mv build /web-vault
########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2018-08-24 as build
# Creates a dummy project used to grab dependencies
RUN USER=root cargo init --bin
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM alpine:3.8
ENV ROCKET_ENV "staging"
ENV ROCKET_WORKERS=10
ENV SSL_CERT_DIR=/etc/ssl/certs
# Install needed libraries
RUN apk add \
openssl\
ca-certificates \
&& rm /var/cache/apk/*
RUN mkdir /data
VOLUME /data
EXPOSE 80
# Copies the files from the context (env file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /volume/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
# Configures the startup!
CMD ./bitwarden_rs

113
Dockerfile.armv7 Normal file
View File

@@ -0,0 +1,113 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault
ENV VAULT_VERSION "v2.2.0"
ENV URL "https://github.com/bitwarden/web.git"
RUN apk add --update-cache --upgrade \
curl \
git \
tar
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build
WORKDIR /web-build
COPY /docker/set-vault-baseurl.patch /web-build/
RUN git apply set-vault-baseurl.patch
RUN npm run sub:init && npm install
RUN npm run dist \
&& mv build /web-vault
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust as build
RUN apt-get update \
&& apt-get install -y \
gcc-arm-linux-gnueabihf \
&& mkdir -p ~/.cargo \
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Prepare openssl armhf libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armhf \
&& apt-get update \
&& apt-get install -y \
libssl-dev:armhf \
libc6-dev:armhf
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
COPY . .
RUN rustup target add armv7-unknown-linux-gnueabihf
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM resin/armv7hf-debian:stretch
ENV ROCKET_ENV "staging"
ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ]
# Install needed libraries
RUN apt-get update && apt-get install -y\
openssl\
ca-certificates\
--no-install-recommends\
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (env file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
# Configures the startup!
CMD ./bitwarden_rs

112
README.md
View File

@@ -1,9 +1,19 @@
This is Bitwarden server API implementation written in rust compatible with [upstream Bitwarden clients](https://bitwarden.com/#download)*, ideal for self-hosted deployment where running official resource-heavy service might not be ideal. ### This is a Bitwarden server API implementation written in Rust compatible with [upstream Bitwarden clients](https://bitwarden.com/#download)*, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.
---
[![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/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)
[![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)
[![Matrix Chat](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#bitwarden_rs:matrix.org)
Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/bitwarden_rs). Image is based on [Rust implementation of Bitwarden API](https://github.com/dani-garcia/bitwarden_rs).
_*Note, that this project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC._ _*Note, that this project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC._
---
**Table of contents** **Table of contents**
- [Features](#features) - [Features](#features)
@@ -13,6 +23,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [Updating the bitwarden image](#updating-the-bitwarden-image) - [Updating the bitwarden image](#updating-the-bitwarden-image)
- [Configuring bitwarden service](#configuring-bitwarden-service) - [Configuring bitwarden service](#configuring-bitwarden-service)
- [Disable registration of new users](#disable-registration-of-new-users) - [Disable registration of new users](#disable-registration-of-new-users)
- [Disable invitations](#disable-invitations)
- [Enabling HTTPS](#enabling-https) - [Enabling HTTPS](#enabling-https)
- [Enabling U2F authentication](#enabling-u2f-authentication) - [Enabling U2F authentication](#enabling-u2f-authentication)
- [Changing persistent data location](#changing-persistent-data-location) - [Changing persistent data location](#changing-persistent-data-location)
@@ -22,6 +33,9 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [icons cache](#icons-cache) - [icons cache](#icons-cache)
- [Changing the API request size limit](#changing-the-api-request-size-limit) - [Changing the API request size limit](#changing-the-api-request-size-limit)
- [Changing the number of workers](#changing-the-number-of-workers) - [Changing the number of workers](#changing-the-number-of-workers)
- [SMTP configuration](#smtp-configuration)
- [Password hint display](#password-hint-display)
- [Disabling or overriding the Vault interface hosting](#disabling-or-overriding-the-vault-interface-hosting)
- [Other configuration](#other-configuration) - [Other configuration](#other-configuration)
- [Building your own image](#building-your-own-image) - [Building your own image](#building-your-own-image)
- [Building binary](#building-binary) - [Building binary](#building-binary)
@@ -33,6 +47,11 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [3. the key files](#3-the-key-files) - [3. the key files](#3-the-key-files)
- [4. Icon Cache](#4-icon-cache) - [4. Icon Cache](#4-icon-cache)
- [Running the server with non-root user](#running-the-server-with-non-root-user) - [Running the server with non-root user](#running-the-server-with-non-root-user)
- [Differences from upstream API implementation](#differences-from-upstream-api-implementation)
- [Changing user email](#changing-user-email)
- [Creating organization](#creating-organization)
- [Inviting users into organization](#inviting-users-into-organization)
- [Running on unencrypted connection](#running-on-unencrypted-connection)
- [Get in touch](#get-in-touch) - [Get in touch](#get-in-touch)
## Features ## Features
@@ -120,6 +139,20 @@ docker run -d --name bitwarden \
-p 80:80 \ -p 80:80 \
mprasil/bitwarden:latest mprasil/bitwarden:latest
``` ```
Note: While users can't register on their own, they can still be invited by already registered users. Read bellow if you also want to disable that.
### Disable invitations
Even when registration is disabled, organization administrators or owners can invite users to join organization. This won't send email invitation to the users, but after they are invited, they can register with the invited email even if `SIGNUPS_ALLOWED` is actually set to `false`. You can disable this functionality completely by setting `INVITATIONS_ALLOWED` env variable to `false`:
```sh
docker run -d --name bitwarden \
-e SIGNUPS_ALLOWED=false \
-e INVITATIONS_ALLOWED=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Enabling HTTPS ### Enabling HTTPS
To enable HTTPS, you need to configure the `ROCKET_TLS`. To enable HTTPS, you need to configure the `ROCKET_TLS`.
@@ -134,10 +167,9 @@ Where:
```sh ```sh
docker run -d --name bitwarden \ docker run -d --name bitwarden \
-e ROCKET_TLS={certs='"/ssl/certs.pem",key="/ssl/key.pem"}' \ -e ROCKET_TLS='{certs="/ssl/certs.pem",key="/ssl/key.pem"}' \
-v /ssl/keys/:/ssl/ \ -v /ssl/keys/:/ssl/ \
-v /bw-data/:/data/ \ -v /bw-data/:/data/ \
-v /icon_cache/ \
-p 443:80 \ -p 443:80 \
mprasil/bitwarden:latest mprasil/bitwarden:latest
``` ```
@@ -248,6 +280,59 @@ docker run -d --name bitwarden \
mprasil/bitwarden:latest mprasil/bitwarden:latest
``` ```
### SMTP configuration
You can configure bitwarden_rs to send emails via a SMTP agent:
```sh
docker run -d --name bitwarden \
-e SMTP_HOST=<smtp.domain.tld> \
-e SMTP_PORT=587 \
-e SMTP_SSL=true \
-e SMTP_USERNAME=<username> \
-e SMTP_PASSWORD=<password> \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
When `SMTP_SSL` is set to `true`(this is the default), only TLSv1.1 and TLSv1.2 protocols will be accepted and `SMTP_PORT` will default to `587`. If set to `false`, `SMTP_PORT` will default to `25` and the connection won't be encrypted. This can be very insecure, use this setting only if you know what you're doing.
### Password hint display
Usually, password hints are sent by email. But as bitwarden_rs is made with small or personal deployment in mind, hints are also available from the password hint page, so you don't have to configure an email service. If you want to disable this feature, you can use the `SHOW_PASSWORD_HINT` variable:
```sh
docker run -d --name bitwarden \
-e SHOW_PASSWORD_HINT=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Disabling or overriding the Vault interface hosting
As a convenience bitwarden_rs image will also host static files for Vault web interface. You can disable this static file hosting completely by setting the WEB_VAULT_ENABLED variable.
```sh
docker run -d --name bitwarden \
-e WEB_VAULT_ENABLED=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Alternatively you can override the Vault files and provide your own static files to host. You can do that by mounting a path with your files over the `/web-vault` directory in the container. Just make sure the directory contains at least `index.html` file.
```sh
docker run -d --name bitwarden \
-v /path/to/static/files_directory:/web-vault \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
Note that you can also change the path where bitwarden_rs looks for static files by providing the `WEB_VAULT_FOLDER` environment variable with the path.
### Other configuration ### Other configuration
Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables). Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables).
@@ -313,6 +398,27 @@ docker run -d --name bitwarden \
-p 80:8080 \ -p 80:8080 \
mprasil/bitwarden:latest mprasil/bitwarden:latest
``` ```
## Differences from upstream API implementation
### Changing user email
Because we don't have any SMTP functionality at the moment, there's no way to deliver the verification token when you try to change the email. User just needs to enter any random token to continue and the change will be applied.
### Creating organization
We use upstream Vault interface directly without any (significant) changes, this is why user is presented with paid options when creating organization. To create an organization, just use the free option, none of the limits apply when using bitwarden_rs as back-end API and after the organization is created it should behave like Enterprise organization.
### Inviting users into organization
If you have [invitations disabled](#disable-invitations), the users must already be registered on your server to invite them. The invited users won't get the invitation email, instead they will appear in the interface as if they already accepted the invitation. (if the user has already registered) Organization admin then just needs to confirm them to be proper Organization members and to give them access to the shared secrets.
### Running on unencrypted connection
It is strongly recommended to run bitwarden_rs service over HTTPS. However the server itself while [supporting it](#enabling-https) does not strictly require such setup. This makes it a bit easier to spin up the service in cases where you can generally trust the connection (internal and secure network, access over VPN,..) or when you want to put the service behind HTTP proxy, that will do the encryption on the proxy end.
Running over HTTP is still reasonably secure provided you use really strong master password and that you avoid using web Vault over connection that is vulnerable to MITM attacks where attacker could inject javascript into your interface. However some forms of 2FA might not work in this setup and [Vault doesn't work in this configuration in Chrome](https://github.com/bitwarden/web/issues/254).
## Get in touch ## Get in touch
To ask an question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine, also please report any bugs spotted here. To ask an question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine, also please report any bugs spotted here.

View File

@@ -0,0 +1,28 @@
--- a/src/app/services/services.module.ts
+++ b/src/app/services/services.module.ts
@@ -120,20 +120,17 @@ const notificationsService = new NotificationsService(userService, syncService,
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
-const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(),
+const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost() || true,
platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window);
export function initFactory(): Function {
return async () => {
await (storageService as HtmlStorageService).init();
- const isDev = platformUtilsService.isDev();
- if (!isDev && platformUtilsService.isSelfHost()) {
- environmentService.baseUrl = window.location.origin;
- } else {
- environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
- 'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
- }
+ const isDev = false;
+ environmentService.baseUrl = window.location.origin;
+ environmentService.notificationsUrl = window.location.origin + '/notifications';
+
await apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://localhost:4000' : null,

View File

@@ -1,9 +0,0 @@
{
"appSettings": {
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "/icons",
"stripeKey": "",
"braintreeKey": ""
}
}

View File

@@ -0,0 +1,3 @@
ALTER TABLE ciphers
ADD COLUMN
password_history TEXT;

View File

@@ -0,0 +1 @@
DROP TABLE invitations;

View File

@@ -0,0 +1,3 @@
CREATE TABLE invitations (
email TEXT NOT NULL PRIMARY KEY
);

View File

@@ -1 +1 @@
nightly-2018-06-26 nightly-2018-08-24

View File

@@ -5,6 +5,8 @@ use db::models::*;
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString}; use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
use auth::Headers; use auth::Headers;
use fast_chemail::is_valid_email;
use mail;
use CONFIG; use CONFIG;
@@ -30,15 +32,34 @@ struct KeysData {
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult { fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
let data: RegisterData = data.into_inner().data; let data: RegisterData = data.into_inner().data;
if !CONFIG.signups_allowed {
err!("Signups not allowed")
}
if User::find_by_mail(&data.Email, &conn).is_some() { let mut user = match User::find_by_mail(&data.Email, &conn) {
err!("Email already exists") Some(mut user) => {
} if Invitation::take(&data.Email, &conn) {
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(&conn);
};
user
} else {
if CONFIG.signups_allowed {
err!("Account with this email already exists")
} else {
err!("Registration not allowed")
}
}
},
None => {
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
User::new(data.Email)
} else {
err!("Registration not allowed")
}
}
};
let mut user = User::new(data.Email, data.Key, data.MasterPasswordHash); user.set_password(&data.MasterPasswordHash);
user.key = data.Key;
// Add extra fields if present // Add extra fields if present
if let Some(name) = data.Name { if let Some(name) = data.Name {
@@ -73,6 +94,11 @@ struct ProfileData {
Name: String, Name: String,
} }
#[put("/accounts/profile", data = "<data>")]
fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
post_profile(data, headers, conn)
}
#[post("/accounts/profile", data = "<data>")] #[post("/accounts/profile", data = "<data>")]
fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: ProfileData = data.into_inner().data; let data: ProfileData = data.into_inner().data;
@@ -80,7 +106,10 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
let mut user = headers.user; let mut user = headers.user;
user.name = data.Name; user.name = data.Name;
user.password_hint = data.MasterPasswordHint; user.password_hint = match data.MasterPasswordHint {
Some(ref h) if h.is_empty() => None,
_ => data.MasterPasswordHint,
};
user.save(&conn); user.save(&conn);
Ok(Json(user.to_json(&conn))) Ok(Json(user.to_json(&conn)))
@@ -244,6 +273,69 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
#[get("/accounts/revision-date")] #[get("/accounts/revision-date")]
fn revision_date(headers: Headers) -> String { fn revision_date(headers: Headers) -> String {
let revision_date = headers.user.updated_at.timestamp(); let revision_date = headers.user.updated_at.timestamp_millis();
revision_date.to_string() revision_date.to_string()
} }
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct PasswordHintData {
Email: String,
}
#[post("/accounts/password-hint", data = "<data>")]
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
let data: PasswordHintData = data.into_inner().data;
if !is_valid_email(&data.Email) {
err!("This email address is not valid...");
}
let user = User::find_by_mail(&data.Email, &conn);
if user.is_none() {
return Ok(());
}
let user = user.unwrap();
if let Some(ref mail_config) = CONFIG.mail {
if let Err(e) = mail::send_password_hint(&user.email, user.password_hint, mail_config) {
err!(format!("There have been a problem sending the email: {}", e));
}
} else if CONFIG.show_password_hint {
if let Some(hint) = user.password_hint {
err!(format!("Your password hint is: {}", &hint));
} else {
err!(format!("Sorry, you have no password hint..."));
}
}
Ok(())
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct PreloginData {
Email: String,
}
#[post("/accounts/prelogin", data = "<data>")]
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
let data: PreloginData = data.into_inner().data;
match User::find_by_mail(&data.Email, &conn) {
Some(user) => {
let kdf_type = 0; // PBKDF2: 0
let _server_iter = user.password_iterations;
let client_iter = 5000; // TODO: Make iterations user configurable
Ok(Json(json!({
"Kdf": kdf_type,
"KdfIterations": client_iter
})))
},
None => err!("Invalid user"),
}
}

View File

@@ -86,7 +86,9 @@ fn get_cipher_details(uuid: String, headers: Headers, conn: DbConn) -> JsonResul
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct CipherData { pub struct CipherData {
// Id is optional as it is included only in bulk share
Id: Option<String>,
// Folder id is not included in import // Folder id is not included in import
FolderId: Option<String>, FolderId: Option<String>,
// TODO: Some of these might appear all the time, no need for Option // TODO: Some of these might appear all the time, no need for Option
@@ -98,8 +100,8 @@ struct CipherData {
Card = 3, Card = 3,
Identity = 4 Identity = 4
*/ */
Type: i32, // TODO: Change this to NumberOrString pub Type: i32, // TODO: Change this to NumberOrString
Name: String, pub Name: String,
Notes: Option<String>, Notes: Option<String>,
Fields: Option<Value>, Fields: Option<Value>,
@@ -110,6 +112,8 @@ struct CipherData {
Identity: Option<Value>, Identity: Option<Value>,
Favorite: Option<bool>, Favorite: Option<bool>,
PasswordHistory: Option<Value>,
} }
#[post("/ciphers/admin", data = "<data>")] #[post("/ciphers/admin", data = "<data>")]
@@ -123,26 +127,26 @@ fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) ->
let data: CipherData = data.into_inner().data; let data: CipherData = data.into_inner().data;
let mut cipher = Cipher::new(data.Type, data.Name.clone()); let mut cipher = Cipher::new(data.Type, data.Name.clone());
update_cipher_from_data(&mut cipher, data, &headers, true, &conn)?; update_cipher_from_data(&mut cipher, data, &headers, false, &conn)?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
} }
fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Headers, is_new_or_shared: bool, conn: &DbConn) -> EmptyResult { pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Headers, shared_to_collection: bool, conn: &DbConn) -> EmptyResult {
if is_new_or_shared { if let Some(org_id) = data.OrganizationId {
if let Some(org_id) = data.OrganizationId { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { None => err!("You don't have permission to add item to organization"),
None => err!("You don't have permission to add item to organization"), Some(org_user) => if shared_to_collection
Some(org_user) => if org_user.has_full_access() { || org_user.has_full_access()
cipher.organization_uuid = Some(org_id); || cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
cipher.user_uuid = None; cipher.organization_uuid = Some(org_id);
} else { cipher.user_uuid = None;
err!("You don't have permission to add cipher directly to organization") } else {
} err!("You don't have permission to add cipher directly to organization")
} }
} else {
cipher.user_uuid = Some(headers.user.uuid.clone());
} }
} else {
cipher.user_uuid = Some(headers.user.uuid.clone());
} }
if let Some(ref folder_id) = data.FolderId { if let Some(ref folder_id) = data.FolderId {
@@ -175,6 +179,7 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
type_data["Name"] = Value::String(data.Name.clone()); type_data["Name"] = Value::String(data.Name.clone());
type_data["Notes"] = data.Notes.clone().map(Value::String).unwrap_or(Value::Null); type_data["Notes"] = data.Notes.clone().map(Value::String).unwrap_or(Value::Null);
type_data["Fields"] = data.Fields.clone().unwrap_or(Value::Null); type_data["Fields"] = data.Fields.clone().unwrap_or(Value::Null);
type_data["PasswordHistory"] = data.PasswordHistory.clone().unwrap_or(Value::Null);
// TODO: ******* Backwards compat end ********** // TODO: ******* Backwards compat end **********
cipher.favorite = data.Favorite.unwrap_or(false); cipher.favorite = data.Favorite.unwrap_or(false);
@@ -182,6 +187,7 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
cipher.notes = data.Notes; cipher.notes = data.Notes;
cipher.fields = data.Fields.map(|f| f.to_string()); cipher.fields = data.Fields.map(|f| f.to_string());
cipher.data = type_data.to_string(); cipher.data = type_data.to_string();
cipher.password_history = data.PasswordHistory.map(|f| f.to_string());
cipher.save(&conn); cipher.save(&conn);
@@ -237,17 +243,26 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
.map(|i| folders[*i].uuid.clone()); .map(|i| folders[*i].uuid.clone());
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, true, &conn)?; update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn)?;
cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn).ok(); cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn).ok();
} }
Ok(()) let mut user = headers.user;
match user.update_revision(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to update the revision, please log out and log back in to finish import.")
}
}
#[put("/ciphers/<uuid>/admin", data = "<data>")]
fn put_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
put_cipher(uuid, data, headers, conn)
} }
#[post("/ciphers/<uuid>/admin", data = "<data>")] #[post("/ciphers/<uuid>/admin", data = "<data>")]
fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult { fn post_cipher_admin(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
// TODO: Implement this correctly
post_cipher(uuid, data, headers, conn) post_cipher(uuid, data, headers, conn)
} }
@@ -285,6 +300,11 @@ fn post_collections_update(uuid: String, data: JsonUpcase<CollectionsAdminData>,
post_collections_admin(uuid, data, headers, conn) post_collections_admin(uuid, data, headers, conn)
} }
#[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
fn put_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn) -> EmptyResult {
post_collections_admin(uuid, data, headers, conn)
}
#[post("/ciphers/<uuid>/collections-admin", data = "<data>")] #[post("/ciphers/<uuid>/collections-admin", data = "<data>")]
fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn) -> EmptyResult { fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: CollectionsAdminData = data.into_inner().data; let data: CollectionsAdminData = data.into_inner().data;
@@ -332,6 +352,65 @@ struct ShareCipherData {
fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn) -> JsonResult { fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: ShareCipherData = data.into_inner().data; let data: ShareCipherData = data.into_inner().data;
share_cipher_by_uuid(&uuid, data, &headers, &conn)
}
#[put("/ciphers/<uuid>/share", data = "<data>")]
fn put_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: ShareCipherData = data.into_inner().data;
share_cipher_by_uuid(&uuid, data, &headers, &conn)
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ShareSelectedCipherData {
Ciphers: Vec<CipherData>,
CollectionIds: Vec<String>
}
#[put("/ciphers/share", data = "<data>")]
fn put_cipher_share_seleted(data: JsonUpcase<ShareSelectedCipherData>, headers: Headers, conn: DbConn) -> EmptyResult {
let mut data: ShareSelectedCipherData = data.into_inner().data;
let mut cipher_ids: Vec<String> = Vec::new();
if data.Ciphers.len() == 0 {
err!("You must select at least one cipher.")
}
if data.CollectionIds.len() == 0 {
err!("You must select at least one collection.")
}
for cipher in data.Ciphers.iter() {
match cipher.Id {
Some(ref id) => cipher_ids.push(id.to_string()),
None => err!("Request missing ids field")
};
}
let attachments = Attachment::find_by_ciphers(cipher_ids, &conn);
if attachments.len() > 0 {
err!("Ciphers should not have any attachments.")
}
while let Some(cipher) = data.Ciphers.pop() {
let mut shared_cipher_data = ShareCipherData {
Cipher: cipher,
CollectionIds: data.CollectionIds.clone()
};
match shared_cipher_data.Cipher.Id.take() {
Some(id) => share_cipher_by_uuid(&id, shared_cipher_data , &headers, &conn)?,
None => err!("Request missing ids field")
};
}
Ok(())
}
fn share_cipher_by_uuid(uuid: &str, data: ShareCipherData, headers: &Headers, conn: &DbConn) -> JsonResult {
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) { let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => { Some(cipher) => {
if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) { if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
@@ -343,22 +422,28 @@ fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: H
None => err!("Cipher doesn't exist") None => err!("Cipher doesn't exist")
}; };
match data.Cipher.OrganizationId { match data.Cipher.OrganizationId.clone() {
None => err!("Organization id not provided"), None => err!("Organization id not provided"),
Some(_) => { Some(organization_uuid) => {
update_cipher_from_data(&mut cipher, data.Cipher, &headers, true, &conn)?; let mut shared_to_collection = false;
for uuid in &data.CollectionIds { for uuid in &data.CollectionIds {
match Collection::find_by_uuid(uuid, &conn) { match Collection::find_by_uuid(uuid, &conn) {
None => err!("Invalid collection ID provided"), None => err!("Invalid collection ID provided"),
Some(collection) => { Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &conn) { if collection.is_writable_by_user(&headers.user.uuid, &conn) {
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn); if collection.org_uuid == organization_uuid {
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn);
shared_to_collection = true;
} else {
err!("Collection does not belong to organization")
}
} else { } else {
err!("No rights to modify the collection") err!("No rights to modify the collection")
} }
} }
} }
} }
update_cipher_from_data(&mut cipher, data.Cipher, &headers, shared_to_collection, &conn)?;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
} }
@@ -409,7 +494,10 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
}; };
let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size); let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
attachment.save(&conn); match attachment.save(&conn) {
Ok(()) => (),
Err(_) => println!("Error: failed to save attachment")
};
}).expect("Error processing multipart data"); }).expect("Error processing multipart data");
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
@@ -441,6 +529,11 @@ fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn) _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)
} }
#[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
fn delete_attachment_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)
}
#[post("/ciphers/<uuid>/delete")] #[post("/ciphers/<uuid>/delete")]
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult { fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn) _delete_cipher_by_uuid(&uuid, &headers, &conn)
@@ -456,13 +549,18 @@ fn delete_cipher(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn) _delete_cipher_by_uuid(&uuid, &headers, &conn)
} }
#[post("/ciphers/delete", data = "<data>")] #[delete("/ciphers/<uuid>/admin")]
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn)
}
#[delete("/ciphers", data = "<data>")]
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult { fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: Value = data.into_inner().data; let data: Value = data.into_inner().data;
let uuids = match data.get("Ids") { let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() { Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(|uuid| { uuid.as_str() }), Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array") None => err!("Posted ids field is not an array")
}, },
None => err!("Request missing ids field") None => err!("Request missing ids field")
@@ -477,6 +575,11 @@ fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbCon
Ok(()) Ok(())
} }
#[post("/ciphers/delete", data = "<data>")]
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
delete_cipher_selected(data, headers, conn)
}
#[post("/ciphers/move", data = "<data>")] #[post("/ciphers/move", data = "<data>")]
fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult { fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
let data = data.into_inner().data; let data = data.into_inner().data;
@@ -503,7 +606,7 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn)
let uuids = match data.get("Ids") { let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() { Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(|uuid| { uuid.as_str() }), Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array") None => err!("Posted ids field is not an array")
}, },
None => err!("Request missing ids field") None => err!("Request missing ids field")
@@ -529,6 +632,11 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn)
Ok(()) Ok(())
} }
#[put("/ciphers/move", data = "<data>")]
fn move_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
move_cipher_selected(data, headers, conn)
}
#[post("/ciphers/purge", data = "<data>")] #[post("/ciphers/purge", data = "<data>")]
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult { fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: PasswordData = data.into_inner().data; let data: PasswordData = data.into_inner().data;
@@ -551,7 +659,7 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
for f in Folder::find_by_user(&user.uuid, &conn) { for f in Folder::find_by_user(&user.uuid, &conn) {
if f.delete(&conn).is_err() { if f.delete(&conn).is_err() {
err!("Failed deleting folder") err!("Failed deleting folder")
} }
} }
Ok(()) Ok(())

View File

@@ -14,6 +14,7 @@ pub fn routes() -> Vec<Route> {
routes![ routes![
register, register,
profile, profile,
put_profile,
post_profile, post_profile,
get_public_keys, get_public_keys,
post_keys, post_keys,
@@ -23,6 +24,8 @@ pub fn routes() -> Vec<Route> {
post_email, post_email,
delete_account, delete_account,
revision_date, revision_date,
password_hint,
prelogin,
sync, sync,
@@ -31,6 +34,7 @@ pub fn routes() -> Vec<Route> {
get_cipher_admin, get_cipher_admin,
get_cipher_details, get_cipher_details,
post_ciphers, post_ciphers,
put_cipher_admin,
post_ciphers_admin, post_ciphers_admin,
post_ciphers_import, post_ciphers_import,
post_attachment, post_attachment,
@@ -39,16 +43,22 @@ pub fn routes() -> Vec<Route> {
delete_attachment_post, delete_attachment_post,
delete_attachment_post_admin, delete_attachment_post_admin,
delete_attachment, delete_attachment,
delete_attachment_admin,
post_cipher_admin, post_cipher_admin,
post_cipher_share, post_cipher_share,
put_cipher_share,
put_cipher_share_seleted,
post_cipher, post_cipher,
put_cipher, put_cipher,
delete_cipher_post, delete_cipher_post,
delete_cipher_post_admin, delete_cipher_post_admin,
delete_cipher, delete_cipher,
delete_cipher_admin,
delete_cipher_selected, delete_cipher_selected,
delete_cipher_selected_post,
delete_all, delete_all,
move_cipher_selected, move_cipher_selected,
move_cipher_selected_put,
get_folders, get_folders,
get_folder, get_folder,
@@ -62,33 +72,45 @@ pub fn routes() -> Vec<Route> {
get_recover, get_recover,
recover, recover,
disable_twofactor, disable_twofactor,
disable_twofactor_put,
generate_authenticator, generate_authenticator,
activate_authenticator, activate_authenticator,
activate_authenticator_put,
generate_u2f, generate_u2f,
activate_u2f, activate_u2f,
activate_u2f_put,
get_organization, get_organization,
create_organization, create_organization,
delete_organization, delete_organization,
post_delete_organization,
leave_organization, leave_organization,
get_user_collections, get_user_collections,
get_org_collections, get_org_collections,
get_org_collection_detail, get_org_collection_detail,
get_collection_users, get_collection_users,
put_organization,
post_organization, post_organization,
post_organization_collections, post_organization_collections,
delete_organization_collection_user,
post_organization_collection_delete_user, post_organization_collection_delete_user,
post_organization_collection_update, post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
post_organization_collection_delete, post_organization_collection_delete,
post_collections_update, post_collections_update,
post_collections_admin, post_collections_admin,
put_collections_admin,
get_org_details, get_org_details,
get_org_users, get_org_users,
send_invite, send_invite,
confirm_invite, confirm_invite,
get_user, get_user,
edit_user, edit_user,
put_organization_user,
delete_user, delete_user,
post_delete_user,
post_org_import,
clear_device_token, clear_device_token,
put_device_token, put_device_token,

View File

@@ -1,13 +1,13 @@
#![allow(unused_imports)]
use rocket_contrib::{Json, Value}; use rocket_contrib::{Json, Value};
use CONFIG;
use db::DbConn; use db::DbConn;
use db::models::*; use db::models::*;
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase}; use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase};
use auth::{Headers, AdminHeaders, OwnerHeaders}; use auth::{Headers, AdminHeaders, OwnerHeaders};
use serde::{Deserialize, Deserializer};
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
@@ -17,7 +17,7 @@ struct OrgData {
Key: String, Key: String,
Name: String, Name: String,
#[serde(rename = "PlanType")] #[serde(rename = "PlanType")]
_PlanType: String, // Ignored, always use the same plan _PlanType: NumberOrString, // Ignored, always use the same plan
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@@ -55,7 +55,7 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
Ok(Json(org.to_json())) Ok(Json(org.to_json()))
} }
#[post("/organizations/<org_id>/delete", data = "<data>")] #[delete("/organizations/<org_id>", data = "<data>")]
fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers: OwnerHeaders, conn: DbConn) -> EmptyResult { fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers: OwnerHeaders, conn: DbConn) -> 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;
@@ -73,6 +73,11 @@ fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers:
} }
} }
#[post("/organizations/<org_id>/delete", data = "<data>")]
fn post_delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers: OwnerHeaders, conn: DbConn) -> EmptyResult {
delete_organization(org_id, data, headers, conn)
}
#[post("/organizations/<org_id>/leave")] #[post("/organizations/<org_id>/leave")]
fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyResult { fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
@@ -104,6 +109,11 @@ fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> Jso
} }
} }
#[put("/organizations/<org_id>", data = "<data>")]
fn put_organization(org_id: String, headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, conn: DbConn) -> JsonResult {
post_organization(org_id, headers, data, conn)
}
#[post("/organizations/<org_id>", data = "<data>")] #[post("/organizations/<org_id>", data = "<data>")]
fn post_organization(org_id: String, _headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, conn: DbConn) -> JsonResult { fn post_organization(org_id: String, _headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, conn: DbConn) -> JsonResult {
let data: OrganizationUpdateData = data.into_inner().data; let data: OrganizationUpdateData = data.into_inner().data;
@@ -128,9 +138,8 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
"Data": "Data":
Collection::find_by_user_uuid(&headers.user.uuid, &conn) Collection::find_by_user_uuid(&headers.user.uuid, &conn)
.iter() .iter()
.map(|collection| { .map(Collection::to_json)
collection.to_json() .collect::<Value>(),
}).collect::<Value>(),
"Object": "list" "Object": "list"
}))) })))
} }
@@ -141,9 +150,8 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
"Data": "Data":
Collection::find_by_organization(&org_id, &conn) Collection::find_by_organization(&org_id, &conn)
.iter() .iter()
.map(|collection| { .map(Collection::to_json)
collection.to_json() .collect::<Value>(),
}).collect::<Value>(),
"Object": "list" "Object": "list"
}))) })))
} }
@@ -164,6 +172,11 @@ fn post_organization_collections(org_id: String, _headers: AdminHeaders, data: J
Ok(Json(collection.to_json())) Ok(Json(collection.to_json()))
} }
#[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
fn put_organization_collection_update(org_id: String, col_id: String, headers: AdminHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn) -> JsonResult {
post_organization_collection_update(org_id, col_id, headers, data, conn)
}
#[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")] #[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
fn post_organization_collection_update(org_id: String, col_id: String, _headers: AdminHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn) -> JsonResult { fn post_organization_collection_update(org_id: String, col_id: String, _headers: AdminHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn) -> JsonResult {
let data: NewCollectionData = data.into_inner().data; let data: NewCollectionData = data.into_inner().data;
@@ -188,8 +201,9 @@ fn post_organization_collection_update(org_id: String, col_id: String, _headers:
Ok(Json(collection.to_json())) Ok(Json(collection.to_json()))
} }
#[post("/organizations/<org_id>/collections/<col_id>/delete-user/<org_user_id>")]
fn post_organization_collection_delete_user(org_id: String, col_id: String, org_user_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult { #[delete("/organizations/<org_id>/collections/<col_id>/user/<org_user_id>")]
fn delete_organization_collection_user(org_id: String, col_id: String, org_user_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let collection = match Collection::find_by_uuid(&col_id, &conn) { let collection = match Collection::find_by_uuid(&col_id, &conn) {
None => err!("Collection not found"), None => err!("Collection not found"),
Some(collection) => if collection.org_uuid == org_id { Some(collection) => if collection.org_uuid == org_id {
@@ -199,7 +213,7 @@ fn post_organization_collection_delete_user(org_id: String, col_id: String, org_
} }
}; };
match UserOrganization::find_by_uuid(&org_user_id, &conn) { match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
None => err!("User not found in organization"), None => err!("User not found in organization"),
Some(user_org) => { Some(user_org) => {
match CollectionUser::find_by_collection_and_user(&collection.uuid, &user_org.user_uuid, &conn) { match CollectionUser::find_by_collection_and_user(&collection.uuid, &user_org.user_uuid, &conn) {
@@ -215,17 +229,13 @@ fn post_organization_collection_delete_user(org_id: String, col_id: String, org_
} }
} }
#[derive(Deserialize, Debug)] #[post("/organizations/<org_id>/collections/<col_id>/delete-user/<org_user_id>")]
#[allow(non_snake_case)] fn post_organization_collection_delete_user(org_id: String, col_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
struct DeleteCollectionData { delete_organization_collection_user(org_id, col_id, org_user_id, headers, conn)
Id: String,
OrgId: String,
} }
#[post("/organizations/<org_id>/collections/<col_id>/delete", data = "<data>")] #[delete("/organizations/<org_id>/collections/<col_id>")]
fn post_organization_collection_delete(org_id: String, col_id: String, _headers: AdminHeaders, data: JsonUpcase<DeleteCollectionData>, conn: DbConn) -> EmptyResult { fn delete_organization_collection(org_id: String, col_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let _data: DeleteCollectionData = data.into_inner().data;
match Collection::find_by_uuid(&col_id, &conn) { match Collection::find_by_uuid(&col_id, &conn) {
None => err!("Collection not found"), None => err!("Collection not found"),
Some(collection) => if collection.org_uuid == org_id { Some(collection) => if collection.org_uuid == org_id {
@@ -239,6 +249,18 @@ fn post_organization_collection_delete(org_id: String, col_id: String, _headers:
} }
} }
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct DeleteCollectionData {
Id: String,
OrgId: String,
}
#[post("/organizations/<org_id>/collections/<col_id>/delete", data = "<_data>")]
fn post_organization_collection_delete(org_id: String, col_id: String, headers: AdminHeaders, _data: JsonUpcase<DeleteCollectionData>, conn: DbConn) -> EmptyResult {
delete_organization_collection(org_id, col_id, headers, conn)
}
#[get("/organizations/<org_id>/collections/<coll_id>/details")] #[get("/organizations/<org_id>/collections/<coll_id>/details")]
fn get_org_collection_detail(org_id: String, coll_id: String, headers: AdminHeaders, conn: DbConn) -> JsonResult { fn get_org_collection_detail(org_id: String, coll_id: String, headers: AdminHeaders, conn: DbConn) -> JsonResult {
match Collection::find_by_uuid_and_user(&coll_id, &headers.user.uuid, &conn) { match Collection::find_by_uuid_and_user(&coll_id, &headers.user.uuid, &conn) {
@@ -308,6 +330,14 @@ fn get_org_users(org_id: String, headers: AdminHeaders, conn: DbConn) -> JsonRes
}))) })))
} }
fn deserialize_collections<'de, D>(deserializer: D) -> Result<Vec<CollectionData>, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize null to empty Vec
Deserialize::deserialize(deserializer).or(Ok(vec![]))
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct CollectionData { struct CollectionData {
@@ -320,6 +350,7 @@ struct CollectionData {
struct InviteData { struct InviteData {
Emails: Vec<String>, Emails: Vec<String>,
Type: NumberOrString, Type: NumberOrString,
#[serde(deserialize_with = "deserialize_collections")]
Collections: Vec<CollectionData>, Collections: Vec<CollectionData>,
AccessAll: Option<bool>, AccessAll: Option<bool>,
} }
@@ -338,54 +369,70 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
err!("Only Owners can invite Admins or Owners") err!("Only Owners can invite Admins or Owners")
} }
for user_opt in data.Emails.iter().map(|email| User::find_by_mail(email, &conn)) { for email in data.Emails.iter() {
match user_opt { let mut user_org_status = UserOrgStatus::Accepted as i32;
None => err!("User email does not exist"), let user = match User::find_by_mail(&email, &conn) {
Some(user) => { None => if CONFIG.invitations_allowed { // Invite user if that's enabled
if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() { let mut invitation = Invitation::new(email.clone());
err!("User already in organization") match invitation.save(&conn) {
Ok(()) => {
let mut user = User::new(email.clone());
if user.save(&conn) {
user_org_status = UserOrgStatus::Invited as i32;
user
} else {
err!("Failed to create placeholder for invited user")
}
}
Err(_) => err!(format!("Failed to invite: {}", email))
} }
} else {
err!(format!("User email does not exist: {}", email))
},
Some(user) => if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
err!(format!("User already in organization: {}", email))
} else {
user
}
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); };
let access_all = data.AccessAll.unwrap_or(false);
new_user.access_all = access_all;
new_user.type_ = new_type;
// If no accessAll, add the collections received let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
if !access_all { let access_all = data.AccessAll.unwrap_or(false);
for col in &data.Collections { new_user.access_all = access_all;
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { new_user.type_ = new_type;
None => err!("Collection not found in Organization"), new_user.status = user_org_status;
Some(collection) => {
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() { // If no accessAll, add the collections received
err!("Failed saving collection access for user") if !access_all {
} for col in &data.Collections {
} match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => {
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
} }
} }
} }
new_user.save(&conn);
} }
} }
new_user.save(&conn);
} }
Ok(()) Ok(())
} }
#[post("/organizations/<org_id>/users/<user_id>/confirm", data = "<data>")] #[post("/organizations/<org_id>/users/<org_user_id>/confirm", data = "<data>")]
fn confirm_invite(org_id: String, user_id: String, data: JsonUpcase<Value>, headers: AdminHeaders, conn: DbConn) -> EmptyResult { fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let data = data.into_inner().data; let data = data.into_inner().data;
let mut user_to_confirm = match UserOrganization::find_by_uuid(&user_id, &conn) { let mut user_to_confirm = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user, Some(user) => user,
None => err!("Failed to find user membership") None => err!("The specified user isn't a member of the organization")
}; };
if user_to_confirm.org_uuid != org_id {
err!("The specified user isn't a member of the organization")
}
if user_to_confirm.type_ != UserOrgType::User as i32 && if user_to_confirm.type_ != UserOrgType::User as i32 &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner as i32 {
err!("Only Owners can confirm Admins or Owners") err!("Only Owners can confirm Admins or Owners")
@@ -406,17 +453,13 @@ fn confirm_invite(org_id: String, user_id: String, data: JsonUpcase<Value>, head
Ok(()) Ok(())
} }
#[get("/organizations/<org_id>/users/<user_id>")] #[get("/organizations/<org_id>/users/<org_user_id>")]
fn get_user(org_id: String, user_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult { fn get_user(org_id: String, org_user_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
let user = match UserOrganization::find_by_uuid(&user_id, &conn) { let user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user, Some(user) => user,
None => err!("Failed to find user membership") None => err!("The specified user isn't a member of the organization")
}; };
if user.org_uuid != org_id {
err!("The specified user isn't a member of the organization")
}
Ok(Json(user.to_json_details(&conn))) Ok(Json(user.to_json_details(&conn)))
} }
@@ -424,12 +467,18 @@ fn get_user(org_id: String, user_id: String, _headers: AdminHeaders, conn: DbCon
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct EditUserData { struct EditUserData {
Type: NumberOrString, Type: NumberOrString,
#[serde(deserialize_with = "deserialize_collections")]
Collections: Vec<CollectionData>, Collections: Vec<CollectionData>,
AccessAll: bool, AccessAll: bool,
} }
#[post("/organizations/<org_id>/users/<user_id>", data = "<data>", rank = 1)] #[put("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult { fn put_organization_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
edit_user(org_id, org_user_id, data, headers, conn)
}
#[post("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let data: EditUserData = data.into_inner().data; let data: EditUserData = data.into_inner().data;
let new_type = match UserOrgType::from_str(&data.Type.into_string()) { let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
@@ -437,19 +486,22 @@ fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, he
None => err!("Invalid type") None => err!("Invalid type")
}; };
let mut user_to_edit = match UserOrganization::find_by_uuid(&user_id, &conn) { let mut user_to_edit = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user, Some(user) => user,
None => err!("The specified user isn't member of the organization") None => err!("The specified user isn't member of the organization")
}; };
if new_type != UserOrgType::User as i32 && if new_type != user_to_edit.type_ as i32 && (
user_to_edit.type_ <= UserOrgType::Admin as i32 ||
new_type <= UserOrgType::Admin as i32
) &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner as i32 {
err!("Only Owners can grant Admin or Owner type") err!("Only Owners can grant and remove Admin or Owner privileges")
} }
if user_to_edit.type_ != UserOrgType::User as i32 && if user_to_edit.type_ == UserOrgType::Owner as i32 &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner as i32 {
err!("Only Owners can edit Admin or Owner") err!("Only Owners can edit Owner users")
} }
if user_to_edit.type_ == UserOrgType::Owner as i32 && if user_to_edit.type_ == UserOrgType::Owner as i32 &&
@@ -494,9 +546,9 @@ fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, he
Ok(()) Ok(())
} }
#[post("/organizations/<org_id>/users/<user_id>/delete")] #[delete("/organizations/<org_id>/users/<org_user_id>")]
fn delete_user(org_id: String, user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult { fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let user_to_delete = match UserOrganization::find_by_uuid(&user_id, &conn) { let user_to_delete = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user, Some(user) => user,
None => err!("User to delete isn't member of the organization") None => err!("User to delete isn't member of the organization")
}; };
@@ -521,4 +573,78 @@ fn delete_user(org_id: String, user_id: String, headers: AdminHeaders, conn: DbC
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed deleting user from organization") Err(_) => err!("Failed deleting user from organization")
} }
}
#[post("/organizations/<org_id>/users/<org_user_id>/delete")]
fn post_delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
delete_user(org_id, org_user_id, headers, conn)
}
use super::ciphers::CipherData;
use super::ciphers::update_cipher_from_data;
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ImportData {
Ciphers: Vec<CipherData>,
Collections: Vec<NewCollectionData>,
CollectionRelationships: Vec<RelationsData>,
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct RelationsData {
// Cipher index
Key: usize,
// Collection index
Value: usize,
}
#[post("/ciphers/import-organization?<query>", data = "<data>")]
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: ImportData = data.into_inner().data;
let org_id = query.organizationId;
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(user) => user,
None => err!("User is not part of the organization")
};
if org_user.type_ > UserOrgType::Admin as i32 {
err!("Only admins or owners can import into an organization")
}
// Read and create the collections
let collections: Vec<_> = data.Collections.into_iter().map(|coll| {
let mut collection = Collection::new(org_id.clone(), coll.Name);
collection.save(&conn);
collection
}).collect();
// Read the relations between collections and ciphers
let mut relations = Vec::new();
for relation in data.CollectionRelationships {
relations.push((relation.Key, relation.Value));
}
// Read and create the ciphers
let ciphers: Vec<_> = data.Ciphers.into_iter().map(|cipher_data| {
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn).ok();
cipher
}).collect();
// Assign the collections
for (cipher_index, coll_index) in relations {
let cipher_id = &ciphers[cipher_index].uuid;
let coll_id = &collections[coll_index].uuid;
CollectionCipher::save(cipher_id, coll_id, &conn);
}
let mut user = headers.user;
match user.update_revision(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to update the revision, please log out and log back in to finish import.")
}
} }

View File

@@ -112,6 +112,15 @@ fn disable_twofactor(
}))) })))
} }
#[put("/two-factor/disable", data = "<data>")]
fn disable_twofactor_put(
data: JsonUpcase<DisableTwoFactorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
disable_twofactor(data, headers, conn)
}
#[post("/two-factor/get-authenticator", data = "<data>")] #[post("/two-factor/get-authenticator", data = "<data>")]
fn generate_authenticator( fn generate_authenticator(
data: JsonUpcase<PasswordData>, data: JsonUpcase<PasswordData>,
@@ -194,6 +203,15 @@ fn activate_authenticator(
}))) })))
} }
#[put("/two-factor/authenticator", data = "<data>")]
fn activate_authenticator_put(
data: JsonUpcase<EnableAuthenticatorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
activate_authenticator(data, headers, conn)
}
fn _generate_recover_code(user: &mut User, conn: &DbConn) { fn _generate_recover_code(user: &mut User, conn: &DbConn) {
if user.totp_recover.is_none() { if user.totp_recover.is_none() {
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20])); let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
@@ -356,6 +374,11 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
} }
} }
#[put("/two-factor/u2f", data = "<data>")]
fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
activate_u2f(data,headers, conn)
}
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge { fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
let challenge = U2F.generate_challenge().unwrap(); let challenge = U2F.generate_challenge().unwrap();

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request}; use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request};
use rocket::{Outcome, Route}; use rocket::{Outcome, Route};
@@ -21,12 +22,12 @@ pub fn routes() -> Vec<Route> {
} }
#[post("/connect/token", data = "<connect_data>")] #[post("/connect/token", data = "<connect_data>")]
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult { fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn, socket: Option<SocketAddr>) -> JsonResult {
let data = connect_data.get(); let data = connect_data.get();
match data.grant_type { match data.grant_type {
GrantType::RefreshToken => _refresh_login(data, device_type, conn), GrantType::RefreshToken => _refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn), GrantType::Password => _password_login(data, device_type, conn, socket),
} }
} }
@@ -57,7 +58,13 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
}))) })))
} }
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) -> JsonResult { fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, remote: Option<SocketAddr>) -> JsonResult {
// Get the ip for error reporting
let ip = match remote {
Some(ip) => ip.ip(),
None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
};
// Validate scope // Validate scope
let scope = data.get("scope"); let scope = data.get("scope");
if scope != "api offline_access" { if scope != "api offline_access" {
@@ -68,13 +75,19 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
let username = data.get("username"); let username = data.get("username");
let user = match User::find_by_mail(username, &conn) { let user = match User::find_by_mail(username, &conn) {
Some(user) => user, Some(user) => user,
None => err!("Username or password is incorrect. Try again."), None => err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip, username
)),
}; };
// Check password // Check password
let password = data.get("password"); let password = data.get("password");
if !user.check_valid_password(password) { if !user.check_valid_password(password) {
err!("Username or password is incorrect. Try again.") err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip, username
))
} }
// Let's only use the header and ignore the 'devicetype' parameter // Let's only use the header and ignore the 'devicetype' parameter

View File

@@ -2,11 +2,13 @@ pub(crate) mod core;
mod icons; mod icons;
mod identity; mod identity;
mod web; mod web;
mod notifications;
pub use self::core::routes as core_routes; pub use self::core::routes as core_routes;
pub use self::icons::routes as icons_routes; pub use self::icons::routes as icons_routes;
pub use self::identity::routes as identity_routes; pub use self::identity::routes as identity_routes;
pub use self::web::routes as web_routes; pub use self::web::routes as web_routes;
pub use self::notifications::routes as notifications_routes;
use rocket::response::status::BadRequest; use rocket::response::status::BadRequest;
use rocket_contrib::Json; use rocket_contrib::Json;

31
src/api/notifications.rs Normal file
View File

@@ -0,0 +1,31 @@
use rocket::Route;
use rocket_contrib::Json;
use db::DbConn;
use api::JsonResult;
use auth::Headers;
pub fn routes() -> Vec<Route> {
routes![negotiate]
}
#[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
use data_encoding::BASE64URL;
use crypto;
// Store this in db?
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
// TODO: Implement transports
// Rocket WS support: https://github.com/SergioBenitez/Rocket/issues/90
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
Ok(Json(json!({
"connectionId": conn_id,
"availableTransports":[
// {"transport":"WebSockets", "transferFormats":["Text","Binary"]},
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
]
})))
}

View File

@@ -53,13 +53,11 @@ use db::schema::attachments;
/// Database methods /// Database methods
impl Attachment { impl Attachment {
pub fn save(&self, conn: &DbConn) -> bool { pub fn save(&self, conn: &DbConn) -> QueryResult<()> {
match diesel::replace_into(attachments::table) diesel::replace_into(attachments::table)
.values(self) .values(self)
.execute(&**conn) { .execute(&**conn)
Ok(1) => true, // One row inserted .and(Ok(()))
_ => false,
}
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
@@ -67,7 +65,7 @@ impl Attachment {
use std::{thread, time}; use std::{thread, time};
let mut retries = 10; let mut retries = 10;
loop { loop {
match diesel::delete( match diesel::delete(
attachments::table.filter( attachments::table.filter(
@@ -111,4 +109,10 @@ impl Attachment {
.filter(attachments::cipher_uuid.eq(cipher_uuid)) .filter(attachments::cipher_uuid.eq(cipher_uuid))
.load::<Self>(&**conn).expect("Error loading attachments") .load::<Self>(&**conn).expect("Error loading attachments")
} }
pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> {
attachments::table
.filter(attachments::cipher_uuid.eq_any(cipher_uuids))
.load::<Self>(&**conn).expect("Error loading attachments")
}
} }

View File

@@ -3,7 +3,7 @@ use serde_json::Value as JsonValue;
use uuid::Uuid; use uuid::Uuid;
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrgType, UserOrgStatus}; use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrganization, UserOrgType, UserOrgStatus};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)] #[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers"] #[table_name = "ciphers"]
@@ -32,6 +32,7 @@ pub struct Cipher {
pub data: String, pub data: String,
pub favorite: bool, pub favorite: bool,
pub password_history: Option<String>,
} }
/// Local methods /// Local methods
@@ -55,6 +56,7 @@ impl Cipher {
fields: None, fields: None,
data: String::new(), data: String::new(),
password_history: None,
} }
} }
} }
@@ -77,6 +79,10 @@ impl Cipher {
let fields_json: JsonValue = if let Some(ref fields) = self.fields { let fields_json: JsonValue = if let Some(ref fields) = self.fields {
serde_json::from_str(fields).unwrap() serde_json::from_str(fields).unwrap()
} else { JsonValue::Null }; } else { JsonValue::Null };
let password_history_json: JsonValue = if let Some(ref password_history) = self.password_history {
serde_json::from_str(password_history).unwrap()
} else { JsonValue::Null };
let mut data_json: JsonValue = serde_json::from_str(&self.data).unwrap(); let mut data_json: JsonValue = serde_json::from_str(&self.data).unwrap();
@@ -108,6 +114,8 @@ impl Cipher {
"Object": "cipher", "Object": "cipher",
"Edit": true, "Edit": true,
"PasswordHistory": password_history_json,
}); });
let key = match self.type_ { let key = match self.type_ {
@@ -122,7 +130,23 @@ impl Cipher {
json_object json_object
} }
pub fn update_users_revision(&self, conn: &DbConn) {
match self.user_uuid {
Some(ref user_uuid) => User::update_uuid_revision(&user_uuid, conn),
None => { // Belongs to Organization, need to update affected users
if let Some(ref org_uuid) = self.organization_uuid {
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn)
});
}
}
};
}
pub fn save(&mut self, conn: &DbConn) -> bool { pub fn save(&mut self, conn: &DbConn) -> bool {
self.update_users_revision(conn);
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
match diesel::replace_into(ciphers::table) match diesel::replace_into(ciphers::table)
@@ -134,6 +158,8 @@ impl Cipher {
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
self.update_users_revision(conn);
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?; FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?; CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?;
Attachment::delete_all_by_cipher(&self.uuid, &conn)?; Attachment::delete_all_by_cipher(&self.uuid, &conn)?;
@@ -157,6 +183,7 @@ impl Cipher {
None => { None => {
match folder_uuid { match folder_uuid {
Some(new_folder) => { Some(new_folder) => {
self.update_users_revision(conn);
let folder_cipher = FolderCipher::new(&new_folder, &self.uuid); let folder_cipher = FolderCipher::new(&new_folder, &self.uuid);
folder_cipher.save(&conn).or(Err("Couldn't save folder setting")) folder_cipher.save(&conn).or(Err("Couldn't save folder setting"))
}, },
@@ -169,6 +196,7 @@ impl Cipher {
if current_folder == new_folder { if current_folder == new_folder {
Ok(()) //nothing to do Ok(()) //nothing to do
} else { } else {
self.update_users_revision(conn);
match FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn) { match FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn) {
Some(current_folder) => { Some(current_folder) => {
current_folder.delete(&conn).or(Err("Failed removing old folder mapping")) current_folder.delete(&conn).or(Err("Failed removing old folder mapping"))
@@ -181,6 +209,7 @@ impl Cipher {
} }
}, },
None => { None => {
self.update_users_revision(conn);
match FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn) { match FolderCipher::find_by_folder_and_cipher(&current_folder, &self.uuid, &conn) {
Some(current_folder) => { Some(current_folder) => {
current_folder.delete(&conn).or(Err("Failed removing old folder mapping")) current_folder.delete(&conn).or(Err("Failed removing old folder mapping"))

View File

@@ -185,6 +185,8 @@ impl CollectionUser {
} }
pub fn save(user_uuid: &str, collection_uuid: &str, read_only:bool, conn: &DbConn) -> QueryResult<()> { pub fn save(user_uuid: &str, collection_uuid: &str, read_only:bool, conn: &DbConn) -> QueryResult<()> {
User::update_uuid_revision(&user_uuid, conn);
diesel::replace_into(users_collections::table) diesel::replace_into(users_collections::table)
.values(( .values((
users_collections::user_uuid.eq(user_uuid), users_collections::user_uuid.eq(user_uuid),
@@ -194,6 +196,8 @@ impl CollectionUser {
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
User::update_uuid_revision(&self.user_uuid, conn);
diesel::delete(users_collections::table diesel::delete(users_collections::table
.filter(users_collections::user_uuid.eq(&self.user_uuid)) .filter(users_collections::user_uuid.eq(&self.user_uuid))
.filter(users_collections::collection_uuid.eq(&self.collection_uuid))) .filter(users_collections::collection_uuid.eq(&self.collection_uuid)))
@@ -216,12 +220,20 @@ impl CollectionUser {
} }
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> QueryResult<()> { pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
CollectionUser::find_by_collection(&collection_uuid, conn)
.iter()
.for_each(|collection| {
User::update_uuid_revision(&collection.user_uuid, conn)
});
diesel::delete(users_collections::table diesel::delete(users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid)) .filter(users_collections::collection_uuid.eq(collection_uuid))
).execute(&**conn).and(Ok(())) ).execute(&**conn).and(Ok(()))
} }
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> { pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
User::update_uuid_revision(&user_uuid, conn);
diesel::delete(users_collections::table diesel::delete(users_collections::table
.filter(users_collections::user_uuid.eq(user_uuid)) .filter(users_collections::user_uuid.eq(user_uuid))
).execute(&**conn).and(Ok(())) ).execute(&**conn).and(Ok(()))

View File

@@ -71,6 +71,7 @@ use db::schema::{folders, folders_ciphers};
/// Database methods /// Database methods
impl Folder { impl Folder {
pub fn save(&mut self, conn: &DbConn) -> bool { pub fn save(&mut self, conn: &DbConn) -> bool {
User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
match diesel::replace_into(folders::table) match diesel::replace_into(folders::table)
@@ -82,6 +83,7 @@ impl Folder {
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
User::update_uuid_revision(&self.user_uuid, conn);
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?; FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
diesel::delete( diesel::delete(

View File

@@ -12,7 +12,7 @@ pub use self::attachment::Attachment;
pub use self::cipher::Cipher; pub use self::cipher::Cipher;
pub use self::device::Device; pub use self::device::Device;
pub use self::folder::{Folder, FolderCipher}; pub use self::folder::{Folder, FolderCipher};
pub use self::user::User; pub use self::user::{User, Invitation};
pub use self::organization::Organization; pub use self::organization::Organization;
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType}; pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
pub use self::collection::{Collection, CollectionUser, CollectionCipher}; pub use self::collection::{Collection, CollectionUser, CollectionCipher};

View File

@@ -1,6 +1,7 @@
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use uuid::Uuid; use uuid::Uuid;
use super::{User, CollectionUser};
#[derive(Debug, Identifiable, Queryable, Insertable)] #[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "organizations"] #[table_name = "organizations"]
@@ -26,7 +27,7 @@ pub struct UserOrganization {
} }
pub enum UserOrgStatus { pub enum UserOrgStatus {
_Invited = 0, // Unused, users are accepted automatically Invited = 0,
Accepted = 1, Accepted = 1,
Confirmed = 2, Confirmed = 2,
} }
@@ -108,12 +109,17 @@ impl UserOrganization {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use db::DbConn;
use db::schema::organizations; use db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
use db::schema::users_organizations;
/// Database methods /// Database methods
impl Organization { impl Organization {
pub fn save(&mut self, conn: &DbConn) -> bool { pub fn save(&mut self, conn: &DbConn) -> bool {
UserOrganization::find_by_org(&self.uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
match diesel::replace_into(organizations::table) match diesel::replace_into(organizations::table)
.values(&*self) .values(&*self)
.execute(&**conn) { .execute(&**conn) {
@@ -172,7 +178,6 @@ impl UserOrganization {
} }
pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue { pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue {
use super::User;
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap(); let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({ json!({
@@ -190,7 +195,6 @@ impl UserOrganization {
} }
pub fn to_json_collection_user_details(&self, read_only: &bool, conn: &DbConn) -> JsonValue { pub fn to_json_collection_user_details(&self, read_only: &bool, conn: &DbConn) -> JsonValue {
use super::User;
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap(); let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({ json!({
@@ -209,7 +213,6 @@ impl UserOrganization {
let coll_uuids = if self.access_all { let coll_uuids = if self.access_all {
vec![] // If we have complete access, no need to fill the array vec![] // If we have complete access, no need to fill the array
} else { } else {
use super::CollectionUser;
let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn); let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn);
collections.iter().map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only})).collect() collections.iter().map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only})).collect()
}; };
@@ -228,6 +231,8 @@ impl UserOrganization {
} }
pub fn save(&mut self, conn: &DbConn) -> bool { pub fn save(&mut self, conn: &DbConn) -> bool {
User::update_uuid_revision(&self.user_uuid, conn);
match diesel::replace_into(users_organizations::table) match diesel::replace_into(users_organizations::table)
.values(&*self) .values(&*self)
.execute(&**conn) { .execute(&**conn) {
@@ -237,7 +242,7 @@ impl UserOrganization {
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
use super::CollectionUser; User::update_uuid_revision(&self.user_uuid, conn);
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?; CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
@@ -265,6 +270,13 @@ impl UserOrganization {
.first::<Self>(&**conn).ok() .first::<Self>(&**conn).ok()
} }
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<Self>(&**conn).ok()
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
@@ -272,6 +284,13 @@ impl UserOrganization {
.load::<Self>(&**conn).unwrap_or(vec![]) .load::<Self>(&**conn).unwrap_or(vec![])
} }
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
.load::<Self>(&**conn).unwrap_or(vec![])
}
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid))
@@ -291,6 +310,26 @@ impl UserOrganization {
.filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid))
.first::<Self>(&**conn).ok() .first::<Self>(&**conn).ok()
} }
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.left_join(users_collections::table.on(
users_collections::user_uuid.eq(users_organizations::user_uuid)
))
.left_join(ciphers_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and(
ciphers_collections::cipher_uuid.eq(&cipher_uuid)
)
))
.filter(
users_organizations::access_all.eq(true).or( // AccessAll..
ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher
)
)
.select(users_organizations::all_columns)
.load::<Self>(&**conn).expect("Error loading user organizations")
}
} }

View File

@@ -39,13 +39,12 @@ pub struct User {
/// Local methods /// Local methods
impl User { impl User {
pub fn new(mail: String, key: String, password: String) -> Self { pub fn new(mail: String) -> Self {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
let email = mail.to_lowercase(); let email = mail.to_lowercase();
let iterations = CONFIG.password_iterations; let iterations = CONFIG.password_iterations;
let salt = crypto::get_random_64(); let salt = crypto::get_random_64();
let password_hash = crypto::hash_password(password.as_bytes(), &salt, iterations as u32);
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: Uuid::new_v4().to_string(),
@@ -53,9 +52,9 @@ impl User {
updated_at: now, updated_at: now,
name: email.clone(), name: email.clone(),
email, email,
key, key: String::new(),
password_hash, password_hash: Vec::new(),
salt, salt,
password_iterations: iterations, password_iterations: iterations,
@@ -103,7 +102,7 @@ impl User {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use db::DbConn;
use db::schema::users; use db::schema::{users, invitations};
/// Database methods /// Database methods
impl User { impl User {
@@ -154,6 +153,25 @@ impl User {
} }
} }
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
if let Some(mut user) = User::find_by_uuid(&uuid, conn) {
if user.update_revision(conn).is_err(){
println!("Warning: Failed to update revision for {}", user.email);
};
};
}
pub fn update_revision(&mut self, conn: &DbConn) -> QueryResult<()> {
self.updated_at = Utc::now().naive_utc();
diesel::update(
users::table.filter(
users::uuid.eq(&self.uuid)
)
)
.set(users::updated_at.eq(&self.updated_at))
.execute(&**conn).and(Ok(()))
}
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase(); let lower_mail = mail.to_lowercase();
users::table users::table
@@ -167,3 +185,47 @@ impl User {
.first::<Self>(&**conn).ok() .first::<Self>(&**conn).ok()
} }
} }
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "invitations"]
#[primary_key(email)]
pub struct Invitation {
pub email: String,
}
impl Invitation {
pub fn new(email: String) -> Self {
Self {
email
}
}
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
diesel::replace_into(invitations::table)
.values(&*self)
.execute(&**conn)
.and(Ok(()))
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
diesel::delete(invitations::table.filter(
invitations::email.eq(self.email)))
.execute(&**conn)
.and(Ok(()))
}
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase();
invitations::table
.filter(invitations::email.eq(lower_mail))
.first::<Self>(&**conn).ok()
}
pub fn take(mail: &str, conn: &DbConn) -> bool {
CONFIG.invitations_allowed &&
match Self::find_by_mail(mail, &conn) {
Some(invitation) => invitation.delete(&conn).is_ok(),
None => false
}
}
}

View File

@@ -21,6 +21,7 @@ table! {
fields -> Nullable<Text>, fields -> Nullable<Text>,
data -> Text, data -> Text,
favorite -> Bool, favorite -> Bool,
password_history -> Nullable<Text>,
} }
} }
@@ -112,6 +113,12 @@ table! {
} }
} }
table! {
invitations (email) {
email -> Text,
}
}
table! { table! {
users_collections (user_uuid, collection_uuid) { users_collections (user_uuid, collection_uuid) {
user_uuid -> Text, user_uuid -> Text,

63
src/mail.rs Normal file
View File

@@ -0,0 +1,63 @@
use std::error::Error;
use native_tls::{Protocol, TlsConnector};
use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity};
use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder};
use lettre::smtp::authentication::Credentials;
use lettre_email::EmailBuilder;
use MailConfig;
fn mailer(config: &MailConfig) -> SmtpTransport {
let client_security = if config.smtp_ssl {
let mut tls_builder = TlsConnector::builder().unwrap();
tls_builder.supported_protocols(&[Protocol::Tlsv11, Protocol::Tlsv12]).unwrap();
ClientSecurity::Required(
ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap())
)
} else {
ClientSecurity::None
};
let smtp_transport = SmtpTransportBuilder::new(
(config.smtp_host.to_owned().as_str(), config.smtp_port),
client_security
).unwrap();
let smtp_transport = match (&config.smtp_username, &config.smtp_password) {
(Some(username), Some(password)) => {
smtp_transport.credentials(Credentials::new(username.to_owned(), password.to_owned()))
},
(_, _) => smtp_transport,
};
smtp_transport
.smtp_utf8(true)
.connection_reuse(ConnectionReuseParameters::NoReuse)
.build()
}
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> Result<(), String> {
let (subject, body) = if let Some(hint) = hint {
("Your master password hint",
format!(
"You (or someone) recently requested your master password hint.\n\n\
Your hint is: \"{}\"\n\n\
If you did not request your master password hint you can safely ignore this email.\n",
hint))
} else {
("Sorry, you have no password hint...",
"Sorry, you have not specified any password hint...\n".to_string())
};
let email = EmailBuilder::new()
.to(address)
.from((config.smtp_from.to_owned(), "Bitwarden-rs"))
.subject(subject)
.body(body)
.build().unwrap();
match mailer(config).send(&email) {
Ok(_) => Ok(()),
Err(e) => Err(e.description().to_string()),
}
}

View File

@@ -1,5 +1,6 @@
#![feature(plugin, custom_derive)] #![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)] #![plugin(rocket_codegen)]
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
extern crate rocket; extern crate rocket;
extern crate rocket_contrib; extern crate rocket_contrib;
extern crate reqwest; extern crate reqwest;
@@ -26,6 +27,10 @@ extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate num_derive; extern crate num_derive;
extern crate num_traits; extern crate num_traits;
extern crate lettre;
extern crate lettre_email;
extern crate native_tls;
extern crate fast_chemail;
use std::{env, path::Path, process::{exit, Command}}; use std::{env, path::Path, process::{exit, Command}};
use rocket::Rocket; use rocket::Rocket;
@@ -37,6 +42,7 @@ mod api;
mod db; mod db;
mod crypto; mod crypto;
mod auth; mod auth;
mod mail;
fn init_rocket() -> Rocket { fn init_rocket() -> Rocket {
rocket::ignite() rocket::ignite()
@@ -44,6 +50,7 @@ fn init_rocket() -> Rocket {
.mount("/api", api::core_routes()) .mount("/api", api::core_routes())
.mount("/identity", api::identity_routes()) .mount("/identity", api::identity_routes())
.mount("/icons", api::icons_routes()) .mount("/icons", api::icons_routes())
.mount("/notifications", api::notifications_routes())
.manage(db::init_pool()) .manage(db::init_pool())
} }
@@ -153,6 +160,57 @@ lazy_static! {
static ref CONFIG: Config = Config::load(); static ref CONFIG: Config = Config::load();
} }
#[derive(Debug)]
pub struct MailConfig {
smtp_host: String,
smtp_port: u16,
smtp_ssl: bool,
smtp_from: String,
smtp_username: Option<String>,
smtp_password: Option<String>,
}
impl MailConfig {
fn load() -> Option<Self> {
let smtp_host = env::var("SMTP_HOST").ok();
// When SMTP_HOST is absent, we assume the user does not want to enable it.
if smtp_host.is_none() {
return None
}
let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(true);
let smtp_port = util::parse_option_string(env::var("SMTP_PORT").ok())
.unwrap_or_else(|| {
if smtp_ssl {
587u16
} else {
25u16
}
});
let smtp_username = env::var("SMTP_USERNAME").ok();
let smtp_password = env::var("SMTP_PASSWORD").ok().or_else(|| {
if smtp_username.as_ref().is_some() {
println!("Please specify SMTP_PASSWORD to enable SMTP support.");
exit(1);
} else {
None
}
});
Some(MailConfig {
smtp_host: smtp_host.unwrap(),
smtp_port: smtp_port,
smtp_ssl: smtp_ssl,
smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok())
.unwrap_or("bitwarden-rs@localhost".to_string()),
smtp_username: smtp_username,
smtp_password: smtp_password,
})
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
database_url: String, database_url: String,
@@ -168,9 +226,14 @@ pub struct Config {
local_icon_extractor: bool, local_icon_extractor: bool,
signups_allowed: bool, signups_allowed: bool,
invitations_allowed: bool,
password_iterations: i32, password_iterations: i32,
show_password_hint: bool,
domain: String, domain: String,
domain_set: bool, domain_set: bool,
mail: Option<MailConfig>,
} }
impl Config { impl Config {
@@ -196,9 +259,14 @@ impl Config {
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false), local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true), signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
invitations_allowed: util::parse_option_string(env::var("INVITATIONS_ALLOWED").ok()).unwrap_or(true),
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000), password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
show_password_hint: util::parse_option_string(env::var("SHOW_PASSWORD_HINT").ok()).unwrap_or(true),
domain_set: domain.is_ok(), domain_set: domain.is_ok(),
domain: domain.unwrap_or("http://localhost".into()), domain: domain.unwrap_or("http://localhost".into()),
mail: MailConfig::load(),
} }
} }
} }

View File

@@ -3,19 +3,20 @@
/// ///
#[macro_export] #[macro_export]
macro_rules! err { macro_rules! err {
($err:expr, $err_desc:expr, $msg:expr) => {{ ($err:expr, $msg:expr) => {{
println!("ERROR: {}", $msg); println!("ERROR: {}", $msg);
err_json!(json!({ err_json!(json!({
"error": $err, "Message": $err,
"error_description": $err_desc, "ValidationErrors": {
"ErrorModel": { "": [$msg,],
"Message": $msg, },
"ValidationErrors": null, "ExceptionMessage": null,
"Object": "error" "ExceptionStackTrace": null,
} "InnerExceptionMessage": null,
"Object": "error",
})) }))
}}; }};
($msg:expr) => { err!("default_error", "default_error_description", $msg) } ($msg:expr) => { err!("The model state is invalid", $msg) }
} }
#[macro_export] #[macro_export]