mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-03-27 12:19:21 -07:00
Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e5306b820 | ||
|
|
6890c25ea1 | ||
|
|
48482fece0 | ||
|
|
1dc1d4df72 | ||
|
|
2b4dd6f137 | ||
|
|
3da44a8d30 | ||
|
|
34ea10475d | ||
|
|
ced7f1771a | ||
|
|
af2235bf88 | ||
|
|
305de2e2cd | ||
|
|
8756c5c255 | ||
|
|
27609ac4cc | ||
|
|
95d906bdbb | ||
|
|
4bb0d7bc05 | ||
|
|
d9599155ae | ||
|
|
244bad3a24 | ||
|
|
f7056bcaa5 | ||
|
|
994669fb69 | ||
|
|
3ab90259f2 | ||
|
|
155109dea1 | ||
|
|
b268c3dd1c | ||
|
|
4e64dbdde4 | ||
|
|
a2955daffe | ||
|
|
d3921b973b | ||
|
|
cf6ad3cb15 | ||
|
|
90e0b7fec6 | ||
|
|
d77333576b | ||
|
|
73ff8d79f7 | ||
|
|
95fc88ae5b | ||
|
|
1d0eaac260 | ||
|
|
3565bfc939 | ||
|
|
a82c04910f | ||
|
|
233f03ca2b | ||
|
|
93c881a7a9 | ||
|
|
0af3956abd | ||
|
|
15feff3e79 | ||
|
|
5c5700caa7 | ||
|
|
3bddc176d6 | ||
|
|
9caf4bf383 | ||
|
|
9b2234fa0e | ||
|
|
1f79fdec4e | ||
|
|
a56f4c97e4 | ||
|
|
3a3390963c | ||
|
|
fd27759a95 | ||
|
|
01d8056c73 | ||
|
|
81fa33ebb5 | ||
|
|
e8aa3bc066 | ||
|
|
0bf0125e82 | ||
|
|
6209e778e5 | ||
|
|
5323283f98 | ||
|
|
57e17d0648 | ||
|
|
da55d5ec70 | ||
|
|
828a060698 | ||
|
|
3e5971b9db | ||
|
|
47c2625d38 | ||
|
|
49af9cf4f5 | ||
|
|
6b1daeba05 | ||
|
|
9f1240d8d9 | ||
|
|
a8138be69b | ||
|
|
ea57dc3bc9 | ||
|
|
131348a49f | ||
|
|
b22564cb00 | ||
|
|
16eb0a56f9 | ||
|
|
3e4ff47a38 | ||
|
|
8ea01a67f6 | ||
|
|
aa5cc642e1 | ||
|
|
a121cb6f00 | ||
|
|
60164182ae | ||
|
|
f842a80cdb | ||
|
|
4b6a574ee0 | ||
|
|
f9ebb780f9 | ||
|
|
1fc6c30652 | ||
|
|
46a1a013cd | ||
|
|
551810c486 | ||
|
|
b987ba506d | ||
|
|
84810f2bb2 | ||
|
|
424d666a50 | ||
|
|
a71359f647 | ||
|
|
d93c344176 | ||
|
|
b9c3213b90 | ||
|
|
95e24ffc51 | ||
|
|
00d56d7295 | ||
|
|
7436b454db | ||
|
|
8da5b99482 | ||
|
|
2969e87b52 | ||
|
|
ce62e898c3 | ||
|
|
431462d839 | ||
|
|
7d0e234b34 | ||
|
|
dad1b1bee9 | ||
|
|
9312cebee3 | ||
|
|
cdf5b6ec2d | ||
|
|
ce99fc8f95 | ||
|
|
a75d050001 | ||
|
|
75cfd10f11 | ||
|
|
9859ba6339 | ||
|
|
513056f711 | ||
|
|
ebe334fcc7 | ||
|
|
0eec12472e | ||
|
|
39106d440a | ||
|
|
9117095764 | ||
|
|
099bba950c | ||
|
|
e37ff60617 | ||
|
|
5b14608041 | ||
|
|
ad92692bab | ||
|
|
d956d42903 | ||
|
|
d69be7d03a | ||
|
|
f82de8d00d | ||
|
|
c836f88ff2 | ||
|
|
8b660ae090 | ||
|
|
9323c57f49 | ||
|
|
85e3c73525 | ||
|
|
a74bc2e58f | ||
|
|
0680638933 | ||
|
|
46d31ee5f7 | ||
|
|
e794b397d3 | ||
|
|
d41350050b | ||
|
|
4cd5b06b7f | ||
|
|
cd768439d2 | ||
|
|
9e5fd2d576 | ||
|
|
ecb46f591c | ||
|
|
d62d53aa8e | ||
|
|
2c515ab13c | ||
|
|
83d556ff0c | ||
|
|
678d313836 | ||
|
|
705d840ea3 | ||
|
|
7dff8c01dd | ||
|
|
5860679624 | ||
|
|
4628e4519d | ||
|
|
b884fd20a1 | ||
|
|
67c657003d | ||
|
|
580c1bbc7d | ||
|
|
2b6383d243 | ||
|
|
f27455a26f | ||
|
|
1d4f900e48 | ||
|
|
c5ca588a6f | ||
|
|
06888251e3 | ||
|
|
1a6e4cf4e4 | ||
|
|
9f86196a9d | ||
|
|
1e31043fb3 | ||
|
|
85adcf1ae5 | ||
|
|
9abb4d2873 | ||
|
|
235ff44736 | ||
|
|
9c2d741749 | ||
|
|
37cc0c34cf | ||
|
|
5633b6ac94 | ||
|
|
175f2aeace | ||
|
|
feefe69094 | ||
|
|
46df3ee7cd | ||
|
|
bb945ad01b | ||
|
|
de86aa671e | ||
|
|
e38771bbbd | ||
|
|
a3f9a8d7dc | ||
|
|
4b6bc6ef66 | ||
|
|
455a23361f | ||
|
|
1a8ec04733 | ||
|
|
4e60df7a08 | ||
|
|
219a9d9f5e | ||
|
|
48baf723a4 | ||
|
|
6530904883 | ||
|
|
d15d24f4ff | ||
|
|
8d992d637e | ||
|
|
6ebc83c3b7 | ||
|
|
b32f4451ee | ||
|
|
99142c7552 | ||
|
|
db710bb931 | ||
|
|
a9e9a397d8 | ||
|
|
d46a6ac687 | ||
|
|
1eb5495802 | ||
|
|
7cf8809d77 | ||
|
|
043aa27aa3 | ||
|
|
9824d94a1c | ||
|
|
e8ef76b8f9 | ||
|
|
be1ddb4203 | ||
|
|
caddf21fca | ||
|
|
5379329ef7 | ||
|
|
6faaeaae66 | ||
|
|
3fed323385 | ||
|
|
58a928547d | ||
|
|
558410c5bd | ||
|
|
0dc0decaa7 | ||
|
|
d11d663c5c | ||
|
|
771233176f | ||
|
|
ed70b07d81 | ||
|
|
e25fc7083d | ||
|
|
fa364c3f2c | ||
|
|
b5f9fe4d3b | ||
|
|
013d4c28b2 | ||
|
|
63acc8619b | ||
|
|
ec920b5756 | ||
|
|
95caaf2a40 | ||
|
|
7099f8bee8 | ||
|
|
b41a0d840c | ||
|
|
c577ade90e | ||
|
|
257b143df1 | ||
|
|
34ee326ce9 | ||
|
|
090104ce1b | ||
|
|
3305d5dc92 | ||
|
|
5bdcfe128d |
@@ -4,6 +4,8 @@ target
|
|||||||
# Data folder
|
# Data folder
|
||||||
data
|
data
|
||||||
.env
|
.env
|
||||||
|
.env.template
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
# IDE files
|
# IDE files
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
23
.editorconfig
Normal file
23
.editorconfig
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.{rs,py}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
## Bitwarden_RS Configuration File
|
## Vaultwarden Configuration File
|
||||||
## Uncomment any of the following lines to change the defaults
|
## Uncomment any of the following lines to change the defaults
|
||||||
##
|
##
|
||||||
## Be aware that most of these settings will be overridden if they were changed
|
## Be aware that most of these settings will be overridden if they were changed
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
# RSA_KEY_FILENAME=data/rsa_key
|
# RSA_KEY_FILENAME=data/rsa_key
|
||||||
# ICON_CACHE_FOLDER=data/icon_cache
|
# ICON_CACHE_FOLDER=data/icon_cache
|
||||||
# ATTACHMENTS_FOLDER=data/attachments
|
# ATTACHMENTS_FOLDER=data/attachments
|
||||||
|
# SENDS_FOLDER=data/sends
|
||||||
|
|
||||||
## Templates data folder, by default uses embedded templates
|
## Templates data folder, by default uses embedded templates
|
||||||
## Check source code to see the format
|
## Check source code to see the format
|
||||||
@@ -35,9 +36,9 @@
|
|||||||
## Automatically reload the templates for every request, slow, use only for development
|
## Automatically reload the templates for every request, slow, use only for development
|
||||||
# RELOAD_TEMPLATES=false
|
# RELOAD_TEMPLATES=false
|
||||||
|
|
||||||
## Client IP Header, used to identify the IP of the client, defaults to "X-Client-IP"
|
## Client IP Header, used to identify the IP of the client, defaults to "X-Real-IP"
|
||||||
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
|
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
|
||||||
# IP_HEADER=X-Client-IP
|
# IP_HEADER=X-Real-IP
|
||||||
|
|
||||||
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
|
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
|
||||||
# ICON_CACHE_TTL=2592000
|
# ICON_CACHE_TTL=2592000
|
||||||
@@ -55,6 +56,23 @@
|
|||||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||||
# WEBSOCKET_PORT=3012
|
# WEBSOCKET_PORT=3012
|
||||||
|
|
||||||
|
## Job scheduler settings
|
||||||
|
##
|
||||||
|
## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron),
|
||||||
|
## and are always in terms of UTC time (regardless of your local time zone settings).
|
||||||
|
##
|
||||||
|
## How often (in ms) the job scheduler thread checks for jobs that need running.
|
||||||
|
## Set to 0 to globally disable scheduled jobs.
|
||||||
|
# JOB_POLL_INTERVAL_MS=30000
|
||||||
|
##
|
||||||
|
## Cron schedule of the job that checks for Sends past their deletion date.
|
||||||
|
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
|
||||||
|
# SEND_PURGE_SCHEDULE="0 5 * * * *"
|
||||||
|
##
|
||||||
|
## Cron schedule of the job that checks for trashed items to delete permanently.
|
||||||
|
## Defaults to daily (5 minutes after midnight). Set blank to disable this job.
|
||||||
|
# TRASH_PURGE_SCHEDULE="0 5 0 * * *"
|
||||||
|
|
||||||
## Enable extended logging, which shows timestamps and targets in the logs
|
## Enable extended logging, which shows timestamps and targets in the logs
|
||||||
# EXTENDED_LOGGING=true
|
# EXTENDED_LOGGING=true
|
||||||
|
|
||||||
@@ -81,7 +99,7 @@
|
|||||||
## Enable WAL for the DB
|
## Enable WAL for the DB
|
||||||
## Set to false to avoid enabling WAL during startup.
|
## Set to false to avoid enabling WAL during startup.
|
||||||
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
||||||
## this setting only prevents bitwarden_rs from automatically enabling it on start.
|
## this setting only prevents vaultwarden from automatically enabling it on start.
|
||||||
## Please read project wiki page about this setting first before changing the value as it can
|
## Please read project wiki page about this setting first before changing the value as it can
|
||||||
## cause performance degradation or might render the service unable to start.
|
## cause performance degradation or might render the service unable to start.
|
||||||
# ENABLE_DB_WAL=true
|
# ENABLE_DB_WAL=true
|
||||||
@@ -104,7 +122,8 @@
|
|||||||
## Icon blacklist Regex
|
## Icon blacklist Regex
|
||||||
## Any domains or IPs that match this regex won't be fetched by the icon service.
|
## Any domains or IPs that match this regex won't be fetched by the icon service.
|
||||||
## Useful to hide other servers in the local network. Check the WIKI for more details
|
## Useful to hide other servers in the local network. Check the WIKI for more details
|
||||||
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^
|
## NOTE: Always enclose this regex withing single quotes!
|
||||||
|
# ICON_BLACKLIST_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
||||||
|
|
||||||
## Any IP which is not defined as a global IP will be blacklisted.
|
## Any IP which is not defined as a global IP will be blacklisted.
|
||||||
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
||||||
@@ -168,7 +187,7 @@
|
|||||||
## Invitations org admins to invite users, even when signups are disabled
|
## Invitations org admins to invite users, even when signups are disabled
|
||||||
# INVITATIONS_ALLOWED=true
|
# INVITATIONS_ALLOWED=true
|
||||||
## Name shown in the invitation emails that don't come from a specific organization
|
## Name shown in the invitation emails that don't come from a specific organization
|
||||||
# INVITATION_ORG_NAME=Bitwarden_RS
|
# INVITATION_ORG_NAME=Vaultwarden
|
||||||
|
|
||||||
## Per-organization attachment limit (KB)
|
## Per-organization attachment limit (KB)
|
||||||
## Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
|
## Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
|
||||||
@@ -240,11 +259,11 @@
|
|||||||
## To make sure the email links are pointing to the correct host, set the DOMAIN variable.
|
## To make sure the email links are pointing to the correct host, set the DOMAIN variable.
|
||||||
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
|
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
|
||||||
# SMTP_HOST=smtp.domain.tld
|
# SMTP_HOST=smtp.domain.tld
|
||||||
# SMTP_FROM=bitwarden-rs@domain.tld
|
# SMTP_FROM=vaultwarden@domain.tld
|
||||||
# SMTP_FROM_NAME=Bitwarden_RS
|
# SMTP_FROM_NAME=Vaultwarden
|
||||||
# SMTP_PORT=587
|
# SMTP_PORT=587 # Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 is outdated and used with Implicit TLS.
|
||||||
# SMTP_SSL=true # (Explicit) - This variable by default configures Explicit STARTTLS, it will upgrade an insecure connection to a secure one. Unless SMTP_EXPLICIT_TLS is set to true.
|
# SMTP_SSL=true # (Explicit) - This variable by default configures Explicit STARTTLS, it will upgrade an insecure connection to a secure one. Unless SMTP_EXPLICIT_TLS is set to true. Either port 587 or 25 are default.
|
||||||
# SMTP_EXPLICIT_TLS=true # (Implicit) - N.B. This variable configures Implicit TLS. It's currently mislabelled (see bug #851) - SMTP_SSL Needs to be set to true for this option to work.
|
# SMTP_EXPLICIT_TLS=true # (Implicit) - N.B. This variable configures Implicit TLS. It's currently mislabelled (see bug #851) - SMTP_SSL Needs to be set to true for this option to work. Usually port 465 is used here.
|
||||||
# SMTP_USERNAME=username
|
# SMTP_USERNAME=username
|
||||||
# SMTP_PASSWORD=password
|
# SMTP_PASSWORD=password
|
||||||
# SMTP_TIMEOUT=15
|
# SMTP_TIMEOUT=15
|
||||||
@@ -259,6 +278,22 @@
|
|||||||
## but might need to be changed in case it trips some anti-spam filters
|
## but might need to be changed in case it trips some anti-spam filters
|
||||||
# HELO_NAME=
|
# HELO_NAME=
|
||||||
|
|
||||||
|
## SMTP debugging
|
||||||
|
## When set to true this will output very detailed SMTP messages.
|
||||||
|
## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
|
||||||
|
# SMTP_DEBUG=false
|
||||||
|
|
||||||
|
## Accept Invalid Hostnames
|
||||||
|
## DANGEROUS: This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
||||||
|
## Only use this as a last resort if you are not able to use a valid certificate.
|
||||||
|
# SMTP_ACCEPT_INVALID_HOSTNAMES=false
|
||||||
|
|
||||||
|
## Accept Invalid Certificates
|
||||||
|
## DANGEROUS: This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
||||||
|
## Only use this as a last resort if you are not able to use a valid certificate.
|
||||||
|
## If the Certificate is valid but the hostname doesn't match, please use SMTP_ACCEPT_INVALID_HOSTNAMES instead.
|
||||||
|
# SMTP_ACCEPT_INVALID_CERTS=false
|
||||||
|
|
||||||
## Require new device emails. When a user logs in an email is required to be sent.
|
## Require new device emails. When a user logs in an email is required to be sent.
|
||||||
## If sending the email fails the login attempt will fail!!
|
## If sending the email fails the login attempt will fail!!
|
||||||
# REQUIRE_DEVICE_EMAIL=false
|
# REQUIRE_DEVICE_EMAIL=false
|
||||||
|
|||||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ignore vendored scripts in GitHub stats
|
||||||
|
src/static/scripts/* linguist-vendored
|
||||||
|
|
||||||
58
.github/ISSUE_TEMPLATE/bug_report.md
vendored
58
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,42 +1,66 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Use this ONLY for bugs in vaultwarden itself. Use the Discourse forum (link below) to request features or get help with usage/configuration. If in doubt, use the forum.
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!--
|
||||||
|
# ###
|
||||||
|
NOTE: Please update to the latest version of vaultwarden before reporting an issue!
|
||||||
|
This saves you and us a lot of time and troubleshooting.
|
||||||
|
See:
|
||||||
|
* https://github.com/dani-garcia/vaultwarden/issues/1180
|
||||||
|
* https://github.com/dani-garcia/vaultwarden/wiki/Updating-the-vaultwarden-image
|
||||||
|
# ###
|
||||||
|
-->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please fill out the following template to make solving your problem easier and faster for us.
|
Please fill out the following template to make solving your problem easier and faster for us.
|
||||||
This is only a guideline. If you think that parts are unneccessary for your issue, feel free to remove them.
|
This is only a guideline. If you think that parts are unnecessary for your issue, feel free to remove them.
|
||||||
|
|
||||||
Remember to hide/obfuscate personal and confidential information,
|
Remember to hide/redact personal or confidential information,
|
||||||
such as names, global IP/DNS adresses and especially passwords, if neccessary.
|
such as passwords, IP addresses, and DNS names as appropriate.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Subject of the issue
|
### Subject of the issue
|
||||||
<!-- Describe your issue here.-->
|
<!-- Describe your issue here. -->
|
||||||
|
|
||||||
### Your environment
|
### Deployment environment
|
||||||
<!-- The version number, obtained from the logs or the admin page -->
|
|
||||||
* Bitwarden_rs version:
|
<!--
|
||||||
<!-- How the server was installed: Docker image / package / built from source -->
|
=========================================================================================
|
||||||
|
Preferably, use the `Generate Support String` button on the admin page's Diagnostics tab.
|
||||||
|
That will auto-generate most of the info requested in this section.
|
||||||
|
=========================================================================================
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- The version number, obtained from the logs (at startup) or the admin diagnostics page -->
|
||||||
|
<!-- This is NOT the version number shown on the web vault, which is versioned separately from vaultwarden -->
|
||||||
|
<!-- Remember to check if your issue exists on the latest version first! -->
|
||||||
|
* vaultwarden version:
|
||||||
|
|
||||||
|
<!-- How the server was installed: Docker image, OS package, built from source, etc. -->
|
||||||
* Install method:
|
* Install method:
|
||||||
* Clients used: <!-- if applicable -->
|
|
||||||
|
* Clients used: <!-- web vault, desktop, Android, iOS, etc. (if applicable) -->
|
||||||
|
|
||||||
* Reverse proxy and version: <!-- if applicable -->
|
* Reverse proxy and version: <!-- if applicable -->
|
||||||
* Version of mysql/postgresql: <!-- if applicable -->
|
|
||||||
* Other relevant information:
|
* MySQL/MariaDB or PostgreSQL version: <!-- if applicable -->
|
||||||
|
|
||||||
|
* Other relevant details:
|
||||||
|
|
||||||
### Steps to reproduce
|
### Steps to reproduce
|
||||||
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
|
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
|
||||||
and how did you start bitwarden_rs? -->
|
and how did you start vaultwarden? -->
|
||||||
|
|
||||||
### Expected behaviour
|
### Expected behaviour
|
||||||
<!-- Tell us what should happen -->
|
<!-- Tell us what you expected to happen -->
|
||||||
|
|
||||||
### Actual behaviour
|
### Actual behaviour
|
||||||
<!-- Tell us what happens instead -->
|
<!-- Tell us what actually happened -->
|
||||||
|
|
||||||
### Relevant logs
|
### Troubleshooting data
|
||||||
<!-- Share some logfiles, screenshots or output of relevant programs with us. -->
|
<!-- Share any log files, screenshots, or other relevant troubleshooting data -->
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Discourse forum for bitwarden_rs
|
||||||
|
url: https://bitwardenrs.discourse.group/
|
||||||
|
about: Use this forum to request features or get help with usage/configuration.
|
||||||
|
- name: GitHub Discussions for vaultwarden
|
||||||
|
url: https://github.com/dani-garcia/vaultwarden/discussions
|
||||||
|
about: An alternative to the Discourse forum, if this is easier for you.
|
||||||
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: better for forum
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Please submit all your feature requests to the forum
|
|
||||||
Link: https://bitwardenrs.discourse.group/c/feature-requests
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Help with installation/configuration
|
|
||||||
about: Any questions about the setup of bitwarden_rs
|
|
||||||
title: ''
|
|
||||||
labels: better for forum
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Please submit all your third party help requests to the forum
|
|
||||||
Link: https://bitwardenrs.discourse.group/c/help
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Help with proxy/database/NAS setup
|
|
||||||
about: Any questions about third party software
|
|
||||||
title: ''
|
|
||||||
labels: better for forum
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Please submit all your third party help requests to the forum
|
|
||||||
Link: https://bitwardenrs.discourse.group/c/third-party-help
|
|
||||||
151
.github/workflows/build.yml
vendored
Normal file
151
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
# Ignore when there are only changes done too one of these paths
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- ".dockerignore"
|
||||||
|
- ".env.template"
|
||||||
|
- ".gitattributes"
|
||||||
|
- ".gitignore"
|
||||||
|
- "azure-pipelines.yml"
|
||||||
|
- "docker/**"
|
||||||
|
- "hooks/**"
|
||||||
|
- "tools/**"
|
||||||
|
- ".github/FUNDING.yml"
|
||||||
|
- ".github/ISSUE_TEMPLATE/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
channel:
|
||||||
|
- nightly
|
||||||
|
# - stable
|
||||||
|
target-triple:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
# - x86_64-unknown-linux-musl
|
||||||
|
include:
|
||||||
|
- target-triple: x86_64-unknown-linux-gnu
|
||||||
|
host-triple: x86_64-unknown-linux-gnu
|
||||||
|
features: "sqlite,mysql,postgresql"
|
||||||
|
channel: nightly
|
||||||
|
os: ubuntu-18.04
|
||||||
|
ext:
|
||||||
|
# - target-triple: x86_64-unknown-linux-gnu
|
||||||
|
# host-triple: x86_64-unknown-linux-gnu
|
||||||
|
# features: "sqlite,mysql,postgresql"
|
||||||
|
# channel: stable
|
||||||
|
# os: ubuntu-18.04
|
||||||
|
# ext:
|
||||||
|
# - target-triple: x86_64-unknown-linux-musl
|
||||||
|
# host-triple: x86_64-unknown-linux-gnu
|
||||||
|
# features: "sqlite,postgresql"
|
||||||
|
# channel: nightly
|
||||||
|
# os: ubuntu-18.04
|
||||||
|
# ext:
|
||||||
|
# - target-triple: x86_64-unknown-linux-musl
|
||||||
|
# host-triple: x86_64-unknown-linux-gnu
|
||||||
|
# features: "sqlite,postgresql"
|
||||||
|
# channel: stable
|
||||||
|
# os: ubuntu-18.04
|
||||||
|
# ext:
|
||||||
|
|
||||||
|
name: Building ${{ matrix.channel }}-${{ matrix.target-triple }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
# Checkout the repo
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
|
# Install musl-tools when needed
|
||||||
|
- name: Install musl tools
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-dev musl-tools cmake
|
||||||
|
if: matrix.target-triple == 'x86_64-unknown-linux-musl'
|
||||||
|
# End Install musl-tools when needed
|
||||||
|
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
- name: Install dependencies Ubuntu
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl sqlite build-essential libmariadb-dev-compat libpq-dev libssl-dev pkgconf
|
||||||
|
if: startsWith( matrix.os, 'ubuntu' )
|
||||||
|
# End Install dependencies
|
||||||
|
|
||||||
|
|
||||||
|
# Enable Rust Caching
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
# End Enable Rust Caching
|
||||||
|
|
||||||
|
|
||||||
|
# Uses the rust-toolchain file to determine version
|
||||||
|
- name: 'Install ${{ matrix.channel }}-${{ matrix.host-triple }} for target: ${{ matrix.target-triple }}'
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
target: ${{ matrix.target-triple }}
|
||||||
|
components: clippy, rustfmt
|
||||||
|
# End Uses the rust-toolchain file to determine version
|
||||||
|
|
||||||
|
|
||||||
|
# Run cargo tests (In release mode to speed up future builds)
|
||||||
|
- name: '`cargo test --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}
|
||||||
|
# End Run cargo tests
|
||||||
|
|
||||||
|
|
||||||
|
# Run cargo clippy (In release mode to speed up future builds)
|
||||||
|
- name: '`cargo clippy --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}
|
||||||
|
# End Run cargo clippy
|
||||||
|
|
||||||
|
|
||||||
|
# Run cargo fmt
|
||||||
|
- name: '`cargo fmt`'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
# End Run cargo fmt
|
||||||
|
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
- name: '`cargo build --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}
|
||||||
|
# End Build the binary
|
||||||
|
|
||||||
|
|
||||||
|
# Upload artifact to Github Actions
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: vaultwarden-${{ matrix.target-triple }}${{ matrix.ext }}
|
||||||
|
path: target/${{ matrix.target-triple }}/release/vaultwarden${{ matrix.ext }}
|
||||||
|
# End Upload artifact to Github Actions
|
||||||
|
|
||||||
|
|
||||||
|
## This is not used at the moment
|
||||||
|
## We could start using this when we can build static binaries
|
||||||
|
# Upload to github actions release
|
||||||
|
# - name: Release
|
||||||
|
# uses: Shopify/upload-to-release@1
|
||||||
|
# if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
# with:
|
||||||
|
# name: vaultwarden-${{ matrix.target-triple }}${{ matrix.ext }}
|
||||||
|
# path: target/${{ matrix.target-triple }}/release/vaultwarden${{ matrix.ext }}
|
||||||
|
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# End Upload to github actions release
|
||||||
35
.github/workflows/hadolint.yml
vendored
Normal file
35
.github/workflows/hadolint.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Hadolint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
# Ignore when there are only changes done too one of these paths
|
||||||
|
paths:
|
||||||
|
- "docker/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
hadolint:
|
||||||
|
name: Validate Dockerfile syntax
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
# Checkout the repo
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
|
# Download hadolint
|
||||||
|
- name: Download hadolint
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
|
||||||
|
sudo chmod +x /usr/local/bin/hadolint
|
||||||
|
env:
|
||||||
|
HADOLINT_VERSION: 2.0.0
|
||||||
|
# End Download hadolint
|
||||||
|
|
||||||
|
# Test Dockerfiles
|
||||||
|
- name: Run hadolint
|
||||||
|
shell: bash
|
||||||
|
run: git ls-files --exclude='docker/*/Dockerfile*' --ignored | xargs hadolint
|
||||||
|
# End Test Dockerfiles
|
||||||
148
.github/workflows/workspace.yml
vendored
148
.github/workflows/workspace.yml
vendored
@@ -1,148 +0,0 @@
|
|||||||
name: Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths-ignore:
|
|
||||||
- "**.md"
|
|
||||||
#pull_request:
|
|
||||||
# paths-ignore:
|
|
||||||
# - "**.md"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
db-backend: [sqlite, mysql, postgresql]
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
# - x86_64-unknown-linux-musl
|
|
||||||
# - x86_64-apple-darwin
|
|
||||||
# - x86_64-pc-windows-msvc
|
|
||||||
include:
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
|
||||||
os: ubuntu-latest
|
|
||||||
ext:
|
|
||||||
# - target: x86_64-unknown-linux-musl
|
|
||||||
# os: ubuntu-latest
|
|
||||||
# ext:
|
|
||||||
# - target: x86_64-apple-darwin
|
|
||||||
# os: macOS-latest
|
|
||||||
# ext:
|
|
||||||
# - target: x86_64-pc-windows-msvc
|
|
||||||
# os: windows-latest
|
|
||||||
# ext: .exe
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
|
|
||||||
# - name: Cache choco cache
|
|
||||||
# uses: actions/cache@v1.0.3
|
|
||||||
# if: matrix.os == 'windows-latest'
|
|
||||||
# with:
|
|
||||||
# path: ~\AppData\Local\Temp\chocolatey
|
|
||||||
# key: ${{ runner.os }}-choco-cache-${{ matrix.db-backend }}
|
|
||||||
|
|
||||||
- name: Cache vcpkg installed
|
|
||||||
uses: actions/cache@v1.0.3
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
with:
|
|
||||||
path: $VCPKG_ROOT/installed
|
|
||||||
key: ${{ runner.os }}-vcpkg-cache-${{ matrix.db-backend }}
|
|
||||||
env:
|
|
||||||
VCPKG_ROOT: 'C:\vcpkg'
|
|
||||||
|
|
||||||
- name: Cache vcpkg downloads
|
|
||||||
uses: actions/cache@v1.0.3
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
with:
|
|
||||||
path: $VCPKG_ROOT/downloads
|
|
||||||
key: ${{ runner.os }}-vcpkg-cache-${{ matrix.db-backend }}
|
|
||||||
env:
|
|
||||||
VCPKG_ROOT: 'C:\vcpkg'
|
|
||||||
|
|
||||||
# - name: Cache homebrew
|
|
||||||
# uses: actions/cache@v1.0.3
|
|
||||||
# if: matrix.os == 'macOS-latest'
|
|
||||||
# with:
|
|
||||||
# path: ~/Library/Caches/Homebrew
|
|
||||||
# key: ${{ runner.os }}-brew-cache
|
|
||||||
|
|
||||||
# - name: Cache apt
|
|
||||||
# uses: actions/cache@v1.0.3
|
|
||||||
# if: matrix.os == 'ubuntu-latest'
|
|
||||||
# with:
|
|
||||||
# path: /var/cache/apt/archives
|
|
||||||
# key: ${{ runner.os }}-apt-cache
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
- name: Install dependencies macOS
|
|
||||||
run: brew update; brew install openssl sqlite libpq mysql
|
|
||||||
if: matrix.os == 'macOS-latest'
|
|
||||||
|
|
||||||
- name: Install dependencies Ubuntu
|
|
||||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends openssl sqlite libpq-dev libmysql++-dev
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
|
|
||||||
- name: Install dependencies Windows
|
|
||||||
run: vcpkg integrate install; vcpkg install sqlite3:x64-windows openssl:x64-windows libpq:x64-windows libmysql:x64-windows
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
env:
|
|
||||||
VCPKG_ROOT: 'C:\vcpkg'
|
|
||||||
# End Install dependencies
|
|
||||||
|
|
||||||
# Install rust nightly toolchain
|
|
||||||
- name: Cache cargo registry
|
|
||||||
uses: actions/cache@v1.0.3
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/registry
|
|
||||||
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v1.0.3
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Cache cargo build
|
|
||||||
uses: actions/cache@v1.0.3
|
|
||||||
with:
|
|
||||||
path: target
|
|
||||||
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Install latest nightly
|
|
||||||
uses: actions-rs/toolchain@v1.0.5
|
|
||||||
with:
|
|
||||||
# Uses rust-toolchain to determine version
|
|
||||||
profile: minimal
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
|
|
||||||
# Build
|
|
||||||
- name: Build Win
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: cargo.exe build --features ${{ matrix.db-backend }} --release --target ${{ matrix.target }}
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
|
||||||
VCPKG_ROOT: 'C:\vcpkg'
|
|
||||||
|
|
||||||
- name: Build macOS / Ubuntu
|
|
||||||
if: matrix.os == 'macOS-latest' || matrix.os == 'ubuntu-latest'
|
|
||||||
run: cargo build --verbose --features ${{ matrix.db-backend }} --release --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
# Test
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --features ${{ matrix.db-backend }}
|
|
||||||
|
|
||||||
# Upload & Release
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v1.0.0
|
|
||||||
with:
|
|
||||||
name: bitwarden_rs-${{ matrix.db-backend }}-${{ matrix.target }}${{ matrix.ext }}
|
|
||||||
path: target/${{ matrix.target }}/release/bitwarden_rs${{ matrix.ext }}
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: Shopify/upload-to-release@1.0.0
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
name: bitwarden_rs-${{ matrix.db-backend }}-${{ matrix.target }}${{ matrix.ext }}
|
|
||||||
path: target/${{ matrix.target }}/release/bitwarden_rs${{ matrix.ext }}
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- HADOLINT_VERSION=1.17.1
|
|
||||||
|
|
||||||
language: rust
|
|
||||||
rust: nightly
|
|
||||||
cache: cargo
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint
|
|
||||||
- sudo chmod +rx /usr/local/bin/hadolint
|
|
||||||
- rustup set profile minimal
|
|
||||||
|
|
||||||
# Nothing to install
|
|
||||||
install: true
|
|
||||||
script:
|
|
||||||
- git ls-files --exclude='Dockerfile*' --ignored | xargs --max-lines=1 hadolint
|
|
||||||
- cargo test --features "sqlite"
|
|
||||||
- cargo test --features "mysql"
|
|
||||||
1716
Cargo.lock
generated
1716
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
81
Cargo.toml
81
Cargo.toml
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bitwarden_rs"
|
name = "vaultwarden"
|
||||||
version = "1.0.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>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/bitwarden_rs"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
publish = false
|
publish = false
|
||||||
@@ -32,53 +32,55 @@ rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
|
|||||||
rocket_contrib = "0.5.0-dev"
|
rocket_contrib = "0.5.0-dev"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
reqwest = { version = "0.10.8", features = ["blocking", "json"] }
|
reqwest = { version = "0.11.3", features = ["blocking", "json", "gzip", "brotli", "socks"] }
|
||||||
|
|
||||||
# multipart/form-data support
|
# multipart/form-data support
|
||||||
multipart = { version = "0.17.0", features = ["server"], default-features = false }
|
multipart = { version = "0.17.1", features = ["server"], default-features = false }
|
||||||
|
|
||||||
# WebSockets library
|
# WebSockets library
|
||||||
ws = "0.9.1"
|
ws = { version = "0.10.0", package = "parity-ws" }
|
||||||
|
|
||||||
# MessagePack library
|
# MessagePack library
|
||||||
rmpv = "0.4.5"
|
rmpv = "0.4.7"
|
||||||
|
|
||||||
# Concurrent hashmap implementation
|
# Concurrent hashmap implementation
|
||||||
chashmap = "2.2.2"
|
chashmap = "2.2.2"
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = "1.0.115"
|
serde = { version = "1.0.125", features = ["derive"] }
|
||||||
serde_derive = "1.0.115"
|
serde_json = "1.0.64"
|
||||||
serde_json = "1.0.57"
|
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.11"
|
log = "0.4.14"
|
||||||
fern = { version = "0.6.0", features = ["syslog-4"] }
|
fern = { version = "0.6.0", features = ["syslog-4"] }
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "1.4.5", features = [ "chrono", "r2d2"] }
|
diesel = { version = "1.4.6", features = [ "chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled SQLite
|
||||||
libsqlite3-sys = { version = "0.18.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.20.1", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.7.3"
|
rand = "0.8.3"
|
||||||
ring = "0.16.15"
|
ring = "0.16.20"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.8.1", features = ["v4"] }
|
uuid = { version = "0.8.2", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = "0.4.15"
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
chrono-tz = "0.5.3"
|
chrono-tz = "0.5.3"
|
||||||
time = "0.2.18"
|
time = "0.2.26"
|
||||||
|
|
||||||
|
# Job scheduler
|
||||||
|
job_scheduler = "1.2.1"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
oath = "0.10.2"
|
oath = "0.10.2"
|
||||||
|
|
||||||
# Data encoding library
|
# Data encoding library
|
||||||
data-encoding = "2.3.0"
|
data-encoding = "2.3.2"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "7.2.0"
|
jsonwebtoken = "7.2.0"
|
||||||
@@ -87,51 +89,60 @@ jsonwebtoken = "7.2.0"
|
|||||||
u2f = "0.2.0"
|
u2f = "0.2.0"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.9.1", features = ["online-tokio"], default-features = false }
|
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# A `dotenv` implementation for Rust
|
||||||
dotenv = { version = "0.15.0", default-features = false }
|
dotenv = { version = "0.15.0", default-features = false }
|
||||||
|
|
||||||
# Lazy initialization
|
# Lazy initialization
|
||||||
once_cell = "1.4.1"
|
once_cell = "1.7.2"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.12"
|
num-traits = "0.2.14"
|
||||||
num-derive = "0.3.2"
|
num-derive = "0.3.3"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.10.0-alpha.2", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname"], default-features = false }
|
tracing = { version = "0.1.25", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled.
|
||||||
newline-converter = "0.1.0"
|
lettre = { version = "0.10.0-beta.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
||||||
|
newline-converter = "0.2.0"
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = { version = "3.4.0", features = ["dir_source"] }
|
handlebars = { version = "3.5.4", features = ["dir_source"] }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
soup = "0.5.0"
|
html5ever = "0.25.1"
|
||||||
regex = "1.3.9"
|
markup5ever_rcdom = "0.1.0"
|
||||||
|
regex = { version = "1.4.5", features = ["std", "perf"], default-features = false }
|
||||||
data-url = "0.1.0"
|
data-url = "0.1.0"
|
||||||
|
|
||||||
# Used by U2F, JWT and Postgres
|
# Used by U2F, JWT and Postgres
|
||||||
openssl = "0.10.30"
|
openssl = "0.10.34"
|
||||||
|
|
||||||
# URL encoding library
|
# URL encoding library
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
# Punycode conversion
|
# Punycode conversion
|
||||||
idna = "0.2.0"
|
idna = "0.2.2"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
structopt = "0.3.17"
|
pico-args = "0.4.0"
|
||||||
|
|
||||||
# Logging panics to logfile instead stderr only
|
# Logging panics to logfile instead stderr only
|
||||||
backtrace = "0.3.50"
|
backtrace = "0.3.56"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
paste = "1.0.0"
|
paste = "1.0.5"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Use newest ring
|
# Use newest ring
|
||||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' }
|
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
||||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' }
|
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '7f1bd6ce1c2fde599a757302a843a60e714c5f72' }
|
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '540ede02d0771824c0c80ff9f57fe8eff38b1291' }
|
||||||
|
|
||||||
|
# The maintainer of the `job_scheduler` crate doesn't seem to have responded
|
||||||
|
# to any issues or PRs for almost a year (as of April 2021). This hopefully
|
||||||
|
# temporary fork updates Cargo.toml to use more up-to-date dependencies.
|
||||||
|
# In particular, `cron` has since implemented parsing of some common syntax
|
||||||
|
# that wasn't previously supported (https://github.com/zslayton/cron/pull/64).
|
||||||
|
job_scheduler = { git = 'https://github.com/jjlin/job_scheduler', rev = 'ee023418dbba2bfe1e30a5fd7d937f9e33739806' }
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -1,15 +1,14 @@
|
|||||||
### 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.
|
### Alternative implementation of the Bitwarden server API written in Rust and compatible with [upstream Bitwarden clients](https://bitwarden.com/#download)*, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://travis-ci.org/dani-garcia/bitwarden_rs)
|
[](https://hub.docker.com/r/vaultwarden/server)
|
||||||
[](https://hub.docker.com/r/bitwardenrs/server)
|
[](https://deps.rs/repo/github/dani-garcia/vaultwarden)
|
||||||
[](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
|
[](https://github.com/dani-garcia/vaultwarden/releases/latest)
|
||||||
[](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
|
[](https://github.com/dani-garcia/vaultwarden/blob/master/LICENSE.txt)
|
||||||
[](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
|
[](https://matrix.to/#/#vaultwarden:matrix.org)
|
||||||
[](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/vaultwarden).
|
||||||
|
|
||||||
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC.**
|
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC.**
|
||||||
|
|
||||||
@@ -33,29 +32,57 @@ Basically full implementation of Bitwarden API is provided including:
|
|||||||
Pull the docker image and mount a volume from the host for persistent storage:
|
Pull the docker image and mount a volume from the host for persistent storage:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull bitwardenrs/server:latest
|
docker pull vaultwarden/server:latest
|
||||||
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 bitwardenrs/server:latest
|
docker run -d --name vaultwarden -v /vw-data/:/data/ -p 80:80 vaultwarden/server:latest
|
||||||
```
|
```
|
||||||
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
|
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
|
||||||
|
|
||||||
**IMPORTANT**: Some web browsers, like Chrome, disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault from HTTPS.
|
**IMPORTANT**: Some web browsers, like Chrome, disallow the use of Web Crypto APIs in insecure contexts. In this case, you might get an error like `Cannot read property 'importKey'`. To solve this problem, you need to access the web vault from HTTPS.
|
||||||
|
|
||||||
This can be configured in [bitwarden_rs directly](https://github.com/dani-garcia/bitwarden_rs/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/bitwarden_rs/wiki/Proxy-examples)).
|
This can be configured in [vaultwarden directly](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS) or using a third-party reverse proxy ([some examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).
|
||||||
|
|
||||||
If you have an available domain name, you can get HTTPS certificates with [Let's Encrypt](https://letsencrypt.org/), or you can generate self-signed certificates with utilities like [mkcert](https://github.com/FiloSottile/mkcert). Some proxies automatically do this step, like Caddy (see examples linked above).
|
If you have an available domain name, you can get HTTPS certificates with [Let's Encrypt](https://letsencrypt.org/), or you can generate self-signed certificates with utilities like [mkcert](https://github.com/FiloSottile/mkcert). Some proxies automatically do this step, like Caddy (see examples linked above).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) for more information on how to configure and run the bitwarden_rs server.
|
See the [vaultwarden wiki](https://github.com/dani-garcia/vaultwarden/wiki) for more information on how to configure and run the vaultwarden server.
|
||||||
|
|
||||||
## Get in touch
|
## Get in touch
|
||||||
To ask a question, offer suggestions or new features or to get help configuring or installing the software, please [use the forum](https://bitwardenrs.discourse.group/).
|
To ask a question, offer suggestions or new features or to get help configuring or installing the software, please [use the forum](https://vaultwarden.discourse.group/).
|
||||||
|
|
||||||
If you spot any bugs or crashes with bitwarden_rs itself, please [create an issue](https://github.com/dani-garcia/bitwarden_rs/issues/). Make sure there aren't any similar issues open, though!
|
If you spot any bugs or crashes with vaultwarden itself, please [create an issue](https://github.com/dani-garcia/vaultwarden/issues/). Make sure there aren't any similar issues open, though!
|
||||||
|
|
||||||
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!
|
If you prefer to chat, we're usually hanging around at [#vaultwarden:matrix.org](https://matrix.to/#/#vaultwarden:matrix.org) room on Matrix. Feel free to join us!
|
||||||
|
|
||||||
### Sponsors
|
### Sponsors
|
||||||
Thanks for your contribution to the project!
|
Thanks for your contribution to the project!
|
||||||
|
|
||||||
- [@ChonoN](https://github.com/ChonoN)
|
<table>
|
||||||
- [@themightychris](https://github.com/themightychris)
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/netdadaltd">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/77323954?s=75&v=4" width="75px;" alt="netdadaltd"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>netDada Ltd.</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ChonoN" style="width: 75px">
|
||||||
|
<sub><b>ChonoN</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/themightychris">
|
||||||
|
<sub><b>themightychris</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pool:
|
pool:
|
||||||
vmImage: 'Ubuntu-16.04'
|
vmImage: 'Ubuntu-18.04'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- script: |
|
- script: |
|
||||||
@@ -10,16 +10,13 @@ steps:
|
|||||||
|
|
||||||
- script: |
|
- script: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libmysql++-dev
|
sudo apt-get install -y --no-install-recommends build-essential libmariadb-dev-compat libpq-dev libssl-dev pkgconf
|
||||||
displayName: Install libmysql
|
displayName: 'Install build libraries.'
|
||||||
|
|
||||||
- script: |
|
- script: |
|
||||||
rustc -Vv
|
rustc -Vv
|
||||||
cargo -V
|
cargo -V
|
||||||
displayName: Query rust and cargo versions
|
displayName: Query rust and cargo versions
|
||||||
|
|
||||||
- script : cargo test --features "sqlite"
|
- script : cargo test --features "sqlite,mysql,postgresql"
|
||||||
displayName: 'Test project with sqlite backend'
|
displayName: 'Test project with sqlite, mysql and postgresql backends'
|
||||||
|
|
||||||
- script : cargo test --features "mysql"
|
|
||||||
displayName: 'Test project with mysql backend'
|
|
||||||
|
|||||||
8
build.rs
8
build.rs
@@ -1,5 +1,5 @@
|
|||||||
use std::process::Command;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros
|
// This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros
|
||||||
@@ -11,7 +11,9 @@ fn main() {
|
|||||||
println!("cargo:rustc-cfg=postgresql");
|
println!("cargo:rustc-cfg=postgresql");
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
|
||||||
compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite");
|
compile_error!(
|
||||||
|
"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"
|
||||||
|
);
|
||||||
|
|
||||||
if let Ok(version) = env::var("BWRS_VERSION") {
|
if let Ok(version) = env::var("BWRS_VERSION") {
|
||||||
println!("cargo:rustc-env=BWRS_VERSION={}", version);
|
println!("cargo:rustc-env=BWRS_VERSION={}", version);
|
||||||
@@ -56,7 +58,7 @@ fn read_git_info() -> Result<(), std::io::Error> {
|
|||||||
// Combined version
|
// Combined version
|
||||||
let version = if let Some(exact) = exact_tag {
|
let version = if let Some(exact) = exact_tag {
|
||||||
exact
|
exact
|
||||||
} else if &branch != "master" {
|
} else if &branch != "main" && &branch != "master" {
|
||||||
format!("{}-{} ({})", last_tag, rev_short, branch)
|
format!("{}-{} ({})", last_tag, rev_short, branch)
|
||||||
} else {
|
} else {
|
||||||
format!("{}-{}", last_tag, rev_short)
|
format!("{}-{}", last_tag, rev_short)
|
||||||
|
|||||||
33
docker/Dockerfile.buildx
Normal file
33
docker/Dockerfile.buildx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# The cross-built images have the build arch (`amd64`) embedded in the image
|
||||||
|
# manifest, rather than the target arch. For example:
|
||||||
|
#
|
||||||
|
# $ docker inspect vaultwarden/server:latest-armv7 | jq -r '.[]|.Architecture'
|
||||||
|
# amd64
|
||||||
|
#
|
||||||
|
# Recent versions of Docker have started printing a warning when the image's
|
||||||
|
# claimed arch doesn't match the host arch. For example:
|
||||||
|
#
|
||||||
|
# WARNING: The requested image's platform (linux/amd64) does not match the
|
||||||
|
# detected host platform (linux/arm/v7) and no specific platform was requested
|
||||||
|
#
|
||||||
|
# The image still works fine, but the spurious warning creates confusion.
|
||||||
|
#
|
||||||
|
# Docker doesn't seem to provide a way to directly set the arch of an image
|
||||||
|
# at build time. To resolve the build vs. target arch discrepancy, we use
|
||||||
|
# Docker Buildx to build a new set of images with the correct target arch.
|
||||||
|
#
|
||||||
|
# Docker Buildx uses this Dockerfile to build an image for each requested
|
||||||
|
# platform. Since the Dockerfile basically consists of a single `FROM`
|
||||||
|
# instruction, we're effectively telling Buildx to build a platform-specific
|
||||||
|
# image by simply copying the existing cross-built image and setting the
|
||||||
|
# correct target arch as a side effect.
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
#
|
||||||
|
# - https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images
|
||||||
|
# - https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
|
||||||
|
# - https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact
|
||||||
|
#
|
||||||
|
ARG LOCAL_REPO
|
||||||
|
ARG DOCKER_TAG
|
||||||
|
FROM ${LOCAL_REPO}:${DOCKER_TAG}-${TARGETARCH}${TARGETVARIANT}
|
||||||
@@ -1,64 +1,89 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
{% set build_stage_base_image = "rust:1.46" %}
|
{% set build_stage_base_image = "rust:1.51" %}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% if "amd64" in target_file %}
|
{% if "amd64" in target_file %}
|
||||||
{% set build_stage_base_image = "clux/muslrust:nightly-2020-10-02" %}
|
{% set build_stage_base_image = "clux/muslrust:nightly-2021-04-14" %}
|
||||||
{% set runtime_stage_base_image = "alpine:3.12" %}
|
{% set runtime_stage_base_image = "alpine:3.13" %}
|
||||||
{% set package_arch_name = "" %}
|
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||||
{% elif "arm32v7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
{% set build_stage_base_image = "messense/rust-musl-cross:armv7-musleabihf" %}
|
{% set build_stage_base_image = "messense/rust-musl-cross:armv7-musleabihf" %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.12" %}
|
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.13" %}
|
||||||
{% set package_arch_name = "" %}
|
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif "amd64" in target_file %}
|
{% elif "amd64" in target_file %}
|
||||||
{% set runtime_stage_base_image = "debian:buster-slim" %}
|
{% set runtime_stage_base_image = "debian:buster-slim" %}
|
||||||
{% set package_arch_name = "" %}
|
{% elif "arm64" in target_file %}
|
||||||
{% elif "arm64v8" in target_file %}
|
|
||||||
{% set runtime_stage_base_image = "balenalib/aarch64-debian:buster" %}
|
{% set runtime_stage_base_image = "balenalib/aarch64-debian:buster" %}
|
||||||
{% set package_arch_name = "arm64" %}
|
{% set package_arch_name = "arm64" %}
|
||||||
{% elif "arm32v6" in target_file %}
|
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
||||||
|
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
||||||
|
{% elif "armv6" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/rpi-debian:buster" %}
|
{% set runtime_stage_base_image = "balenalib/rpi-debian:buster" %}
|
||||||
{% set package_arch_name = "armel" %}
|
{% set package_arch_name = "armel" %}
|
||||||
{% elif "arm32v7" in target_file %}
|
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
||||||
|
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
||||||
|
{% elif "armv7" in target_file %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:buster" %}
|
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:buster" %}
|
||||||
{% set package_arch_name = "armhf" %}
|
{% set package_arch_name = "armhf" %}
|
||||||
|
{% set package_arch_target = "armv7-unknown-linux-gnueabihf" %}
|
||||||
|
{% set package_cross_compiler = "arm-linux-gnueabihf" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set package_arch_prefix = ":" + package_arch_name %}
|
{% if package_arch_name is defined %}
|
||||||
{% if package_arch_name == "" %}
|
{% set package_arch_prefix = ":" + package_arch_name %}
|
||||||
|
{% else %}
|
||||||
{% set package_arch_prefix = "" %}
|
{% set package_arch_prefix = "" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if package_arch_target is defined %}
|
||||||
|
{% set package_arch_target_param = " --target=" + package_arch_target %}
|
||||||
|
{% else %}
|
||||||
|
{% set package_arch_target_param = "" %}
|
||||||
|
{% endif %}
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
{% set vault_image_hash = "sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303" %}
|
{% set vault_version = "2.19.0d" %}
|
||||||
{% raw %}
|
{% set vault_image_digest = "sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233" %}
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# It can be viewed in multiple ways:
|
# Using the digest instead of the tag name provides better security,
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the console, with the following commands:
|
# be changed to point to a malicious image.
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
{% endraw %}
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
FROM bitwardenrs/web-vault@{{ vault_image_hash }} as vault
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v{{ vault_version }}
|
||||||
|
# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" vaultwarden/web-vault:v{{ vault_version }}
|
||||||
|
# [vaultwarden/web-vault@{{ vault_image_digest }}]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
|
# [vaultwarden/web-vault:v{{ vault_version }}]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@{{ vault_image_digest }} as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM {{ build_stage_base_image }} as build
|
FROM {{ build_stage_base_image }} as build
|
||||||
|
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
# Alpine only works on SQlite
|
{% if "amd64" in target_file %}
|
||||||
|
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
||||||
|
ARG DB=sqlite,postgresql
|
||||||
|
{% set features = "sqlite,postgresql" %}
|
||||||
|
{% else %}
|
||||||
|
# Alpine-based ARM (musl) only supports sqlite during compile time.
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
{% set features = "sqlite" %}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
|
{% set features = "sqlite,mysql,postgresql" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
|
|
||||||
@@ -68,7 +93,9 @@ RUN rustup set profile minimal
|
|||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
ENV RUSTFLAGS='-C link-arg=-s'
|
||||||
|
{% if "armv7" in target_file %}
|
||||||
|
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
||||||
|
{% endif %}
|
||||||
{% elif "arm" in target_file %}
|
{% elif "arm" in target_file %}
|
||||||
# Install required build libs for {{ package_arch_name }} architecture.
|
# Install required build libs for {{ package_arch_name }} architecture.
|
||||||
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
||||||
@@ -85,44 +112,19 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|||||||
libmariadb-dev{{ package_arch_prefix }} \
|
libmariadb-dev{{ package_arch_prefix }} \
|
||||||
libmariadb-dev-compat{{ package_arch_prefix }}
|
libmariadb-dev-compat{{ package_arch_prefix }}
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y \
|
||||||
|
--no-install-recommends \
|
||||||
|
gcc-{{ package_cross_compiler }} \
|
||||||
|
&& mkdir -p ~/.cargo \
|
||||||
|
&& echo '[target.{{ package_arch_target }}]' >> ~/.cargo/config \
|
||||||
|
&& echo 'linker = "{{ package_cross_compiler }}-gcc"' >> ~/.cargo/config \
|
||||||
|
&& echo 'rustflags = ["-L/usr/lib/{{ package_cross_compiler }}"]' >> ~/.cargo/config
|
||||||
|
|
||||||
|
ENV CARGO_HOME "/root/.cargo"
|
||||||
|
ENV USER "root"
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% if "arm64v8" in target_file %}
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
gcc-aarch64-linux-gnu \
|
|
||||||
&& mkdir -p ~/.cargo \
|
|
||||||
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
|
|
||||||
&& echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config \
|
|
||||||
&& echo 'rustflags = ["-L/usr/lib/aarch64-linux-gnu"]' >> ~/.cargo/config
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
{% elif "arm32v6" in target_file %}
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
gcc-arm-linux-gnueabi \
|
|
||||||
&& mkdir -p ~/.cargo \
|
|
||||||
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
|
|
||||||
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.cargo/config \
|
|
||||||
&& echo 'rustflags = ["-L/usr/lib/arm-linux-gnueabi"]' >> ~/.cargo/config
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
{% elif "arm32v7" in target_file and "alpine" not in target_file %}
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
gcc-arm-linux-gnueabihf \
|
|
||||||
&& mkdir -p ~/.cargo \
|
|
||||||
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
|
|
||||||
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config \
|
|
||||||
&& echo 'rustflags = ["-L/usr/lib/arm-linux-gnueabihf"]' >> ~/.cargo/config
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
{% endif %}
|
|
||||||
{% if "amd64" in target_file and "alpine" not in target_file %}
|
{% if "amd64" in target_file and "alpine" not in target_file %}
|
||||||
# Install DB packages
|
# Install DB packages
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
@@ -148,71 +150,31 @@ COPY ./build.rs ./build.rs
|
|||||||
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
||||||
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the {{ package_arch_prefix }} version.
|
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the {{ package_arch_prefix }} version.
|
||||||
# What we can do is a force install, because nothing important is overlapping each other.
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
RUN apt-get install -y libmariadb3:amd64 && \
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
mkdir -pv /tmp/dpkg && \
|
|
||||||
cd /tmp/dpkg && \
|
|
||||||
apt-get download libmariadb-dev-compat:amd64 && \
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
dpkg --force-all -i *.deb && \
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
rm -rf /tmp/dpkg
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
||||||
# The libpq5{{ package_arch_prefix }} package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
# The libpq5{{ package_arch_prefix }} package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
||||||
# This is only provided by the libpq-dev package which can't be installed for both arch at the same time.
|
# This is only provided by the libpq-dev package which can't be installed for both arch at the same time.
|
||||||
# Without this specific file the ld command will fail and compilation fails with it.
|
# Without this specific file the ld command will fail and compilation fails with it.
|
||||||
|
RUN ln -sfnr /usr/lib/{{ package_cross_compiler }}/libpq.so.5 /usr/lib/{{ package_cross_compiler }}/libpq.so
|
||||||
|
|
||||||
|
ENV CC_{{ package_arch_target | replace("-", "_") }}="/usr/bin/{{ package_cross_compiler }}-gcc"
|
||||||
|
ENV CROSS_COMPILE="1"
|
||||||
|
ENV OPENSSL_INCLUDE_DIR="/usr/include/{{ package_cross_compiler }}"
|
||||||
|
ENV OPENSSL_LIB_DIR="/usr/lib/{{ package_cross_compiler }}"
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% if "arm64v8" in target_file %}
|
{% endif %}
|
||||||
RUN ln -sfnr /usr/lib/aarch64-linux-gnu/libpq.so.5 /usr/lib/aarch64-linux-gnu/libpq.so
|
{% if package_arch_target is defined %}
|
||||||
|
RUN rustup target add {{ package_arch_target }}
|
||||||
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
|
||||||
ENV CROSS_COMPILE="1"
|
|
||||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
|
|
||||||
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
|
||||||
RUN rustup target add aarch64-unknown-linux-gnu
|
|
||||||
{% elif "arm32v6" in target_file %}
|
|
||||||
RUN ln -sfnr /usr/lib/arm-linux-gnueabi/libpq.so.5 /usr/lib/arm-linux-gnueabi/libpq.so
|
|
||||||
|
|
||||||
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
|
|
||||||
ENV CROSS_COMPILE="1"
|
|
||||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
|
|
||||||
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
|
||||||
RUN rustup target add arm-unknown-linux-gnueabi
|
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
RUN ln -sfnr /usr/lib/arm-linux-gnueabihf/libpq.so.5 /usr/lib/arm-linux-gnueabihf/libpq.so
|
|
||||||
|
|
||||||
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"
|
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
|
||||||
{% endif -%}
|
|
||||||
{% else -%}
|
|
||||||
{% if "amd64" in target_file %}
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
RUN rustup target add armv7-unknown-linux-musleabihf
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
# Builds your dependencies and removes the
|
||||||
# dummy project, except the target folder
|
# dummy project, except the target folder
|
||||||
# This folder contains the compiled dependencies
|
# This folder contains the compiled dependencies
|
||||||
{% if "alpine" in target_file %}
|
RUN cargo build --features ${DB} --release{{ package_arch_target_param }}
|
||||||
{% if "amd64" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
|
||||||
{% endif %}
|
|
||||||
{% elif "alpine" not in target_file %}
|
|
||||||
{% if "amd64" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
{% elif "arm64v8" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
|
||||||
{% elif "arm32v6" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
@@ -224,22 +186,10 @@ RUN touch src/main.rs
|
|||||||
|
|
||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
|
RUN cargo build --features ${DB} --release{{ package_arch_target_param }}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% if "amd64" in target_file %}
|
{% if "armv7" in target_file %}
|
||||||
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
RUN musl-strip target/{{ package_arch_target }}/release/vaultwarden
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
|
||||||
RUN musl-strip target/armv7-unknown-linux-musleabihf/release/bitwarden_rs
|
|
||||||
{% endif %}
|
|
||||||
{% elif "alpine" not in target_file %}
|
|
||||||
{% if "amd64" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
{% elif "arm64v8" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
|
||||||
{% elif "arm32v6" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -264,11 +214,11 @@ RUN [ "cross-build-start" ]
|
|||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
{% if "sqlite" in target_file %}
|
dumb-init \
|
||||||
sqlite \
|
{% if "mysql" in features %}
|
||||||
{% elif "mysql" in target_file %}
|
|
||||||
mariadb-connector-c \
|
mariadb-connector-c \
|
||||||
{% elif "postgresql" in target_file %}
|
{% endif %}
|
||||||
|
{% if "postgresql" in features %}
|
||||||
postgresql-libs \
|
postgresql-libs \
|
||||||
{% endif %}
|
{% endif %}
|
||||||
ca-certificates
|
ca-certificates
|
||||||
@@ -278,14 +228,11 @@ RUN apt-get update && apt-get install -y \
|
|||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
dumb-init \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "alpine" in target_file and "arm32v7" in target_file %}
|
|
||||||
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/community catatonit
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
{% if "amd64" not in target_file %}
|
{% if "amd64" not in target_file %}
|
||||||
@@ -299,24 +246,13 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
{% if "alpine" in target_file %}
|
{% if package_arch_target is defined %}
|
||||||
{% if "amd64" in target_file %}
|
COPY --from=build /app/target/{{ package_arch_target }}/release/vaultwarden .
|
||||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
{% else %}
|
||||||
{% elif "arm32v7" in target_file %}
|
COPY --from=build /app/target/release/vaultwarden .
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/bitwarden_rs .
|
|
||||||
{% endif %}
|
|
||||||
{% elif "alpine" not in target_file %}
|
|
||||||
{% if "arm64v8" in target_file %}
|
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
|
|
||||||
{% elif "arm32v6" in target_file %}
|
|
||||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
|
|
||||||
{% elif "arm32v7" in target_file %}
|
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
|
|
||||||
{% else %}
|
|
||||||
COPY --from=build app/target/release/bitwarden_rs .
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
@@ -325,9 +261,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
{% if "alpine" in target_file and "arm32v7" in target_file %}
|
|
||||||
CMD ["catatonit", "/start.sh"]
|
|
||||||
{% else %}
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
{% endif %}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
OBJECTS := $(shell find -mindepth 2 -name 'Dockerfile*')
|
OBJECTS := $(shell find ./ -mindepth 2 -name 'Dockerfile*')
|
||||||
|
|
||||||
all: $(OBJECTS)
|
all: $(OBJECTS)
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# Using the digest instead of the tag name provides better security,
|
||||||
# It can be viewed in multiple ways:
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# be changed to point to a malicious image.
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
FROM bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303 as vault
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v2.19.0d
|
||||||
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d
|
||||||
|
# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233
|
||||||
|
# [vaultwarden/web-vault:v2.19.0d]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.46 as build
|
FROM rust:1.51 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
@@ -78,7 +85,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
dumb-init \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -90,9 +97,10 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build app/target/release/bitwarden_rs .
|
COPY --from=build /app/target/release/vaultwarden .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
COPY docker/start.sh /start.sh
|
COPY docker/start.sh /start.sh
|
||||||
@@ -100,6 +108,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# Using the digest instead of the tag name provides better security,
|
||||||
# It can be viewed in multiple ways:
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# be changed to point to a malicious image.
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
FROM bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303 as vault
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v2.19.0d
|
||||||
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d
|
||||||
|
# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233
|
||||||
|
# [vaultwarden/web-vault:v2.19.0d]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM clux/muslrust:nightly-2020-10-02 as build
|
FROM clux/muslrust:nightly-2021-04-14 as build
|
||||||
|
|
||||||
# Alpine only works on SQlite
|
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite,postgresql
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
@@ -32,7 +39,6 @@ RUN rustup set profile minimal
|
|||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
ENV RUSTFLAGS='-C link-arg=-s'
|
||||||
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -64,7 +70,7 @@ RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.12
|
FROM alpine:3.13
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -75,6 +81,8 @@ ENV SSL_CERT_DIR=/etc/ssl/certs
|
|||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
|
dumb-init \
|
||||||
|
postgresql-libs \
|
||||||
ca-certificates
|
ca-certificates
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
@@ -84,9 +92,10 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/vaultwarden .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
COPY docker/start.sh /start.sh
|
COPY docker/start.sh /start.sh
|
||||||
@@ -94,6 +103,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# Using the digest instead of the tag name provides better security,
|
||||||
# It can be viewed in multiple ways:
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# be changed to point to a malicious image.
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
FROM bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303 as vault
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v2.19.0d
|
||||||
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d
|
||||||
|
# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233
|
||||||
|
# [vaultwarden/web-vault:v2.19.0d]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.46 as build
|
FROM rust:1.51 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
@@ -70,12 +77,10 @@ COPY ./build.rs ./build.rs
|
|||||||
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
||||||
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the :arm64 version.
|
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the :arm64 version.
|
||||||
# What we can do is a force install, because nothing important is overlapping each other.
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
RUN apt-get install -y libmariadb3:amd64 && \
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
mkdir -pv /tmp/dpkg && \
|
|
||||||
cd /tmp/dpkg && \
|
|
||||||
apt-get download libmariadb-dev-compat:amd64 && \
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
dpkg --force-all -i *.deb && \
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
rm -rf /tmp/dpkg
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
||||||
# The libpq5:arm64 package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
# The libpq5:arm64 package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
||||||
@@ -123,7 +128,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
dumb-init \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -138,9 +143,10 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
|
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/vaultwarden .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
COPY docker/start.sh /start.sh
|
COPY docker/start.sh /start.sh
|
||||||
@@ -148,6 +154,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# Using the digest instead of the tag name provides better security,
|
||||||
# It can be viewed in multiple ways:
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# be changed to point to a malicious image.
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
FROM bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303 as vault
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v2.19.0d
|
||||||
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d
|
||||||
|
# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233
|
||||||
|
# [vaultwarden/web-vault:v2.19.0d]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.46 as build
|
FROM rust:1.51 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
@@ -70,12 +77,10 @@ COPY ./build.rs ./build.rs
|
|||||||
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
||||||
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the :armel version.
|
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the :armel version.
|
||||||
# What we can do is a force install, because nothing important is overlapping each other.
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
RUN apt-get install -y libmariadb3:amd64 && \
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
mkdir -pv /tmp/dpkg && \
|
|
||||||
cd /tmp/dpkg && \
|
|
||||||
apt-get download libmariadb-dev-compat:amd64 && \
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
dpkg --force-all -i *.deb && \
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
rm -rf /tmp/dpkg
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
||||||
# The libpq5:armel package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
# The libpq5:armel package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
||||||
@@ -123,7 +128,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
dumb-init \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -138,9 +143,10 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
|
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/vaultwarden .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
COPY docker/start.sh /start.sh
|
COPY docker/start.sh /start.sh
|
||||||
@@ -148,6 +154,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# Using the digest instead of the tag name provides better security,
|
||||||
# It can be viewed in multiple ways:
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# be changed to point to a malicious image.
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
FROM bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303 as vault
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v2.19.0d
|
||||||
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d
|
||||||
|
# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233
|
||||||
|
# [vaultwarden/web-vault:v2.19.0d]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.46 as build
|
FROM rust:1.51 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
@@ -70,12 +77,10 @@ COPY ./build.rs ./build.rs
|
|||||||
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
# We at least need libmariadb3:amd64 installed for the x86_64 version of libmariadb.so (client)
|
||||||
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the :armhf version.
|
# We also need the libmariadb-dev-compat:amd64 but it can not be installed together with the :armhf version.
|
||||||
# What we can do is a force install, because nothing important is overlapping each other.
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
RUN apt-get install -y libmariadb3:amd64 && \
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
mkdir -pv /tmp/dpkg && \
|
|
||||||
cd /tmp/dpkg && \
|
|
||||||
apt-get download libmariadb-dev-compat:amd64 && \
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
dpkg --force-all -i *.deb && \
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
rm -rf /tmp/dpkg
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
# For Diesel-RS migrations_macros to compile with PostgreSQL we need to do some magic.
|
||||||
# The libpq5:armhf package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
# The libpq5:armhf package seems to not provide a symlink to libpq.so.5 with the name libpq.so.
|
||||||
@@ -123,7 +128,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
dumb-init \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -138,9 +143,10 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
|
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/vaultwarden .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
COPY docker/start.sh /start.sh
|
COPY docker/start.sh /start.sh
|
||||||
@@ -148,6 +154,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|
||||||
@@ -1,26 +1,33 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
# Using multistage build:
|
# Using multistage build:
|
||||||
# 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 #######################
|
||||||
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
# Using the digest instead of the tag name provides better security,
|
||||||
# It can be viewed in multiple ways:
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# be changed to point to a malicious image.
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.16.1
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.16.1
|
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# To verify the current digest for a given tag name:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
FROM bitwardenrs/web-vault@sha256:e40228f94cead5e50af6575fb39850a002dad146dab6836e5da5663e6d214303 as vault
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
|
# - From the command line:
|
||||||
|
# $ docker pull vaultwarden/web-vault:v2.19.0d
|
||||||
|
# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d
|
||||||
|
# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233]
|
||||||
|
#
|
||||||
|
# - Conversely, to get the tag name from the digest:
|
||||||
|
# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233
|
||||||
|
# [vaultwarden/web-vault:v2.19.0d]
|
||||||
|
#
|
||||||
|
FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
||||||
|
|
||||||
# Alpine only works on SQlite
|
# Alpine-based ARM (musl) only supports sqlite during compile time.
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite
|
||||||
|
|
||||||
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
# Build time options to avoid dpkg warnings and help with reproducible builds.
|
||||||
@@ -31,7 +38,7 @@ RUN rustup set profile minimal
|
|||||||
|
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
ENV RUSTFLAGS='-C link-arg=-s'
|
||||||
|
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -60,12 +67,12 @@ RUN touch src/main.rs
|
|||||||
# Builds again, this time it'll just be
|
# Builds again, this time it'll just be
|
||||||
# your actual source files being built
|
# your actual source files being built
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
RUN musl-strip target/armv7-unknown-linux-musleabihf/release/bitwarden_rs
|
RUN musl-strip target/armv7-unknown-linux-musleabihf/release/vaultwarden
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-alpine:3.12
|
FROM balenalib/armv7hf-alpine:3.13
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
@@ -78,8 +85,8 @@ RUN [ "cross-build-start" ]
|
|||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
|
dumb-init \
|
||||||
ca-certificates
|
ca-certificates
|
||||||
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/community catatonit
|
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
|
|
||||||
@@ -91,9 +98,10 @@ EXPOSE 3012
|
|||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
|
WORKDIR /
|
||||||
COPY Rocket.toml .
|
COPY Rocket.toml .
|
||||||
COPY --from=vault /web-vault ./web-vault
|
COPY --from=vault /web-vault ./web-vault
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/bitwarden_rs .
|
COPY --from=build /app/target/armv7-unknown-linux-musleabihf/release/vaultwarden .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh /healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
COPY docker/start.sh /start.sh
|
COPY docker/start.sh /start.sh
|
||||||
@@ -101,6 +109,5 @@ COPY docker/start.sh /start.sh
|
|||||||
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["catatonit", "/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
if [ -r /etc/bitwarden_rs.sh ]; then
|
if [ -r /etc/vaultwarden.sh ]; then
|
||||||
|
. /etc/vaultwarden.sh
|
||||||
|
elif [ -r /etc/bitwarden_rs.sh ]; then
|
||||||
|
echo "### You are using the old /etc/bitwarden_rs.sh script, please migrate to /etc/vaultwarden.sh ###"
|
||||||
. /etc/bitwarden_rs.sh
|
. /etc/bitwarden_rs.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d /etc/bitwarden_rs.d ]; then
|
if [ -d /etc/vaultwarden.d ]; then
|
||||||
|
for f in /etc/vaultwarden.d/*.sh; do
|
||||||
|
if [ -r $f ]; then
|
||||||
|
. $f
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
elif [ -d /etc/bitwarden_rs.d ]; then
|
||||||
|
echo "### You are using the old /etc/bitwarden_rs.d script directory, please migrate to /etc/vaultwarden.d ###"
|
||||||
for f in /etc/bitwarden_rs.d/*.sh; do
|
for f in /etc/bitwarden_rs.d/*.sh; do
|
||||||
if [ -r $f ]; then
|
if [ -r $f ]; then
|
||||||
. $f
|
. $f
|
||||||
@@ -12,4 +22,4 @@ if [ -d /etc/bitwarden_rs.d ]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec /bitwarden_rs "${@}"
|
exec /vaultwarden "${@}"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Docker Hub hooks provide these predefined [environment variables](https://docs.d
|
|||||||
* `DOCKER_TAG`: the Docker repository tag being built.
|
* `DOCKER_TAG`: the Docker repository tag being built.
|
||||||
* `IMAGE_NAME`: the name and tag of the Docker repository being built. (This variable is a combination of `DOCKER_REPO:DOCKER_TAG`.)
|
* `IMAGE_NAME`: the name and tag of the Docker repository being built. (This variable is a combination of `DOCKER_REPO:DOCKER_TAG`.)
|
||||||
|
|
||||||
The current multi-arch image build relies on the original bitwarden_rs Dockerfiles, which use cross-compilation for architectures other than `amd64`, and don't yet support all arch/database/OS combinations. However, cross-compilation is much faster than QEMU-based builds (e.g., using `docker buildx`). This situation may need to be revisited at some point.
|
The current multi-arch image build relies on the original vaultwarden Dockerfiles, which use cross-compilation for architectures other than `amd64`, and don't yet support all arch/distro combinations. However, cross-compilation is much faster than QEMU-based builds (e.g., using `docker buildx`). This situation may need to be revisited at some point.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
# The default Debian-based images support these arches for all database connections
|
# The default Debian-based images support these arches for all database backends.
|
||||||
#
|
|
||||||
# Other images (Alpine-based) currently
|
|
||||||
# support only a subset of these.
|
|
||||||
arches=(
|
arches=(
|
||||||
amd64
|
amd64
|
||||||
arm32v6
|
armv6
|
||||||
arm32v7
|
armv7
|
||||||
arm64v8
|
arm64
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
||||||
# The Alpine build currently only works for amd64.
|
# The Alpine image build currently only works for certain arches.
|
||||||
os_suffix=.alpine
|
distro_suffix=.alpine
|
||||||
arches=(
|
arches=(
|
||||||
amd64
|
amd64
|
||||||
arm32v7
|
armv7
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
|
|||||||
33
hooks/build
33
hooks/build
@@ -4,11 +4,42 @@ echo ">>> Building images..."
|
|||||||
|
|
||||||
source ./hooks/arches.sh
|
source ./hooks/arches.sh
|
||||||
|
|
||||||
|
if [[ -z "${SOURCE_COMMIT}" ]]; then
|
||||||
|
# This var is typically predefined by Docker Hub, but it won't be
|
||||||
|
# when testing locally.
|
||||||
|
SOURCE_COMMIT="$(git rev-parse HEAD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Construct a version string in the style of `build.rs`.
|
||||||
|
GIT_EXACT_TAG="$(git describe --tags --abbrev=0 --exact-match 2>/dev/null)"
|
||||||
|
if [[ -n "${GIT_EXACT_TAG}" ]]; then
|
||||||
|
SOURCE_VERSION="${GIT_EXACT_TAG}"
|
||||||
|
else
|
||||||
|
GIT_LAST_TAG="$(git describe --tags --abbrev=0)"
|
||||||
|
SOURCE_VERSION="${GIT_LAST_TAG}-${SOURCE_COMMIT:0:8}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LABELS=(
|
||||||
|
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||||
|
org.opencontainers.image.created="$(date --utc --iso-8601=seconds)"
|
||||||
|
org.opencontainers.image.documentation="https://github.com/dani-garcia/vaultwarden/wiki"
|
||||||
|
org.opencontainers.image.licenses="GPL-3.0-only"
|
||||||
|
org.opencontainers.image.revision="${SOURCE_COMMIT}"
|
||||||
|
org.opencontainers.image.source="${SOURCE_REPOSITORY_URL}"
|
||||||
|
org.opencontainers.image.url="https://hub.docker.com/r/${DOCKER_REPO#*/}"
|
||||||
|
org.opencontainers.image.version="${SOURCE_VERSION}"
|
||||||
|
)
|
||||||
|
LABEL_ARGS=()
|
||||||
|
for label in "${LABELS[@]}"; do
|
||||||
|
LABEL_ARGS+=(--label "${label}")
|
||||||
|
done
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
for arch in "${arches[@]}"; do
|
for arch in "${arches[@]}"; do
|
||||||
docker build \
|
docker build \
|
||||||
|
"${LABEL_ARGS[@]}" \
|
||||||
-t "${DOCKER_REPO}:${DOCKER_TAG}-${arch}" \
|
-t "${DOCKER_REPO}:${DOCKER_TAG}-${arch}" \
|
||||||
-f docker/${arch}/Dockerfile${os_suffix} \
|
-f docker/${arch}/Dockerfile${distro_suffix} \
|
||||||
.
|
.
|
||||||
done
|
done
|
||||||
|
|||||||
28
hooks/pre_build
Executable file
28
hooks/pre_build
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
# If requested, print some environment info for troubleshooting.
|
||||||
|
if [[ -n "${DOCKER_HUB_DEBUG}" ]]; then
|
||||||
|
id
|
||||||
|
pwd
|
||||||
|
df -h
|
||||||
|
env
|
||||||
|
docker info
|
||||||
|
docker version
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install build dependencies.
|
||||||
|
deps=(
|
||||||
|
jq
|
||||||
|
)
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y "${deps[@]}"
|
||||||
|
|
||||||
|
# Docker Hub uses a shallow clone and doesn't fetch tags, which breaks some
|
||||||
|
# Git operations that we perform later, so fetch the complete history and
|
||||||
|
# tags first. Note that if the build is cached, the clone may have been
|
||||||
|
# unshallowed already; if so, unshallowing will fail, so skip it.
|
||||||
|
if [[ -f .git/shallow ]]; then
|
||||||
|
git fetch --unshallow --tags
|
||||||
|
fi
|
||||||
211
hooks/push
211
hooks/push
@@ -1,117 +1,138 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo ">>> Pushing images..."
|
|
||||||
|
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
|
||||||
|
|
||||||
declare -A annotations=(
|
|
||||||
[amd64]="--os linux --arch amd64"
|
|
||||||
[arm32v6]="--os linux --arch arm --variant v6"
|
|
||||||
[arm32v7]="--os linux --arch arm --variant v7"
|
|
||||||
[arm64v8]="--os linux --arch arm64 --variant v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
source ./hooks/arches.sh
|
source ./hooks/arches.sh
|
||||||
|
|
||||||
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
|
|
||||||
|
# Join a list of args with a single char.
|
||||||
|
# Ref: https://stackoverflow.com/a/17841619
|
||||||
|
join() { local IFS="$1"; shift; echo "$*"; }
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
declare -A images
|
echo ">>> Starting local Docker registry..."
|
||||||
|
|
||||||
|
# Docker Buildx's `docker-container` driver is needed for multi-platform
|
||||||
|
# builds, but it can't access existing images on the Docker host (like the
|
||||||
|
# cross-compiled ones we just built). Those images first need to be pushed to
|
||||||
|
# a registry -- Docker Hub could be used, but since it's not trivial to clean
|
||||||
|
# up those intermediate images on Docker Hub, it's easier to just run a local
|
||||||
|
# Docker registry, which gets cleaned up automatically once the build job ends.
|
||||||
|
#
|
||||||
|
# https://docs.docker.com/registry/deploying/
|
||||||
|
# https://hub.docker.com/_/registry
|
||||||
|
#
|
||||||
|
# Use host networking so the buildx container can access the registry via
|
||||||
|
# localhost.
|
||||||
|
#
|
||||||
|
docker run -d --name registry --network host registry:2 # defaults to port 5000
|
||||||
|
|
||||||
|
# Docker Hub sets a `DOCKER_REPO` env var with the format `index.docker.io/user/repo`.
|
||||||
|
# Strip the registry portion to construct a local repo path for use in `Dockerfile.buildx`.
|
||||||
|
LOCAL_REGISTRY="localhost:5000"
|
||||||
|
REPO="${DOCKER_REPO#*/}"
|
||||||
|
LOCAL_REPO="${LOCAL_REGISTRY}/${REPO}"
|
||||||
|
|
||||||
|
echo ">>> Pushing images to local registry..."
|
||||||
|
|
||||||
for arch in ${arches[@]}; do
|
for arch in ${arches[@]}; do
|
||||||
images[$arch]="${DOCKER_REPO}:${DOCKER_TAG}-${arch}"
|
docker_image="${DOCKER_REPO}:${DOCKER_TAG}-${arch}"
|
||||||
|
local_image="${LOCAL_REPO}:${DOCKER_TAG}-${arch}"
|
||||||
|
docker tag "${docker_image}" "${local_image}"
|
||||||
|
docker push "${local_image}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Push the images that were just built; manifest list creation fails if the
|
echo ">>> Setting up Docker Buildx..."
|
||||||
# images (manifests) referenced don't already exist in the Docker registry.
|
|
||||||
for image in "${images[@]}"; do
|
|
||||||
docker push "${image}"
|
|
||||||
done
|
|
||||||
|
|
||||||
manifest_lists=("${DOCKER_REPO}:${DOCKER_TAG}")
|
# Same as earlier, use host networking so the buildx container can access the
|
||||||
|
# registry via localhost.
|
||||||
|
#
|
||||||
|
# Ref: https://github.com/docker/buildx/issues/94#issuecomment-534367714
|
||||||
|
#
|
||||||
|
docker buildx create --name builder --use --driver-opt network=host
|
||||||
|
|
||||||
# If the Docker tag starts with a version number, assume the latest release is
|
echo ">>> Running Docker Buildx..."
|
||||||
# being pushed. Add an extra manifest (`latest` or `alpine`, as appropriate)
|
|
||||||
|
tags=("${DOCKER_REPO}:${DOCKER_TAG}")
|
||||||
|
|
||||||
|
# If the Docker tag starts with a version number, assume the latest release
|
||||||
|
# is being pushed. Add an extra tag (`latest` or `alpine`, as appropriate)
|
||||||
# to make it easier for users to track the latest release.
|
# to make it easier for users to track the latest release.
|
||||||
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
||||||
manifest_lists+=(${DOCKER_REPO}:alpine)
|
tags+=(${DOCKER_REPO}:alpine)
|
||||||
else
|
else
|
||||||
manifest_lists+=(${DOCKER_REPO}:latest)
|
tags+=(${DOCKER_REPO}:latest)
|
||||||
|
|
||||||
# Add an extra `latest-arm32v6` tag; Docker can't seem to properly
|
|
||||||
# auto-select that image on Armv6 platforms like Raspberry Pi 1 and Zero
|
|
||||||
# (https://github.com/moby/moby/issues/41017).
|
|
||||||
#
|
|
||||||
# Add this tag only for the SQLite image, as the MySQL and PostgreSQL
|
|
||||||
# builds don't currently work on non-amd64 arches.
|
|
||||||
#
|
|
||||||
# TODO: Also add an `alpine-arm32v6` tag if multi-arch support for
|
|
||||||
# Alpine-based bitwarden_rs images is implemented before this Docker
|
|
||||||
# issue is fixed.
|
|
||||||
if [[ ${DOCKER_REPO} == *server ]]; then
|
|
||||||
docker tag "${DOCKER_REPO}:${DOCKER_TAG}-arm32v6" "${DOCKER_REPO}:latest-arm32v6"
|
|
||||||
docker push "${DOCKER_REPO}:latest-arm32v6"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for manifest_list in "${manifest_lists[@]}"; do
|
tag_args=()
|
||||||
# Create the (multi-arch) manifest list of arch-specific images.
|
for tag in "${tags[@]}"; do
|
||||||
docker manifest create ${manifest_list} ${images[@]}
|
tag_args+=(--tag "${tag}")
|
||||||
|
|
||||||
# Make sure each image manifest is annotated with the correct arch info.
|
|
||||||
# Docker does not auto-detect the arch of each cross-compiled image, so
|
|
||||||
# everything would appear as `linux/amd64` otherwise.
|
|
||||||
for arch in "${arches[@]}"; do
|
|
||||||
docker manifest annotate ${annotations[$arch]} ${manifest_list} ${images[$arch]}
|
|
||||||
done
|
|
||||||
|
|
||||||
# Push the manifest list.
|
|
||||||
docker manifest push --purge ${manifest_list}
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Avoid logging credentials and tokens.
|
# Docker Buildx takes a list of target platforms (OS/arch/variant), so map
|
||||||
set +ex
|
# the arch list to a platform list (assuming the OS is always `linux`).
|
||||||
|
declare -A arch_to_platform=(
|
||||||
# Delete the arch-specific tags, if credentials for doing so are available.
|
[amd64]="linux/amd64"
|
||||||
# Note that `DOCKER_PASSWORD` must be the actual user password. Passing a JWT
|
[armv6]="linux/arm/v6"
|
||||||
# obtained using a personal access token results in a 403 error with
|
[armv7]="linux/arm/v7"
|
||||||
# {"detail": "access to the resource is forbidden with personal access token"}
|
[arm64]="linux/arm64"
|
||||||
if [[ -z "${DOCKER_USERNAME}" || -z "${DOCKER_PASSWORD}" ]]; then
|
)
|
||||||
exit 0
|
platforms=()
|
||||||
fi
|
|
||||||
|
|
||||||
# Given a JSON input on stdin, extract the string value associated with the
|
|
||||||
# specified key. This avoids an extra dependency on a tool like `jq`.
|
|
||||||
extract() {
|
|
||||||
local key="$1"
|
|
||||||
# Extract "<key>":"<val>" (assumes key/val won't contain double quotes).
|
|
||||||
# The colon may have whitespace on either side.
|
|
||||||
grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]\+\"" |
|
|
||||||
# Extract just <val> by deleting the last '"', and then greedily deleting
|
|
||||||
# everything up to '"'.
|
|
||||||
sed -e 's/"$//' -e 's/.*"//'
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ">>> Getting API token..."
|
|
||||||
jwt=$(curl -sS -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"${DOCKER_USERNAME}\",\"password\": \"${DOCKER_PASSWORD}\"}" \
|
|
||||||
"https://hub.docker.com/v2/users/login" |
|
|
||||||
extract 'token')
|
|
||||||
|
|
||||||
# Strip the registry portion from `index.docker.io/user/repo`.
|
|
||||||
repo="${DOCKER_REPO#*/}"
|
|
||||||
|
|
||||||
for arch in ${arches[@]}; do
|
for arch in ${arches[@]}; do
|
||||||
# Don't delete the `arm32v6` tag; Docker can't seem to properly
|
platforms+=("${arch_to_platform[$arch]}")
|
||||||
# auto-select that image on Armv6 platforms like Raspberry Pi 1 and Zero
|
|
||||||
# (https://github.com/moby/moby/issues/41017).
|
|
||||||
if [[ ${arch} == 'arm32v6' ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
tag="${DOCKER_TAG}-${arch}"
|
|
||||||
echo ">>> Deleting '${repo}:${tag}'..."
|
|
||||||
curl -sS -X DELETE \
|
|
||||||
-H "Authorization: Bearer ${jwt}" \
|
|
||||||
"https://hub.docker.com/v2/repositories/${repo}/tags/${tag}/"
|
|
||||||
done
|
done
|
||||||
|
platforms="$(join "," "${platforms[@]}")"
|
||||||
|
|
||||||
|
# Run the build, pushing the resulting images and multi-arch manifest list to
|
||||||
|
# Docker Hub. The Dockerfile is read from stdin to avoid sending any build
|
||||||
|
# context, which isn't needed here since the actual cross-compiled images
|
||||||
|
# have already been built.
|
||||||
|
docker buildx build \
|
||||||
|
--network host \
|
||||||
|
--build-arg LOCAL_REPO="${LOCAL_REPO}" \
|
||||||
|
--build-arg DOCKER_TAG="${DOCKER_TAG}" \
|
||||||
|
--platform "${platforms}" \
|
||||||
|
"${tag_args[@]}" \
|
||||||
|
--push \
|
||||||
|
- < ./docker/Dockerfile.buildx
|
||||||
|
|
||||||
|
# Add an extra arch-specific tag for `arm32v6`; Docker can't seem to properly
|
||||||
|
# auto-select that image on ARMv6 platforms like Raspberry Pi 1 and Zero
|
||||||
|
# (https://github.com/moby/moby/issues/41017).
|
||||||
|
#
|
||||||
|
# Note that we use `arm32v6` instead of `armv6` to be consistent with the
|
||||||
|
# existing vaultwarden tags, which adhere to the naming conventions of the
|
||||||
|
# Docker per-architecture repos (e.g., https://hub.docker.com/u/arm32v6).
|
||||||
|
# Unfortunately, these per-arch repo names aren't always consistent with the
|
||||||
|
# corresponding platform (OS/arch/variant) IDs, particularly in the case of
|
||||||
|
# 32-bit ARM arches (e.g., `linux/arm/v6` is used, not `linux/arm32/v6`).
|
||||||
|
#
|
||||||
|
# TODO: It looks like this issue should be fixed starting in Docker 20.10.0,
|
||||||
|
# so this step can be removed once fixed versions are in wider distribution.
|
||||||
|
#
|
||||||
|
# Tags:
|
||||||
|
#
|
||||||
|
# testing => testing-arm32v6
|
||||||
|
# testing-alpine => <ignored>
|
||||||
|
# x.y.z => x.y.z-arm32v6, latest-arm32v6
|
||||||
|
# x.y.z-alpine => <ignored>
|
||||||
|
#
|
||||||
|
if [[ "${DOCKER_TAG}" != *alpine ]]; then
|
||||||
|
image="${DOCKER_REPO}":"${DOCKER_TAG}"
|
||||||
|
|
||||||
|
# Fetch the multi-arch manifest list and find the digest of the armv6 image.
|
||||||
|
filter='.manifests|.[]|select(.platform.architecture=="arm" and .platform.variant=="v6")|.digest'
|
||||||
|
digest="$(docker manifest inspect "${image}" | jq -r "${filter}")"
|
||||||
|
|
||||||
|
# Pull the armv6 image by digest, retag it, and repush it.
|
||||||
|
docker pull "${DOCKER_REPO}"@"${digest}"
|
||||||
|
docker tag "${DOCKER_REPO}"@"${digest}" "${image}"-arm32v6
|
||||||
|
docker push "${image}"-arm32v6
|
||||||
|
|
||||||
|
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
|
docker tag "${image}"-arm32v6 "${DOCKER_REPO}:latest"-arm32v6
|
||||||
|
docker push "${DOCKER_REPO}:latest"-arm32v6
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 1;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;
|
||||||
1
migrations/mysql/2021-03-11-190243_add_sends/down.sql
Normal file
1
migrations/mysql/2021-03-11-190243_add_sends/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sends;
|
||||||
25
migrations/mysql/2021-03-11-190243_add_sends/up.sql
Normal file
25
migrations/mysql/2021-03-11-190243_add_sends/up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE TABLE sends (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid CHAR(36) REFERENCES users (uuid),
|
||||||
|
organization_uuid CHAR(36) REFERENCES organizations (uuid),
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
akey TEXT NOT NULL,
|
||||||
|
password_hash BLOB,
|
||||||
|
password_salt BLOB,
|
||||||
|
password_iter INTEGER,
|
||||||
|
|
||||||
|
max_access_count INTEGER,
|
||||||
|
access_count INTEGER NOT NULL,
|
||||||
|
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL,
|
||||||
|
expiration_date DATETIME,
|
||||||
|
deletion_date DATETIME NOT NULL,
|
||||||
|
|
||||||
|
disabled BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sends;
|
||||||
25
migrations/postgresql/2021-03-11-190243_add_sends/up.sql
Normal file
25
migrations/postgresql/2021-03-11-190243_add_sends/up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE TABLE sends (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid CHAR(36) REFERENCES users (uuid),
|
||||||
|
organization_uuid CHAR(36) REFERENCES organizations (uuid),
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
password_hash BYTEA,
|
||||||
|
password_salt BYTEA,
|
||||||
|
password_iter INTEGER,
|
||||||
|
|
||||||
|
max_access_count INTEGER,
|
||||||
|
access_count INTEGER NOT NULL,
|
||||||
|
|
||||||
|
creation_date TIMESTAMP NOT NULL,
|
||||||
|
revision_date TIMESTAMP NOT NULL,
|
||||||
|
expiration_date TIMESTAMP,
|
||||||
|
deletion_date TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
disabled BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sends RENAME COLUMN key TO akey;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 1;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;
|
||||||
1
migrations/sqlite/2021-03-11-190243_add_sends/down.sql
Normal file
1
migrations/sqlite/2021-03-11-190243_add_sends/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sends;
|
||||||
25
migrations/sqlite/2021-03-11-190243_add_sends/up.sql
Normal file
25
migrations/sqlite/2021-03-11-190243_add_sends/up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE TABLE sends (
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid TEXT REFERENCES users (uuid),
|
||||||
|
organization_uuid TEXT REFERENCES organizations (uuid),
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
password_hash BLOB,
|
||||||
|
password_salt BLOB,
|
||||||
|
password_iter INTEGER,
|
||||||
|
|
||||||
|
max_access_count INTEGER,
|
||||||
|
access_count INTEGER NOT NULL,
|
||||||
|
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL,
|
||||||
|
expiration_date DATETIME,
|
||||||
|
deletion_date DATETIME NOT NULL,
|
||||||
|
|
||||||
|
disabled BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sends RENAME COLUMN key TO akey;
|
||||||
@@ -1 +1 @@
|
|||||||
nightly-2020-07-11
|
nightly-2021-04-14
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
version = "Two"
|
version = "Two"
|
||||||
|
edition = "2018"
|
||||||
max_width = 120
|
max_width = 120
|
||||||
|
newline_style = "Unix"
|
||||||
|
use_small_heuristics = "Off"
|
||||||
|
struct_lit_single_line = false
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
|||||||
269
src/api/admin.rs
269
src/api/admin.rs
@@ -1,7 +1,7 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::process::Command;
|
use std::{env, time::Duration};
|
||||||
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{Cookie, Cookies, SameSite},
|
http::{Cookie, Cookies, SameSite},
|
||||||
@@ -12,13 +12,13 @@ use rocket::{
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, JsonResult},
|
api::{ApiResult, EmptyResult, NumberOrString},
|
||||||
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{backup_database, models::*, DbConn, DbConnType},
|
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
mail,
|
mail,
|
||||||
util::get_display_size,
|
util::{format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +36,10 @@ pub fn routes() -> Vec<Route> {
|
|||||||
logout,
|
logout,
|
||||||
delete_user,
|
delete_user,
|
||||||
deauth_user,
|
deauth_user,
|
||||||
|
disable_user,
|
||||||
|
enable_user,
|
||||||
remove_2fa,
|
remove_2fa,
|
||||||
|
update_user_org_type,
|
||||||
update_revision_users,
|
update_revision_users,
|
||||||
post_config,
|
post_config,
|
||||||
delete_config,
|
delete_config,
|
||||||
@@ -44,17 +47,25 @@ pub fn routes() -> Vec<Route> {
|
|||||||
test_smtp,
|
test_smtp,
|
||||||
users_overview,
|
users_overview,
|
||||||
organizations_overview,
|
organizations_overview,
|
||||||
|
delete_organization,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
get_diagnostics_config
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
static CAN_BACKUP: Lazy<bool> = Lazy::new(|| {
|
static DB_TYPE: Lazy<&str> = Lazy::new(|| {
|
||||||
DbConnType::from_url(&CONFIG.database_url())
|
DbConnType::from_url(&CONFIG.database_url())
|
||||||
.map(|t| t == DbConnType::sqlite)
|
.map(|t| match t {
|
||||||
.unwrap_or(false)
|
DbConnType::sqlite => "SQLite",
|
||||||
&& Command::new("sqlite3").arg("-version").status().is_ok()
|
DbConnType::mysql => "MySQL",
|
||||||
|
DbConnType::postgresql => "PostgreSQL",
|
||||||
|
})
|
||||||
|
.unwrap_or("Unknown")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static CAN_BACKUP: Lazy<bool> =
|
||||||
|
Lazy::new(|| DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false));
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn admin_disabled() -> &'static str {
|
fn admin_disabled() -> &'static str {
|
||||||
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
|
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
|
||||||
@@ -80,6 +91,27 @@ impl<'a, 'r> FromRequest<'a, 'r> for Referer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct IpHeader(Option<String>);
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for IpHeader {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
|
if req.headers().get_one(&CONFIG.ip_header()).is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(CONFIG.ip_header())))
|
||||||
|
} else if req.headers().get_one("X-Client-IP").is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(String::from("X-Client-IP"))))
|
||||||
|
} else if req.headers().get_one("X-Real-IP").is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(String::from("X-Real-IP"))))
|
||||||
|
} else if req.headers().get_one("X-Forwarded-For").is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(String::from("X-Forwarded-For"))))
|
||||||
|
} else {
|
||||||
|
Outcome::Success(IpHeader(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Used for `Location` response headers, which must specify an absolute URI
|
/// Used for `Location` response headers, which must specify an absolute URI
|
||||||
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
||||||
fn admin_url(referer: Referer) -> String {
|
fn admin_url(referer: Referer) -> String {
|
||||||
@@ -105,7 +137,12 @@ fn admin_url(referer: Referer) -> String {
|
|||||||
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
|
||||||
// If there is an error, show it
|
// If there is an error, show it
|
||||||
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
|
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
|
||||||
let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg, "urlpath": CONFIG.domain_path()});
|
let json = json!({
|
||||||
|
"page_content": "admin/login",
|
||||||
|
"version": VERSION,
|
||||||
|
"error": msg,
|
||||||
|
"urlpath": CONFIG.domain_path()
|
||||||
|
});
|
||||||
|
|
||||||
// Return the page
|
// Return the page
|
||||||
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
|
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
|
||||||
@@ -129,10 +166,7 @@ fn post_admin_login(
|
|||||||
// If the token is invalid, redirect to login page
|
// If the token is invalid, redirect to login page
|
||||||
if !_validate_token(&data.token) {
|
if !_validate_token(&data.token) {
|
||||||
error!("Invalid admin token. IP: {}", ip.ip);
|
error!("Invalid admin token. IP: {}", ip.ip);
|
||||||
Err(Flash::error(
|
Err(Flash::error(Redirect::to(admin_url(referer)), "Invalid admin token, please try again."))
|
||||||
Redirect::to(admin_url(referer)),
|
|
||||||
"Invalid admin token, please try again.",
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
// If the token received is valid, generate JWT and save it as a cookie
|
// If the token received is valid, generate JWT and save it as a cookie
|
||||||
let claims = generate_admin_claims();
|
let claims = generate_admin_claims();
|
||||||
@@ -275,30 +309,39 @@ fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
fn logout(mut cookies: Cookies, referer: Referer) -> Result<Redirect, ()> {
|
fn logout(mut cookies: Cookies, referer: Referer) -> Redirect {
|
||||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||||
Ok(Redirect::to(admin_url(referer)))
|
Redirect::to(admin_url(referer))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
fn get_users_json(_token: AdminToken, conn: DbConn) -> JsonResult {
|
fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&conn);
|
||||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
||||||
|
|
||||||
Ok(Json(Value::Array(users_json)))
|
Json(Value::Array(users_json))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/overview")]
|
#[get("/users/overview")]
|
||||||
fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&conn);
|
||||||
let users_json: Vec<Value> = users.iter()
|
let dt_fmt = "%Y-%m-%d %H:%M:%S %Z";
|
||||||
|
let users_json: Vec<Value> = users
|
||||||
|
.iter()
|
||||||
.map(|u| {
|
.map(|u| {
|
||||||
let mut usr = u.to_json(&conn);
|
let mut usr = u.to_json(&conn);
|
||||||
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn));
|
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn));
|
||||||
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn));
|
usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn));
|
||||||
usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn) as i32));
|
usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn) as i32));
|
||||||
|
usr["user_enabled"] = json!(u.enabled);
|
||||||
|
usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt));
|
||||||
|
usr["last_active"] = match u.last_active(&conn) {
|
||||||
|
Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)),
|
||||||
|
None => json!("Never"),
|
||||||
|
};
|
||||||
usr
|
usr
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let text = AdminTemplateData::users(users_json).render()?;
|
let text = AdminTemplateData::users(users_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
@@ -319,6 +362,24 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
|||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/users/<uuid>/disable")]
|
||||||
|
fn disable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
|
let mut user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
||||||
|
Device::delete_all_by_user(&user.uuid, &conn)?;
|
||||||
|
user.reset_security_stamp();
|
||||||
|
user.enabled = false;
|
||||||
|
|
||||||
|
user.save(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/users/<uuid>/enable")]
|
||||||
|
fn enable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
|
let mut user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
||||||
|
user.enabled = true;
|
||||||
|
|
||||||
|
user.save(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/remove-2fa")]
|
#[post("/users/<uuid>/remove-2fa")]
|
||||||
fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
let mut user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
let mut user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
||||||
@@ -327,6 +388,40 @@ fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
|||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct UserOrgTypeData {
|
||||||
|
user_type: NumberOrString,
|
||||||
|
user_uuid: String,
|
||||||
|
org_uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/users/org_type", data = "<data>")]
|
||||||
|
fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
|
let data: UserOrgTypeData = data.into_inner();
|
||||||
|
|
||||||
|
let mut user_to_edit = match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &conn) {
|
||||||
|
Some(user) => user,
|
||||||
|
None => err!("The specified user isn't member of the organization"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_type = match UserOrgType::from_str(&data.user_type.into_string()) {
|
||||||
|
Some(new_type) => new_type as i32,
|
||||||
|
None => err!("Invalid type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
|
||||||
|
// Removing owner permmission, check that there are at least another owner
|
||||||
|
let num_owners = UserOrganization::find_by_org_and_type(&data.org_uuid, UserOrgType::Owner as i32, &conn).len();
|
||||||
|
|
||||||
|
if num_owners <= 1 {
|
||||||
|
err!("Can't change the type of the last owner")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_to_edit.atype = new_type as i32;
|
||||||
|
user_to_edit.save(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/users/update_revision")]
|
#[post("/users/update_revision")]
|
||||||
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
User::update_all_revisions(&conn)
|
User::update_all_revisions(&conn)
|
||||||
@@ -335,19 +430,28 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
|||||||
#[get("/organizations/overview")]
|
#[get("/organizations/overview")]
|
||||||
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let organizations = Organization::get_all(&conn);
|
let organizations = Organization::get_all(&conn);
|
||||||
let organizations_json: Vec<Value> = organizations.iter().map(|o| {
|
let organizations_json: Vec<Value> = organizations
|
||||||
|
.iter()
|
||||||
|
.map(|o| {
|
||||||
let mut org = o.to_json();
|
let mut org = o.to_json();
|
||||||
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn));
|
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn));
|
||||||
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn));
|
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn));
|
||||||
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn));
|
org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn));
|
||||||
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32));
|
org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32));
|
||||||
org
|
org
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let text = AdminTemplateData::organizations(organizations_json).render()?;
|
let text = AdminTemplateData::organizations(organizations_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/organizations/<uuid>/delete")]
|
||||||
|
fn delete_organization(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
|
let org = Organization::find_by_uuid(&uuid, &conn).map_res("Organization doesn't exist")?;
|
||||||
|
org.delete(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct WebVaultVersion {
|
struct WebVaultVersion {
|
||||||
version: String,
|
version: String,
|
||||||
@@ -364,77 +468,120 @@ struct GitCommit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
use reqwest::{blocking::Client, header::USER_AGENT};
|
let github_api = get_reqwest_client();
|
||||||
use std::time::Duration;
|
|
||||||
let github_api = Client::builder().build()?;
|
|
||||||
|
|
||||||
Ok(
|
Ok(github_api.get(url).timeout(Duration::from_secs(10)).send()?.error_for_status()?.json::<T>()?)
|
||||||
github_api.get(url)
|
}
|
||||||
.timeout(Duration::from_secs(10))
|
|
||||||
.header(USER_AGENT, "Bitwarden_RS")
|
fn has_http_access() -> bool {
|
||||||
.send()?
|
let http_access = get_reqwest_client();
|
||||||
.error_for_status()?
|
|
||||||
.json::<T>()?
|
match http_access.head("https://github.com/dani-garcia/vaultwarden").timeout(Duration::from_secs(10)).send() {
|
||||||
)
|
Ok(r) => r.status().is_success(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/diagnostics")]
|
#[get("/diagnostics")]
|
||||||
fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use crate::util::read_file_string;
|
use crate::util::read_file_string;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
|
||||||
let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json");
|
// Get current running versions
|
||||||
let vault_version_str = read_file_string(&vault_version_path)?;
|
let web_vault_version: WebVaultVersion =
|
||||||
let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?;
|
match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "bwrs-version.json")) {
|
||||||
|
Ok(s) => serde_json::from_str(&s)?,
|
||||||
|
_ => match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) {
|
||||||
|
Ok(s) => serde_json::from_str(&s)?,
|
||||||
|
_ => WebVaultVersion {
|
||||||
|
version: String::from("Version file missing"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next());
|
// Execute some environment checks
|
||||||
let (dns_resolved, dns_ok) = match github_ips {
|
let running_within_docker = is_running_in_docker();
|
||||||
Ok(Some(a)) => (a.ip().to_string(), true),
|
let has_http_access = has_http_access();
|
||||||
_ => ("Could not resolve domain name.".to_string(), false),
|
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
||||||
|
|| env::var_os("http_proxy").is_some()
|
||||||
|
|| env::var_os("HTTPS_PROXY").is_some()
|
||||||
|
|| env::var_os("https_proxy").is_some();
|
||||||
|
|
||||||
|
// Check if we are able to resolve DNS entries
|
||||||
|
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
|
||||||
|
Ok(Some(a)) => a.ip().to_string(),
|
||||||
|
_ => "Could not resolve domain name.".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the DNS Check failed, do not even attempt to check for new versions since we were not able to resolve github.com
|
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
|
||||||
let (latest_release, latest_commit, latest_web_build) = if dns_ok {
|
// TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already.
|
||||||
|
let (latest_release, latest_commit, latest_web_build) = if has_http_access {
|
||||||
(
|
(
|
||||||
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") {
|
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") {
|
||||||
Ok(r) => r.tag_name,
|
Ok(r) => r.tag_name,
|
||||||
_ => "-".to_string()
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") {
|
match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main") {
|
||||||
Ok(mut c) => {
|
Ok(mut c) => {
|
||||||
c.sha.truncate(8);
|
c.sha.truncate(8);
|
||||||
c.sha
|
c.sha
|
||||||
|
}
|
||||||
|
_ => "-".to_string(),
|
||||||
},
|
},
|
||||||
_ => "-".to_string()
|
// Do not fetch the web-vault version when running within Docker.
|
||||||
},
|
// The web-vault version is embedded within the container it self, and should not be updated manually
|
||||||
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") {
|
if running_within_docker {
|
||||||
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
"-".to_string()
|
||||||
_ => "-".to_string()
|
} else {
|
||||||
|
match get_github_api::<GitRelease>(
|
||||||
|
"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
|
||||||
|
) {
|
||||||
|
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
|
||||||
|
_ => "-".to_string(),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("-".to_string(), "-".to_string(), "-".to_string())
|
("-".to_string(), "-".to_string(), "-".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the date check as the last item right before filling the json.
|
let ip_header_name = match &ip_header.0 {
|
||||||
// This should ensure that the time difference between the browser and the server is as minimal as possible.
|
Some(h) => h,
|
||||||
let dt = Utc::now();
|
_ => "",
|
||||||
let server_time = dt.format("%Y-%m-%d %H:%M:%S").to_string();
|
};
|
||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
"server_time": server_time,
|
|
||||||
"web_vault_version": web_vault_version.version,
|
|
||||||
"latest_release": latest_release,
|
"latest_release": latest_release,
|
||||||
"latest_commit": latest_commit,
|
"latest_commit": latest_commit,
|
||||||
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
|
"web_vault_version": web_vault_version.version,
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
|
"running_within_docker": running_within_docker,
|
||||||
|
"has_http_access": has_http_access,
|
||||||
|
"ip_header_exists": &ip_header.0.is_some(),
|
||||||
|
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
||||||
|
"ip_header_name": ip_header_name,
|
||||||
|
"ip_header_config": &CONFIG.ip_header(),
|
||||||
|
"uses_proxy": uses_proxy,
|
||||||
|
"db_type": *DB_TYPE,
|
||||||
|
"db_version": get_sql_server_version(&conn),
|
||||||
|
"admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
|
||||||
|
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
|
||||||
|
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
|
||||||
});
|
});
|
||||||
|
|
||||||
let text = AdminTemplateData::diagnostics(diagnostics_json).render()?;
|
let text = AdminTemplateData::diagnostics(diagnostics_json).render()?;
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/diagnostics/config")]
|
||||||
|
fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
|
||||||
|
let support_json = CONFIG.get_support_json();
|
||||||
|
Json(support_json)
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/config", data = "<data>")]
|
#[post("/config", data = "<data>")]
|
||||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: ConfigBuilder = data.into_inner();
|
let data: ConfigBuilder = data.into_inner();
|
||||||
@@ -447,11 +594,11 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/config/backup_db")]
|
#[post("/config/backup_db")]
|
||||||
fn backup_db(_token: AdminToken) -> EmptyResult {
|
fn backup_db(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
if *CAN_BACKUP {
|
if *CAN_BACKUP {
|
||||||
backup_database()
|
backup_database(&conn)
|
||||||
} else {
|
} else {
|
||||||
err!("Can't back up current DB (either it's not SQLite or the 'sqlite' binary is not present)");
|
err!("Can't back up current DB (Only SQLite supports this feature)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
|
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
|
||||||
@@ -94,7 +95,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Order is important here; the invitation check must come first
|
// Order is important here; the invitation check must come first
|
||||||
// because the bitwarden_rs admin can invite anyone, regardless
|
// because the vaultwarden admin can invite anyone, regardless
|
||||||
// of other signup restrictions.
|
// of other signup restrictions.
|
||||||
if Invitation::take(&data.Email, &conn) || CONFIG.is_signup_allowed(&data.Email) {
|
if Invitation::take(&data.Email, &conn) || CONFIG.is_signup_allowed(&data.Email) {
|
||||||
User::new(data.Email.clone())
|
User::new(data.Email.clone())
|
||||||
@@ -115,7 +116,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
user.client_kdf_type = client_kdf_type;
|
user.client_kdf_type = client_kdf_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.set_password(&data.MasterPasswordHash);
|
user.set_password(&data.MasterPasswordHash, None);
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
|
|
||||||
// Add extra fields if present
|
// Add extra fields if present
|
||||||
@@ -139,10 +140,8 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.last_verifying_at = Some(user.created_at);
|
user.last_verifying_at = Some(user.created_at);
|
||||||
} else {
|
} else if let Err(e) = mail::send_welcome(&user.email) {
|
||||||
if let Err(e) = mail::send_welcome(&user.email) {
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
error!("Error sending welcome email: {:#?}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +149,8 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/accounts/profile")]
|
#[get("/accounts/profile")]
|
||||||
fn profile(headers: Headers, conn: DbConn) -> JsonResult {
|
fn profile(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(headers.user.to_json(&conn)))
|
Json(headers.user.to_json(&conn))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -232,7 +231,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
|
|||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
user.set_password(&data.NewMasterPasswordHash);
|
user.set_password(&data.NewMasterPasswordHash, Some("post_rotatekey"));
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
@@ -259,7 +258,7 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
|
|||||||
|
|
||||||
user.client_kdf_iter = data.KdfIterations;
|
user.client_kdf_iter = data.KdfIterations;
|
||||||
user.client_kdf_type = data.Kdf;
|
user.client_kdf_type = data.Kdf;
|
||||||
user.set_password(&data.NewMasterPasswordHash);
|
user.set_password(&data.NewMasterPasswordHash, None);
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
@@ -321,15 +320,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
|||||||
err!("The cipher is not owned by the user")
|
err!("The cipher is not owned by the user")
|
||||||
}
|
}
|
||||||
|
|
||||||
update_cipher_from_data(
|
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::CipherUpdate)?
|
||||||
&mut saved_cipher,
|
|
||||||
cipher_data,
|
|
||||||
&headers,
|
|
||||||
false,
|
|
||||||
&conn,
|
|
||||||
&nt,
|
|
||||||
UpdateType::CipherUpdate,
|
|
||||||
)?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user data
|
// Update user data
|
||||||
@@ -338,6 +329,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
|
|||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
user.private_key = Some(data.PrivateKey);
|
user.private_key = Some(data.PrivateKey);
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
|
user.reset_stamp_exception();
|
||||||
|
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
}
|
}
|
||||||
@@ -445,7 +437,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
|||||||
user.email_new = None;
|
user.email_new = None;
|
||||||
user.email_new_token = None;
|
user.email_new_token = None;
|
||||||
|
|
||||||
user.set_password(&data.NewMasterPasswordHash);
|
user.set_password(&data.NewMasterPasswordHash, None);
|
||||||
user.akey = data.Key;
|
user.akey = data.Key;
|
||||||
|
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
@@ -460,7 +452,7 @@ fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) {
|
||||||
error!("Error sending delete account email: {:#?}", e);
|
error!("Error sending verify_email email: {:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -611,7 +603,7 @@ struct PreloginData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/prelogin", data = "<data>")]
|
#[post("/accounts/prelogin", data = "<data>")]
|
||||||
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> {
|
||||||
let data: PreloginData = data.into_inner().data;
|
let data: PreloginData = data.into_inner().data;
|
||||||
|
|
||||||
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
||||||
@@ -619,10 +611,10 @@ fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
|||||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Kdf": kdf_type,
|
"Kdf": kdf_type,
|
||||||
"KdfIterations": kdf_iter
|
"KdfIterations": kdf_iter
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use rocket::{http::ContentType, request::Form, Data, Route};
|
use rocket::{http::ContentType, request::Form, Data, Route};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -12,11 +13,21 @@ use crate::{
|
|||||||
api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
|
api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn, DbPool},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
|
// Note that many routes have an `admin` variant; this seems to be
|
||||||
|
// because the stored procedure that upstream Bitwarden uses to determine
|
||||||
|
// whether the user can edit a cipher doesn't take into account whether
|
||||||
|
// the user is an org owner/admin. The `admin` variant first checks
|
||||||
|
// whether the user is an owner/admin of the relevant org, and if so,
|
||||||
|
// allows the operation unconditionally.
|
||||||
|
//
|
||||||
|
// vaultwarden factors in the org owner/admin status as part of
|
||||||
|
// determining the write accessibility of a cipher, so most
|
||||||
|
// admin/non-admin implementations can be shared.
|
||||||
routes![
|
routes![
|
||||||
sync,
|
sync,
|
||||||
get_ciphers,
|
get_ciphers,
|
||||||
@@ -38,7 +49,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
post_cipher_admin,
|
post_cipher_admin,
|
||||||
post_cipher_share,
|
post_cipher_share,
|
||||||
put_cipher_share,
|
put_cipher_share,
|
||||||
put_cipher_share_seleted,
|
put_cipher_share_selected,
|
||||||
post_cipher,
|
post_cipher,
|
||||||
put_cipher,
|
put_cipher,
|
||||||
delete_cipher_post,
|
delete_cipher_post,
|
||||||
@@ -50,6 +61,9 @@ pub fn routes() -> Vec<Route> {
|
|||||||
delete_cipher_selected,
|
delete_cipher_selected,
|
||||||
delete_cipher_selected_post,
|
delete_cipher_selected_post,
|
||||||
delete_cipher_selected_put,
|
delete_cipher_selected_put,
|
||||||
|
delete_cipher_selected_admin,
|
||||||
|
delete_cipher_selected_post_admin,
|
||||||
|
delete_cipher_selected_put_admin,
|
||||||
restore_cipher_put,
|
restore_cipher_put,
|
||||||
restore_cipher_put_admin,
|
restore_cipher_put_admin,
|
||||||
restore_cipher_selected,
|
restore_cipher_selected,
|
||||||
@@ -63,6 +77,15 @@ pub fn routes() -> Vec<Route> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn purge_trashed_ciphers(pool: DbPool) {
|
||||||
|
debug!("Purging trashed ciphers");
|
||||||
|
if let Ok(conn) = pool.get() {
|
||||||
|
Cipher::purge_trash(&conn);
|
||||||
|
} else {
|
||||||
|
error!("Failed to get DB connection while purging trashed ciphers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(FromForm, Default)]
|
#[derive(FromForm, Default)]
|
||||||
struct SyncData {
|
struct SyncData {
|
||||||
#[form(field = "excludeDomains")]
|
#[form(field = "excludeDomains")]
|
||||||
@@ -70,55 +93,57 @@ struct SyncData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sync?<data..>")]
|
#[get("/sync?<data..>")]
|
||||||
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let user_json = headers.user.to_json(&conn);
|
let user_json = headers.user.to_json(&conn);
|
||||||
|
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||||
|
|
||||||
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
|
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
|
||||||
let collections_json: Vec<Value> = collections.iter().map(Collection::to_json).collect();
|
let collections_json: Vec<Value> =
|
||||||
|
collections.iter().map(|c| c.to_json_details(&headers.user.uuid, &conn)).collect();
|
||||||
|
|
||||||
let policies = OrgPolicy::find_by_user(&headers.user.uuid, &conn);
|
let policies = OrgPolicy::find_by_user(&headers.user.uuid, &conn);
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
||||||
let ciphers_json: Vec<Value> = ciphers
|
let ciphers_json: Vec<Value> =
|
||||||
.iter()
|
ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
|
||||||
.collect();
|
let sends = Send::find_by_user(&headers.user.uuid, &conn);
|
||||||
|
let sends_json: Vec<Value> = sends.iter().map(|s| s.to_json()).collect();
|
||||||
|
|
||||||
let domains_json = if data.exclude_domains {
|
let domains_json = if data.exclude_domains {
|
||||||
Value::Null
|
Value::Null
|
||||||
} else {
|
} else {
|
||||||
api::core::_get_eq_domains(headers, true).unwrap().into_inner()
|
api::core::_get_eq_domains(headers, true).into_inner()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Profile": user_json,
|
"Profile": user_json,
|
||||||
"Folders": folders_json,
|
"Folders": folders_json,
|
||||||
"Collections": collections_json,
|
"Collections": collections_json,
|
||||||
"Policies": policies_json,
|
"Policies": policies_json,
|
||||||
"Ciphers": ciphers_json,
|
"Ciphers": ciphers_json,
|
||||||
"Domains": domains_json,
|
"Domains": domains_json,
|
||||||
|
"Sends": sends_json,
|
||||||
|
"unofficialServer": true,
|
||||||
"Object": "sync"
|
"Object": "sync"
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers")]
|
#[get("/ciphers")]
|
||||||
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_ciphers(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
||||||
|
|
||||||
let ciphers_json: Vec<Value> = ciphers
|
let ciphers_json: Vec<Value> =
|
||||||
.iter()
|
ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": ciphers_json,
|
"Data": ciphers_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<uuid>")]
|
#[get("/ciphers/<uuid>")]
|
||||||
@@ -181,6 +206,14 @@ pub struct CipherData {
|
|||||||
#[serde(rename = "Attachments")]
|
#[serde(rename = "Attachments")]
|
||||||
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
|
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
|
||||||
Attachments2: Option<HashMap<String, Attachments2Data>>,
|
Attachments2: Option<HashMap<String, Attachments2Data>>,
|
||||||
|
|
||||||
|
// The revision datetime (in ISO 8601 format) of the client's local copy
|
||||||
|
// of the cipher. This is used to prevent a client from updating a cipher
|
||||||
|
// when it doesn't have the latest version, as that can result in data
|
||||||
|
// loss. It's not an error when no value is provided; this can happen
|
||||||
|
// when using older client versions, or if the operation doesn't involve
|
||||||
|
// updating an existing cipher.
|
||||||
|
LastKnownRevisionDate: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -190,22 +223,46 @@ pub struct Attachments2Data {
|
|||||||
Key: String,
|
Key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when an org admin clones an org cipher.
|
||||||
#[post("/ciphers/admin", data = "<data>")]
|
#[post("/ciphers/admin", data = "<data>")]
|
||||||
fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
let data: ShareCipherData = data.into_inner().data;
|
post_ciphers_create(data, headers, conn, nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when creating a new org-owned cipher, or cloning a cipher (whether
|
||||||
|
/// user- or org-owned). When cloning a cipher to a user-owned cipher,
|
||||||
|
/// `organizationId` is null.
|
||||||
|
#[post("/ciphers/create", data = "<data>")]
|
||||||
|
fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
let mut data: ShareCipherData = data.into_inner().data;
|
||||||
|
|
||||||
|
// Check if there are one more more collections selected when this cipher is part of an organization.
|
||||||
|
// err if this is not the case before creating an empty cipher.
|
||||||
|
if data.Cipher.OrganizationId.is_some() && data.CollectionIds.is_empty() {
|
||||||
|
err!("You must select at least one collection.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This check is usually only needed in update_cipher_from_data(), but we
|
||||||
|
// need it here as well to avoid creating an empty cipher in the call to
|
||||||
|
// cipher.save() below.
|
||||||
|
enforce_personal_ownership_policy(&data.Cipher, &headers, &conn)?;
|
||||||
|
|
||||||
let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone());
|
let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone());
|
||||||
cipher.user_uuid = Some(headers.user.uuid.clone());
|
cipher.user_uuid = Some(headers.user.uuid.clone());
|
||||||
cipher.save(&conn)?;
|
cipher.save(&conn)?;
|
||||||
|
|
||||||
|
// When cloning a cipher, the Bitwarden clients seem to set this field
|
||||||
|
// based on the cipher being cloned (when creating a new cipher, it's set
|
||||||
|
// to null as expected). However, `cipher.created_at` is initialized to
|
||||||
|
// the current time, so the stale data check will end up failing down the
|
||||||
|
// line. Since this function only creates new ciphers (whether by cloning
|
||||||
|
// or otherwise), we can just ignore this field entirely.
|
||||||
|
data.Cipher.LastKnownRevisionDate = None;
|
||||||
|
|
||||||
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &nt)
|
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/create", data = "<data>")]
|
/// Called when creating a new user-owned cipher.
|
||||||
fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
|
||||||
post_ciphers_admin(data, headers, conn, nt)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/ciphers", data = "<data>")]
|
#[post("/ciphers", data = "<data>")]
|
||||||
fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
let data: CipherData = data.into_inner().data;
|
let data: CipherData = data.into_inner().data;
|
||||||
@@ -216,6 +273,24 @@ fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, nt
|
|||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enforces the personal ownership policy on user-owned ciphers, if applicable.
|
||||||
|
/// A non-owner/admin user belonging to an org with the personal ownership policy
|
||||||
|
/// enabled isn't allowed to create new user-owned ciphers or modify existing ones
|
||||||
|
/// (that were created before the policy was applicable to the user). The user is
|
||||||
|
/// allowed to delete or share such ciphers to an org, however.
|
||||||
|
///
|
||||||
|
/// Ref: https://bitwarden.com/help/article/policies/#personal-ownership
|
||||||
|
fn enforce_personal_ownership_policy(data: &CipherData, headers: &Headers, conn: &DbConn) -> EmptyResult {
|
||||||
|
if data.OrganizationId.is_none() {
|
||||||
|
let user_uuid = &headers.user.uuid;
|
||||||
|
let policy_type = OrgPolicyType::PersonalOwnership;
|
||||||
|
if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) {
|
||||||
|
err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_cipher_from_data(
|
pub fn update_cipher_from_data(
|
||||||
cipher: &mut Cipher,
|
cipher: &mut Cipher,
|
||||||
data: CipherData,
|
data: CipherData,
|
||||||
@@ -225,6 +300,20 @@ pub fn update_cipher_from_data(
|
|||||||
nt: &Notify,
|
nt: &Notify,
|
||||||
ut: UpdateType,
|
ut: UpdateType,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
enforce_personal_ownership_policy(&data, headers, conn)?;
|
||||||
|
|
||||||
|
// Check that the client isn't updating an existing cipher with stale data.
|
||||||
|
if let Some(dt) = data.LastKnownRevisionDate {
|
||||||
|
match NaiveDateTime::parse_from_str(&dt, "%+") {
|
||||||
|
// ISO 8601 format
|
||||||
|
Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err),
|
||||||
|
Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => {
|
||||||
|
err!("The client copy of this cipher is out of date. Resync the client and try again.")
|
||||||
|
}
|
||||||
|
Ok(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.OrganizationId {
|
if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.OrganizationId {
|
||||||
err!("Organization mismatch. Please resync the client before updating the cipher")
|
err!("Organization mismatch. Please resync the client before updating the cipher")
|
||||||
}
|
}
|
||||||
@@ -238,6 +327,11 @@ pub fn update_cipher_from_data(
|
|||||||
|| cipher.is_write_accessible_to_user(&headers.user.uuid, &conn)
|
|| cipher.is_write_accessible_to_user(&headers.user.uuid, &conn)
|
||||||
{
|
{
|
||||||
cipher.organization_uuid = Some(org_id);
|
cipher.organization_uuid = Some(org_id);
|
||||||
|
// After some discussion in PR #1329 re-added the user_uuid = None again.
|
||||||
|
// TODO: Audit/Check the whole save/update cipher chain.
|
||||||
|
// Upstream uses the user_uuid to allow a cipher added by a user to an org to still allow the user to view/edit the cipher
|
||||||
|
// even when the user has hide-passwords configured as there policy.
|
||||||
|
// Removing the line below would fix that, but we have to check which effect this would have on the rest of the code.
|
||||||
cipher.user_uuid = None;
|
cipher.user_uuid = None;
|
||||||
} else {
|
} else {
|
||||||
err!("You don't have permission to add cipher directly to organization")
|
err!("You don't have permission to add cipher directly to organization")
|
||||||
@@ -281,6 +375,20 @@ pub fn update_cipher_from_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup cipher data, like removing the 'Response' key.
|
||||||
|
// This key is somewhere generated during Javascript so no way for us this fix this.
|
||||||
|
// Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.
|
||||||
|
// We do not mind which data is in it, the keep our model more flexible when there are upstream changes.
|
||||||
|
// But, we at least know we do not need to store and return this specific key.
|
||||||
|
fn _clean_cipher_data(mut json_data: Value) -> Value {
|
||||||
|
if json_data.is_array() {
|
||||||
|
json_data.as_array_mut().unwrap().iter_mut().for_each(|ref mut f| {
|
||||||
|
f.as_object_mut().unwrap().remove("Response");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
json_data
|
||||||
|
}
|
||||||
|
|
||||||
let type_data_opt = match data.Type {
|
let type_data_opt = match data.Type {
|
||||||
1 => data.Login,
|
1 => data.Login,
|
||||||
2 => data.SecureNote,
|
2 => data.SecureNote,
|
||||||
@@ -289,23 +397,22 @@ pub fn update_cipher_from_data(
|
|||||||
_ => err!("Invalid type"),
|
_ => err!("Invalid type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut type_data = match type_data_opt {
|
let type_data = match type_data_opt {
|
||||||
Some(data) => data,
|
Some(mut data) => {
|
||||||
|
// Remove the 'Response' key from the base object.
|
||||||
|
data.as_object_mut().unwrap().remove("Response");
|
||||||
|
// Remove the 'Response' key from every Uri.
|
||||||
|
if data["Uris"].is_array() {
|
||||||
|
data["Uris"] = _clean_cipher_data(data["Uris"].clone());
|
||||||
|
}
|
||||||
|
data
|
||||||
|
}
|
||||||
None => err!("Data missing"),
|
None => err!("Data missing"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: ******* Backwards compat start **********
|
|
||||||
// To remove backwards compatibility, just delete this code,
|
|
||||||
// and remove the compat code from cipher::to_json
|
|
||||||
type_data["Name"] = Value::String(data.Name.clone());
|
|
||||||
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["PasswordHistory"] = data.PasswordHistory.clone().unwrap_or(Value::Null);
|
|
||||||
// TODO: ******* Backwards compat end **********
|
|
||||||
|
|
||||||
cipher.name = data.Name;
|
cipher.name = data.Name;
|
||||||
cipher.notes = data.Notes;
|
cipher.notes = data.Notes;
|
||||||
cipher.fields = data.Fields.map(|f| f.to_string());
|
cipher.fields = data.Fields.map(|f| _clean_cipher_data(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.password_history = data.PasswordHistory.map(|f| f.to_string());
|
||||||
|
|
||||||
@@ -374,6 +481,7 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when an org admin modifies an existing org cipher.
|
||||||
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
#[put("/ciphers/<uuid>/admin", data = "<data>")]
|
||||||
fn put_cipher_admin(
|
fn put_cipher_admin(
|
||||||
uuid: String,
|
uuid: String,
|
||||||
@@ -479,11 +587,8 @@ fn post_collections_admin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let posted_collections: HashSet<String> = data.CollectionIds.iter().cloned().collect();
|
let posted_collections: HashSet<String> = data.CollectionIds.iter().cloned().collect();
|
||||||
let current_collections: HashSet<String> = cipher
|
let current_collections: HashSet<String> =
|
||||||
.get_collections(&headers.user.uuid, &conn)
|
cipher.get_collections(&headers.user.uuid, &conn).iter().cloned().collect();
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for collection in posted_collections.symmetric_difference(¤t_collections) {
|
for collection in posted_collections.symmetric_difference(¤t_collections) {
|
||||||
match Collection::find_by_uuid(&collection, &conn) {
|
match Collection::find_by_uuid(&collection, &conn) {
|
||||||
@@ -548,7 +653,7 @@ struct ShareSelectedCipherData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/share", data = "<data>")]
|
#[put("/ciphers/share", data = "<data>")]
|
||||||
fn put_cipher_share_seleted(
|
fn put_cipher_share_selected(
|
||||||
data: JsonUpcase<ShareSelectedCipherData>,
|
data: JsonUpcase<ShareSelectedCipherData>,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
@@ -719,24 +824,25 @@ fn post_attachment(
|
|||||||
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
|
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
|
||||||
let path = base_path.join(&file_name);
|
let path = base_path.join(&file_name);
|
||||||
|
|
||||||
let size = match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) {
|
let size =
|
||||||
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) {
|
||||||
SaveResult::Full(other) => {
|
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||||
std::fs::remove_file(path).ok();
|
SaveResult::Full(other) => {
|
||||||
error = Some(format!("Attachment is not a file: {:?}", other));
|
std::fs::remove_file(path).ok();
|
||||||
return;
|
error = Some(format!("Attachment is not a file: {:?}", other));
|
||||||
}
|
return;
|
||||||
SaveResult::Partial(_, reason) => {
|
}
|
||||||
std::fs::remove_file(path).ok();
|
SaveResult::Partial(_, reason) => {
|
||||||
error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason));
|
std::fs::remove_file(path).ok();
|
||||||
return;
|
error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason));
|
||||||
}
|
return;
|
||||||
SaveResult::Error(e) => {
|
}
|
||||||
std::fs::remove_file(path).ok();
|
SaveResult::Error(e) => {
|
||||||
error = Some(format!("Error: {:?}", e));
|
std::fs::remove_file(path).ok();
|
||||||
return;
|
error = Some(format!("Error: {:?}", e));
|
||||||
}
|
return;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
|
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
|
||||||
attachment.akey = attachment_key.clone();
|
attachment.akey = attachment_key.clone();
|
||||||
@@ -862,22 +968,47 @@ fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn:
|
|||||||
|
|
||||||
#[put("/ciphers/delete", data = "<data>")]
|
#[put("/ciphers/delete", data = "<data>")]
|
||||||
fn delete_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_multiple_ciphers(data, headers, conn, true, nt)
|
_delete_multiple_ciphers(data, headers, conn, true, nt) // soft delete
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/ciphers/admin", data = "<data>")]
|
||||||
|
fn delete_cipher_selected_admin(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
delete_cipher_selected(data, headers, conn, nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/ciphers/delete-admin", data = "<data>")]
|
||||||
|
fn delete_cipher_selected_post_admin(
|
||||||
|
data: JsonUpcase<Value>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify,
|
||||||
|
) -> EmptyResult {
|
||||||
|
delete_cipher_selected_post(data, headers, conn, nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/delete-admin", data = "<data>")]
|
||||||
|
fn delete_cipher_selected_put_admin(
|
||||||
|
data: JsonUpcase<Value>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify,
|
||||||
|
) -> EmptyResult {
|
||||||
|
delete_cipher_selected_put(data, headers, conn, nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>/restore")]
|
#[put("/ciphers/<uuid>/restore")]
|
||||||
fn restore_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn restore_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<uuid>/restore-admin")]
|
#[put("/ciphers/<uuid>/restore-admin")]
|
||||||
fn restore_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn restore_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/restore", data = "<data>")]
|
#[put("/ciphers/restore", data = "<data>")]
|
||||||
fn restore_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn restore_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
_restore_multiple_ciphers(data, headers, conn, nt)
|
_restore_multiple_ciphers(data, &headers, &conn, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -963,7 +1094,6 @@ fn delete_all(
|
|||||||
Some(user_org) => {
|
Some(user_org) => {
|
||||||
if user_org.atype == UserOrgType::Owner {
|
if user_org.atype == UserOrgType::Owner {
|
||||||
Cipher::delete_all_by_organization(&org_data.org_id, &conn)?;
|
Cipher::delete_all_by_organization(&org_data.org_id, &conn)?;
|
||||||
Collection::delete_all_by_organization(&org_data.org_id, &conn)?;
|
|
||||||
nt.send_user_update(UpdateType::Vault, &user);
|
nt.send_user_update(UpdateType::Vault, &user);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@@ -1002,7 +1132,7 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_del
|
|||||||
}
|
}
|
||||||
|
|
||||||
if soft_delete {
|
if soft_delete {
|
||||||
cipher.deleted_at = Some(chrono::Utc::now().naive_utc());
|
cipher.deleted_at = Some(Utc::now().naive_utc());
|
||||||
cipher.save(&conn)?;
|
cipher.save(&conn)?;
|
||||||
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||||
} else {
|
} else {
|
||||||
@@ -1013,7 +1143,13 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_del
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _delete_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, soft_delete: bool, nt: Notify) -> EmptyResult {
|
fn _delete_multiple_ciphers(
|
||||||
|
data: JsonUpcase<Value>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
soft_delete: bool,
|
||||||
|
nt: Notify,
|
||||||
|
) -> 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") {
|
||||||
@@ -1033,7 +1169,7 @@ fn _delete_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbC
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> JsonResult {
|
||||||
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||||
Some(cipher) => cipher,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
@@ -1047,10 +1183,10 @@ fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &No
|
|||||||
cipher.save(&conn)?;
|
cipher.save(&conn)?;
|
||||||
|
|
||||||
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||||
Ok(())
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: &Headers, conn: &DbConn, nt: &Notify) -> JsonResult {
|
||||||
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") {
|
||||||
@@ -1061,13 +1197,19 @@ fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: Db
|
|||||||
None => err!("Request missing ids field"),
|
None => err!("Request missing ids field"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut ciphers: Vec<Value> = Vec::new();
|
||||||
for uuid in uuids {
|
for uuid in uuids {
|
||||||
if let error @ Err(_) = _restore_cipher_by_uuid(uuid, &headers, &conn, &nt) {
|
match _restore_cipher_by_uuid(uuid, headers, conn, nt) {
|
||||||
return error;
|
Ok(json) => ciphers.push(json.into_inner()),
|
||||||
};
|
err => return err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(Json(json!({
|
||||||
|
"Data": ciphers,
|
||||||
|
"Object": "list",
|
||||||
|
"ContinuationToken": null
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _delete_cipher_attachment_by_id(
|
fn _delete_cipher_attachment_by_id(
|
||||||
|
|||||||
@@ -8,28 +8,20 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<rocket::Route> {
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![
|
routes![get_folders, get_folder, post_folders, post_folder, put_folder, delete_folder_post, delete_folder,]
|
||||||
get_folders,
|
|
||||||
get_folder,
|
|
||||||
post_folders,
|
|
||||||
post_folder,
|
|
||||||
put_folder,
|
|
||||||
delete_folder_post,
|
|
||||||
delete_folder,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/folders")]
|
#[get("/folders")]
|
||||||
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||||
|
|
||||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": folders_json,
|
"Data": folders_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/folders/<uuid>")]
|
#[get("/folders/<uuid>")]
|
||||||
|
|||||||
@@ -2,17 +2,15 @@ mod accounts;
|
|||||||
mod ciphers;
|
mod ciphers;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
|
mod sends;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
|
pub use ciphers::purge_trashed_ciphers;
|
||||||
|
pub use sends::purge_sends;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut mod_routes = routes![
|
let mut mod_routes =
|
||||||
clear_device_token,
|
routes![clear_device_token, put_device_token, get_eq_domains, post_eq_domains, put_eq_domains, hibp_breach,];
|
||||||
put_device_token,
|
|
||||||
get_eq_domains,
|
|
||||||
post_eq_domains,
|
|
||||||
put_eq_domains,
|
|
||||||
hibp_breach,
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut routes = Vec::new();
|
let mut routes = Vec::new();
|
||||||
routes.append(&mut accounts::routes());
|
routes.append(&mut accounts::routes());
|
||||||
@@ -20,6 +18,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
routes.append(&mut folders::routes());
|
routes.append(&mut folders::routes());
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
|
routes.append(&mut sends::routes());
|
||||||
routes.append(&mut mod_routes);
|
routes.append(&mut mod_routes);
|
||||||
|
|
||||||
routes
|
routes
|
||||||
@@ -28,19 +27,21 @@ pub fn routes() -> Vec<Route> {
|
|||||||
//
|
//
|
||||||
// Move this somewhere else
|
// Move this somewhere else
|
||||||
//
|
//
|
||||||
|
use rocket::response::Response;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase},
|
api::{JsonResult, JsonUpcase},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::DbConn,
|
db::DbConn,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
util::get_reqwest_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
fn clear_device_token<'a>(uuid: String) -> Response<'a> {
|
||||||
// This endpoint doesn't have auth header
|
// This endpoint doesn't have auth header
|
||||||
|
|
||||||
let _ = uuid;
|
let _ = uuid;
|
||||||
@@ -49,11 +50,11 @@ fn clear_device_token(uuid: String) -> EmptyResult {
|
|||||||
// This only clears push token
|
// This only clears push token
|
||||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
||||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||||
Ok(())
|
Response::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||||
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> JsonResult {
|
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> Json<Value> {
|
||||||
let _data: Value = data.into_inner().data;
|
let _data: Value = data.into_inner().data;
|
||||||
// Data has a single string value "PushToken"
|
// Data has a single string value "PushToken"
|
||||||
let _ = uuid;
|
let _ = uuid;
|
||||||
@@ -61,13 +62,13 @@ fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) ->
|
|||||||
|
|
||||||
// TODO: This should save the push token, but we don't have push functionality
|
// TODO: This should save the push token, but we don't have push functionality
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Id": headers.device.uuid,
|
"Id": headers.device.uuid,
|
||||||
"Name": headers.device.name,
|
"Name": headers.device.name,
|
||||||
"Type": headers.device.atype,
|
"Type": headers.device.atype,
|
||||||
"Identifier": headers.device.uuid,
|
"Identifier": headers.device.uuid,
|
||||||
"CreationDate": crate::util::format_date(&headers.device.created_at),
|
"CreationDate": crate::util::format_date(&headers.device.created_at),
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -81,11 +82,11 @@ struct GlobalDomain {
|
|||||||
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
||||||
|
|
||||||
#[get("/settings/domains")]
|
#[get("/settings/domains")]
|
||||||
fn get_eq_domains(headers: Headers) -> JsonResult {
|
fn get_eq_domains(headers: Headers) -> Json<Value> {
|
||||||
_get_eq_domains(headers, false)
|
_get_eq_domains(headers, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _get_eq_domains(headers: Headers, no_excluded: bool) -> JsonResult {
|
fn _get_eq_domains(headers: Headers, no_excluded: bool) -> Json<Value> {
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
@@ -102,11 +103,11 @@ fn _get_eq_domains(headers: Headers, no_excluded: bool) -> JsonResult {
|
|||||||
globals.retain(|g| !g.Excluded);
|
globals.retain(|g| !g.Excluded);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"EquivalentDomains": equivalent_domains,
|
"EquivalentDomains": equivalent_domains,
|
||||||
"GlobalEquivalentDomains": globals,
|
"GlobalEquivalentDomains": globals,
|
||||||
"Object": "domains",
|
"Object": "domains",
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -141,22 +142,15 @@ fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbC
|
|||||||
|
|
||||||
#[get("/hibp/breach?<username>")]
|
#[get("/hibp/breach?<username>")]
|
||||||
fn hibp_breach(username: String) -> JsonResult {
|
fn hibp_breach(username: String) -> JsonResult {
|
||||||
let user_agent = "Bitwarden_RS";
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{}?truncateResponse=false&includeUnverified=false",
|
"https://haveibeenpwned.com/api/v3/breachedaccount/{}?truncateResponse=false&includeUnverified=false",
|
||||||
username
|
username
|
||||||
);
|
);
|
||||||
|
|
||||||
use reqwest::{blocking::Client, header::USER_AGENT};
|
|
||||||
|
|
||||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||||
let hibp_client = Client::builder().build()?;
|
let hibp_client = get_reqwest_client();
|
||||||
|
|
||||||
let res = hibp_client
|
let res = hibp_client.get(&url).header("hibp-api-key", api_key).send()?;
|
||||||
.get(&url)
|
|
||||||
.header(USER_AGENT, user_agent)
|
|
||||||
.header("hibp-api-key", api_key)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
// If we get a 404, return a 404, it means no breached accounts
|
// If we get a 404, return a 404, it means no breached accounts
|
||||||
if res.status() == 404 {
|
if res.status() == 404 {
|
||||||
@@ -172,7 +166,7 @@ fn hibp_breach(username: String) -> JsonResult {
|
|||||||
"Domain": "haveibeenpwned.com",
|
"Domain": "haveibeenpwned.com",
|
||||||
"BreachDate": "2019-08-18T00:00:00Z",
|
"BreachDate": "2019-08-18T00:00:00Z",
|
||||||
"AddedDate": "2019-08-18T00:00:00Z",
|
"AddedDate": "2019-08-18T00:00:00Z",
|
||||||
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
|
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
|
||||||
"LogoPath": "bwrs_static/hibp.png",
|
"LogoPath": "bwrs_static/hibp.png",
|
||||||
"PwnCount": 0,
|
"PwnCount": 0,
|
||||||
"DataClasses": [
|
"DataClasses": [
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType},
|
api::{EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType},
|
||||||
auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders},
|
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
mail, CONFIG,
|
mail, CONFIG,
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,10 @@ pub fn routes() -> Vec<Route> {
|
|||||||
list_policies_token,
|
list_policies_token,
|
||||||
get_policy,
|
get_policy,
|
||||||
put_policy,
|
put_policy,
|
||||||
|
get_organization_tax,
|
||||||
get_plans,
|
get_plans,
|
||||||
|
get_plans_tax_rates,
|
||||||
|
import,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,8 +192,8 @@ fn post_organization(
|
|||||||
|
|
||||||
// GET /api/collections?writeOnly=false
|
// GET /api/collections?writeOnly=false
|
||||||
#[get("/collections")]
|
#[get("/collections")]
|
||||||
fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data":
|
"Data":
|
||||||
Collection::find_by_user_uuid(&headers.user.uuid, &conn)
|
Collection::find_by_user_uuid(&headers.user.uuid, &conn)
|
||||||
.iter()
|
.iter()
|
||||||
@@ -198,12 +201,12 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
.collect::<Value>(),
|
.collect::<Value>(),
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/collections")]
|
#[get("/organizations/<org_id>/collections")]
|
||||||
fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data":
|
"Data":
|
||||||
Collection::find_by_organization(&org_id, &conn)
|
Collection::find_by_organization(&org_id, &conn)
|
||||||
.iter()
|
.iter()
|
||||||
@@ -211,13 +214,13 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
|
|||||||
.collect::<Value>(),
|
.collect::<Value>(),
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/collections", data = "<data>")]
|
#[post("/organizations/<org_id>/collections", data = "<data>")]
|
||||||
fn post_organization_collections(
|
fn post_organization_collections(
|
||||||
org_id: String,
|
org_id: String,
|
||||||
_headers: AdminHeaders,
|
headers: ManagerHeadersLoose,
|
||||||
data: JsonUpcase<NewCollectionData>,
|
data: JsonUpcase<NewCollectionData>,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
@@ -228,9 +231,22 @@ fn post_organization_collections(
|
|||||||
None => err!("Can't find organization details"),
|
None => err!("Can't find organization details"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the user_organization record so that we can check if the user has access to all collections.
|
||||||
|
let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||||
|
Some(u) => u,
|
||||||
|
None => err!("User is not part of organization"),
|
||||||
|
};
|
||||||
|
|
||||||
let collection = Collection::new(org.uuid, data.Name);
|
let collection = Collection::new(org.uuid, data.Name);
|
||||||
collection.save(&conn)?;
|
collection.save(&conn)?;
|
||||||
|
|
||||||
|
// If the user doesn't have access to all collections, only in case of a Manger,
|
||||||
|
// then we need to save the creating user uuid (Manager) to the users_collection table.
|
||||||
|
// Else the user will not have access to his own created collection.
|
||||||
|
if !user_org.access_all {
|
||||||
|
CollectionUser::save(&headers.user.uuid, &collection.uuid, false, false, &conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
Ok(Json(collection.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +254,7 @@ fn post_organization_collections(
|
|||||||
fn put_organization_collection_update(
|
fn put_organization_collection_update(
|
||||||
org_id: String,
|
org_id: String,
|
||||||
col_id: String,
|
col_id: String,
|
||||||
headers: AdminHeaders,
|
headers: ManagerHeaders,
|
||||||
data: JsonUpcase<NewCollectionData>,
|
data: JsonUpcase<NewCollectionData>,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
@@ -249,7 +265,7 @@ fn put_organization_collection_update(
|
|||||||
fn post_organization_collection_update(
|
fn post_organization_collection_update(
|
||||||
org_id: String,
|
org_id: String,
|
||||||
col_id: String,
|
col_id: String,
|
||||||
_headers: AdminHeaders,
|
_headers: ManagerHeaders,
|
||||||
data: JsonUpcase<NewCollectionData>,
|
data: JsonUpcase<NewCollectionData>,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
@@ -317,7 +333,12 @@ fn post_organization_collection_delete_user(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/organizations/<org_id>/collections/<col_id>")]
|
#[delete("/organizations/<org_id>/collections/<col_id>")]
|
||||||
fn delete_organization_collection(org_id: String, col_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
|
fn delete_organization_collection(
|
||||||
|
org_id: String,
|
||||||
|
col_id: String,
|
||||||
|
_headers: ManagerHeaders,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
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) => {
|
Some(collection) => {
|
||||||
@@ -341,7 +362,7 @@ struct DeleteCollectionData {
|
|||||||
fn post_organization_collection_delete(
|
fn post_organization_collection_delete(
|
||||||
org_id: String,
|
org_id: String,
|
||||||
col_id: String,
|
col_id: String,
|
||||||
headers: AdminHeaders,
|
headers: ManagerHeaders,
|
||||||
_data: JsonUpcase<DeleteCollectionData>,
|
_data: JsonUpcase<DeleteCollectionData>,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
@@ -349,7 +370,7 @@ fn post_organization_collection_delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[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: ManagerHeaders, 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) {
|
||||||
None => err!("Collection not found"),
|
None => err!("Collection not found"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
@@ -363,7 +384,7 @@ fn get_org_collection_detail(org_id: String, coll_id: String, headers: AdminHead
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/collections/<coll_id>/users")]
|
#[get("/organizations/<org_id>/collections/<coll_id>/users")]
|
||||||
fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn get_collection_users(org_id: String, coll_id: String, _headers: ManagerHeaders, conn: DbConn) -> JsonResult {
|
||||||
// Get org and collection, check that collection is from org
|
// Get org and collection, check that collection is from org
|
||||||
let collection = match Collection::find_by_uuid_and_org(&coll_id, &org_id, &conn) {
|
let collection = match Collection::find_by_uuid_and_org(&coll_id, &org_id, &conn) {
|
||||||
None => err!("Collection not found in Organization"),
|
None => err!("Collection not found in Organization"),
|
||||||
@@ -388,7 +409,7 @@ fn put_collection_users(
|
|||||||
org_id: String,
|
org_id: String,
|
||||||
coll_id: String,
|
coll_id: String,
|
||||||
data: JsonUpcaseVec<CollectionData>,
|
data: JsonUpcaseVec<CollectionData>,
|
||||||
_headers: AdminHeaders,
|
_headers: ManagerHeaders,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
// Get org and collection, check that collection is from org
|
// Get org and collection, check that collection is from org
|
||||||
@@ -410,9 +431,7 @@ fn put_collection_users(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::save(&user.user_uuid, &coll_id,
|
CollectionUser::save(&user.user_uuid, &coll_id, d.ReadOnly, d.HidePasswords, &conn)?;
|
||||||
d.ReadOnly, d.HidePasswords,
|
|
||||||
&conn)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -425,30 +444,28 @@ struct OrgIdData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/organization-details?<data..>")]
|
#[get("/ciphers/organization-details?<data..>")]
|
||||||
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
|
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
|
||||||
let ciphers_json: Vec<Value> = ciphers
|
let ciphers_json: Vec<Value> =
|
||||||
.iter()
|
ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": ciphers_json,
|
"Data": ciphers_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/users")]
|
#[get("/organizations/<org_id>/users")]
|
||||||
fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) -> Json<Value> {
|
||||||
let users = UserOrganization::find_by_org(&org_id, &conn);
|
let users = UserOrganization::find_by_org(&org_id, &conn);
|
||||||
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
|
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": users_json,
|
"Data": users_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -528,9 +545,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
|||||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||||
None => err!("Collection not found in Organization"),
|
None => err!("Collection not found in Organization"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
CollectionUser::save(&user.uuid, &collection.uuid,
|
CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, col.HidePasswords, &conn)?;
|
||||||
col.ReadOnly, col.HidePasswords,
|
|
||||||
&conn)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,7 +654,7 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let mut org_name = String::from("bitwarden_rs");
|
let mut org_name = CONFIG.invitation_org_name();
|
||||||
if let Some(org_id) = &claims.org_id {
|
if let Some(org_id) = &claims.org_id {
|
||||||
org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||||
Some(org) => org.name,
|
Some(org) => org.name,
|
||||||
@@ -785,9 +800,13 @@ fn edit_user(
|
|||||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||||
None => err!("Collection not found in Organization"),
|
None => err!("Collection not found in Organization"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid,
|
CollectionUser::save(
|
||||||
col.ReadOnly, col.HidePasswords,
|
&user_to_edit.user_uuid,
|
||||||
&conn)?;
|
&collection.uuid,
|
||||||
|
col.ReadOnly,
|
||||||
|
col.HidePasswords,
|
||||||
|
&conn,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -883,16 +902,8 @@ fn post_org_import(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|cipher_data| {
|
.map(|cipher_data| {
|
||||||
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
|
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
|
||||||
update_cipher_from_data(
|
update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::CipherCreate)
|
||||||
&mut cipher,
|
.ok();
|
||||||
cipher_data,
|
|
||||||
&headers,
|
|
||||||
false,
|
|
||||||
&conn,
|
|
||||||
&nt,
|
|
||||||
UpdateType::CipherCreate,
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
cipher
|
cipher
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -914,15 +925,15 @@ fn post_org_import(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies")]
|
#[get("/organizations/<org_id>/policies")]
|
||||||
fn list_policies(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn list_policies(org_id: String, _headers: AdminHeaders, conn: DbConn) -> Json<Value> {
|
||||||
let policies = OrgPolicy::find_by_org(&org_id, &conn);
|
let policies = OrgPolicy::find_by_org(&org_id, &conn);
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": policies_json,
|
"Data": policies_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies/token?<token>")]
|
#[get("/organizations/<org_id>/policies/token?<token>")]
|
||||||
@@ -953,7 +964,7 @@ fn list_policies_token(org_id: String, token: String, conn: DbConn) -> JsonResul
|
|||||||
fn get_policy(org_id: String, pol_type: i32, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn get_policy(org_id: String, pol_type: i32, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||||
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
|
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
|
||||||
Some(pt) => pt,
|
Some(pt) => pt,
|
||||||
None => err!("Invalid policy type"),
|
None => err!("Invalid or unsupported policy type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) {
|
let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) {
|
||||||
@@ -973,7 +984,13 @@ struct PolicyData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/organizations/<org_id>/policies/<pol_type>", data = "<data>")]
|
#[put("/organizations/<org_id>/policies/<pol_type>", data = "<data>")]
|
||||||
fn put_policy(org_id: String, pol_type: i32, data: Json<PolicyData>, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn put_policy(
|
||||||
|
org_id: String,
|
||||||
|
pol_type: i32,
|
||||||
|
data: Json<PolicyData>,
|
||||||
|
_headers: AdminHeaders,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
let data: PolicyData = data.into_inner();
|
let data: PolicyData = data.into_inner();
|
||||||
|
|
||||||
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
|
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
|
||||||
@@ -993,9 +1010,16 @@ fn put_policy(org_id: String, pol_type: i32, data: Json<PolicyData>, _headers: A
|
|||||||
Ok(Json(policy.to_json()))
|
Ok(Json(policy.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[get("/organizations/<org_id>/tax")]
|
||||||
|
fn get_organization_tax(org_id: String, _headers: Headers, _conn: DbConn) -> EmptyResult {
|
||||||
|
// Prevent a 404 error, which also causes Javascript errors.
|
||||||
|
err!("Only allowed when not self hosted.")
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/plans")]
|
#[get("/plans")]
|
||||||
fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
|
fn get_plans(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"Data": [
|
"Data": [
|
||||||
{
|
{
|
||||||
@@ -1042,5 +1066,111 @@ fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/plans/sales-tax-rates")]
|
||||||
|
fn get_plans_tax_rates(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
||||||
|
// Prevent a 404 error, which also causes Javascript errors.
|
||||||
|
Json(json!({
|
||||||
|
"Object": "list",
|
||||||
|
"Data": [],
|
||||||
|
"ContinuationToken": null
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgImportGroupData {
|
||||||
|
Name: String, // "GroupName"
|
||||||
|
ExternalId: String, // "cn=GroupName,ou=Groups,dc=example,dc=com"
|
||||||
|
Users: Vec<String>, // ["uid=user,ou=People,dc=example,dc=com"]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgImportUserData {
|
||||||
|
Email: String, // "user@maildomain.net"
|
||||||
|
ExternalId: String, // "uid=user,ou=People,dc=example,dc=com"
|
||||||
|
Deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrgImportData {
|
||||||
|
Groups: Vec<OrgImportGroupData>,
|
||||||
|
OverwriteExisting: bool,
|
||||||
|
Users: Vec<OrgImportUserData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/organizations/<org_id>/import", data = "<data>")]
|
||||||
|
fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||||
|
let data = data.into_inner().data;
|
||||||
|
|
||||||
|
// TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way
|
||||||
|
// to differentiate between auto-imported users and manually added ones.
|
||||||
|
// This means that this endpoint can end up removing users that were added manually by an admin,
|
||||||
|
// as opposed to upstream which only removes auto-imported users.
|
||||||
|
|
||||||
|
// User needs to be admin or owner to use the Directry Connector
|
||||||
|
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||||
|
Some(user_org) if user_org.atype >= UserOrgType::Admin => { /* Okay, nothing to do */ }
|
||||||
|
Some(_) => err!("User has insufficient permissions to use Directory Connector"),
|
||||||
|
None => err!("User not part of organization"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for user_data in &data.Users {
|
||||||
|
if user_data.Deleted {
|
||||||
|
// If user is marked for deletion and it exists, delete it
|
||||||
|
if let Some(user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &conn) {
|
||||||
|
user_org.delete(&conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user is not part of the organization, but it exists
|
||||||
|
} else if UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &conn).is_none() {
|
||||||
|
if let Some(user) = User::find_by_mail(&user_data.Email, &conn) {
|
||||||
|
let user_org_status = if CONFIG.mail_enabled() {
|
||||||
|
UserOrgStatus::Invited as i32
|
||||||
|
} else {
|
||||||
|
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||||
|
new_org_user.access_all = false;
|
||||||
|
new_org_user.atype = UserOrgType::User as i32;
|
||||||
|
new_org_user.status = user_org_status;
|
||||||
|
|
||||||
|
new_org_user.save(&conn)?;
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||||
|
Some(org) => org.name,
|
||||||
|
None => err!("Error looking up organization"),
|
||||||
|
};
|
||||||
|
|
||||||
|
mail::send_invite(
|
||||||
|
&user_data.Email,
|
||||||
|
&user.uuid,
|
||||||
|
Some(org_id.clone()),
|
||||||
|
Some(new_org_user.uuid),
|
||||||
|
&org_name,
|
||||||
|
Some(headers.user.email.clone()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
|
||||||
|
if data.OverwriteExisting {
|
||||||
|
for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User as i32, &conn) {
|
||||||
|
if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).map(|u| u.email) {
|
||||||
|
if !data.Users.iter().any(|u| u.Email == user_email) {
|
||||||
|
user_org.delete(&conn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
391
src/api/core/sends.rs
Normal file
391
src/api/core/sends.rs
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
use std::{io::Read, path::Path};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use multipart::server::{save::SavedData, Multipart, SaveResult};
|
||||||
|
use rocket::{http::ContentType, Data};
|
||||||
|
use rocket_contrib::json::Json;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
|
||||||
|
auth::{Headers, Host},
|
||||||
|
db::{models::*, DbConn, DbPool},
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
|
routes![post_send, post_send_file, post_access, post_access_file, put_send, delete_send, put_remove_password]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn purge_sends(pool: DbPool) {
|
||||||
|
debug!("Purging sends");
|
||||||
|
if let Ok(conn) = pool.get() {
|
||||||
|
Send::purge(&conn);
|
||||||
|
} else {
|
||||||
|
error!("Failed to get DB connection while purging sends")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SendData {
|
||||||
|
pub Type: i32,
|
||||||
|
pub Key: String,
|
||||||
|
pub Password: Option<String>,
|
||||||
|
pub MaxAccessCount: Option<i32>,
|
||||||
|
pub ExpirationDate: Option<DateTime<Utc>>,
|
||||||
|
pub DeletionDate: DateTime<Utc>,
|
||||||
|
pub Disabled: bool,
|
||||||
|
|
||||||
|
// Data field
|
||||||
|
pub Name: String,
|
||||||
|
pub Notes: Option<String>,
|
||||||
|
pub Text: Option<Value>,
|
||||||
|
pub File: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
||||||
|
/// an org with this policy enabled isn't allowed to create new Sends or
|
||||||
|
/// modify existing ones, but is allowed to delete them.
|
||||||
|
///
|
||||||
|
/// Ref: https://bitwarden.com/help/article/policies/#disable-send
|
||||||
|
fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {
|
||||||
|
let user_uuid = &headers.user.uuid;
|
||||||
|
let policy_type = OrgPolicyType::DisableSend;
|
||||||
|
if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) {
|
||||||
|
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
||||||
|
let data_val = if data.Type == SendType::Text as i32 {
|
||||||
|
data.Text
|
||||||
|
} else if data.Type == SendType::File as i32 {
|
||||||
|
data.File
|
||||||
|
} else {
|
||||||
|
err!("Invalid Send type")
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_str = if let Some(mut d) = data_val {
|
||||||
|
d.as_object_mut().and_then(|o| o.remove("Response"));
|
||||||
|
serde_json::to_string(&d)?
|
||||||
|
} else {
|
||||||
|
err!("Send data not provided");
|
||||||
|
};
|
||||||
|
|
||||||
|
if data.DeletionDate > Utc::now() + Duration::days(31) {
|
||||||
|
err!(
|
||||||
|
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc());
|
||||||
|
send.user_uuid = Some(user_uuid);
|
||||||
|
send.notes = data.Notes;
|
||||||
|
send.max_access_count = data.MaxAccessCount;
|
||||||
|
send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc());
|
||||||
|
send.disabled = data.Disabled;
|
||||||
|
send.atype = data.Type;
|
||||||
|
|
||||||
|
send.set_password(data.Password.as_deref());
|
||||||
|
|
||||||
|
Ok(send)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends", data = "<data>")]
|
||||||
|
fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let data: SendData = data.into_inner().data;
|
||||||
|
|
||||||
|
if data.Type == SendType::File as i32 {
|
||||||
|
err!("File sends should use /api/sends/file")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut send = create_send(data, headers.user.uuid.clone())?;
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendCreate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
||||||
|
fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let boundary = content_type.params().next().expect("No boundary provided").1;
|
||||||
|
|
||||||
|
let mut mpart = Multipart::with_body(data.open(), boundary);
|
||||||
|
|
||||||
|
// First entry is the SendData JSON
|
||||||
|
let mut model_entry = match mpart.read_entry()? {
|
||||||
|
Some(e) if &*e.headers.name == "model" => e,
|
||||||
|
Some(_) => err!("Invalid entry name"),
|
||||||
|
None => err!("No model entry present"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
model_entry.data.read_to_string(&mut buf)?;
|
||||||
|
let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?;
|
||||||
|
|
||||||
|
// Get the file length and add an extra 10% to avoid issues
|
||||||
|
const SIZE_110_MB: u64 = 115_343_360;
|
||||||
|
|
||||||
|
let size_limit = match CONFIG.user_attachment_limit() {
|
||||||
|
Some(0) => err!("File uploads are disabled"),
|
||||||
|
Some(limit_kb) => {
|
||||||
|
let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn);
|
||||||
|
if left <= 0 {
|
||||||
|
err!("Attachment size limit reached! Delete some files to open space")
|
||||||
|
}
|
||||||
|
std::cmp::Ord::max(left as u64, SIZE_110_MB)
|
||||||
|
}
|
||||||
|
None => SIZE_110_MB,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Send
|
||||||
|
let mut send = create_send(data.data, headers.user.uuid.clone())?;
|
||||||
|
let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32]));
|
||||||
|
|
||||||
|
if send.atype != SendType::File as i32 {
|
||||||
|
err!("Send content is not a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id);
|
||||||
|
|
||||||
|
// Read the data entry and save the file
|
||||||
|
let mut data_entry = match mpart.read_entry()? {
|
||||||
|
Some(e) if &*e.headers.name == "data" => e,
|
||||||
|
Some(_) => err!("Invalid entry name"),
|
||||||
|
None => err!("No model entry present"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = match data_entry.data.save().memory_threshold(0).size_limit(size_limit).with_path(&file_path) {
|
||||||
|
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||||
|
SaveResult::Full(other) => {
|
||||||
|
std::fs::remove_file(&file_path).ok();
|
||||||
|
err!(format!("Attachment is not a file: {:?}", other));
|
||||||
|
}
|
||||||
|
SaveResult::Partial(_, reason) => {
|
||||||
|
std::fs::remove_file(&file_path).ok();
|
||||||
|
err!(format!("Attachment size limit exceeded with this file: {:?}", reason));
|
||||||
|
}
|
||||||
|
SaveResult::Error(e) => {
|
||||||
|
std::fs::remove_file(&file_path).ok();
|
||||||
|
err!(format!("Error: {:?}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set ID and sizes
|
||||||
|
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||||
|
if let Some(o) = data_value.as_object_mut() {
|
||||||
|
o.insert(String::from("Id"), Value::String(file_id));
|
||||||
|
o.insert(String::from("Size"), Value::Number(size.into()));
|
||||||
|
o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(size)));
|
||||||
|
}
|
||||||
|
send.data = serde_json::to_string(&data_value)?;
|
||||||
|
|
||||||
|
// Save the changes in the database
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendCreate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SendAccessData {
|
||||||
|
pub Password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||||
|
fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn) -> JsonResult {
|
||||||
|
let mut send = match Send::find_by_access_id(&access_id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_access_count) = send.max_access_count {
|
||||||
|
if send.access_count >= max_access_count {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expiration) = send.expiration_date {
|
||||||
|
if Utc::now().naive_utc() >= expiration {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Utc::now().naive_utc() >= send.deletion_date {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.disabled {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.password_hash.is_some() {
|
||||||
|
match data.into_inner().data.Password {
|
||||||
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||||
|
Some(_) => err!("Invalid password."),
|
||||||
|
None => err_code!("Password not provided", 401),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files are incremented during the download
|
||||||
|
if send.atype == SendType::Text as i32 {
|
||||||
|
send.access_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
send.save(&conn)?;
|
||||||
|
|
||||||
|
Ok(Json(send.to_json_access()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||||
|
fn post_access_file(
|
||||||
|
send_id: String,
|
||||||
|
file_id: String,
|
||||||
|
data: JsonUpcase<SendAccessData>,
|
||||||
|
host: Host,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
let mut send = match Send::find_by_uuid(&send_id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_access_count) = send.max_access_count {
|
||||||
|
if send.access_count >= max_access_count {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expiration) = send.expiration_date {
|
||||||
|
if Utc::now().naive_utc() >= expiration {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Utc::now().naive_utc() >= send.deletion_date {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.disabled {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.password_hash.is_some() {
|
||||||
|
match data.into_inner().data.Password {
|
||||||
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||||
|
Some(_) => err!("Invalid password."),
|
||||||
|
None => err_code!("Password not provided", 401),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send.access_count += 1;
|
||||||
|
|
||||||
|
send.save(&conn)?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Object": "send-fileDownload",
|
||||||
|
"Id": file_id,
|
||||||
|
"Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/sends/<id>", data = "<data>")]
|
||||||
|
fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let data: SendData = data.into_inner().data;
|
||||||
|
|
||||||
|
let mut send = match Send::find_by_uuid(&id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err!("Send not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||||
|
err!("Send is not owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.atype != data.Type {
|
||||||
|
err!("Sends can't change type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// When updating a file Send, we receive nulls in the File field, as it's immutable,
|
||||||
|
// so we only need to update the data field in the Text case
|
||||||
|
if data.Type == SendType::Text as i32 {
|
||||||
|
let data_str = if let Some(mut d) = data.Text {
|
||||||
|
d.as_object_mut().and_then(|d| d.remove("Response"));
|
||||||
|
serde_json::to_string(&d)?
|
||||||
|
} else {
|
||||||
|
err!("Send data not provided");
|
||||||
|
};
|
||||||
|
send.data = data_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.DeletionDate > Utc::now() + Duration::days(31) {
|
||||||
|
err!(
|
||||||
|
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
send.name = data.Name;
|
||||||
|
send.akey = data.Key;
|
||||||
|
send.deletion_date = data.DeletionDate.naive_utc();
|
||||||
|
send.notes = data.Notes;
|
||||||
|
send.max_access_count = data.MaxAccessCount;
|
||||||
|
send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc());
|
||||||
|
send.disabled = data.Disabled;
|
||||||
|
|
||||||
|
// Only change the value if it's present
|
||||||
|
if let Some(password) = data.Password {
|
||||||
|
send.set_password(Some(&password));
|
||||||
|
}
|
||||||
|
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/sends/<id>")]
|
||||||
|
fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
let send = match Send::find_by_uuid(&id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err!("Send not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||||
|
err!("Send is not owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
send.delete(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendDelete, &headers.user);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/sends/<id>/remove-password")]
|
||||||
|
fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let mut send = match Send::find_by_uuid(&id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err!("Send not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||||
|
err!("Send is not owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
send.set_password(None);
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
@@ -17,11 +17,7 @@ use crate::{
|
|||||||
pub use crate::config::CONFIG;
|
pub use crate::config::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![generate_authenticator, activate_authenticator, activate_authenticator_put,]
|
||||||
generate_authenticator,
|
|
||||||
activate_authenticator,
|
|
||||||
activate_authenticator_put,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-authenticator", data = "<data>")]
|
#[post("/two-factor/get-authenticator", data = "<data>")]
|
||||||
@@ -141,7 +137,7 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, ip: &Cl
|
|||||||
// The amount of steps back and forward in time
|
// The amount of steps back and forward in time
|
||||||
// Also check if we need to disable time drifted TOTP codes.
|
// Also check if we need to disable time drifted TOTP codes.
|
||||||
// If that is the case, we set the steps to 0 so only the current TOTP is valid.
|
// If that is the case, we set the steps to 0 so only the current TOTP is valid.
|
||||||
let steps: i64 = if CONFIG.authenticator_disable_time_drift() { 0 } else { 1 };
|
let steps = !CONFIG.authenticator_disable_time_drift() as i64;
|
||||||
|
|
||||||
for step in -steps..=steps {
|
for step in -steps..=steps {
|
||||||
let time_step = current_timestamp / 30i64 + step;
|
let time_step = current_timestamp / 30i64 + step;
|
||||||
@@ -163,22 +159,11 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, ip: &Cl
|
|||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if generated == totp_code && time_step <= twofactor.last_used as i64 {
|
} else if generated == totp_code && time_step <= twofactor.last_used as i64 {
|
||||||
warn!(
|
warn!("This or a TOTP code within {} steps back and forward has already been used!", steps);
|
||||||
"This or a TOTP code within {} steps back and forward has already been used!",
|
err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip));
|
||||||
steps
|
|
||||||
);
|
|
||||||
err!(format!(
|
|
||||||
"Invalid TOTP code! Server time: {} IP: {}",
|
|
||||||
current_time.format("%F %T UTC"),
|
|
||||||
ip.ip
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else no valide code received, deny access
|
// Else no valide code received, deny access
|
||||||
err!(format!(
|
err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip));
|
||||||
"Invalid TOTP code! Server time: {} IP: {}",
|
|
||||||
current_time.format("%F %T UTC"),
|
|
||||||
ip.ip
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::{
|
|||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
|
util::get_reqwest_client,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +60,11 @@ impl DuoData {
|
|||||||
ik.replace_range(digits.., replaced);
|
ik.replace_range(digits.., replaced);
|
||||||
sk.replace_range(digits.., replaced);
|
sk.replace_range(digits.., replaced);
|
||||||
|
|
||||||
Self { host, ik, sk }
|
Self {
|
||||||
|
host,
|
||||||
|
ik,
|
||||||
|
sk,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +190,7 @@ fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbC
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||||
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
|
use reqwest::{header, Method};
|
||||||
|
|
||||||
use reqwest::{blocking::Client, header::*, Method};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// https://duo.com/docs/authapi#api-details
|
// https://duo.com/docs/authapi#api-details
|
||||||
@@ -199,11 +202,13 @@ fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> Em
|
|||||||
|
|
||||||
let m = Method::from_str(method).unwrap_or_default();
|
let m = Method::from_str(method).unwrap_or_default();
|
||||||
|
|
||||||
Client::new()
|
let client = get_reqwest_client();
|
||||||
|
|
||||||
|
client
|
||||||
.request(m, &url)
|
.request(m, &url)
|
||||||
.basic_auth(username, Some(password))
|
.basic_auth(username, Some(password))
|
||||||
.header(USER_AGENT, AGENT)
|
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
|
||||||
.header(DATE, date)
|
.header(header::DATE, date)
|
||||||
.send()?
|
.send()?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
|
|
||||||
|
|||||||
@@ -125,11 +125,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
|||||||
let twofactor_data = EmailTokenData::new(data.Email, generated_token);
|
let twofactor_data = EmailTokenData::new(data.Email, generated_token);
|
||||||
|
|
||||||
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
||||||
let twofactor = TwoFactor::new(
|
let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json());
|
||||||
user.uuid,
|
|
||||||
TwoFactorType::EmailVerificationChallenge,
|
|
||||||
twofactor_data.to_json(),
|
|
||||||
);
|
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?;
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?;
|
||||||
@@ -186,7 +182,8 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
|||||||
/// Validate the email code when used as TwoFactor token mechanism
|
/// Validate the email code when used as TwoFactor token mechanism
|
||||||
pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult {
|
||||||
let mut email_data = EmailTokenData::from_json(&data)?;
|
let mut email_data = EmailTokenData::from_json(&data)?;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Email as i32, &conn).map_res("Two factor not found")?;
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Email as i32, &conn)
|
||||||
|
.map_res("Two factor not found")?;
|
||||||
let issued_token = match &email_data.last_token {
|
let issued_token = match &email_data.last_token {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
_ => err!("No token available"),
|
_ => err!("No token available"),
|
||||||
|
|||||||
@@ -20,13 +20,7 @@ pub mod u2f;
|
|||||||
pub mod yubikey;
|
pub mod yubikey;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = routes![
|
let mut routes = routes![get_twofactor, get_recover, recover, disable_twofactor, disable_twofactor_put,];
|
||||||
get_twofactor,
|
|
||||||
get_recover,
|
|
||||||
recover,
|
|
||||||
disable_twofactor,
|
|
||||||
disable_twofactor_put,
|
|
||||||
];
|
|
||||||
|
|
||||||
routes.append(&mut authenticator::routes());
|
routes.append(&mut authenticator::routes());
|
||||||
routes.append(&mut duo::routes());
|
routes.append(&mut duo::routes());
|
||||||
@@ -38,15 +32,15 @@ pub fn routes() -> Vec<Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/two-factor")]
|
#[get("/two-factor")]
|
||||||
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
||||||
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": twofactors_json,
|
"Data": twofactors_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-recover", data = "<data>")]
|
#[post("/two-factor/get-recover", data = "<data>")]
|
||||||
|
|||||||
@@ -28,13 +28,7 @@ static APP_ID: Lazy<String> = Lazy::new(|| format!("{}/app-id.json", &CONFIG.dom
|
|||||||
static U2F: Lazy<U2f> = Lazy::new(|| U2f::new(APP_ID.clone()));
|
static U2F: Lazy<U2f> = Lazy::new(|| U2f::new(APP_ID.clone()));
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![generate_u2f, generate_u2f_challenge, activate_u2f, activate_u2f_put, delete_u2f,]
|
||||||
generate_u2f,
|
|
||||||
generate_u2f_challenge,
|
|
||||||
activate_u2f,
|
|
||||||
activate_u2f_put,
|
|
||||||
delete_u2f,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-u2f", data = "<data>")]
|
#[post("/two-factor/get-u2f", data = "<data>")]
|
||||||
@@ -131,12 +125,12 @@ struct RegisterResponseCopy {
|
|||||||
pub error_code: Option<NumberOrString>,
|
pub error_code: Option<NumberOrString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<RegisterResponse> for RegisterResponseCopy {
|
impl From<RegisterResponseCopy> for RegisterResponse {
|
||||||
fn into(self) -> RegisterResponse {
|
fn from(r: RegisterResponseCopy) -> RegisterResponse {
|
||||||
RegisterResponse {
|
RegisterResponse {
|
||||||
registration_data: self.registration_data,
|
registration_data: r.registration_data,
|
||||||
version: self.version,
|
version: r.version,
|
||||||
client_data: self.client_data,
|
client_data: r.client_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,10 +155,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
|
|||||||
|
|
||||||
let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
|
let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
|
||||||
|
|
||||||
let error_code = response
|
let error_code = response.error_code.clone().map_or("0".into(), NumberOrString::into_string);
|
||||||
.error_code
|
|
||||||
.clone()
|
|
||||||
.map_or("0".into(), NumberOrString::into_string);
|
|
||||||
|
|
||||||
if error_code != "0" {
|
if error_code != "0" {
|
||||||
err!("Error registering U2F token")
|
err!("Error registering U2F token")
|
||||||
@@ -300,20 +291,13 @@ fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
|
|||||||
|
|
||||||
let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
|
let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
|
||||||
|
|
||||||
regs.into_iter()
|
regs.into_iter().map(|r| serde_json::from_value(r).unwrap()).map(|Helper(r)| r).collect()
|
||||||
.map(|r| serde_json::from_value(r).unwrap())
|
|
||||||
.map(|Helper(r)| r)
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
|
pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
|
||||||
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
|
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
|
||||||
|
|
||||||
let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
|
let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?.1.into_iter().map(|r| r.reg).collect();
|
||||||
.1
|
|
||||||
.into_iter()
|
|
||||||
.map(|r| r.reg)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if registrations.is_empty() {
|
if registrations.is_empty() {
|
||||||
err!("No U2F devices registered")
|
err!("No U2F devices registered")
|
||||||
|
|||||||
394
src/api/icons.rs
394
src/api/icons.rs
@@ -1,49 +1,107 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
||||||
io::prelude::*,
|
io::prelude::*,
|
||||||
net::{IpAddr, ToSocketAddrs},
|
net::{IpAddr, ToSocketAddrs},
|
||||||
|
sync::RwLock,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{blocking::Client, blocking::Response, header::HeaderMap, Url};
|
use reqwest::{blocking::Client, blocking::Response, header, Url};
|
||||||
use rocket::{http::ContentType, http::Cookie, response::Content, Route};
|
use rocket::{http::ContentType, http::Cookie, response::Content, Route};
|
||||||
use soup::prelude::*;
|
|
||||||
|
|
||||||
use crate::{error::Error, util::Cached, CONFIG};
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
util::{get_reqwest_client_builder, Cached},
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![icon]
|
routes![icon]
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
|
|
||||||
|
|
||||||
const ALLOWED_CHARS: &str = "_-.";
|
|
||||||
|
|
||||||
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||||
|
// Generate the default headers
|
||||||
|
let mut default_headers = header::HeaderMap::new();
|
||||||
|
default_headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15"));
|
||||||
|
default_headers.insert(header::ACCEPT_LANGUAGE, header::HeaderValue::from_static("en-US,en;q=0.8"));
|
||||||
|
default_headers.insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-cache"));
|
||||||
|
default_headers.insert(header::PRAGMA, header::HeaderValue::from_static("no-cache"));
|
||||||
|
default_headers.insert(
|
||||||
|
header::ACCEPT,
|
||||||
|
header::HeaderValue::from_static(
|
||||||
|
"text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Reuse the client between requests
|
// Reuse the client between requests
|
||||||
Client::builder()
|
get_reqwest_client_builder()
|
||||||
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
||||||
.default_headers(_header_map())
|
.default_headers(default_headers)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.expect("Failed to build icon client")
|
||||||
});
|
});
|
||||||
|
|
||||||
static ICON_REL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"icon$|apple.*icon").unwrap());
|
// Build Regex only once since this takes a lot of time.
|
||||||
static ICON_HREF_REGEX: Lazy<Regex> =
|
static ICON_REL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)icon$|apple.*icon").unwrap());
|
||||||
Lazy::new(|| Regex::new(r"(?i)\w+\.(jpg|jpeg|png|ico)(\?.*)?$|^data:image.*base64").unwrap());
|
static ICON_REL_BLACKLIST: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)mask-icon").unwrap());
|
||||||
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
|
static ICON_SIZE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap());
|
||||||
|
|
||||||
|
// Special HashMap which holds the user defined Regex to speedup matching the regex.
|
||||||
|
static ICON_BLACKLIST_REGEX: Lazy<RwLock<HashMap<String, Regex>>> = Lazy::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
#[get("/<domain>/icon.png")]
|
||||||
|
fn icon(domain: String) -> Cached<Content<Vec<u8>>> {
|
||||||
|
const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png");
|
||||||
|
|
||||||
|
if !is_valid_domain(&domain) {
|
||||||
|
warn!("Invalid domain: {}", domain);
|
||||||
|
return Cached::ttl(
|
||||||
|
Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||||
|
CONFIG.icon_cache_negttl(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match get_icon(&domain) {
|
||||||
|
Some((icon, icon_type)) => {
|
||||||
|
Cached::ttl(Content(ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl())
|
||||||
|
}
|
||||||
|
_ => Cached::ttl(Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), CONFIG.icon_cache_negttl()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns if the domain provided is valid or not.
|
||||||
|
///
|
||||||
|
/// This does some manual checks and makes use of Url to do some basic checking.
|
||||||
|
/// domains can't be larger then 63 characters (not counting multiple subdomains) according to the RFC's, but we limit the total size to 255.
|
||||||
fn is_valid_domain(domain: &str) -> bool {
|
fn is_valid_domain(domain: &str) -> bool {
|
||||||
// Don't allow empty or too big domains or path traversal
|
const ALLOWED_CHARS: &str = "_-.";
|
||||||
if domain.is_empty() || domain.len() > 255 || domain.contains("..") {
|
|
||||||
|
// If parsing the domain fails using Url, it will not work with reqwest.
|
||||||
|
if let Err(parse_error) = Url::parse(format!("https://{}", domain).as_str()) {
|
||||||
|
debug!("Domain parse error: '{}' - {:?}", domain, parse_error);
|
||||||
|
return false;
|
||||||
|
} else if domain.is_empty()
|
||||||
|
|| domain.contains("..")
|
||||||
|
|| domain.starts_with('.')
|
||||||
|
|| domain.starts_with('-')
|
||||||
|
|| domain.ends_with('-')
|
||||||
|
{
|
||||||
|
debug!(
|
||||||
|
"Domain validation error: '{}' is either empty, contains '..', starts with an '.', starts or ends with a '-'",
|
||||||
|
domain
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} else if domain.len() > 255 {
|
||||||
|
debug!("Domain validation error: '{}' exceeds 255 characters", domain);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only alphanumeric or specific characters
|
|
||||||
for c in domain.chars() {
|
for c in domain.chars() {
|
||||||
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
|
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
|
||||||
|
debug!("Domain validation error: '{}' contains an invalid character '{}'", domain, c);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,21 +109,10 @@ fn is_valid_domain(domain: &str) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
|
||||||
fn icon(domain: String) -> Cached<Content<Vec<u8>>> {
|
|
||||||
let icon_type = ContentType::new("image", "x-icon");
|
|
||||||
|
|
||||||
if !is_valid_domain(&domain) {
|
|
||||||
warn!("Invalid domain: {:#?}", domain);
|
|
||||||
return Cached::long(Content(icon_type, FALLBACK_ICON.to_vec()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Cached::long(Content(icon_type, get_icon(&domain)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
||||||
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
||||||
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
||||||
|
#[allow(clippy::nonminimal_bool)]
|
||||||
#[cfg(not(feature = "unstable"))]
|
#[cfg(not(feature = "unstable"))]
|
||||||
fn is_global(ip: IpAddr) -> bool {
|
fn is_global(ip: IpAddr) -> bool {
|
||||||
match ip {
|
match ip {
|
||||||
@@ -161,7 +208,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
fn is_domain_blacklisted(domain: &str) -> bool {
|
||||||
let mut is_blacklisted = CONFIG.icon_blacklist_non_global_ips()
|
let mut is_blacklisted = CONFIG.icon_blacklist_non_global_ips()
|
||||||
&& (domain, 0)
|
&& (domain, 0)
|
||||||
.to_socket_addrs()
|
.to_socket_addrs()
|
||||||
@@ -179,7 +226,31 @@ fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
|||||||
// Skip the regex check if the previous one is true already
|
// Skip the regex check if the previous one is true already
|
||||||
if !is_blacklisted {
|
if !is_blacklisted {
|
||||||
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||||
let regex = Regex::new(&blacklist).expect("Valid Regex");
|
let mut regex_hashmap = ICON_BLACKLIST_REGEX.read().unwrap();
|
||||||
|
|
||||||
|
// Use the pre-generate Regex stored in a Lazy HashMap if there's one, else generate it.
|
||||||
|
let regex = if let Some(regex) = regex_hashmap.get(&blacklist) {
|
||||||
|
regex
|
||||||
|
} else {
|
||||||
|
drop(regex_hashmap);
|
||||||
|
|
||||||
|
let mut regex_hashmap_write = ICON_BLACKLIST_REGEX.write().unwrap();
|
||||||
|
// Clear the current list if the previous key doesn't exists.
|
||||||
|
// To prevent growing of the HashMap after someone has changed it via the admin interface.
|
||||||
|
if regex_hashmap_write.len() >= 1 {
|
||||||
|
regex_hashmap_write.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the regex to store in too the Lazy Static HashMap.
|
||||||
|
let blacklist_regex = Regex::new(&blacklist).unwrap();
|
||||||
|
regex_hashmap_write.insert(blacklist.to_string(), blacklist_regex);
|
||||||
|
drop(regex_hashmap_write);
|
||||||
|
|
||||||
|
regex_hashmap = ICON_BLACKLIST_REGEX.read().unwrap();
|
||||||
|
regex_hashmap.get(&blacklist).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the pre-generate Regex stored in a Lazy HashMap.
|
||||||
if regex.is_match(&domain) {
|
if regex.is_match(&domain) {
|
||||||
warn!("Blacklisted domain: {:#?} matched {:#?}", domain, blacklist);
|
warn!("Blacklisted domain: {:#?} matched {:#?}", domain, blacklist);
|
||||||
is_blacklisted = true;
|
is_blacklisted = true;
|
||||||
@@ -190,39 +261,42 @@ fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
|||||||
is_blacklisted
|
is_blacklisted
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_icon(domain: &str) -> Vec<u8> {
|
fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||||
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
||||||
|
|
||||||
|
// Check for expiration of negatively cached copy
|
||||||
|
if icon_is_negcached(&path) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(icon) = get_cached_icon(&path) {
|
if let Some(icon) = get_cached_icon(&path) {
|
||||||
return icon;
|
let icon_type = match get_icon_type(&icon) {
|
||||||
|
Some(x) => x,
|
||||||
|
_ => "x-icon",
|
||||||
|
};
|
||||||
|
return Some((icon, icon_type.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.disable_icon_download() {
|
if CONFIG.disable_icon_download() {
|
||||||
return FALLBACK_ICON.to_vec();
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the icon, or fallback in case of error
|
// Get the icon, or None in case of error
|
||||||
match download_icon(&domain) {
|
match download_icon(&domain) {
|
||||||
Ok(icon) => {
|
Ok((icon, icon_type)) => {
|
||||||
save_icon(&path, &icon);
|
save_icon(&path, &icon);
|
||||||
icon
|
Some((icon, icon_type.unwrap_or("x-icon").to_string()))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error downloading icon: {:?}", e);
|
error!("Error downloading icon: {:?}", e);
|
||||||
let miss_indicator = path + ".miss";
|
let miss_indicator = path + ".miss";
|
||||||
let empty_icon = Vec::new();
|
save_icon(&miss_indicator, &[]);
|
||||||
save_icon(&miss_indicator, &empty_icon);
|
None
|
||||||
FALLBACK_ICON.to_vec()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
||||||
// Check for expiration of negatively cached copy
|
|
||||||
if icon_is_negcached(path) {
|
|
||||||
return Some(FALLBACK_ICON.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for expiration of successfully cached copy
|
// Check for expiration of successfully cached copy
|
||||||
if icon_is_expired(path) {
|
if icon_is_expired(path) {
|
||||||
return None;
|
return None;
|
||||||
@@ -272,7 +346,6 @@ fn icon_is_expired(path: &str) -> bool {
|
|||||||
expired.unwrap_or(true)
|
expired.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Icon {
|
struct Icon {
|
||||||
priority: u8,
|
priority: u8,
|
||||||
href: String,
|
href: String,
|
||||||
@@ -280,10 +353,63 @@ struct Icon {
|
|||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
const fn new(priority: u8, href: String) -> Self {
|
const fn new(priority: u8, href: String) -> Self {
|
||||||
Self { href, priority }
|
Self {
|
||||||
|
href,
|
||||||
|
priority,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_favicons_node(node: &std::rc::Rc<markup5ever_rcdom::Node>, icons: &mut Vec<Icon>, url: &Url) {
|
||||||
|
if let markup5ever_rcdom::NodeData::Element {
|
||||||
|
name,
|
||||||
|
attrs,
|
||||||
|
..
|
||||||
|
} = &node.data
|
||||||
|
{
|
||||||
|
if name.local.as_ref() == "link" {
|
||||||
|
let mut has_rel = false;
|
||||||
|
let mut href = None;
|
||||||
|
let mut sizes = None;
|
||||||
|
|
||||||
|
let attrs = attrs.borrow();
|
||||||
|
for attr in attrs.iter() {
|
||||||
|
let attr_name = attr.name.local.as_ref();
|
||||||
|
let attr_value = attr.value.as_ref();
|
||||||
|
|
||||||
|
if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) && !ICON_REL_BLACKLIST.is_match(attr_value)
|
||||||
|
{
|
||||||
|
has_rel = true;
|
||||||
|
} else if attr_name == "href" {
|
||||||
|
href = Some(attr_value);
|
||||||
|
} else if attr_name == "sizes" {
|
||||||
|
sizes = Some(attr_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_rel {
|
||||||
|
if let Some(inner_href) = href {
|
||||||
|
if let Ok(full_href) = url.join(&inner_href).map(|h| h.into_string()) {
|
||||||
|
let priority = get_icon_priority(&full_href, sizes);
|
||||||
|
icons.push(Icon::new(priority, full_href));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Might want to limit the recursion depth?
|
||||||
|
for child in node.children.borrow().iter() {
|
||||||
|
get_favicons_node(child, icons, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IconUrlResult {
|
||||||
|
iconlist: Vec<Icon>,
|
||||||
|
cookies: String,
|
||||||
|
referer: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response.
|
/// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response.
|
||||||
/// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies.
|
/// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies.
|
||||||
/// This does not mean that that location does exists, but it is the default location browser use.
|
/// This does not mean that that location does exists, but it is the default location browser use.
|
||||||
@@ -296,24 +422,65 @@ impl Icon {
|
|||||||
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
|
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
|
||||||
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
|
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
||||||
// Default URL with secure and insecure schemes
|
// Default URL with secure and insecure schemes
|
||||||
let ssldomain = format!("https://{}", domain);
|
let ssldomain = format!("https://{}", domain);
|
||||||
let httpdomain = format!("http://{}", domain);
|
let httpdomain = format!("http://{}", domain);
|
||||||
|
|
||||||
|
// First check the domain as given during the request for both HTTPS and HTTP.
|
||||||
|
let resp = match get_page(&ssldomain).or_else(|_| get_page(&httpdomain)) {
|
||||||
|
Ok(c) => Ok(c),
|
||||||
|
Err(e) => {
|
||||||
|
let mut sub_resp = Err(e);
|
||||||
|
|
||||||
|
// When the domain is not an IP, and has more then one dot, remove all subdomains.
|
||||||
|
let is_ip = domain.parse::<IpAddr>();
|
||||||
|
if is_ip.is_err() && domain.matches('.').count() > 1 {
|
||||||
|
let mut domain_parts = domain.split('.');
|
||||||
|
let base_domain = format!(
|
||||||
|
"{base}.{tld}",
|
||||||
|
tld = domain_parts.next_back().unwrap(),
|
||||||
|
base = domain_parts.next_back().unwrap()
|
||||||
|
);
|
||||||
|
if is_valid_domain(&base_domain) {
|
||||||
|
let sslbase = format!("https://{}", base_domain);
|
||||||
|
let httpbase = format!("http://{}", base_domain);
|
||||||
|
debug!("[get_icon_url]: Trying without subdomains '{}'", base_domain);
|
||||||
|
|
||||||
|
sub_resp = get_page(&sslbase).or_else(|_| get_page(&httpbase));
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.
|
||||||
|
} else if is_ip.is_err() && domain.matches('.').count() < 2 {
|
||||||
|
let www_domain = format!("www.{}", domain);
|
||||||
|
if is_valid_domain(&www_domain) {
|
||||||
|
let sslwww = format!("https://{}", www_domain);
|
||||||
|
let httpwww = format!("http://{}", www_domain);
|
||||||
|
debug!("[get_icon_url]: Trying with www. prefix '{}'", www_domain);
|
||||||
|
|
||||||
|
sub_resp = get_page(&sslwww).or_else(|_| get_page(&httpwww));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_resp
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create the iconlist
|
// Create the iconlist
|
||||||
let mut iconlist: Vec<Icon> = Vec::new();
|
let mut iconlist: Vec<Icon> = Vec::new();
|
||||||
|
|
||||||
// Create the cookie_str to fill it all the cookies from the response
|
// Create the cookie_str to fill it all the cookies from the response
|
||||||
// These cookies can be used to request/download the favicon image.
|
// These cookies can be used to request/download the favicon image.
|
||||||
// Some sites have extra security in place with for example XSRF Tokens.
|
// Some sites have extra security in place with for example XSRF Tokens.
|
||||||
let mut cookie_str = String::new();
|
let mut cookie_str = "".to_string();
|
||||||
|
let mut referer = "".to_string();
|
||||||
|
|
||||||
let resp = get_page(&ssldomain).or_else(|_| get_page(&httpdomain));
|
|
||||||
if let Ok(content) = resp {
|
if let Ok(content) = resp {
|
||||||
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
||||||
let url = content.url().clone();
|
let url = content.url().clone();
|
||||||
|
|
||||||
|
// Get all the cookies and pass it on to the next function.
|
||||||
|
// Needed for XSRF Cookies for example (like @ mijn.ing.nl)
|
||||||
let raw_cookies = content.headers().get_all("set-cookie");
|
let raw_cookies = content.headers().get_all("set-cookie");
|
||||||
cookie_str = raw_cookies
|
cookie_str = raw_cookies
|
||||||
.iter()
|
.iter()
|
||||||
@@ -327,31 +494,23 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
|||||||
})
|
})
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
|
||||||
|
// Set the referer to be used on the final request, some sites check this.
|
||||||
|
// Mostly used to prevent direct linking and other security resons.
|
||||||
|
referer = url.as_str().to_string();
|
||||||
|
|
||||||
// Add the default favicon.ico to the list with the domain the content responded from.
|
// Add the default favicon.ico to the list with the domain the content responded from.
|
||||||
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
||||||
|
|
||||||
// 512KB should be more than enough for the HTML, though as we only really need
|
// 512KB should be more than enough for the HTML, though as we only really need
|
||||||
// the HTML header, it could potentially be reduced even further
|
// the HTML header, it could potentially be reduced even further
|
||||||
let limited_reader = content.take(512 * 1024);
|
let mut limited_reader = content.take(512 * 1024);
|
||||||
|
|
||||||
let soup = Soup::from_reader(limited_reader)?;
|
use html5ever::tendril::TendrilSink;
|
||||||
// Search for and filter
|
let dom = html5ever::parse_document(markup5ever_rcdom::RcDom::default(), Default::default())
|
||||||
let favicons = soup
|
.from_utf8()
|
||||||
.tag("link")
|
.read_from(&mut limited_reader)?;
|
||||||
.attr("rel", ICON_REL_REGEX.clone()) // Only use icon rels
|
|
||||||
.attr("href", ICON_HREF_REGEX.clone()) // Only allow specific extensions
|
|
||||||
.find_all();
|
|
||||||
|
|
||||||
// Loop through all the found icons and determine it's priority
|
get_favicons_node(&dom.document, &mut iconlist, &url);
|
||||||
for favicon in favicons {
|
|
||||||
let sizes = favicon.get("sizes");
|
|
||||||
let href = favicon.get("href").expect("Missing href");
|
|
||||||
let full_href = url.join(&href).unwrap().into_string();
|
|
||||||
|
|
||||||
let priority = get_icon_priority(&full_href, sizes);
|
|
||||||
|
|
||||||
iconlist.push(Icon::new(priority, full_href))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Add the default favicon.ico to the list with just the given domain
|
// Add the default favicon.ico to the list with just the given domain
|
||||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
||||||
@@ -362,28 +521,31 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
|||||||
iconlist.sort_by_key(|x| x.priority);
|
iconlist.sort_by_key(|x| x.priority);
|
||||||
|
|
||||||
// There always is an icon in the list, so no need to check if it exists, and just return the first one
|
// There always is an icon in the list, so no need to check if it exists, and just return the first one
|
||||||
Ok((iconlist, cookie_str))
|
Ok(IconUrlResult {
|
||||||
|
iconlist,
|
||||||
|
cookies: cookie_str,
|
||||||
|
referer,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_page(url: &str) -> Result<Response, Error> {
|
fn get_page(url: &str) -> Result<Response, Error> {
|
||||||
get_page_with_cookies(url, "")
|
get_page_with_cookies(url, "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> {
|
fn get_page_with_cookies(url: &str, cookie_str: &str, referer: &str) -> Result<Response, Error> {
|
||||||
if check_icon_domain_is_blacklisted(Url::parse(url).unwrap().host_str().unwrap_or_default()) {
|
if is_domain_blacklisted(Url::parse(url).unwrap().host_str().unwrap_or_default()) {
|
||||||
err!("Favicon rel linked to a non blacklisted domain!");
|
err!("Favicon rel linked to a blacklisted domain!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookie_str.is_empty() {
|
let mut client = CLIENT.get(url);
|
||||||
CLIENT.get(url).send()?.error_for_status().map_err(Into::into)
|
if !cookie_str.is_empty() {
|
||||||
} else {
|
client = client.header("Cookie", cookie_str)
|
||||||
CLIENT
|
|
||||||
.get(url)
|
|
||||||
.header("cookie", cookie_str)
|
|
||||||
.send()?
|
|
||||||
.error_for_status()
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
if !referer.is_empty() {
|
||||||
|
client = client.header("Referer", referer)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.send()?.error_for_status().map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Integer with the priority of the type of the icon which to prefer.
|
/// Returns a Integer with the priority of the type of the icon which to prefer.
|
||||||
@@ -398,7 +560,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error>
|
|||||||
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
||||||
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
fn get_icon_priority(href: &str, sizes: Option<&str>) -> u8 {
|
||||||
// Check if there is a dimension set
|
// Check if there is a dimension set
|
||||||
let (width, height) = parse_sizes(sizes);
|
let (width, height) = parse_sizes(sizes);
|
||||||
|
|
||||||
@@ -411,7 +573,7 @@ fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
|||||||
1
|
1
|
||||||
} else if width == 64 {
|
} else if width == 64 {
|
||||||
2
|
2
|
||||||
} else if width >= 24 && width <= 128 {
|
} else if (24..=128).contains(&width) {
|
||||||
3
|
3
|
||||||
} else if width == 16 {
|
} else if width == 16 {
|
||||||
4
|
4
|
||||||
@@ -446,7 +608,7 @@ fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
|||||||
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
||||||
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
||||||
/// ```
|
/// ```
|
||||||
fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
fn parse_sizes(sizes: Option<&str>) -> (u16, u16) {
|
||||||
let mut width: u16 = 0;
|
let mut width: u16 = 0;
|
||||||
let mut height: u16 = 0;
|
let mut height: u16 = 0;
|
||||||
|
|
||||||
@@ -465,18 +627,19 @@ fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
|||||||
(width, height)
|
(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
fn download_icon(domain: &str) -> Result<(Vec<u8>, Option<&str>), Error> {
|
||||||
if check_icon_domain_is_blacklisted(domain) {
|
if is_domain_blacklisted(domain) {
|
||||||
err!("Domain is blacklisted", domain)
|
err!("Domain is blacklisted", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (iconlist, cookie_str) = get_icon_url(&domain)?;
|
let icon_result = get_icon_url(&domain)?;
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
let mut icon_type: Option<&str> = None;
|
||||||
|
|
||||||
use data_url::DataUrl;
|
use data_url::DataUrl;
|
||||||
|
|
||||||
for icon in iconlist.iter().take(5) {
|
for icon in icon_result.iconlist.iter().take(5) {
|
||||||
if icon.href.starts_with("data:image") {
|
if icon.href.starts_with("data:image") {
|
||||||
let datauri = DataUrl::process(&icon.href).unwrap();
|
let datauri = DataUrl::process(&icon.href).unwrap();
|
||||||
// Check if we are able to decode the data uri
|
// Check if we are able to decode the data uri
|
||||||
@@ -484,29 +647,43 @@ fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
|||||||
Ok((body, _fragment)) => {
|
Ok((body, _fragment)) => {
|
||||||
// Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create
|
// Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create
|
||||||
if body.len() >= 67 {
|
if body.len() >= 67 {
|
||||||
|
// Check if the icon type is allowed, else try an icon from the list.
|
||||||
|
icon_type = get_icon_type(&body);
|
||||||
|
if icon_type.is_none() {
|
||||||
|
debug!("Icon from {} data:image uri, is not a valid image type", domain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info!("Extracted icon from data:image uri for {}", domain);
|
||||||
buffer = body;
|
buffer = body;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => warn!("data uri is invalid"),
|
_ => warn!("Extracted icon from data:image uri is invalid"),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match get_page_with_cookies(&icon.href, &cookie_str) {
|
match get_page_with_cookies(&icon.href, &icon_result.cookies, &icon_result.referer) {
|
||||||
Ok(mut res) => {
|
Ok(mut res) => {
|
||||||
info!("Downloaded icon from {}", icon.href);
|
|
||||||
res.copy_to(&mut buffer)?;
|
res.copy_to(&mut buffer)?;
|
||||||
|
// Check if the icon type is allowed, else try an icon from the list.
|
||||||
|
icon_type = get_icon_type(&buffer);
|
||||||
|
if icon_type.is_none() {
|
||||||
|
buffer.clear();
|
||||||
|
debug!("Icon from {}, is not a valid image type", icon.href);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info!("Downloaded icon from {}", icon.href);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(_) => info!("Download failed for {}", icon.href),
|
_ => warn!("Download failed for {}", icon.href),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
err!("Empty response")
|
err!("Empty response downloading icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(buffer)
|
Ok((buffer, icon_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_icon(path: &str, icon: &[u8]) {
|
fn save_icon(path: &str, icon: &[u8]) {
|
||||||
@@ -518,29 +695,18 @@ fn save_icon(path: &str, icon: &[u8]) {
|
|||||||
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache");
|
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Icon save error: {:?}", e);
|
warn!("Icon save error: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _header_map() -> HeaderMap {
|
fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
|
||||||
// Set some default headers for the request.
|
match bytes {
|
||||||
// Use a browser like user-agent to make sure most websites will return there correct website.
|
[137, 80, 78, 71, ..] => Some("png"),
|
||||||
use reqwest::header::*;
|
[0, 0, 1, 0, ..] => Some("x-icon"),
|
||||||
|
[82, 73, 70, 70, ..] => Some("webp"),
|
||||||
macro_rules! headers {
|
[255, 216, 255, ..] => Some("jpeg"),
|
||||||
($( $name:ident : $value:literal),+ $(,)? ) => {
|
[66, 77, ..] => Some("bmp"),
|
||||||
let mut headers = HeaderMap::new();
|
_ => None,
|
||||||
$( headers.insert($name, HeaderValue::from_static($value)); )+
|
|
||||||
headers
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
headers! {
|
|
||||||
USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299",
|
|
||||||
ACCEPT_LANGUAGE: "en-US,en;q=0.8",
|
|
||||||
CACHE_CONTROL: "no-cache",
|
|
||||||
PRAGMA: "no-cache",
|
|
||||||
ACCEPT: "text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
|||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||||
"scope": "api offline_access"
|
"scope": "api offline_access",
|
||||||
|
"unofficialServer": true,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,26 +88,28 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
let username = data.username.as_ref().unwrap();
|
let username = data.username.as_ref().unwrap();
|
||||||
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!(
|
None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)),
|
||||||
"Username or password is incorrect. Try again",
|
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check password
|
// Check password
|
||||||
let password = data.password.as_ref().unwrap();
|
let password = data.password.as_ref().unwrap();
|
||||||
if !user.check_valid_password(password) {
|
if !user.check_valid_password(password) {
|
||||||
err!(
|
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username))
|
||||||
"Username or password is incorrect. Try again",
|
}
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username)
|
|
||||||
)
|
// Check if the user is disabled
|
||||||
|
if !user.enabled {
|
||||||
|
err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
|
|
||||||
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||||
let now = now.naive_utc();
|
let now = now.naive_utc();
|
||||||
if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 {
|
if user.last_verifying_at.is_none()
|
||||||
|
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
|
||||||
|
> CONFIG.signups_verify_resend_time() as i64
|
||||||
|
{
|
||||||
let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
|
let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
|
||||||
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
||||||
// We want to send another email verification if we require signups to verify
|
// We want to send another email verification if we require signups to verify
|
||||||
@@ -126,10 +129,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We still want the login to fail until they actually verified the email address
|
// We still want the login to fail until they actually verified the email address
|
||||||
err!(
|
err!("Please verify your email before trying again.", format!("IP: {}. Username: {}.", ip.ip, username))
|
||||||
"Please verify your email before trying again.",
|
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, &conn, &user);
|
let (mut device, new_device) = get_device(&data, &conn, &user);
|
||||||
@@ -164,7 +164,8 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
|||||||
"Kdf": user.client_kdf_type,
|
"Kdf": user.client_kdf_type,
|
||||||
"KdfIterations": user.client_kdf_iter,
|
"KdfIterations": user.client_kdf_iter,
|
||||||
"ResetMasterPassword": false,// TODO: Same as above
|
"ResetMasterPassword": false,// TODO: Same as above
|
||||||
"scope": "api offline_access"
|
"scope": "api offline_access",
|
||||||
|
"unofficialServer": true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(token) = twofactor_token {
|
if let Some(token) = twofactor_token {
|
||||||
@@ -226,9 +227,7 @@ fn twofactor_auth(
|
|||||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA token not provided"),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA token not provided"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors
|
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
.into_iter()
|
|
||||||
.find(|tf| tf.atype == selected_id && tf.enabled);
|
|
||||||
|
|
||||||
use crate::api::core::two_factor as _tf;
|
use crate::api::core::two_factor as _tf;
|
||||||
use crate::crypto::ct_eq;
|
use crate::crypto::ct_eq;
|
||||||
@@ -237,18 +236,26 @@ fn twofactor_auth(
|
|||||||
let mut remember = data.two_factor_remember.unwrap_or(0);
|
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||||
|
|
||||||
match TwoFactorType::from_i32(selected_id) {
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)?,
|
Some(TwoFactorType::Authenticator) => {
|
||||||
|
_tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)?
|
||||||
|
}
|
||||||
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||||
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||||
Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
Some(TwoFactorType::Duo) => {
|
||||||
Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)?,
|
_tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?
|
||||||
|
}
|
||||||
|
Some(TwoFactorType::Email) => {
|
||||||
|
_tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)?
|
||||||
|
}
|
||||||
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||||
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||||
}
|
}
|
||||||
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided"),
|
_ => {
|
||||||
|
err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => err!("Invalid two factor provider"),
|
_ => err!("Invalid two factor provider"),
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use serde_json::Value;
|
|||||||
|
|
||||||
pub use crate::api::{
|
pub use crate::api::{
|
||||||
admin::routes as admin_routes,
|
admin::routes as admin_routes,
|
||||||
|
core::purge_sends,
|
||||||
|
core::purge_trashed_ciphers,
|
||||||
core::routes as core_routes,
|
core::routes as core_routes,
|
||||||
icons::routes as icons_routes,
|
icons::routes as icons_routes,
|
||||||
identity::routes as identity_routes,
|
identity::routes as identity_routes,
|
||||||
@@ -53,9 +55,9 @@ impl NumberOrString {
|
|||||||
use std::num::ParseIntError as PIE;
|
use std::num::ParseIntError as PIE;
|
||||||
match self {
|
match self {
|
||||||
NumberOrString::Number(n) => Ok(n),
|
NumberOrString::Number(n) => Ok(n),
|
||||||
NumberOrString::String(s) => s
|
NumberOrString::String(s) => {
|
||||||
.parse()
|
s.parse().map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string()))
|
||||||
.map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ use rocket::Route;
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::{
|
use crate::{api::EmptyResult, auth::Headers, db::DbConn, Error, CONFIG};
|
||||||
api::{EmptyResult, JsonResult},
|
|
||||||
auth::Headers,
|
|
||||||
db::DbConn,
|
|
||||||
Error, CONFIG,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![negotiate, websockets_err]
|
routes![negotiate, websockets_err]
|
||||||
@@ -19,12 +14,15 @@ static SHOW_WEBSOCKETS_MSG: AtomicBool = AtomicBool::new(true);
|
|||||||
|
|
||||||
#[get("/hub")]
|
#[get("/hub")]
|
||||||
fn websockets_err() -> EmptyResult {
|
fn websockets_err() -> EmptyResult {
|
||||||
if CONFIG.websocket_enabled() && SHOW_WEBSOCKETS_MSG.compare_and_swap(true, false, Ordering::Relaxed) {
|
if CONFIG.websocket_enabled()
|
||||||
|
&& SHOW_WEBSOCKETS_MSG.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed).is_ok()
|
||||||
|
{
|
||||||
err!(
|
err!(
|
||||||
"###########################################################
|
"
|
||||||
|
###########################################################
|
||||||
'/notifications/hub' should be proxied to the websocket server or notifications won't work.
|
'/notifications/hub' should be proxied to the websocket server or notifications won't work.
|
||||||
Go to the Wiki for more info, or disable WebSockets setting WEBSOCKET_ENABLED=false.
|
Go to the Wiki for more info, or disable WebSockets setting WEBSOCKET_ENABLED=false.
|
||||||
###########################################################################################"
|
###########################################################################################\n"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::empty())
|
Err(Error::empty())
|
||||||
@@ -32,7 +30,7 @@ fn websockets_err() -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/hub/negotiate")]
|
#[post("/hub/negotiate")]
|
||||||
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
fn negotiate(_headers: Headers, _conn: DbConn) -> Json<JsonValue> {
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::BASE64URL;
|
||||||
|
|
||||||
@@ -48,10 +46,10 @@ fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
|||||||
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
||||||
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
|
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
|
||||||
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"connectionId": conn_id,
|
"connectionId": conn_id,
|
||||||
"availableTransports": available_transports
|
"availableTransports": available_transports
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -121,7 +119,7 @@ fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Server WebSocket handler
|
// Server WebSocket handler
|
||||||
pub struct WSHandler {
|
pub struct WsHandler {
|
||||||
out: Sender,
|
out: Sender,
|
||||||
user_uuid: Option<String>,
|
user_uuid: Option<String>,
|
||||||
users: WebSocketUsers,
|
users: WebSocketUsers,
|
||||||
@@ -141,7 +139,7 @@ const PING: Token = Token(1);
|
|||||||
|
|
||||||
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
||||||
|
|
||||||
impl WSHandler {
|
impl WsHandler {
|
||||||
fn err(&self, msg: &'static str) -> ws::Result<()> {
|
fn err(&self, msg: &'static str) -> ws::Result<()> {
|
||||||
self.out.close(ws::CloseCode::Invalid)?;
|
self.out.close(ws::CloseCode::Invalid)?;
|
||||||
|
|
||||||
@@ -167,8 +165,8 @@ impl WSHandler {
|
|||||||
if let Some(params) = path.split('?').nth(1) {
|
if let Some(params) = path.split('?').nth(1) {
|
||||||
let params_iter = params.split('&').take(1);
|
let params_iter = params.split('&').take(1);
|
||||||
for val in params_iter {
|
for val in params_iter {
|
||||||
if val.starts_with(ACCESS_TOKEN_KEY) {
|
if let Some(stripped) = val.strip_prefix(ACCESS_TOKEN_KEY) {
|
||||||
return Some(val[ACCESS_TOKEN_KEY.len()..].into());
|
return Some(stripped.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -177,7 +175,7 @@ impl WSHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler for WSHandler {
|
impl Handler for WsHandler {
|
||||||
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
||||||
// Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
|
// Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
|
||||||
//
|
//
|
||||||
@@ -205,9 +203,7 @@ impl Handler for WSHandler {
|
|||||||
let handler_insert = self.out.clone();
|
let handler_insert = self.out.clone();
|
||||||
let handler_update = self.out.clone();
|
let handler_update = self.out.clone();
|
||||||
|
|
||||||
self.users
|
self.users.map.upsert(user_uuid, || vec![handler_insert], |ref mut v| v.push(handler_update));
|
||||||
.map
|
|
||||||
.upsert(user_uuid, || vec![handler_insert], |ref mut v| v.push(handler_update));
|
|
||||||
|
|
||||||
// Schedule a ping to keep the connection alive
|
// Schedule a ping to keep the connection alive
|
||||||
self.out.timeout(PING_MS, PING)
|
self.out.timeout(PING_MS, PING)
|
||||||
@@ -217,7 +213,11 @@ impl Handler for WSHandler {
|
|||||||
if let Message::Text(text) = msg.clone() {
|
if let Message::Text(text) = msg.clone() {
|
||||||
let json = &text[..text.len() - 1]; // Remove last char
|
let json = &text[..text.len() - 1]; // Remove last char
|
||||||
|
|
||||||
if let Ok(InitialMessage { protocol, version }) = from_str::<InitialMessage>(json) {
|
if let Ok(InitialMessage {
|
||||||
|
protocol,
|
||||||
|
version,
|
||||||
|
}) = from_str::<InitialMessage>(json)
|
||||||
|
{
|
||||||
if &protocol == "messagepack" && version == 1 {
|
if &protocol == "messagepack" && version == 1 {
|
||||||
return self.out.send(&INITIAL_RESPONSE[..]); // Respond to initial message
|
return self.out.send(&INITIAL_RESPONSE[..]); // Respond to initial message
|
||||||
}
|
}
|
||||||
@@ -241,13 +241,13 @@ impl Handler for WSHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WSFactory {
|
struct WsFactory {
|
||||||
pub users: WebSocketUsers,
|
pub users: WebSocketUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WSFactory {
|
impl WsFactory {
|
||||||
pub fn init() -> Self {
|
pub fn init() -> Self {
|
||||||
WSFactory {
|
WsFactory {
|
||||||
users: WebSocketUsers {
|
users: WebSocketUsers {
|
||||||
map: Arc::new(CHashMap::new()),
|
map: Arc::new(CHashMap::new()),
|
||||||
},
|
},
|
||||||
@@ -255,11 +255,11 @@ impl WSFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Factory for WSFactory {
|
impl Factory for WsFactory {
|
||||||
type Handler = WSHandler;
|
type Handler = WsHandler;
|
||||||
|
|
||||||
fn connection_made(&mut self, out: Sender) -> Self::Handler {
|
fn connection_made(&mut self, out: Sender) -> Self::Handler {
|
||||||
WSHandler {
|
WsHandler {
|
||||||
out,
|
out,
|
||||||
user_uuid: None,
|
user_uuid: None,
|
||||||
users: self.users.clone(),
|
users: self.users.clone(),
|
||||||
@@ -296,10 +296,7 @@ impl WebSocketUsers {
|
|||||||
// NOTE: The last modified date needs to be updated before calling these methods
|
// NOTE: The last modified date needs to be updated before calling these methods
|
||||||
pub fn send_user_update(&self, ut: UpdateType, user: &User) {
|
pub fn send_user_update(&self, ut: UpdateType, user: &User) {
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![
|
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||||
("UserId".into(), user.uuid.clone().into()),
|
|
||||||
("Date".into(), serialize_date(user.updated_at)),
|
|
||||||
],
|
|
||||||
ut,
|
ut,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -395,6 +392,10 @@ pub enum UpdateType {
|
|||||||
|
|
||||||
LogOut = 11,
|
LogOut = 11,
|
||||||
|
|
||||||
|
SyncSendCreate = 12,
|
||||||
|
SyncSendUpdate = 13,
|
||||||
|
SyncSendDelete = 14,
|
||||||
|
|
||||||
None = 100,
|
None = 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,15 +403,17 @@ use rocket::State;
|
|||||||
pub type Notify<'a> = State<'a, WebSocketUsers>;
|
pub type Notify<'a> = State<'a, WebSocketUsers>;
|
||||||
|
|
||||||
pub fn start_notification_server() -> WebSocketUsers {
|
pub fn start_notification_server() -> WebSocketUsers {
|
||||||
let factory = WSFactory::init();
|
let factory = WsFactory::init();
|
||||||
let users = factory.users.clone();
|
let users = factory.users.clone();
|
||||||
|
|
||||||
if CONFIG.websocket_enabled() {
|
if CONFIG.websocket_enabled() {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut settings = ws::Settings::default();
|
let settings = ws::Settings {
|
||||||
settings.max_connections = 500;
|
max_connections: 500,
|
||||||
settings.queue_size = 2;
|
queue_size: 2,
|
||||||
settings.panic_on_internal = false;
|
panic_on_internal: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
ws::Builder::new()
|
ws::Builder::new()
|
||||||
.with_settings(settings)
|
.with_settings(settings)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
// If addding more routes here, consider also adding them to
|
// If addding more routes here, consider also adding them to
|
||||||
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||||
if CONFIG.web_vault_enabled() {
|
if CONFIG.web_vault_enabled() {
|
||||||
routes![web_index, app_id, web_files, attachments, alive, static_files]
|
routes![web_index, app_id, web_files, attachments, sends, alive, static_files]
|
||||||
} else {
|
} else {
|
||||||
routes![attachments, alive, static_files]
|
routes![attachments, alive, static_files]
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,11 @@ fn attachments(uuid: String, file: PathBuf) -> Option<NamedFile> {
|
|||||||
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok()
|
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/sends/<send_id>/<file_id>")]
|
||||||
|
fn sends(send_id: String, file_id: String) -> Option<NamedFile> {
|
||||||
|
NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/alive")]
|
#[get("/alive")]
|
||||||
fn alive() -> Json<String> {
|
fn alive() -> Json<String> {
|
||||||
use crate::util::format_date;
|
use crate::util::format_date;
|
||||||
@@ -78,12 +83,15 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
|
|||||||
"hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
|
"hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
|
||||||
|
|
||||||
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||||
"bootstrap-native.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
"bootstrap-native.js" => {
|
||||||
"md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.js"))),
|
Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js")))
|
||||||
|
}
|
||||||
"identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
|
"identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
|
||||||
"datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
"datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
||||||
"datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
"datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
||||||
"jquery-3.5.1.slim.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.5.1.slim.js"))),
|
"jquery-3.5.1.slim.js" => {
|
||||||
|
Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.5.1.slim.js")))
|
||||||
|
}
|
||||||
_ => err!(format!("Static file not found: {}", filename)),
|
_ => err!(format!("Static file not found: {}", filename)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
249
src/auth.rs
249
src/auth.rs
@@ -58,28 +58,28 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
|
|||||||
.map_res("Error decoding JWT")
|
.map_res("Error decoding JWT")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_login(token: &str) -> Result<LoginJWTClaims, Error> {
|
pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_invite(token: &str) -> Result<InviteJWTClaims, Error> {
|
pub fn decode_invite(token: &str) -> Result<InviteJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_delete(token: &str) -> Result<DeleteJWTClaims, Error> {
|
pub fn decode_delete(token: &str) -> Result<DeleteJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_DELETE_ISSUER.to_string())
|
decode_jwt(token, JWT_DELETE_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_verify_email(token: &str) -> Result<VerifyEmailJWTClaims, Error> {
|
pub fn decode_verify_email(token: &str) -> Result<VerifyEmailJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())
|
decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_admin(token: &str) -> Result<AdminJWTClaims, Error> {
|
pub fn decode_admin(token: &str) -> Result<AdminJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJWTClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -110,7 +110,7 @@ pub struct LoginJWTClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct InviteJWTClaims {
|
pub struct InviteJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -132,9 +132,9 @@ pub fn generate_invite_claims(
|
|||||||
org_id: Option<String>,
|
org_id: Option<String>,
|
||||||
user_org_id: Option<String>,
|
user_org_id: Option<String>,
|
||||||
invited_by_email: Option<String>,
|
invited_by_email: Option<String>,
|
||||||
) -> InviteJWTClaims {
|
) -> InviteJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
InviteJWTClaims {
|
InviteJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_INVITE_ISSUER.to_string(),
|
iss: JWT_INVITE_ISSUER.to_string(),
|
||||||
@@ -147,7 +147,7 @@ pub fn generate_invite_claims(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct DeleteJWTClaims {
|
pub struct DeleteJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -158,9 +158,9 @@ pub struct DeleteJWTClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
|
pub fn generate_delete_claims(uuid: String) -> DeleteJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
DeleteJWTClaims {
|
DeleteJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_DELETE_ISSUER.to_string(),
|
iss: JWT_DELETE_ISSUER.to_string(),
|
||||||
@@ -169,7 +169,7 @@ pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct VerifyEmailJWTClaims {
|
pub struct VerifyEmailJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -180,9 +180,9 @@ pub struct VerifyEmailJWTClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
|
pub fn generate_verify_email_claims(uuid: String) -> DeleteJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
DeleteJWTClaims {
|
DeleteJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||||
@@ -191,7 +191,7 @@ pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AdminJWTClaims {
|
pub struct AdminJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -202,9 +202,9 @@ pub struct AdminJWTClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_admin_claims() -> AdminJWTClaims {
|
pub fn generate_admin_claims() -> AdminJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
AdminJWTClaims {
|
AdminJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::minutes(20)).timestamp(),
|
exp: (time_now + Duration::minutes(20)).timestamp(),
|
||||||
iss: JWT_ADMIN_ISSUER.to_string(),
|
iss: JWT_ADMIN_ISSUER.to_string(),
|
||||||
@@ -215,22 +215,18 @@ pub fn generate_admin_claims() -> AdminJWTClaims {
|
|||||||
//
|
//
|
||||||
// Bearer token authentication
|
// Bearer token authentication
|
||||||
//
|
//
|
||||||
use rocket::{
|
use rocket::request::{FromRequest, Outcome, Request};
|
||||||
request::{FromRequest, Request, Outcome},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization},
|
models::{CollectionUser, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
|
||||||
DbConn,
|
DbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Headers {
|
pub struct Host {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub device: Device,
|
|
||||||
pub user: User,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
impl<'a, 'r> FromRequest<'a, 'r> for Host {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
@@ -264,6 +260,30 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
|||||||
format!("{}://{}", protocol, host)
|
format!("{}://{}", protocol, host)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Outcome::Success(Host {
|
||||||
|
host,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Headers {
|
||||||
|
pub host: String,
|
||||||
|
pub device: Device,
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
|
||||||
|
let host = match Host::from_request(request) {
|
||||||
|
Outcome::Forward(_) => return Outcome::Forward(()),
|
||||||
|
Outcome::Failure(f) => return Outcome::Failure(f),
|
||||||
|
Outcome::Success(host) => host.host,
|
||||||
|
};
|
||||||
|
|
||||||
// Get access_token
|
// Get access_token
|
||||||
let access_token: &str = match headers.get_one("Authorization") {
|
let access_token: &str = match headers.get_one("Authorization") {
|
||||||
Some(a) => match a.rsplit("Bearer ").next() {
|
Some(a) => match a.rsplit("Bearer ").next() {
|
||||||
@@ -298,10 +318,30 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if user.security_stamp != claims.sstamp {
|
if user.security_stamp != claims.sstamp {
|
||||||
err_handler!("Invalid security stamp")
|
if let Some(stamp_exception) =
|
||||||
|
user.stamp_exception.as_deref().and_then(|s| serde_json::from_str::<UserStampException>(s).ok())
|
||||||
|
{
|
||||||
|
let current_route = match request.route().and_then(|r| r.name) {
|
||||||
|
Some(name) => name,
|
||||||
|
_ => err_handler!("Error getting current route for stamp exception"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if both match, if not this route is not allowed with the current security stamp.
|
||||||
|
if stamp_exception.route != current_route {
|
||||||
|
err_handler!("Invalid security stamp: Current route and exception route do not match")
|
||||||
|
} else if stamp_exception.security_stamp != claims.sstamp {
|
||||||
|
err_handler!("Invalid security stamp for matched stamp exception")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err_handler!("Invalid security stamp")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Outcome::Success(Headers { host, device, user })
|
Outcome::Success(Headers {
|
||||||
|
host,
|
||||||
|
device,
|
||||||
|
user,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,11 +350,13 @@ pub struct OrgHeaders {
|
|||||||
pub device: Device,
|
pub device: Device,
|
||||||
pub user: User,
|
pub user: User,
|
||||||
pub org_user_type: UserOrgType,
|
pub org_user_type: UserOrgType,
|
||||||
|
pub org_user: UserOrganization,
|
||||||
|
pub org_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// org_id is usually the second param ("/organizations/<org_id>")
|
// org_id is usually the second path param ("/organizations/<org_id>"),
|
||||||
// But there are cases where it is located in a query value.
|
// but there are cases where it is a query value.
|
||||||
// First check the param, if this is not a valid uuid, we will try the query value.
|
// First check the path, if this is not a valid uuid, try the query values.
|
||||||
fn get_org_id(request: &Request) -> Option<String> {
|
fn get_org_id(request: &Request) -> Option<String> {
|
||||||
if let Some(Ok(org_id)) = request.get_param::<String>(1) {
|
if let Some(Ok(org_id)) = request.get_param::<String>(1) {
|
||||||
if uuid::Uuid::parse_str(&org_id).is_ok() {
|
if uuid::Uuid::parse_str(&org_id).is_ok() {
|
||||||
@@ -370,6 +412,8 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
|||||||
err_handler!("Unknown user type in the database")
|
err_handler!("Unknown user type in the database")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
org_user,
|
||||||
|
org_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => err_handler!("Error getting the organization id"),
|
_ => err_handler!("Error getting the organization id"),
|
||||||
@@ -409,12 +453,137 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Headers> for AdminHeaders {
|
impl From<AdminHeaders> for Headers {
|
||||||
fn into(self) -> Headers {
|
fn from(h: AdminHeaders) -> Headers {
|
||||||
Headers {
|
Headers {
|
||||||
host: self.host,
|
host: h.host,
|
||||||
device: self.device,
|
device: h.device,
|
||||||
user: self.user,
|
user: h.user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
|
||||||
|
// but there could be cases where it is a query value.
|
||||||
|
// First check the path, if this is not a valid uuid, try the query values.
|
||||||
|
fn get_col_id(request: &Request) -> Option<String> {
|
||||||
|
if let Some(Ok(col_id)) = request.get_param::<String>(3) {
|
||||||
|
if uuid::Uuid::parse_str(&col_id).is_ok() {
|
||||||
|
return Some(col_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Ok(col_id)) = request.get_query_value::<String>("collectionId") {
|
||||||
|
if uuid::Uuid::parse_str(&col_id).is_ok() {
|
||||||
|
return Some(col_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ManagerHeaders are used to check if you are at least a Manager
|
||||||
|
/// and have access to the specific collection provided via the <col_id>/collections/collectionId.
|
||||||
|
/// This does strict checking on the collection_id, ManagerHeadersLoose does not.
|
||||||
|
pub struct ManagerHeaders {
|
||||||
|
pub host: String,
|
||||||
|
pub device: Device,
|
||||||
|
pub user: User,
|
||||||
|
pub org_user_type: UserOrgType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
|
match request.guard::<OrgHeaders>() {
|
||||||
|
Outcome::Forward(_) => Outcome::Forward(()),
|
||||||
|
Outcome::Failure(f) => Outcome::Failure(f),
|
||||||
|
Outcome::Success(headers) => {
|
||||||
|
if headers.org_user_type >= UserOrgType::Manager {
|
||||||
|
match get_col_id(request) {
|
||||||
|
Some(col_id) => {
|
||||||
|
let conn = match request.guard::<DbConn>() {
|
||||||
|
Outcome::Success(conn) => conn,
|
||||||
|
_ => err_handler!("Error getting DB"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !headers.org_user.has_full_access() {
|
||||||
|
match CollectionUser::find_by_collection_and_user(
|
||||||
|
&col_id,
|
||||||
|
&headers.org_user.user_uuid,
|
||||||
|
&conn,
|
||||||
|
) {
|
||||||
|
Some(_) => (),
|
||||||
|
None => err_handler!("The current user isn't a manager for this collection"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => err_handler!("Error getting the collection id"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Outcome::Success(Self {
|
||||||
|
host: headers.host,
|
||||||
|
device: headers.device,
|
||||||
|
user: headers.user,
|
||||||
|
org_user_type: headers.org_user_type,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ManagerHeaders> for Headers {
|
||||||
|
fn from(h: ManagerHeaders) -> Headers {
|
||||||
|
Headers {
|
||||||
|
host: h.host,
|
||||||
|
device: h.device,
|
||||||
|
user: h.user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ManagerHeadersLoose is used when you at least need to be a Manager,
|
||||||
|
/// but there is no collection_id sent with the request (either in the path or as form data).
|
||||||
|
pub struct ManagerHeadersLoose {
|
||||||
|
pub host: String,
|
||||||
|
pub device: Device,
|
||||||
|
pub user: User,
|
||||||
|
pub org_user_type: UserOrgType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeadersLoose {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
|
match request.guard::<OrgHeaders>() {
|
||||||
|
Outcome::Forward(_) => Outcome::Forward(()),
|
||||||
|
Outcome::Failure(f) => Outcome::Failure(f),
|
||||||
|
Outcome::Success(headers) => {
|
||||||
|
if headers.org_user_type >= UserOrgType::Manager {
|
||||||
|
Outcome::Success(Self {
|
||||||
|
host: headers.host,
|
||||||
|
device: headers.device,
|
||||||
|
user: headers.user,
|
||||||
|
org_user_type: headers.org_user_type,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ManagerHeadersLoose> for Headers {
|
||||||
|
fn from(h: ManagerHeadersLoose) -> Headers {
|
||||||
|
Headers {
|
||||||
|
host: h.host,
|
||||||
|
device: h.device,
|
||||||
|
user: h.user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,10 +643,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let ip = ip
|
let ip = ip.or_else(|| req.remote().map(|r| r.ip())).unwrap_or_else(|| "0.0.0.0".parse().unwrap());
|
||||||
.or_else(|| req.remote().map(|r| r.ip()))
|
|
||||||
.unwrap_or_else(|| "0.0.0.0".parse().unwrap());
|
|
||||||
|
|
||||||
Outcome::Success(ClientIp { ip })
|
Outcome::Success(ClientIp {
|
||||||
|
ip,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
197
src/config.rs
197
src/config.rs
@@ -2,6 +2,7 @@ use std::process::exit;
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -22,6 +23,21 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static PRIVACY_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\w]").unwrap());
|
||||||
|
const PRIVACY_CONFIG: &[&str] = &[
|
||||||
|
"allowed_iframe_ancestors",
|
||||||
|
"database_url",
|
||||||
|
"domain_origin",
|
||||||
|
"domain_path",
|
||||||
|
"domain",
|
||||||
|
"helo_name",
|
||||||
|
"org_creation_users",
|
||||||
|
"signups_domains_whitelist",
|
||||||
|
"smtp_from",
|
||||||
|
"smtp_host",
|
||||||
|
"smtp_username",
|
||||||
|
];
|
||||||
|
|
||||||
pub type Pass = String;
|
pub type Pass = String;
|
||||||
|
|
||||||
macro_rules! make_config {
|
macro_rules! make_config {
|
||||||
@@ -52,8 +68,34 @@ macro_rules! make_config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigBuilder {
|
impl ConfigBuilder {
|
||||||
|
#[allow(clippy::field_reassign_with_default)]
|
||||||
fn from_env() -> Self {
|
fn from_env() -> Self {
|
||||||
dotenv::from_path(".env").ok();
|
match dotenv::from_path(".env") {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => match e {
|
||||||
|
dotenv::Error::LineParse(msg, pos) => {
|
||||||
|
panic!("Error loading the .env file:\nNear {:?} on position {}\nPlease fix and restart!\n", msg, pos);
|
||||||
|
},
|
||||||
|
dotenv::Error::Io(ioerr) => match ioerr.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
println!("[INFO] No .env file found.\n");
|
||||||
|
()
|
||||||
|
},
|
||||||
|
std::io::ErrorKind::PermissionDenied => {
|
||||||
|
println!("[WARNING] Permission Denied while trying to read the .env file!\n");
|
||||||
|
()
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("[WARNING] Reading the .env file failed:\n{:?}\n", ioerr);
|
||||||
|
()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("[WARNING] Reading the .env file failed:\n{:?}\n", e);
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut builder = ConfigBuilder::default();
|
let mut builder = ConfigBuilder::default();
|
||||||
$($(
|
$($(
|
||||||
@@ -171,9 +213,38 @@ macro_rules! make_config {
|
|||||||
}, )+
|
}, )+
|
||||||
]}, )+ ])
|
]}, )+ ])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_support_json(&self) -> serde_json::Value {
|
||||||
|
let cfg = {
|
||||||
|
let inner = &self.inner.read().unwrap();
|
||||||
|
inner.config.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
json!({ $($(
|
||||||
|
stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action },
|
||||||
|
)+)+ })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Support string print
|
||||||
|
( @supportstr $name:ident, $value:expr, Pass, option ) => { $value.as_ref().map(|_| String::from("***")) }; // Optional pass, we map to an Option<String> with "***"
|
||||||
|
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { String::from("***") }; // Required pass, we return "***"
|
||||||
|
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
|
||||||
|
if PRIVACY_CONFIG.contains(&stringify!($name)) {
|
||||||
|
json!($value.as_ref().map(|x| PRIVACY_REGEX.replace_all(&x.to_string(), "${1}*").to_string()))
|
||||||
|
} else {
|
||||||
|
json!($value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
|
||||||
|
if PRIVACY_CONFIG.contains(&stringify!($name)) {
|
||||||
|
json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string())
|
||||||
|
} else {
|
||||||
|
json!($value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Group or empty string
|
// Group or empty string
|
||||||
( @show ) => { "" };
|
( @show ) => { "" };
|
||||||
( @show $lit:literal ) => { $lit };
|
( @show $lit:literal ) => { $lit };
|
||||||
@@ -228,6 +299,8 @@ make_config! {
|
|||||||
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
||||||
/// Attachments folder
|
/// Attachments folder
|
||||||
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
|
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
|
||||||
|
/// Sends folder
|
||||||
|
sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends");
|
||||||
/// Templates folder
|
/// Templates folder
|
||||||
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
||||||
/// Session JWT key
|
/// Session JWT key
|
||||||
@@ -243,6 +316,17 @@ make_config! {
|
|||||||
/// Websocket port
|
/// Websocket port
|
||||||
websocket_port: u16, false, def, 3012;
|
websocket_port: u16, false, def, 3012;
|
||||||
},
|
},
|
||||||
|
jobs {
|
||||||
|
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
||||||
|
/// Set to 0 to globally disable scheduled jobs.
|
||||||
|
job_poll_interval_ms: u64, false, def, 30_000;
|
||||||
|
/// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date.
|
||||||
|
/// Defaults to hourly. Set blank to disable this job.
|
||||||
|
send_purge_schedule: String, false, def, "0 5 * * * *".to_string();
|
||||||
|
/// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently.
|
||||||
|
/// Defaults to daily. Set blank to disable this job.
|
||||||
|
trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string();
|
||||||
|
},
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
settings {
|
settings {
|
||||||
@@ -266,11 +350,16 @@ make_config! {
|
|||||||
/// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
|
/// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
|
||||||
org_attachment_limit: i64, true, option;
|
org_attachment_limit: i64, true, option;
|
||||||
|
|
||||||
|
/// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item.
|
||||||
|
/// If unset, trashed items are not auto-deleted. This setting applies globally, so make
|
||||||
|
/// sure to inform all users of any changes to this setting.
|
||||||
|
trash_auto_delete_days: i64, true, option;
|
||||||
|
|
||||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
||||||
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||||
/// otherwise it will delete them and they won't be downloaded again.
|
/// otherwise it will delete them and they won't be downloaded again.
|
||||||
disable_icon_download: bool, true, def, false;
|
disable_icon_download: bool, true, def, false;
|
||||||
/// Allow new signups |> Controls whether new users can register. Users can be invited by the bitwarden_rs admin even if this is disabled
|
/// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled
|
||||||
signups_allowed: bool, true, def, true;
|
signups_allowed: bool, true, def, true;
|
||||||
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
|
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
|
||||||
signups_verify: bool, true, def, false;
|
signups_verify: bool, true, def, false;
|
||||||
@@ -296,7 +385,7 @@ make_config! {
|
|||||||
admin_token: Pass, true, option;
|
admin_token: Pass, true, option;
|
||||||
|
|
||||||
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
|
||||||
invitation_org_name: String, true, def, "Bitwarden_RS".to_string();
|
invitation_org_name: String, true, def, "Vaultwarden".to_string();
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Advanced settings
|
/// Advanced settings
|
||||||
@@ -345,7 +434,7 @@ make_config! {
|
|||||||
/// Log level
|
/// Log level
|
||||||
log_level: String, false, def, "Info".to_string();
|
log_level: String, false, def, "Info".to_string();
|
||||||
|
|
||||||
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
|
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems,
|
||||||
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||||
enable_db_wal: bool, false, def, true;
|
enable_db_wal: bool, false, def, true;
|
||||||
|
|
||||||
@@ -388,29 +477,35 @@ make_config! {
|
|||||||
/// SMTP Email Settings
|
/// SMTP Email Settings
|
||||||
smtp: _enable_smtp {
|
smtp: _enable_smtp {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
_enable_smtp: bool, true, def, true;
|
_enable_smtp: bool, true, def, true;
|
||||||
/// Host
|
/// Host
|
||||||
smtp_host: String, true, option;
|
smtp_host: String, true, option;
|
||||||
/// Enable Secure SMTP |> (Explicit) - Enabling this by default would use STARTTLS (Standard ports 587 or 25)
|
/// Enable Secure SMTP |> (Explicit) - Enabling this by default would use STARTTLS (Standard ports 587 or 25)
|
||||||
smtp_ssl: bool, true, def, true;
|
smtp_ssl: bool, true, def, true;
|
||||||
/// Force TLS |> (Implicit) - Enabling this would force the use of an SSL/TLS connection, instead of upgrading an insecure one with STARTTLS (Standard port 465)
|
/// Force TLS |> (Implicit) - Enabling this would force the use of an SSL/TLS connection, instead of upgrading an insecure one with STARTTLS (Standard port 465)
|
||||||
smtp_explicit_tls: bool, true, def, false;
|
smtp_explicit_tls: bool, true, def, false;
|
||||||
/// Port
|
/// Port
|
||||||
smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
|
smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
|
||||||
/// From Address
|
/// From Address
|
||||||
smtp_from: String, true, def, String::new();
|
smtp_from: String, true, def, String::new();
|
||||||
/// From Name
|
/// From Name
|
||||||
smtp_from_name: String, true, def, "Bitwarden_RS".to_string();
|
smtp_from_name: String, true, def, "Vaultwarden".to_string();
|
||||||
/// Username
|
/// Username
|
||||||
smtp_username: String, true, option;
|
smtp_username: String, true, option;
|
||||||
/// Password
|
/// Password
|
||||||
smtp_password: Pass, true, option;
|
smtp_password: Pass, true, option;
|
||||||
/// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','.
|
/// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','.
|
||||||
smtp_auth_mechanism: String, true, option;
|
smtp_auth_mechanism: String, true, option;
|
||||||
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
||||||
smtp_timeout: u64, true, def, 15;
|
smtp_timeout: u64, true, def, 15;
|
||||||
/// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
/// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
||||||
helo_name: String, true, option;
|
helo_name: String, true, option;
|
||||||
|
/// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
|
||||||
|
smtp_debug: bool, true, def, false;
|
||||||
|
/// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
||||||
|
smtp_accept_invalid_certs: bool, true, def, false;
|
||||||
|
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
||||||
|
smtp_accept_invalid_hostnames: bool, true, def, false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Email 2FA Settings
|
/// Email 2FA Settings
|
||||||
@@ -427,21 +522,19 @@ make_config! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
|
|
||||||
// Validate connection URL is valid and DB feature is enabled
|
// Validate connection URL is valid and DB feature is enabled
|
||||||
DbConnType::from_url(&cfg.database_url)?;
|
DbConnType::from_url(&cfg.database_url)?;
|
||||||
|
|
||||||
let limit = 256;
|
let limit = 256;
|
||||||
if cfg.database_max_conns < 1 || cfg.database_max_conns > limit {
|
if cfg.database_max_conns < 1 || cfg.database_max_conns > limit {
|
||||||
err!(format!(
|
err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {}.", limit,));
|
||||||
"`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {}.",
|
|
||||||
limit,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let dom = cfg.domain.to_lowercase();
|
let dom = cfg.domain.to_lowercase();
|
||||||
if !dom.starts_with("http://") && !dom.starts_with("https://") {
|
if !dom.starts_with("http://") && !dom.starts_with("https://") {
|
||||||
err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'");
|
err!(
|
||||||
|
"DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let whitelist = &cfg.signups_domains_whitelist;
|
let whitelist = &cfg.signups_domains_whitelist;
|
||||||
@@ -450,10 +543,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let org_creation_users = cfg.org_creation_users.trim().to_lowercase();
|
let org_creation_users = cfg.org_creation_users.trim().to_lowercase();
|
||||||
if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") {
|
if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none")
|
||||||
if org_creation_users.split(',').any(|u| !u.contains('@')) {
|
&& org_creation_users.split(',').any(|u| !u.contains('@'))
|
||||||
err!("`ORG_CREATION_USERS` contains invalid email addresses");
|
{
|
||||||
}
|
err!("`ORG_CREATION_USERS` contains invalid email addresses");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref token) = cfg.admin_token {
|
if let Some(ref token) = cfg.admin_token {
|
||||||
@@ -479,6 +572,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support")
|
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.smtp_host.is_some() && !cfg.smtp_from.contains('@') {
|
||||||
|
err!("SMTP_FROM does not contain a mandatory @ sign")
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() {
|
if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() {
|
||||||
err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication")
|
err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication")
|
||||||
}
|
}
|
||||||
@@ -496,6 +593,15 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the icon blacklist regex is valid
|
||||||
|
if let Some(ref r) = cfg.icon_blacklist_regex {
|
||||||
|
let validate_regex = Regex::new(&r);
|
||||||
|
match validate_regex {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +642,12 @@ impl Config {
|
|||||||
validate_config(&config)?;
|
validate_config(&config)?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
inner: RwLock::new(Inner { templates: load_templates(&config.templates_folder), config, _env, _usr }),
|
inner: RwLock::new(Inner {
|
||||||
|
templates: load_templates(&config.templates_folder),
|
||||||
|
config,
|
||||||
|
_env,
|
||||||
|
_usr,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,7 +720,7 @@ impl Config {
|
|||||||
/// Tests whether the specified user is allowed to create an organization.
|
/// Tests whether the specified user is allowed to create an organization.
|
||||||
pub fn is_org_creation_allowed(&self, email: &str) -> bool {
|
pub fn is_org_creation_allowed(&self, email: &str) -> bool {
|
||||||
let users = self.org_creation_users();
|
let users = self.org_creation_users();
|
||||||
if users == "" || users == "all" {
|
if users.is_empty() || users == "all" {
|
||||||
true
|
true
|
||||||
} else if users == "none" {
|
} else if users == "none" {
|
||||||
false
|
false
|
||||||
@@ -663,8 +774,10 @@ impl Config {
|
|||||||
let akey_s = data_encoding::BASE64.encode(&akey);
|
let akey_s = data_encoding::BASE64.encode(&akey);
|
||||||
|
|
||||||
// Save the new value
|
// Save the new value
|
||||||
let mut builder = ConfigBuilder::default();
|
let builder = ConfigBuilder {
|
||||||
builder._duo_akey = Some(akey_s.clone());
|
_duo_akey: Some(akey_s.clone()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
self.update_config_partial(builder).ok();
|
self.update_config_partial(builder).ok();
|
||||||
|
|
||||||
akey_s
|
akey_s
|
||||||
@@ -755,9 +868,7 @@ fn case_helper<'reg, 'rc>(
|
|||||||
rc: &mut RenderContext<'reg, 'rc>,
|
rc: &mut RenderContext<'reg, 'rc>,
|
||||||
out: &mut dyn Output,
|
out: &mut dyn Output,
|
||||||
) -> HelperResult {
|
) -> HelperResult {
|
||||||
let param = h
|
let param = h.param(0).ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?;
|
||||||
.param(0)
|
|
||||||
.ok_or_else(|| RenderError::new("Param not found for helper \"case\""))?;
|
|
||||||
let value = param.value().clone();
|
let value = param.value().clone();
|
||||||
|
|
||||||
if h.params().iter().skip(1).any(|x| x.value() == &value) {
|
if h.params().iter().skip(1).any(|x| x.value() == &value) {
|
||||||
@@ -774,18 +885,18 @@ fn js_escape_helper<'reg, 'rc>(
|
|||||||
_rc: &mut RenderContext<'reg, 'rc>,
|
_rc: &mut RenderContext<'reg, 'rc>,
|
||||||
out: &mut dyn Output,
|
out: &mut dyn Output,
|
||||||
) -> HelperResult {
|
) -> HelperResult {
|
||||||
let param = h
|
let param = h.param(0).ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
|
||||||
.param(0)
|
|
||||||
.ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
|
|
||||||
|
|
||||||
let value = param
|
let no_quote = h.param(1).is_some();
|
||||||
.value()
|
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
|
|
||||||
|
|
||||||
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
let value =
|
||||||
let quoted_value = format!(""{}"", escaped_value);
|
param.value().as_str().ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
|
||||||
|
|
||||||
out.write("ed_value)?;
|
let mut escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
||||||
|
if !no_quote {
|
||||||
|
escaped_value = format!(""{}"", escaped_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(&escaped_value)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ pub fn get_random_64() -> Vec<u8> {
|
|||||||
pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
|
pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
|
||||||
use ring::rand::{SecureRandom, SystemRandom};
|
use ring::rand::{SecureRandom, SystemRandom};
|
||||||
|
|
||||||
SystemRandom::new()
|
SystemRandom::new().fill(&mut array).expect("Error generating random values");
|
||||||
.fill(&mut array)
|
|
||||||
.expect("Error generating random values");
|
|
||||||
|
|
||||||
array
|
array
|
||||||
}
|
}
|
||||||
@@ -67,7 +65,7 @@ pub fn generate_token(token_size: u32) -> Result<String, Error> {
|
|||||||
// token of fixed width, left-padding with 0 as needed.
|
// token of fixed width, left-padding with 0 as needed.
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
let number: u64 = rng.gen_range(low, high);
|
let number: u64 = rng.gen_range(low..high);
|
||||||
let token = format!("{:0size$}", number, size = token_size as usize);
|
let token = format!("{:0size$}", number, size = token_size as usize);
|
||||||
|
|
||||||
Ok(token)
|
Ok(token)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::Status,
|
http::Status,
|
||||||
@@ -25,7 +22,6 @@ pub mod __mysql_schema;
|
|||||||
#[path = "schemas/postgresql/schema.rs"]
|
#[path = "schemas/postgresql/schema.rs"]
|
||||||
pub mod __postgresql_schema;
|
pub mod __postgresql_schema;
|
||||||
|
|
||||||
|
|
||||||
// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported
|
// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported
|
||||||
macro_rules! generate_connections {
|
macro_rules! generate_connections {
|
||||||
( $( $name:ident: $ty:ty ),+ ) => {
|
( $( $name:ident: $ty:ty ),+ ) => {
|
||||||
@@ -37,6 +33,7 @@ macro_rules! generate_connections {
|
|||||||
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
|
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
|
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
|
||||||
|
|
||||||
impl DbPool {
|
impl DbPool {
|
||||||
@@ -109,7 +106,6 @@ impl DbConnType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! db_run {
|
macro_rules! db_run {
|
||||||
// Same for all dbs
|
// Same for all dbs
|
||||||
@@ -134,8 +130,26 @@ macro_rules! db_run {
|
|||||||
)+)+
|
)+)+
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
// Same for all dbs
|
||||||
|
( @raw $conn:ident: $body:block ) => {
|
||||||
|
db_run! { @raw $conn: sqlite, mysql, postgresql $body }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Different code for each db
|
||||||
|
( @raw $conn:ident: $( $($db:ident),+ $body:block )+ ) => {
|
||||||
|
#[allow(unused)] use diesel::prelude::*;
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
match $conn {
|
||||||
|
$($(
|
||||||
|
#[cfg($db)]
|
||||||
|
crate::db::DbConn::$db(ref $conn) => {
|
||||||
|
$body
|
||||||
|
},
|
||||||
|
)+)+
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FromDb {
|
pub trait FromDb {
|
||||||
type Output;
|
type Output;
|
||||||
@@ -202,23 +216,36 @@ macro_rules! db_object {
|
|||||||
// Reexport the models, needs to be after the macros are defined so it can access them
|
// Reexport the models, needs to be after the macros are defined so it can access them
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
|
||||||
/// Creates a back-up of the database using sqlite3
|
/// Creates a back-up of the sqlite database
|
||||||
pub fn backup_database() -> Result<(), Error> {
|
/// MySQL/MariaDB and PostgreSQL are not supported.
|
||||||
use std::path::Path;
|
pub fn backup_database(conn: &DbConn) -> Result<(), Error> {
|
||||||
let db_url = CONFIG.database_url();
|
db_run! {@raw conn:
|
||||||
let db_path = Path::new(&db_url).parent().unwrap();
|
postgresql, mysql {
|
||||||
|
err!("PostgreSQL and MySQL/MariaDB do not support this backup feature");
|
||||||
|
}
|
||||||
|
sqlite {
|
||||||
|
use std::path::Path;
|
||||||
|
let db_url = CONFIG.database_url();
|
||||||
|
let db_path = Path::new(&db_url).parent().unwrap().to_string_lossy();
|
||||||
|
let file_date = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
|
||||||
|
diesel::sql_query(format!("VACUUM INTO '{}/db_{}.sqlite3'", db_path, file_date)).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let now: DateTime<Utc> = Utc::now();
|
/// Get the SQL Server version
|
||||||
let file_date = now.format("%Y%m%d").to_string();
|
pub fn get_sql_server_version(conn: &DbConn) -> String {
|
||||||
let backup_command: String = format!("{}{}{}", ".backup 'db_", file_date, ".sqlite3'");
|
db_run! {@raw conn:
|
||||||
|
postgresql, mysql {
|
||||||
Command::new("sqlite3")
|
no_arg_sql_function!(version, diesel::sql_types::Text);
|
||||||
.current_dir(db_path)
|
diesel::select(version).get_result::<String>(conn).unwrap_or_else(|_| "Unknown".to_string())
|
||||||
.args(&["db.sqlite3", &backup_command])
|
}
|
||||||
.output()
|
sqlite {
|
||||||
.expect("Can't open database, sqlite3 is not available, make sure it's installed and available on the PATH");
|
no_arg_sql_function!(sqlite_version, diesel::sql_types::Text);
|
||||||
|
diesel::select(sqlite_version).get_result::<String>(conn).unwrap_or_else(|_| "Unknown".to_string())
|
||||||
Ok(())
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to retrieve a single connection from the managed database pool. If
|
/// Attempts to retrieve a single connection from the managed database pool. If
|
||||||
@@ -259,8 +286,7 @@ mod sqlite_migrations {
|
|||||||
|
|
||||||
use diesel::{Connection, RunQueryDsl};
|
use diesel::{Connection, RunQueryDsl};
|
||||||
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||||
let connection =
|
let connection = diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
|
|
||||||
// Disable Foreign Key Checks during migration
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
// Scoped to a connection.
|
// Scoped to a connection.
|
||||||
@@ -270,9 +296,7 @@ mod sqlite_migrations {
|
|||||||
|
|
||||||
// Turn on WAL in SQLite
|
// Turn on WAL in SQLite
|
||||||
if crate::CONFIG.enable_db_wal() {
|
if crate::CONFIG.enable_db_wal() {
|
||||||
diesel::sql_query("PRAGMA journal_mode=wal")
|
diesel::sql_query("PRAGMA journal_mode=wal").execute(&connection).expect("Failed to turn on WAL");
|
||||||
.execute(&connection)
|
|
||||||
.expect("Failed to turn on WAL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?;
|
embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?;
|
||||||
@@ -288,8 +312,7 @@ mod mysql_migrations {
|
|||||||
pub fn run_migrations() -> Result<(), super::Error> {
|
pub fn run_migrations() -> Result<(), super::Error> {
|
||||||
use diesel::{Connection, RunQueryDsl};
|
use diesel::{Connection, RunQueryDsl};
|
||||||
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||||
let connection =
|
let connection = diesel::mysql::MysqlConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
diesel::mysql::MysqlConnection::establish(&crate::CONFIG.database_url())?;
|
|
||||||
// Disable Foreign Key Checks during migration
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
// Scoped to a connection/session.
|
// Scoped to a connection/session.
|
||||||
@@ -310,8 +333,7 @@ mod postgresql_migrations {
|
|||||||
pub fn run_migrations() -> Result<(), super::Error> {
|
pub fn run_migrations() -> Result<(), super::Error> {
|
||||||
use diesel::{Connection, RunQueryDsl};
|
use diesel::{Connection, RunQueryDsl};
|
||||||
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||||
let connection =
|
let connection = diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
|
|
||||||
// Disable Foreign Key Checks during migration
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
|
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use super::Cipher;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "attachments"]
|
#[table_name = "attachments"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]
|
||||||
@@ -59,7 +59,6 @@ use crate::error::MapResult;
|
|||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Attachment {
|
impl Attachment {
|
||||||
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
db_run! { conn:
|
db_run! { conn:
|
||||||
sqlite, mysql {
|
sqlite, mysql {
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::CONFIG;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Attachment,
|
Attachment, CollectionCipher, Favorite, FolderCipher, Organization, User, UserOrgStatus, UserOrgType,
|
||||||
CollectionCipher,
|
|
||||||
Favorite,
|
|
||||||
FolderCipher,
|
|
||||||
Organization,
|
|
||||||
User,
|
|
||||||
UserOrgStatus,
|
|
||||||
UserOrgType,
|
|
||||||
UserOrganization,
|
UserOrganization,
|
||||||
};
|
};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "ciphers"]
|
#[table_name = "ciphers"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
@@ -83,46 +78,54 @@ impl Cipher {
|
|||||||
use crate::util::format_date;
|
use crate::util::format_date;
|
||||||
|
|
||||||
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
|
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
|
||||||
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
|
// When there are no attachments use null instead of an empty array
|
||||||
|
let attachments_json = if attachments.is_empty() {
|
||||||
|
Value::Null
|
||||||
|
} else {
|
||||||
|
attachments.iter().map(|c| c.to_json(host)).collect()
|
||||||
|
};
|
||||||
|
|
||||||
let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
||||||
let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
let password_history_json =
|
||||||
|
self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
||||||
|
|
||||||
let (read_only, hide_passwords) =
|
let (read_only, hide_passwords) = match self.get_access_restrictions(&user_uuid, conn) {
|
||||||
match self.get_access_restrictions(&user_uuid, conn) {
|
Some((ro, hp)) => (ro, hp),
|
||||||
Some((ro, hp)) => (ro, hp),
|
None => {
|
||||||
None => {
|
error!("Cipher ownership assertion failure");
|
||||||
error!("Cipher ownership assertion failure");
|
(true, true)
|
||||||
(true, true)
|
}
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Get the data or a default empty value to avoid issues with the mobile apps
|
// Get the type_data or a default to an empty json object '{}'.
|
||||||
let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({
|
// If not passing an empty object, mobile clients will crash.
|
||||||
"Fields":null,
|
let mut type_data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({}));
|
||||||
"Name": self.name,
|
|
||||||
"Notes":null,
|
|
||||||
"Password":null,
|
|
||||||
"PasswordHistory":null,
|
|
||||||
"PasswordRevisionDate":null,
|
|
||||||
"Response":null,
|
|
||||||
"Totp":null,
|
|
||||||
"Uris":null,
|
|
||||||
"Username":null
|
|
||||||
}));
|
|
||||||
|
|
||||||
// TODO: ******* Backwards compat start **********
|
// NOTE: This was marked as *Backwards Compatibilty Code*, but as of January 2021 this is still being used by upstream
|
||||||
// To remove backwards compatibility, just remove this entire section
|
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
|
||||||
// and remove the compat code from ciphers::update_cipher_from_data
|
if self.atype == 1 {
|
||||||
if self.atype == 1 && data_json["Uris"].is_array() {
|
if type_data_json["Uris"].is_array() {
|
||||||
let uri = data_json["Uris"][0]["Uri"].clone();
|
let uri = type_data_json["Uris"][0]["Uri"].clone();
|
||||||
data_json["Uri"] = uri;
|
type_data_json["Uri"] = uri;
|
||||||
|
} else {
|
||||||
|
// Upstream always has an Uri key/value
|
||||||
|
type_data_json["Uri"] = Value::Null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: ******* Backwards compat end **********
|
|
||||||
|
// Clone the type_data and add some default value.
|
||||||
|
let mut data_json = type_data_json.clone();
|
||||||
|
|
||||||
|
// NOTE: This was marked as *Backwards Compatibilty Code*, but as of January 2021 this is still being used by upstream
|
||||||
|
// data_json should always contain the following keys with every atype
|
||||||
|
data_json["Fields"] = json!(fields_json);
|
||||||
|
data_json["Name"] = json!(self.name);
|
||||||
|
data_json["Notes"] = json!(self.notes);
|
||||||
|
data_json["PasswordHistory"] = json!(password_history_json);
|
||||||
|
|
||||||
// There are three types of cipher response models in upstream
|
// There are three types of cipher response models in upstream
|
||||||
// Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order
|
// Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order
|
||||||
// of increasing level of detail). bitwarden_rs currently only
|
// of increasing level of detail). vaultwarden currently only
|
||||||
// supports the "cipherDetails" type, though it seems like the
|
// supports the "cipherDetails" type, though it seems like the
|
||||||
// Bitwarden clients will ignore extra fields.
|
// Bitwarden clients will ignore extra fields.
|
||||||
//
|
//
|
||||||
@@ -137,6 +140,8 @@ impl Cipher {
|
|||||||
"Favorite": self.is_favorite(&user_uuid, conn),
|
"Favorite": self.is_favorite(&user_uuid, conn),
|
||||||
"OrganizationId": self.organization_uuid,
|
"OrganizationId": self.organization_uuid,
|
||||||
"Attachments": attachments_json,
|
"Attachments": attachments_json,
|
||||||
|
// We have UseTotp set to true by default within the Organization model.
|
||||||
|
// This variable together with UsersGetPremium is used to show or hide the TOTP counter.
|
||||||
"OrganizationUseTotp": true,
|
"OrganizationUseTotp": true,
|
||||||
|
|
||||||
// This field is specific to the cipherDetails type.
|
// This field is specific to the cipherDetails type.
|
||||||
@@ -155,6 +160,12 @@ impl Cipher {
|
|||||||
"ViewPassword": !hide_passwords,
|
"ViewPassword": !hide_passwords,
|
||||||
|
|
||||||
"PasswordHistory": password_history_json,
|
"PasswordHistory": password_history_json,
|
||||||
|
|
||||||
|
// All Cipher types are included by default as null, but only the matching one will be populated
|
||||||
|
"Login": null,
|
||||||
|
"SecureNote": null,
|
||||||
|
"Card": null,
|
||||||
|
"Identity": null,
|
||||||
});
|
});
|
||||||
|
|
||||||
let key = match self.atype {
|
let key = match self.atype {
|
||||||
@@ -165,7 +176,7 @@ impl Cipher {
|
|||||||
_ => panic!("Wrong type"),
|
_ => panic!("Wrong type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
json_object[key] = data_json;
|
json_object[key] = type_data_json;
|
||||||
json_object
|
json_object
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,12 +190,10 @@ impl Cipher {
|
|||||||
None => {
|
None => {
|
||||||
// Belongs to Organization, need to update affected users
|
// Belongs to Organization, need to update affected users
|
||||||
if let Some(ref org_uuid) = self.organization_uuid {
|
if let Some(ref org_uuid) = self.organization_uuid {
|
||||||
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn)
|
UserOrganization::find_by_cipher_and_org(&self.uuid, &org_uuid, conn).iter().for_each(|user_org| {
|
||||||
.iter()
|
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||||
.for_each(|user_org| {
|
user_uuids.push(user_org.user_uuid.clone())
|
||||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
});
|
||||||
user_uuids.push(user_org.user_uuid.clone())
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -255,6 +264,17 @@ impl Cipher {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Purge all ciphers that are old enough to be auto-deleted.
|
||||||
|
pub fn purge_trash(conn: &DbConn) {
|
||||||
|
if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
let dt = now - Duration::days(auto_delete_days);
|
||||||
|
for cipher in Self::find_deleted_before(&dt, conn) {
|
||||||
|
cipher.delete(&conn).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(user_uuid, conn);
|
User::update_uuid_revision(user_uuid, conn);
|
||||||
|
|
||||||
@@ -448,7 +468,10 @@ impl Cipher {
|
|||||||
pub fn find_owned_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_owned_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
ciphers::table
|
ciphers::table
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid))
|
.filter(
|
||||||
|
ciphers::user_uuid.eq(user_uuid)
|
||||||
|
.and(ciphers::organization_uuid.is_null())
|
||||||
|
)
|
||||||
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
|
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
@@ -492,6 +515,15 @@ impl Cipher {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find all ciphers that were deleted before the specified datetime.
|
||||||
|
pub fn find_deleted_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
ciphers::table
|
||||||
|
.filter(ciphers::deleted_at.lt(dt))
|
||||||
|
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec<String> {
|
pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec<String> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
ciphers_collections::table
|
ciphers_collections::table
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher};
|
use super::{Cipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "collections"]
|
#[table_name = "collections"]
|
||||||
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -13,7 +13,7 @@ db_object! {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "users_collections"]
|
#[table_name = "users_collections"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
||||||
@@ -25,7 +25,7 @@ db_object! {
|
|||||||
pub hide_passwords: bool,
|
pub hide_passwords: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "ciphers_collections"]
|
#[table_name = "ciphers_collections"]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
||||||
@@ -49,12 +49,21 @@ impl Collection {
|
|||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
|
"ExternalId": null, // Not support by us
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"OrganizationId": self.org_uuid,
|
"OrganizationId": self.org_uuid,
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"Object": "collection",
|
"Object": "collection",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_json_details(&self, user_uuid: &str, conn: &DbConn) -> Value {
|
||||||
|
let mut json_object = self.to_json();
|
||||||
|
json_object["Object"] = json!("collectionDetails");
|
||||||
|
json_object["ReadOnly"] = json!(!self.is_writable_by_user(user_uuid, conn));
|
||||||
|
json_object["HidePasswords"] = json!(self.hide_passwords_for_user(user_uuid, conn));
|
||||||
|
json_object
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
@@ -118,11 +127,9 @@ impl Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_users_revision(&self, conn: &DbConn) {
|
pub fn update_users_revision(&self, conn: &DbConn) {
|
||||||
UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn)
|
UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).iter().for_each(|user_org| {
|
||||||
.iter()
|
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||||
.for_each(|user_org| {
|
});
|
||||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
@@ -161,10 +168,7 @@ impl Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
Self::find_by_user_uuid(user_uuid, conn)
|
Self::find_by_user_uuid(user_uuid, conn).into_iter().filter(|c| c.org_uuid == org_uuid).collect()
|
||||||
.into_iter()
|
|
||||||
.filter(|c| c.org_uuid == org_uuid)
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
@@ -236,6 +240,28 @@ impl Collection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hide_passwords_for_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
|
match UserOrganization::find_by_user_and_org(&user_uuid, &self.org_uuid, &conn) {
|
||||||
|
None => true, // Not in Org
|
||||||
|
Some(user_org) => {
|
||||||
|
if user_org.has_full_access() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_run! { conn: {
|
||||||
|
users_collections::table
|
||||||
|
.filter(users_collections::collection_uuid.eq(&self.uuid))
|
||||||
|
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_collections::hide_passwords.eq(true))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0) != 0
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
@@ -253,7 +279,13 @@ impl CollectionUser {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult {
|
pub fn save(
|
||||||
|
user_uuid: &str,
|
||||||
|
collection_uuid: &str,
|
||||||
|
read_only: bool,
|
||||||
|
hide_passwords: bool,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
User::update_uuid_revision(&user_uuid, conn);
|
User::update_uuid_revision(&user_uuid, conn);
|
||||||
|
|
||||||
db_run! { conn:
|
db_run! { conn:
|
||||||
@@ -343,11 +375,9 @@ impl CollectionUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
CollectionUser::find_by_collection(&collection_uuid, conn)
|
CollectionUser::find_by_collection(&collection_uuid, conn).iter().for_each(|collection| {
|
||||||
.iter()
|
User::update_uuid_revision(&collection.user_uuid, conn);
|
||||||
.for_each(|collection| {
|
});
|
||||||
User::update_uuid_revision(&collection.user_uuid, conn);
|
|
||||||
});
|
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
|
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
|
||||||
@@ -356,13 +386,19 @@ impl CollectionUser {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(&user_uuid, conn);
|
let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn);
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid)))
|
for user in collectionusers {
|
||||||
.execute(conn)
|
diesel::delete(users_collections::table.filter(
|
||||||
.map_res("Error removing user from collections")
|
users_collections::user_uuid.eq(user_uuid)
|
||||||
|
.and(users_collections::collection_uuid.eq(user.collection_uuid))
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error removing user from collections")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use super::User;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "devices"]
|
#[table_name = "devices"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
@@ -80,8 +80,8 @@ impl Device {
|
|||||||
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
|
||||||
// Create the JWT claims struct, to send to the client
|
// Create the JWT claims struct, to send to the client
|
||||||
use crate::auth::{encode_jwt, LoginJWTClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
||||||
let claims = LoginJWTClaims {
|
let claims = LoginJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
||||||
iss: JWT_LOGIN_ISSUER.to_string(),
|
iss: JWT_LOGIN_ISSUER.to_string(),
|
||||||
@@ -178,4 +178,15 @@ impl Device {
|
|||||||
.from_db()
|
.from_db()
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_latest_active_by_user(user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
devices::table
|
||||||
|
.filter(devices::user_uuid.eq(user_uuid))
|
||||||
|
.order(devices::updated_at.desc())
|
||||||
|
.first::<DeviceDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{Cipher, User};
|
use super::{Cipher, User};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "favorites"]
|
#[table_name = "favorites"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
@@ -20,7 +20,7 @@ use crate::error::MapResult;
|
|||||||
impl Favorite {
|
impl Favorite {
|
||||||
// Returns whether the specified cipher is a favorite of the specified user.
|
// Returns whether the specified cipher is a favorite of the specified user.
|
||||||
pub fn is_favorite(cipher_uuid: &str, user_uuid: &str, conn: &DbConn) -> bool {
|
pub fn is_favorite(cipher_uuid: &str, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
db_run!{ conn: {
|
db_run! { conn: {
|
||||||
let query = favorites::table
|
let query = favorites::table
|
||||||
.filter(favorites::cipher_uuid.eq(cipher_uuid))
|
.filter(favorites::cipher_uuid.eq(cipher_uuid))
|
||||||
.filter(favorites::user_uuid.eq(user_uuid))
|
.filter(favorites::user_uuid.eq(user_uuid))
|
||||||
@@ -36,19 +36,19 @@ impl Favorite {
|
|||||||
match (old, new) {
|
match (old, new) {
|
||||||
(false, true) => {
|
(false, true) => {
|
||||||
User::update_uuid_revision(user_uuid, &conn);
|
User::update_uuid_revision(user_uuid, &conn);
|
||||||
db_run!{ conn: {
|
db_run! { conn: {
|
||||||
diesel::insert_into(favorites::table)
|
diesel::insert_into(favorites::table)
|
||||||
.values((
|
.values((
|
||||||
favorites::user_uuid.eq(user_uuid),
|
favorites::user_uuid.eq(user_uuid),
|
||||||
favorites::cipher_uuid.eq(cipher_uuid),
|
favorites::cipher_uuid.eq(cipher_uuid),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding favorite")
|
.map_res("Error adding favorite")
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
(true, false) => {
|
(true, false) => {
|
||||||
User::update_uuid_revision(user_uuid, &conn);
|
User::update_uuid_revision(user_uuid, &conn);
|
||||||
db_run!{ conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(
|
diesel::delete(
|
||||||
favorites::table
|
favorites::table
|
||||||
.filter(favorites::user_uuid.eq(user_uuid))
|
.filter(favorites::user_uuid.eq(user_uuid))
|
||||||
@@ -59,7 +59,7 @@ impl Favorite {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
// Otherwise, the favorite status is already what it should be.
|
// Otherwise, the favorite status is already what it should be.
|
||||||
_ => Ok(())
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use serde_json::Value;
|
|||||||
use super::{Cipher, User};
|
use super::{Cipher, User};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "folders"]
|
#[table_name = "folders"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -16,7 +16,7 @@ db_object! {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "folders_ciphers"]
|
#[table_name = "folders_ciphers"]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
#[belongs_to(Folder, foreign_key = "folder_uuid")]
|
#[belongs_to(Folder, foreign_key = "folder_uuid")]
|
||||||
@@ -109,7 +109,6 @@ impl Folder {
|
|||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
||||||
|
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
|
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod favorite;
|
|||||||
mod folder;
|
mod folder;
|
||||||
mod org_policy;
|
mod org_policy;
|
||||||
mod organization;
|
mod organization;
|
||||||
|
mod send;
|
||||||
mod two_factor;
|
mod two_factor;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
@@ -17,5 +18,6 @@ pub use self::favorite::Favorite;
|
|||||||
pub use self::folder::{Folder, FolderCipher};
|
pub use self::folder::{Folder, FolderCipher};
|
||||||
pub use self::org_policy::{OrgPolicy, OrgPolicyType};
|
pub use self::org_policy::{OrgPolicy, OrgPolicyType};
|
||||||
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
|
pub use self::send::{Send, SendType};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::user::{Invitation, User};
|
pub use self::user::{Invitation, User, UserStampException};
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ use crate::api::EmptyResult;
|
|||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
use super::Organization;
|
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "org_policies"]
|
#[table_name = "org_policies"]
|
||||||
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -20,12 +20,15 @@ db_object! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[derive(Copy, Clone, num_derive::FromPrimitive)]
|
||||||
#[derive(num_derive::FromPrimitive)]
|
|
||||||
pub enum OrgPolicyType {
|
pub enum OrgPolicyType {
|
||||||
TwoFactorAuthentication = 0,
|
TwoFactorAuthentication = 0,
|
||||||
MasterPassword = 1,
|
MasterPassword = 1,
|
||||||
PasswordGenerator = 2,
|
PasswordGenerator = 2,
|
||||||
|
// SingleOrg = 3, // Not currently supported.
|
||||||
|
// RequireSso = 4, // Not currently supported.
|
||||||
|
PersonalOwnership = 5,
|
||||||
|
DisableSend = 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
@@ -40,6 +43,10 @@ impl OrgPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_type(&self, policy_type: OrgPolicyType) -> bool {
|
||||||
|
self.atype == policy_type as i32
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
|
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
|
||||||
json!({
|
json!({
|
||||||
@@ -129,11 +136,14 @@ impl OrgPolicy {
|
|||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
org_policies::table
|
org_policies::table
|
||||||
.left_join(
|
.inner_join(
|
||||||
users_organizations::table.on(
|
users_organizations::table.on(
|
||||||
users_organizations::org_uuid.eq(org_policies::org_uuid)
|
users_organizations::org_uuid.eq(org_policies::org_uuid)
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid)))
|
.and(users_organizations::user_uuid.eq(user_uuid)))
|
||||||
)
|
)
|
||||||
|
.filter(
|
||||||
|
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||||
|
)
|
||||||
.select(org_policies::all_columns)
|
.select(org_policies::all_columns)
|
||||||
.load::<OrgPolicyDb>(conn)
|
.load::<OrgPolicyDb>(conn)
|
||||||
.expect("Error loading org_policy")
|
.expect("Error loading org_policy")
|
||||||
@@ -160,6 +170,24 @@ impl OrgPolicy {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the user belongs to an org that has enabled the specified policy type,
|
||||||
|
/// and the user is not an owner or admin of that org. This is only useful for checking
|
||||||
|
/// applicability of policy types that have these particular semantics.
|
||||||
|
pub fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
|
||||||
|
// Returns confirmed users only.
|
||||||
|
for policy in OrgPolicy::find_by_user(user_uuid, conn) {
|
||||||
|
if policy.enabled && policy.has_type(policy_type) {
|
||||||
|
let org_uuid = &policy.org_uuid;
|
||||||
|
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
||||||
|
if user.atype < UserOrgType::Admin {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
use num_traits::FromPrimitive;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use num_traits::FromPrimitive;
|
|
||||||
|
|
||||||
use super::{CollectionUser, User, OrgPolicy};
|
use super::{CollectionUser, OrgPolicy, User};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "organizations"]
|
#[table_name = "organizations"]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct Organization {
|
pub struct Organization {
|
||||||
@@ -14,7 +14,7 @@ db_object! {
|
|||||||
pub billing_email: String,
|
pub billing_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "users_organizations"]
|
#[table_name = "users_organizations"]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct UserOrganization {
|
pub struct UserOrganization {
|
||||||
@@ -35,8 +35,7 @@ pub enum UserOrgStatus {
|
|||||||
Confirmed = 2,
|
Confirmed = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
|
||||||
#[derive(num_derive::FromPrimitive)]
|
|
||||||
pub enum UserOrgType {
|
pub enum UserOrgType {
|
||||||
Owner = 0,
|
Owner = 0,
|
||||||
Admin = 1,
|
Admin = 1,
|
||||||
@@ -90,17 +89,11 @@ impl PartialOrd<i32> for UserOrgType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gt(&self, other: &i32) -> bool {
|
fn gt(&self, other: &i32) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Greater))
|
||||||
Some(Ordering::Less) | Some(Ordering::Equal) => false,
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ge(&self, other: &i32) -> bool {
|
fn ge(&self, other: &i32) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Greater) | Some(Ordering::Equal))
|
||||||
Some(Ordering::Less) => false,
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,17 +112,11 @@ impl PartialOrd<UserOrgType> for i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn lt(&self, other: &UserOrgType) -> bool {
|
fn lt(&self, other: &UserOrgType) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Less) | None)
|
||||||
Some(Ordering::Less) | None => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn le(&self, other: &UserOrgType) -> bool {
|
fn le(&self, other: &UserOrgType) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Less) | Some(Ordering::Equal) | None)
|
||||||
Some(Ordering::Less) | Some(Ordering::Equal) | None => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,9 +134,10 @@ impl Organization {
|
|||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
|
"Identifier": null, // not supported by us
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"Seats": 10,
|
"Seats": 10, // The value doesn't matter, we don't check server-side
|
||||||
"MaxCollections": 10,
|
"MaxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||||
"Use2fa": true,
|
"Use2fa": true,
|
||||||
"UseDirectory": false,
|
"UseDirectory": false,
|
||||||
@@ -157,6 +145,9 @@ impl Organization {
|
|||||||
"UseGroups": false,
|
"UseGroups": false,
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
|
"UseSso": false, // We do not support SSO
|
||||||
|
"SelfHost": true,
|
||||||
|
"UseApi": false, // not supported by us
|
||||||
|
|
||||||
"BusinessName": null,
|
"BusinessName": null,
|
||||||
"BusinessAddress1": null,
|
"BusinessAddress1": null,
|
||||||
@@ -198,11 +189,9 @@ use crate::error::MapResult;
|
|||||||
/// Database methods
|
/// Database methods
|
||||||
impl Organization {
|
impl Organization {
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
UserOrganization::find_by_org(&self.uuid, conn)
|
UserOrganization::find_by_org(&self.uuid, conn).iter().for_each(|user_org| {
|
||||||
.iter()
|
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||||
.for_each(|user_org| {
|
});
|
||||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
|
||||||
});
|
|
||||||
|
|
||||||
db_run! { conn:
|
db_run! { conn:
|
||||||
sqlite, mysql {
|
sqlite, mysql {
|
||||||
@@ -244,7 +233,6 @@ impl Organization {
|
|||||||
UserOrganization::delete_all_by_organization(&self.uuid, &conn)?;
|
UserOrganization::delete_all_by_organization(&self.uuid, &conn)?;
|
||||||
OrgPolicy::delete_all_by_organization(&self.uuid, &conn)?;
|
OrgPolicy::delete_all_by_organization(&self.uuid, &conn)?;
|
||||||
|
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
|
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
@@ -274,9 +262,10 @@ impl UserOrganization {
|
|||||||
|
|
||||||
json!({
|
json!({
|
||||||
"Id": self.org_uuid,
|
"Id": self.org_uuid,
|
||||||
|
"Identifier": null, // not supported by us
|
||||||
"Name": org.name,
|
"Name": org.name,
|
||||||
"Seats": 10,
|
"Seats": 10, // The value doesn't matter, we don't check server-side
|
||||||
"MaxCollections": 10,
|
"MaxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||||
"UsersGetPremium": true,
|
"UsersGetPremium": true,
|
||||||
|
|
||||||
"Use2fa": true,
|
"Use2fa": true,
|
||||||
@@ -285,8 +274,30 @@ impl UserOrganization {
|
|||||||
"UseGroups": false,
|
"UseGroups": false,
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
"UseApi": false,
|
"UseApi": false, // not supported by us
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
|
"SsoBound": false, // We do not support SSO
|
||||||
|
"UseSso": false, // We do not support SSO
|
||||||
|
// TODO: Add support for Business Portal
|
||||||
|
// Upstream is moving Policies and SSO management outside of the web-vault to /portal
|
||||||
|
// For now they still have that code also in the web-vault, but they will remove it at some point.
|
||||||
|
// https://github.com/bitwarden/server/tree/master/bitwarden_license/src/
|
||||||
|
"UseBusinessPortal": false, // Disable BusinessPortal Button
|
||||||
|
|
||||||
|
// TODO: Add support for Custom User Roles
|
||||||
|
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
||||||
|
// "Permissions": {
|
||||||
|
// "AccessBusinessPortal": false,
|
||||||
|
// "AccessEventLogs": false,
|
||||||
|
// "AccessImportExport": false,
|
||||||
|
// "AccessReports": false,
|
||||||
|
// "ManageAllCollections": false,
|
||||||
|
// "ManageAssignedCollections": false,
|
||||||
|
// "ManageGroups": false,
|
||||||
|
// "ManagePolicies": false,
|
||||||
|
// "ManageSso": false,
|
||||||
|
// "ManageUsers": false
|
||||||
|
// },
|
||||||
|
|
||||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||||
|
|
||||||
@@ -332,11 +343,13 @@ impl UserOrganization {
|
|||||||
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
|
collections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| json!({
|
.map(|c| {
|
||||||
"Id": c.collection_uuid,
|
json!({
|
||||||
"ReadOnly": c.read_only,
|
"Id": c.collection_uuid,
|
||||||
"HidePasswords": c.hide_passwords,
|
"ReadOnly": c.read_only,
|
||||||
}))
|
"HidePasswords": c.hide_passwords,
|
||||||
|
})
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -389,7 +402,7 @@ impl UserOrganization {
|
|||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
|
|
||||||
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
|
CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, &conn)?;
|
||||||
|
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
|
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
|
||||||
@@ -412,13 +425,26 @@ impl UserOrganization {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_status(self, status: UserOrgStatus) -> bool {
|
pub fn find_by_email_and_org(email: &str, org_id: &str, conn: &DbConn) -> Option<UserOrganization> {
|
||||||
|
if let Some(user) = super::User::find_by_mail(email, conn) {
|
||||||
|
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, &conn) {
|
||||||
|
return Some(user_org);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_status(&self, status: UserOrgStatus) -> bool {
|
||||||
self.status == status as i32
|
self.status == status as i32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_full_access(self) -> bool {
|
pub fn has_type(&self, user_type: UserOrgType) -> bool {
|
||||||
(self.access_all || self.atype >= UserOrgType::Admin) &&
|
self.atype == user_type as i32
|
||||||
self.has_status(UserOrgStatus::Confirmed)
|
}
|
||||||
|
|
||||||
|
pub fn has_full_access(&self) -> bool {
|
||||||
|
(self.access_all || self.atype >= UserOrgType::Admin) && self.has_status(UserOrgStatus::Confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
|||||||
284
src/db/models/send.rs
Normal file
284
src/db/models/send.rs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::{Organization, User};
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
|
#[table_name = "sends"]
|
||||||
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
|
#[belongs_to(Organization, foreign_key = "organization_uuid")]
|
||||||
|
#[primary_key(uuid)]
|
||||||
|
pub struct Send {
|
||||||
|
pub uuid: String,
|
||||||
|
|
||||||
|
pub user_uuid: Option<String>,
|
||||||
|
pub organization_uuid: Option<String>,
|
||||||
|
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
|
||||||
|
pub atype: i32,
|
||||||
|
pub data: String,
|
||||||
|
pub akey: String,
|
||||||
|
pub password_hash: Option<Vec<u8>>,
|
||||||
|
password_salt: Option<Vec<u8>>,
|
||||||
|
password_iter: Option<i32>,
|
||||||
|
|
||||||
|
pub max_access_count: Option<i32>,
|
||||||
|
pub access_count: i32,
|
||||||
|
|
||||||
|
pub creation_date: NaiveDateTime,
|
||||||
|
pub revision_date: NaiveDateTime,
|
||||||
|
pub expiration_date: Option<NaiveDateTime>,
|
||||||
|
pub deletion_date: NaiveDateTime,
|
||||||
|
|
||||||
|
pub disabled: bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
|
||||||
|
pub enum SendType {
|
||||||
|
Text = 0,
|
||||||
|
File = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Send {
|
||||||
|
pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
uuid: crate::util::get_uuid(),
|
||||||
|
user_uuid: None,
|
||||||
|
organization_uuid: None,
|
||||||
|
|
||||||
|
name,
|
||||||
|
notes: None,
|
||||||
|
|
||||||
|
atype,
|
||||||
|
data,
|
||||||
|
akey,
|
||||||
|
password_hash: None,
|
||||||
|
password_salt: None,
|
||||||
|
password_iter: None,
|
||||||
|
|
||||||
|
max_access_count: None,
|
||||||
|
access_count: 0,
|
||||||
|
|
||||||
|
creation_date: now,
|
||||||
|
revision_date: now,
|
||||||
|
expiration_date: None,
|
||||||
|
deletion_date,
|
||||||
|
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_password(&mut self, password: Option<&str>) {
|
||||||
|
const PASSWORD_ITER: i32 = 100_000;
|
||||||
|
|
||||||
|
if let Some(password) = password {
|
||||||
|
self.password_iter = Some(PASSWORD_ITER);
|
||||||
|
let salt = crate::crypto::get_random_64();
|
||||||
|
let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32);
|
||||||
|
self.password_salt = Some(salt);
|
||||||
|
self.password_hash = Some(hash);
|
||||||
|
} else {
|
||||||
|
self.password_iter = None;
|
||||||
|
self.password_salt = None;
|
||||||
|
self.password_hash = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_password(&self, password: &str) -> bool {
|
||||||
|
match (&self.password_hash, &self.password_salt, self.password_iter) {
|
||||||
|
(Some(hash), Some(salt), Some(iter)) => {
|
||||||
|
crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
use crate::util::format_date;
|
||||||
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
let data: Value = serde_json::from_str(&self.data).unwrap_or_default();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"Id": self.uuid,
|
||||||
|
"AccessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),
|
||||||
|
"Type": self.atype,
|
||||||
|
|
||||||
|
"Name": self.name,
|
||||||
|
"Notes": self.notes,
|
||||||
|
"Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||||
|
"File": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||||
|
|
||||||
|
"Key": self.akey,
|
||||||
|
"MaxAccessCount": self.max_access_count,
|
||||||
|
"AccessCount": self.access_count,
|
||||||
|
"Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
||||||
|
"Disabled": self.disabled,
|
||||||
|
|
||||||
|
"RevisionDate": format_date(&self.revision_date),
|
||||||
|
"ExpirationDate": self.expiration_date.as_ref().map(format_date),
|
||||||
|
"DeletionDate": format_date(&self.deletion_date),
|
||||||
|
"Object": "send",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json_access(&self) -> Value {
|
||||||
|
use crate::util::format_date;
|
||||||
|
|
||||||
|
let data: Value = serde_json::from_str(&self.data).unwrap_or_default();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"Id": self.uuid,
|
||||||
|
"Type": self.atype,
|
||||||
|
|
||||||
|
"Name": self.name,
|
||||||
|
"Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||||
|
"File": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||||
|
|
||||||
|
"ExpirationDate": self.expiration_date.as_ref().map(format_date),
|
||||||
|
"Object": "send-access",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::db::DbConn;
|
||||||
|
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::error::MapResult;
|
||||||
|
|
||||||
|
impl Send {
|
||||||
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
|
self.update_users_revision(conn);
|
||||||
|
self.revision_date = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite, mysql {
|
||||||
|
match diesel::replace_into(sends::table)
|
||||||
|
.values(SendDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
||||||
|
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
||||||
|
diesel::update(sends::table)
|
||||||
|
.filter(sends::uuid.eq(&self.uuid))
|
||||||
|
.set(SendDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving send")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving send")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = SendDb::to_db(self);
|
||||||
|
diesel::insert_into(sends::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(sends::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||||
|
self.update_users_revision(conn);
|
||||||
|
|
||||||
|
if self.atype == SendType::File as i32 {
|
||||||
|
std::fs::remove_dir_all(std::path::Path::new(&crate::CONFIG.sends_folder()).join(&self.uuid)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting send")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Purge all sends that are past their deletion date.
|
||||||
|
pub fn purge(conn: &DbConn) {
|
||||||
|
for send in Self::find_by_past_deletion_date(&conn) {
|
||||||
|
send.delete(&conn).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_users_revision(&self, conn: &DbConn) {
|
||||||
|
match &self.user_uuid {
|
||||||
|
Some(user_uuid) => {
|
||||||
|
User::update_uuid_revision(&user_uuid, conn);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Belongs to Organization, not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
for send in Self::find_by_user(user_uuid, &conn) {
|
||||||
|
send.delete(&conn)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uuid = match Uuid::from_slice(&uuid_vec) {
|
||||||
|
Ok(u) => u.to_string(),
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::find_by_uuid(&uuid, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::uuid.eq(uuid))
|
||||||
|
.first::<SendDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::user_uuid.eq(user_uuid))
|
||||||
|
.load::<SendDb>(conn).expect("Error loading sends").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::organization_uuid.eq(org_uuid))
|
||||||
|
.load::<SendDb>(conn).expect("Error loading sends").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_past_deletion_date(conn: &DbConn) -> Vec<Self> {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::deletion_date.lt(now))
|
||||||
|
.load::<SendDb>(conn).expect("Error loading sends").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ use crate::error::MapResult;
|
|||||||
use super::User;
|
use super::User;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "twofactor"]
|
#[table_name = "twofactor"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ use crate::crypto;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
|
pub enabled: bool,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
pub verified_at: Option<NaiveDateTime>,
|
pub verified_at: Option<NaiveDateTime>,
|
||||||
@@ -36,6 +37,7 @@ db_object! {
|
|||||||
pub totp_recover: Option<String>,
|
pub totp_recover: Option<String>,
|
||||||
|
|
||||||
pub security_stamp: String,
|
pub security_stamp: String,
|
||||||
|
pub stamp_exception: Option<String>,
|
||||||
|
|
||||||
pub equivalent_domains: String,
|
pub equivalent_domains: String,
|
||||||
pub excluded_globals: String,
|
pub excluded_globals: String,
|
||||||
@@ -45,7 +47,7 @@ db_object! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
#[table_name = "invitations"]
|
#[table_name = "invitations"]
|
||||||
#[primary_key(email)]
|
#[primary_key(email)]
|
||||||
pub struct Invitation {
|
pub struct Invitation {
|
||||||
@@ -59,6 +61,12 @@ enum UserStatus {
|
|||||||
_Disabled = 2,
|
_Disabled = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct UserStampException {
|
||||||
|
pub route: String,
|
||||||
|
pub security_stamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl User {
|
impl User {
|
||||||
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
||||||
@@ -70,6 +78,7 @@ impl User {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
uuid: crate::util::get_uuid(),
|
uuid: crate::util::get_uuid(),
|
||||||
|
enabled: true,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
verified_at: None,
|
verified_at: None,
|
||||||
@@ -86,6 +95,7 @@ impl User {
|
|||||||
password_iterations: CONFIG.password_iterations(),
|
password_iterations: CONFIG.password_iterations(),
|
||||||
|
|
||||||
security_stamp: crate::util::get_uuid(),
|
security_stamp: crate::util::get_uuid(),
|
||||||
|
stamp_exception: None,
|
||||||
|
|
||||||
password_hint: None,
|
password_hint: None,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
@@ -119,17 +129,55 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_password(&mut self, password: &str) {
|
/// Set the password hash generated
|
||||||
|
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `password` - A str which contains a hashed version of the users master password.
|
||||||
|
/// * `allow_next_route` - A Option<&str> with the function name of the next allowed (rocket) route.
|
||||||
|
///
|
||||||
|
pub fn set_password(&mut self, password: &str, allow_next_route: Option<&str>) {
|
||||||
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
|
||||||
self.reset_security_stamp();
|
|
||||||
|
if let Some(route) = allow_next_route {
|
||||||
|
self.set_stamp_exception(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reset_security_stamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_security_stamp(&mut self) {
|
pub fn reset_security_stamp(&mut self) {
|
||||||
self.security_stamp = crate::util::get_uuid();
|
self.security_stamp = crate::util::get_uuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `route_exception` - A str with the function name of the next allowed (rocket) route.
|
||||||
|
///
|
||||||
|
/// ### Future
|
||||||
|
/// In the future it could be posible that we need more of these exception routes.
|
||||||
|
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
|
||||||
|
pub fn set_stamp_exception(&mut self, route_exception: &str) {
|
||||||
|
let stamp_exception = UserStampException {
|
||||||
|
route: route_exception.to_string(),
|
||||||
|
security_stamp: self.security_stamp.to_string(),
|
||||||
|
};
|
||||||
|
self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the stamp_exception to prevent re-use of the previous security-stamp
|
||||||
|
///
|
||||||
|
/// ### Future
|
||||||
|
/// In the future it could be posible that we need more of these exception routes.
|
||||||
|
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
|
||||||
|
pub fn reset_stamp_exception(&mut self) {
|
||||||
|
self.stamp_exception = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization};
|
use super::{Cipher, Device, Favorite, Folder, Send, TwoFactor, UserOrgType, UserOrganization};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -215,6 +263,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Send::delete_all_by_user(&self.uuid, conn)?;
|
||||||
UserOrganization::delete_all_by_user(&self.uuid, conn)?;
|
UserOrganization::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Cipher::delete_all_by_user(&self.uuid, conn)?;
|
Cipher::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Favorite::delete_all_by_user(&self.uuid, conn)?;
|
Favorite::delete_all_by_user(&self.uuid, conn)?;
|
||||||
@@ -288,11 +337,20 @@ impl User {
|
|||||||
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_active(&self, conn: &DbConn) -> Option<NaiveDateTime> {
|
||||||
|
match Device::find_latest_active_by_user(&self.uuid, conn) {
|
||||||
|
Some(device) => Some(device.updated_at),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Invitation {
|
impl Invitation {
|
||||||
pub const fn new(email: String) -> Self {
|
pub const fn new(email: String) -> Self {
|
||||||
Self { email }
|
Self {
|
||||||
|
email,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
|
|||||||
@@ -102,6 +102,29 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Datetime,
|
||||||
|
revision_date -> Datetime,
|
||||||
|
expiration_date -> Nullable<Datetime>,
|
||||||
|
deletion_date -> Datetime,
|
||||||
|
disabled -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -116,6 +139,7 @@ table! {
|
|||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
enabled -> Bool,
|
||||||
created_at -> Datetime,
|
created_at -> Datetime,
|
||||||
updated_at -> Datetime,
|
updated_at -> Datetime,
|
||||||
verified_at -> Nullable<Datetime>,
|
verified_at -> Nullable<Datetime>,
|
||||||
@@ -135,6 +159,7 @@ table! {
|
|||||||
totp_secret -> Nullable<Text>,
|
totp_secret -> Nullable<Text>,
|
||||||
totp_recover -> Nullable<Text>,
|
totp_recover -> Nullable<Text>,
|
||||||
security_stamp -> Text,
|
security_stamp -> Text,
|
||||||
|
stamp_exception -> Nullable<Text>,
|
||||||
equivalent_domains -> Text,
|
equivalent_domains -> Text,
|
||||||
excluded_globals -> Text,
|
excluded_globals -> Text,
|
||||||
client_kdf_type -> Integer,
|
client_kdf_type -> Integer,
|
||||||
@@ -174,6 +199,8 @@ joinable!(folders -> users (user_uuid));
|
|||||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
joinable!(org_policies -> organizations (org_uuid));
|
joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
joinable!(sends -> organizations (organization_uuid));
|
||||||
|
joinable!(sends -> users (user_uuid));
|
||||||
joinable!(twofactor -> users (user_uuid));
|
joinable!(twofactor -> users (user_uuid));
|
||||||
joinable!(users_collections -> collections (collection_uuid));
|
joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
@@ -191,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
org_policies,
|
org_policies,
|
||||||
organizations,
|
organizations,
|
||||||
|
sends,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
|
|||||||
@@ -102,6 +102,29 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
expiration_date -> Nullable<Timestamp>,
|
||||||
|
deletion_date -> Timestamp,
|
||||||
|
disabled -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -116,6 +139,7 @@ table! {
|
|||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
enabled -> Bool,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
verified_at -> Nullable<Timestamp>,
|
verified_at -> Nullable<Timestamp>,
|
||||||
@@ -135,6 +159,7 @@ table! {
|
|||||||
totp_secret -> Nullable<Text>,
|
totp_secret -> Nullable<Text>,
|
||||||
totp_recover -> Nullable<Text>,
|
totp_recover -> Nullable<Text>,
|
||||||
security_stamp -> Text,
|
security_stamp -> Text,
|
||||||
|
stamp_exception -> Nullable<Text>,
|
||||||
equivalent_domains -> Text,
|
equivalent_domains -> Text,
|
||||||
excluded_globals -> Text,
|
excluded_globals -> Text,
|
||||||
client_kdf_type -> Integer,
|
client_kdf_type -> Integer,
|
||||||
@@ -174,6 +199,8 @@ joinable!(folders -> users (user_uuid));
|
|||||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
joinable!(org_policies -> organizations (org_uuid));
|
joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
joinable!(sends -> organizations (organization_uuid));
|
||||||
|
joinable!(sends -> users (user_uuid));
|
||||||
joinable!(twofactor -> users (user_uuid));
|
joinable!(twofactor -> users (user_uuid));
|
||||||
joinable!(users_collections -> collections (collection_uuid));
|
joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
@@ -191,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
org_policies,
|
org_policies,
|
||||||
organizations,
|
organizations,
|
||||||
|
sends,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
|
|||||||
@@ -102,6 +102,29 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
expiration_date -> Nullable<Timestamp>,
|
||||||
|
deletion_date -> Timestamp,
|
||||||
|
disabled -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -116,6 +139,7 @@ table! {
|
|||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
enabled -> Bool,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
verified_at -> Nullable<Timestamp>,
|
verified_at -> Nullable<Timestamp>,
|
||||||
@@ -135,6 +159,7 @@ table! {
|
|||||||
totp_secret -> Nullable<Text>,
|
totp_secret -> Nullable<Text>,
|
||||||
totp_recover -> Nullable<Text>,
|
totp_recover -> Nullable<Text>,
|
||||||
security_stamp -> Text,
|
security_stamp -> Text,
|
||||||
|
stamp_exception -> Nullable<Text>,
|
||||||
equivalent_domains -> Text,
|
equivalent_domains -> Text,
|
||||||
excluded_globals -> Text,
|
excluded_globals -> Text,
|
||||||
client_kdf_type -> Integer,
|
client_kdf_type -> Integer,
|
||||||
@@ -174,6 +199,8 @@ joinable!(folders -> users (user_uuid));
|
|||||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
joinable!(org_policies -> organizations (org_uuid));
|
joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
joinable!(sends -> organizations (organization_uuid));
|
||||||
|
joinable!(sends -> users (user_uuid));
|
||||||
joinable!(twofactor -> users (user_uuid));
|
joinable!(twofactor -> users (user_uuid));
|
||||||
joinable!(users_collections -> collections (collection_uuid));
|
joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
@@ -191,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
org_policies,
|
org_policies,
|
||||||
organizations,
|
organizations,
|
||||||
|
sends,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
|
|||||||
34
src/error.rs
34
src/error.rs
@@ -33,16 +33,16 @@ macro_rules! make_error {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use diesel::r2d2::PoolError as R2d2Err;
|
||||||
use diesel::result::Error as DieselErr;
|
use diesel::result::Error as DieselErr;
|
||||||
use diesel::ConnectionError as DieselConErr;
|
use diesel::ConnectionError as DieselConErr;
|
||||||
use diesel_migrations::RunMigrationsError as DieselMigErr;
|
use diesel_migrations::RunMigrationsError as DieselMigErr;
|
||||||
use diesel::r2d2::PoolError as R2d2Err;
|
|
||||||
use handlebars::RenderError as HbErr;
|
use handlebars::RenderError as HbErr;
|
||||||
use jsonwebtoken::errors::Error as JWTErr;
|
use jsonwebtoken::errors::Error as JwtErr;
|
||||||
use regex::Error as RegexErr;
|
use regex::Error as RegexErr;
|
||||||
use reqwest::Error as ReqErr;
|
use reqwest::Error as ReqErr;
|
||||||
use serde_json::{Error as SerdeErr, Value};
|
use serde_json::{Error as SerdeErr, Value};
|
||||||
use std::io::Error as IOErr;
|
use std::io::Error as IoErr;
|
||||||
|
|
||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use u2f::u2ferror::U2fError as U2fErr;
|
use u2f::u2ferror::U2fError as U2fErr;
|
||||||
@@ -72,10 +72,10 @@ make_error! {
|
|||||||
R2d2Error(R2d2Err): _has_source, _api_error,
|
R2d2Error(R2d2Err): _has_source, _api_error,
|
||||||
U2fError(U2fErr): _has_source, _api_error,
|
U2fError(U2fErr): _has_source, _api_error,
|
||||||
SerdeError(SerdeErr): _has_source, _api_error,
|
SerdeError(SerdeErr): _has_source, _api_error,
|
||||||
JWTError(JWTErr): _has_source, _api_error,
|
JWtError(JwtErr): _has_source, _api_error,
|
||||||
TemplError(HbErr): _has_source, _api_error,
|
TemplError(HbErr): _has_source, _api_error,
|
||||||
//WsError(ws::Error): _has_source, _api_error,
|
//WsError(ws::Error): _has_source, _api_error,
|
||||||
IOError(IOErr): _has_source, _api_error,
|
IoError(IoErr): _has_source, _api_error,
|
||||||
TimeError(TimeErr): _has_source, _api_error,
|
TimeError(TimeErr): _has_source, _api_error,
|
||||||
ReqError(ReqErr): _has_source, _api_error,
|
ReqError(ReqErr): _has_source, _api_error,
|
||||||
RegexError(RegexErr): _has_source, _api_error,
|
RegexError(RegexErr): _has_source, _api_error,
|
||||||
@@ -152,6 +152,7 @@ impl<S> MapResult<S> for Option<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
const fn _has_source<T>(e: T) -> Option<T> {
|
const fn _has_source<T>(e: T) -> Option<T> {
|
||||||
Some(e)
|
Some(e)
|
||||||
}
|
}
|
||||||
@@ -190,17 +191,14 @@ use rocket::response::{self, Responder, Response};
|
|||||||
impl<'r> Responder<'r> for Error {
|
impl<'r> Responder<'r> for Error {
|
||||||
fn respond_to(self, _: &Request) -> response::Result<'r> {
|
fn respond_to(self, _: &Request) -> response::Result<'r> {
|
||||||
match self.error {
|
match self.error {
|
||||||
ErrorKind::EmptyError(_) => {} // Don't print the error in this situation
|
ErrorKind::EmptyError(_) => {} // Don't print the error in this situation
|
||||||
|
ErrorKind::SimpleError(_) => {} // Don't print the error in this situation
|
||||||
_ => error!(target: "error", "{:#?}", self),
|
_ => error!(target: "error", "{:#?}", self),
|
||||||
};
|
};
|
||||||
|
|
||||||
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
|
||||||
|
|
||||||
Response::build()
|
Response::build().status(code).header(ContentType::JSON).sized_body(Cursor::new(format!("{}", self))).ok()
|
||||||
.status(code)
|
|
||||||
.header(ContentType::JSON)
|
|
||||||
.sized_body(Cursor::new(format!("{}", self)))
|
|
||||||
.ok()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,13 +208,27 @@ impl<'r> Responder<'r> for Error {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! err {
|
macro_rules! err {
|
||||||
($msg:expr) => {{
|
($msg:expr) => {{
|
||||||
|
error!("{}", $msg);
|
||||||
return Err(crate::error::Error::new($msg, $msg));
|
return Err(crate::error::Error::new($msg, $msg));
|
||||||
}};
|
}};
|
||||||
($usr_msg:expr, $log_value:expr) => {{
|
($usr_msg:expr, $log_value:expr) => {{
|
||||||
|
error!("{}. {}", $usr_msg, $log_value);
|
||||||
return Err(crate::error::Error::new($usr_msg, $log_value));
|
return Err(crate::error::Error::new($usr_msg, $log_value));
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! err_code {
|
||||||
|
($msg:expr, $err_code: literal) => {{
|
||||||
|
error!("{}", $msg);
|
||||||
|
return Err(crate::error::Error::new($msg, $msg).with_code($err_code));
|
||||||
|
}};
|
||||||
|
($usr_msg:expr, $log_value:expr, $err_code: literal) => {{
|
||||||
|
error!("{}. {}", $usr_msg, $log_value);
|
||||||
|
return Err(crate::error::Error::new($usr_msg, $log_value).with_code($err_code));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! err_discard {
|
macro_rules! err_discard {
|
||||||
($msg:expr, $data:expr) => {{
|
($msg:expr, $data:expr) => {{
|
||||||
|
|||||||
143
src/mail.rs
143
src/mail.rs
@@ -1,12 +1,12 @@
|
|||||||
use std::{env, str::FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use chrono_tz::Tz;
|
|
||||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||||
|
|
||||||
use lettre::{
|
use lettre::{
|
||||||
message::{header, Mailbox, Message, MultiPart, SinglePart},
|
message::{header, Mailbox, Message, MultiPart, SinglePart},
|
||||||
transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism},
|
transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism},
|
||||||
|
transport::smtp::client::{Tls, TlsParameters},
|
||||||
transport::smtp::extension::ClientId,
|
transport::smtp::extension::ClientId,
|
||||||
Address, SmtpTransport, Transport,
|
Address, SmtpTransport, Transport,
|
||||||
};
|
};
|
||||||
@@ -22,21 +22,30 @@ fn mailer() -> SmtpTransport {
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
let host = CONFIG.smtp_host().unwrap();
|
let host = CONFIG.smtp_host().unwrap();
|
||||||
|
|
||||||
// Determine security
|
let smtp_client = SmtpTransport::builder_dangerous(host.as_str())
|
||||||
let smtp_client = if CONFIG.smtp_ssl() {
|
|
||||||
if CONFIG.smtp_explicit_tls() {
|
|
||||||
SmtpTransport::relay(host.as_str())
|
|
||||||
} else {
|
|
||||||
SmtpTransport::starttls_relay(host.as_str())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(SmtpTransport::builder_dangerous(host.as_str()))
|
|
||||||
};
|
|
||||||
|
|
||||||
let smtp_client = smtp_client.unwrap()
|
|
||||||
.port(CONFIG.smtp_port())
|
.port(CONFIG.smtp_port())
|
||||||
.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())));
|
.timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())));
|
||||||
|
|
||||||
|
// Determine security
|
||||||
|
let smtp_client = if CONFIG.smtp_ssl() {
|
||||||
|
let mut tls_parameters = TlsParameters::builder(host);
|
||||||
|
if CONFIG.smtp_accept_invalid_hostnames() {
|
||||||
|
tls_parameters = tls_parameters.dangerous_accept_invalid_hostnames(true);
|
||||||
|
}
|
||||||
|
if CONFIG.smtp_accept_invalid_certs() {
|
||||||
|
tls_parameters = tls_parameters.dangerous_accept_invalid_certs(true);
|
||||||
|
}
|
||||||
|
let tls_parameters = tls_parameters.build().unwrap();
|
||||||
|
|
||||||
|
if CONFIG.smtp_explicit_tls() {
|
||||||
|
smtp_client.tls(Tls::Wrapper(tls_parameters))
|
||||||
|
} else {
|
||||||
|
smtp_client.tls(Tls::Required(tls_parameters))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smtp_client
|
||||||
|
};
|
||||||
|
|
||||||
let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) {
|
let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) {
|
||||||
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user, pass)),
|
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user, pass)),
|
||||||
_ => smtp_client,
|
_ => smtp_client,
|
||||||
@@ -49,15 +58,17 @@ fn mailer() -> SmtpTransport {
|
|||||||
|
|
||||||
let smtp_client = match CONFIG.smtp_auth_mechanism() {
|
let smtp_client = match CONFIG.smtp_auth_mechanism() {
|
||||||
Some(mechanism) => {
|
Some(mechanism) => {
|
||||||
let allowed_mechanisms = vec![SmtpAuthMechanism::Plain, SmtpAuthMechanism::Login, SmtpAuthMechanism::Xoauth2];
|
let allowed_mechanisms = [SmtpAuthMechanism::Plain, SmtpAuthMechanism::Login, SmtpAuthMechanism::Xoauth2];
|
||||||
let mut selected_mechanisms = vec![];
|
let mut selected_mechanisms = vec![];
|
||||||
for wanted_mechanism in mechanism.split(',') {
|
for wanted_mechanism in mechanism.split(',') {
|
||||||
for m in &allowed_mechanisms {
|
for m in &allowed_mechanisms {
|
||||||
if m.to_string().to_lowercase() == wanted_mechanism.trim_matches(|c| c == '"' || c == '\'' || c == ' ').to_lowercase() {
|
if m.to_string().to_lowercase()
|
||||||
|
== wanted_mechanism.trim_matches(|c| c == '"' || c == '\'' || c == ' ').to_lowercase()
|
||||||
|
{
|
||||||
selected_mechanisms.push(*m);
|
selected_mechanisms.push(*m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if !selected_mechanisms.is_empty() {
|
if !selected_mechanisms.is_empty() {
|
||||||
smtp_client.authentication(selected_mechanisms)
|
smtp_client.authentication(selected_mechanisms)
|
||||||
@@ -97,22 +108,6 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String
|
|||||||
Ok((subject, body))
|
Ok((subject, body))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_datetime(dt: &DateTime<Local>) -> String {
|
|
||||||
let fmt = "%A, %B %_d, %Y at %r %Z";
|
|
||||||
|
|
||||||
// With a DateTime<Local>, `%Z` formats as the time zone's UTC offset
|
|
||||||
// (e.g., `+00:00`). If the `TZ` environment variable is set, try to
|
|
||||||
// format as a time zone abbreviation instead (e.g., `UTC`).
|
|
||||||
if let Ok(tz) = env::var("TZ") {
|
|
||||||
if let Ok(tz) = tz.parse::<Tz>() {
|
|
||||||
return dt.with_timezone(&tz).format(fmt).to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, fall back to just displaying the UTC offset.
|
|
||||||
dt.format(fmt).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
||||||
let template_name = if hint.is_some() {
|
let template_name = if hint.is_some() {
|
||||||
"email/pw_hint_some"
|
"email/pw_hint_some"
|
||||||
@@ -122,7 +117,7 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
|||||||
|
|
||||||
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -139,7 +134,7 @@ pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -156,7 +151,7 @@ pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_welcome(address: &str) -> EmptyResult {
|
pub fn send_welcome(address: &str) -> EmptyResult {
|
||||||
@@ -167,7 +162,7 @@ pub fn send_welcome(address: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -183,7 +178,7 @@ pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite(
|
pub fn send_invite(
|
||||||
@@ -207,15 +202,15 @@ pub fn send_invite(
|
|||||||
"email/send_org_invite",
|
"email/send_org_invite",
|
||||||
json!({
|
json!({
|
||||||
"url": CONFIG.domain(),
|
"url": CONFIG.domain(),
|
||||||
"org_id": org_id.unwrap_or_else(|| "_".to_string()),
|
"org_id": org_id.as_deref().unwrap_or("_"),
|
||||||
"org_user_id": org_user_id.unwrap_or_else(|| "_".to_string()),
|
"org_user_id": org_user_id.as_deref().unwrap_or("_"),
|
||||||
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||||
"org_name": org_name,
|
"org_name": org_name,
|
||||||
"token": invite_token,
|
"token": invite_token,
|
||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
||||||
@@ -228,7 +223,7 @@ pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str)
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||||
@@ -240,24 +235,25 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>, device: &str) -> EmptyResult {
|
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>, device: &str) -> EmptyResult {
|
||||||
use crate::util::upcase_first;
|
use crate::util::upcase_first;
|
||||||
let device = upcase_first(device);
|
let device = upcase_first(device);
|
||||||
|
|
||||||
|
let fmt = "%A, %B %_d, %Y at %r %Z";
|
||||||
let (subject, body_html, body_text) = get_text(
|
let (subject, body_html, body_text) = get_text(
|
||||||
"email/new_device_logged_in",
|
"email/new_device_logged_in",
|
||||||
json!({
|
json!({
|
||||||
"url": CONFIG.domain(),
|
"url": CONFIG.domain(),
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"device": device,
|
"device": device,
|
||||||
"datetime": format_datetime(dt),
|
"datetime": crate::util::format_datetime_local(dt, fmt),
|
||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
||||||
@@ -269,7 +265,7 @@ pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
||||||
@@ -281,7 +277,7 @@ pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_test(address: &str) -> EmptyResult {
|
pub fn send_test(address: &str) -> EmptyResult {
|
||||||
@@ -292,10 +288,10 @@ pub fn send_test(address: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {
|
||||||
let address_split: Vec<&str> = address.rsplitn(2, '@').collect();
|
let address_split: Vec<&str> = address.rsplitn(2, '@').collect();
|
||||||
if address_split.len() != 2 {
|
if address_split.len() != 2 {
|
||||||
err!("Invalid email address (no @)");
|
err!("Invalid email address (no @)");
|
||||||
@@ -308,46 +304,41 @@ fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) ->
|
|||||||
|
|
||||||
let address = format!("{}@{}", address_split[1], domain_puny);
|
let address = format!("{}@{}", address_split[1], domain_puny);
|
||||||
|
|
||||||
let html = SinglePart::base64()
|
let html = SinglePart::builder()
|
||||||
|
// We force Base64 encoding because in the past we had issues with different encodings.
|
||||||
|
.header(header::ContentTransferEncoding::Base64)
|
||||||
.header(header::ContentType("text/html; charset=utf-8".parse()?))
|
.header(header::ContentType("text/html; charset=utf-8".parse()?))
|
||||||
.body(body_html);
|
.body(body_html);
|
||||||
|
|
||||||
let text = SinglePart::base64()
|
let text = SinglePart::builder()
|
||||||
|
// We force Base64 encoding because in the past we had issues with different encodings.
|
||||||
|
.header(header::ContentTransferEncoding::Base64)
|
||||||
.header(header::ContentType("text/plain; charset=utf-8".parse()?))
|
.header(header::ContentType("text/plain; charset=utf-8".parse()?))
|
||||||
.body(body_text);
|
.body(body_text);
|
||||||
|
|
||||||
// The boundary generated by Lettre it self is mostly too large based on the RFC822, so we generate one our selfs.
|
let smtp_from = &CONFIG.smtp_from();
|
||||||
use uuid::Uuid;
|
|
||||||
let boundary = format!("_Part_{}_", Uuid::new_v4().to_simple());
|
|
||||||
let alternative = MultiPart::alternative().boundary(boundary).singlepart(text).singlepart(html);
|
|
||||||
|
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
|
.message_id(Some(format!("<{}@{}>", crate::util::get_uuid(), smtp_from.split('@').collect::<Vec<&str>>()[1])))
|
||||||
.to(Mailbox::new(None, Address::from_str(&address)?))
|
.to(Mailbox::new(None, Address::from_str(&address)?))
|
||||||
.from(Mailbox::new(
|
.from(Mailbox::new(Some(CONFIG.smtp_from_name()), Address::from_str(smtp_from)?))
|
||||||
Some(CONFIG.smtp_from_name()),
|
|
||||||
Address::from_str(&CONFIG.smtp_from())?,
|
|
||||||
))
|
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.multipart(alternative)?;
|
.multipart(MultiPart::alternative().singlepart(text).singlepart(html))?;
|
||||||
|
|
||||||
match mailer().send(&email) {
|
match mailer().send(&email) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
// Match some common errors and make them more user friendly
|
// Match some common errors and make them more user friendly
|
||||||
Err(e) => match e {
|
Err(e) => {
|
||||||
lettre::transport::smtp::Error::Client(x) => {
|
if e.is_client() {
|
||||||
err!(format!("SMTP Client error: {}", x));
|
err!(format!("SMTP Client error: {}", e));
|
||||||
},
|
} else if e.is_transient() {
|
||||||
lettre::transport::smtp::Error::Transient(x) => {
|
err!(format!("SMTP 4xx error: {:?}", e));
|
||||||
err!(format!("SMTP 4xx error: {:?}", x.message));
|
} else if e.is_permanent() {
|
||||||
},
|
err!(format!("SMTP 5xx error: {:?}", e));
|
||||||
lettre::transport::smtp::Error::Permanent(x) => {
|
} else if e.is_timeout() {
|
||||||
err!(format!("SMTP 5xx error: {:?}", x.message));
|
err!(format!("SMTP timeout error: {:?}", e));
|
||||||
},
|
} else {
|
||||||
lettre::transport::smtp::Error::Io(x) => {
|
Err(e.into())
|
||||||
err!(format!("SMTP IO error: {}", x));
|
}
|
||||||
},
|
|
||||||
// Fallback for all other errors
|
|
||||||
_ => Err(e.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
152
src/main.rs
152
src/main.rs
@@ -1,12 +1,12 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![cfg_attr(feature = "unstable", feature(ip))]
|
#![cfg_attr(feature = "unstable", feature(ip))]
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "512"]
|
||||||
|
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -16,6 +16,7 @@ extern crate diesel;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
|
use job_scheduler::{Job, JobScheduler};
|
||||||
use std::{
|
use std::{
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
panic,
|
panic,
|
||||||
@@ -23,10 +24,9 @@ use std::{
|
|||||||
process::{exit, Command},
|
process::{exit, Command},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
thread,
|
thread,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use structopt::StructOpt;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod error;
|
mod error;
|
||||||
mod api;
|
mod api;
|
||||||
@@ -40,14 +40,7 @@ mod util;
|
|||||||
|
|
||||||
pub use config::CONFIG;
|
pub use config::CONFIG;
|
||||||
pub use error::{Error, MapResult};
|
pub use error::{Error, MapResult};
|
||||||
|
pub use util::is_running_in_docker;
|
||||||
#[derive(Debug, StructOpt)]
|
|
||||||
#[structopt(name = "bitwarden_rs", about = "A Bitwarden API server written in Rust")]
|
|
||||||
struct Opt {
|
|
||||||
/// Prints the app version
|
|
||||||
#[structopt(short, long)]
|
|
||||||
version: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
parse_args();
|
parse_args();
|
||||||
@@ -57,34 +50,47 @@ fn main() {
|
|||||||
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
|
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
|
||||||
init_logging(level).ok();
|
init_logging(level).ok();
|
||||||
|
|
||||||
let extra_debug = match level {
|
let extra_debug = matches!(level, LF::Trace | LF::Debug);
|
||||||
LF::Trace | LF::Debug => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
check_data_folder();
|
||||||
check_rsa_keys();
|
check_rsa_keys();
|
||||||
check_web_vault();
|
check_web_vault();
|
||||||
|
|
||||||
create_icon_cache_folder();
|
create_icon_cache_folder();
|
||||||
|
|
||||||
launch_rocket(extra_debug);
|
let pool = create_db_pool();
|
||||||
|
schedule_jobs(pool.clone());
|
||||||
|
launch_rocket(pool, extra_debug); // Blocks until program termination.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HELP: &str = "\
|
||||||
|
Alternative implementation of the Bitwarden server API written in Rust
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
vaultwarden
|
||||||
|
|
||||||
|
FLAGS:
|
||||||
|
-h, --help Prints help information
|
||||||
|
-v, --version Prints the app version
|
||||||
|
";
|
||||||
|
|
||||||
fn parse_args() {
|
fn parse_args() {
|
||||||
let opt = Opt::from_args();
|
const NO_VERSION: &str = "(Version info from Git not present)";
|
||||||
if opt.version {
|
let mut pargs = pico_args::Arguments::from_env();
|
||||||
if let Some(version) = option_env!("BWRS_VERSION") {
|
|
||||||
println!("bitwarden_rs {}", version);
|
if pargs.contains(["-h", "--help"]) {
|
||||||
} else {
|
println!("vaultwarden {}", option_env!("BWRS_VERSION").unwrap_or(NO_VERSION));
|
||||||
println!("bitwarden_rs (Version info from Git not present)");
|
print!("{}", HELP);
|
||||||
}
|
exit(0);
|
||||||
|
} else if pargs.contains(["-v", "--version"]) {
|
||||||
|
println!("vaultwarden {}", option_env!("BWRS_VERSION").unwrap_or(NO_VERSION));
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch_info() {
|
fn launch_info() {
|
||||||
println!("/--------------------------------------------------------------------\\");
|
println!("/--------------------------------------------------------------------\\");
|
||||||
println!("| Starting Bitwarden_RS |");
|
println!("| Starting Vaultwarden |");
|
||||||
|
|
||||||
if let Some(version) = option_env!("BWRS_VERSION") {
|
if let Some(version) = option_env!("BWRS_VERSION") {
|
||||||
println!("|{:^68}|", format!("Version {}", version));
|
println!("|{:^68}|", format!("Version {}", version));
|
||||||
@@ -96,7 +102,7 @@ fn launch_info() {
|
|||||||
println!("| Send usage/configuration questions or feature requests to: |");
|
println!("| Send usage/configuration questions or feature requests to: |");
|
||||||
println!("| https://bitwardenrs.discourse.group/ |");
|
println!("| https://bitwardenrs.discourse.group/ |");
|
||||||
println!("| Report suspected bugs/issues in the software itself at: |");
|
println!("| Report suspected bugs/issues in the software itself at: |");
|
||||||
println!("| https://github.com/dani-garcia/bitwarden_rs/issues/new |");
|
println!("| https://github.com/dani-garcia/vaultwarden/issues/new |");
|
||||||
println!("\\--------------------------------------------------------------------/\n");
|
println!("\\--------------------------------------------------------------------/\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +119,23 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
.level_for("launch_", log::LevelFilter::Off)
|
.level_for("launch_", log::LevelFilter::Off)
|
||||||
.level_for("rocket::rocket", log::LevelFilter::Off)
|
.level_for("rocket::rocket", log::LevelFilter::Off)
|
||||||
.level_for("rocket::fairing", log::LevelFilter::Off)
|
.level_for("rocket::fairing", log::LevelFilter::Off)
|
||||||
|
// Never show html5ever and hyper::proto logs, too noisy
|
||||||
|
.level_for("html5ever", log::LevelFilter::Off)
|
||||||
|
.level_for("hyper::proto", log::LevelFilter::Off)
|
||||||
.chain(std::io::stdout());
|
.chain(std::io::stdout());
|
||||||
|
|
||||||
|
// Enable smtp debug logging only specifically for smtp when need.
|
||||||
|
// This can contain sensitive information we do not want in the default debug/trace logging.
|
||||||
|
if CONFIG.smtp_debug() {
|
||||||
|
println!(
|
||||||
|
"[WARNING] SMTP Debugging is enabled (SMTP_DEBUG=true). Sensitive information could be disclosed via logs!"
|
||||||
|
);
|
||||||
|
println!("[WARNING] Only enable SMTP_DEBUG during troubleshooting!\n");
|
||||||
|
logger = logger.level_for("lettre::transport::smtp", log::LevelFilter::Debug)
|
||||||
|
} else {
|
||||||
|
logger = logger.level_for("lettre::transport::smtp", log::LevelFilter::Off)
|
||||||
|
}
|
||||||
|
|
||||||
if CONFIG.extended_logging() {
|
if CONFIG.extended_logging() {
|
||||||
logger = logger.format(|out, message, record| {
|
logger = logger.format(|out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
@@ -186,7 +207,7 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
|||||||
let syslog_fmt = syslog::Formatter3164 {
|
let syslog_fmt = syslog::Formatter3164 {
|
||||||
facility: syslog::Facility::LOG_USER,
|
facility: syslog::Facility::LOG_USER,
|
||||||
hostname: None,
|
hostname: None,
|
||||||
process: "bitwarden_rs".into(),
|
process: "vaultwarden".into(),
|
||||||
pid: 0,
|
pid: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,9 +220,28 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir(path: &str, description: &str) {
|
||||||
|
// Try to create the specified dir, if it doesn't already exist.
|
||||||
|
let err_msg = format!("Error creating {} directory '{}'", description, path);
|
||||||
|
create_dir_all(path).expect(&err_msg);
|
||||||
|
}
|
||||||
|
|
||||||
fn create_icon_cache_folder() {
|
fn create_icon_cache_folder() {
|
||||||
// Try to create the icon cache folder, and generate an error if it could not.
|
create_dir(&CONFIG.icon_cache_folder(), "icon cache");
|
||||||
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory");
|
}
|
||||||
|
|
||||||
|
fn check_data_folder() {
|
||||||
|
let data_folder = &CONFIG.data_folder();
|
||||||
|
let path = Path::new(data_folder);
|
||||||
|
if !path.exists() {
|
||||||
|
error!("Data folder '{}' doesn't exist.", data_folder);
|
||||||
|
if is_running_in_docker() {
|
||||||
|
error!("Verify that your data volume is mounted at the correct location.");
|
||||||
|
} else {
|
||||||
|
error!("Create the data folder and try again.");
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_rsa_keys() {
|
fn check_rsa_keys() {
|
||||||
@@ -260,22 +300,27 @@ fn check_web_vault() {
|
|||||||
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
|
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
|
||||||
|
|
||||||
if !index_path.exists() {
|
if !index_path.exists() {
|
||||||
error!("Web vault is not found at '{}'. To install it, please follow the steps in: ", CONFIG.web_vault_folder());
|
error!(
|
||||||
error!("https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault");
|
"Web vault is not found at '{}'. To install it, please follow the steps in: ",
|
||||||
|
CONFIG.web_vault_folder()
|
||||||
|
);
|
||||||
|
error!("https://github.com/dani-garcia/vaultwarden/wiki/Building-binary#install-the-web-vault");
|
||||||
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
|
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch_rocket(extra_debug: bool) {
|
fn create_db_pool() -> db::DbPool {
|
||||||
let pool = match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()) {
|
match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error creating database pool: {:?}", e);
|
error!("Error creating database pool: {:?}", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn launch_rocket(pool: db::DbPool, extra_debug: bool) {
|
||||||
let basepath = &CONFIG.domain_path();
|
let basepath = &CONFIG.domain_path();
|
||||||
|
|
||||||
// If adding more paths here, consider also adding them to
|
// If adding more paths here, consider also adding them to
|
||||||
@@ -290,7 +335,7 @@ fn launch_rocket(extra_debug: bool) {
|
|||||||
.manage(pool)
|
.manage(pool)
|
||||||
.manage(api::start_notification_server())
|
.manage(api::start_notification_server())
|
||||||
.attach(util::AppHeaders())
|
.attach(util::AppHeaders())
|
||||||
.attach(util::CORS())
|
.attach(util::Cors())
|
||||||
.attach(util::BetterLogging(extra_debug))
|
.attach(util::BetterLogging(extra_debug))
|
||||||
.launch();
|
.launch();
|
||||||
|
|
||||||
@@ -298,3 +343,40 @@ fn launch_rocket(extra_debug: bool) {
|
|||||||
// The launch will restore the original logging level
|
// The launch will restore the original logging level
|
||||||
error!("Launch error {:#?}", result);
|
error!("Launch error {:#?}", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schedule_jobs(pool: db::DbPool) {
|
||||||
|
if CONFIG.job_poll_interval_ms() == 0 {
|
||||||
|
info!("Job scheduler disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("job-scheduler".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
let mut sched = JobScheduler::new();
|
||||||
|
|
||||||
|
// Purge sends that are past their deletion date.
|
||||||
|
if !CONFIG.send_purge_schedule().is_empty() {
|
||||||
|
sched.add(Job::new(CONFIG.send_purge_schedule().parse().unwrap(), || {
|
||||||
|
api::purge_sends(pool.clone());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge trashed items that are old enough to be auto-deleted.
|
||||||
|
if !CONFIG.trash_purge_schedule().is_empty() {
|
||||||
|
sched.add(Job::new(CONFIG.trash_purge_schedule().parse().unwrap(), || {
|
||||||
|
api::purge_trashed_ciphers(pool.clone());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically check for jobs to run. We probably won't need any
|
||||||
|
// jobs that run more often than once a minute, so a default poll
|
||||||
|
// interval of 30 seconds should be sufficient. Users who want to
|
||||||
|
// schedule jobs to run more frequently for some reason can reduce
|
||||||
|
// the poll interval accordingly.
|
||||||
|
loop {
|
||||||
|
sched.tick();
|
||||||
|
thread::sleep(Duration::from_millis(CONFIG.job_poll_interval_ms()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Error spawning job scheduler thread");
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 344 B |
@@ -508,7 +508,8 @@
|
|||||||
"disneymoviesanywhere.com",
|
"disneymoviesanywhere.com",
|
||||||
"go.com",
|
"go.com",
|
||||||
"disney.com",
|
"disney.com",
|
||||||
"dadt.com"
|
"dadt.com",
|
||||||
|
"disneyplus.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
@@ -771,7 +772,8 @@
|
|||||||
"stackoverflow.com",
|
"stackoverflow.com",
|
||||||
"serverfault.com",
|
"serverfault.com",
|
||||||
"mathoverflow.net",
|
"mathoverflow.net",
|
||||||
"askubuntu.com"
|
"askubuntu.com",
|
||||||
|
"stackapps.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
},
|
},
|
||||||
@@ -885,5 +887,21 @@
|
|||||||
"yandex.uz"
|
"yandex.uz"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": 84,
|
||||||
|
"Domains": [
|
||||||
|
"sonyentertainmentnetwork.com",
|
||||||
|
"sony.com"
|
||||||
|
],
|
||||||
|
"Excluded": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": 85,
|
||||||
|
"Domains": [
|
||||||
|
"protonmail.com",
|
||||||
|
"protonvpn.com"
|
||||||
|
],
|
||||||
|
"Excluded": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
BIN
src/static/images/fallback-icon.png
Normal file
BIN
src/static/images/fallback-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 B |
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user