mirror of
https://github.com/ViViDboarder/bitwarden_rs.git
synced 2024-06-02 09:32:34 +00:00
Compare commits
258 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
296063e135 | ||
|
b9daa59e5d | ||
|
5bdcfe128d | ||
|
1842a796fb | ||
|
ce99e5c583 | ||
|
0c96c2d305 | ||
|
5796b6b554 | ||
|
c7ab27c86f | ||
|
8c03746a67 | ||
|
8746d36845 | ||
|
448e6ac917 | ||
|
729c9cff41 | ||
|
22b9c80007 | ||
|
ab4355cfed | ||
|
948dc82228 | ||
|
bc74fd23e7 | ||
|
37776241be | ||
|
feba41ec88 | ||
|
6a8f42da8a | ||
|
670d8cb83a | ||
|
2f7fbde789 | ||
|
c698bca2b9 | ||
|
0b6a003a8b | ||
|
c64560016e | ||
|
978be0b4a9 | ||
|
b58bff1178 | ||
|
2f3e18caa9 | ||
|
6a291040bd | ||
|
dbc082dc75 | ||
|
32a0dd09bf | ||
|
f847c6e225 | ||
|
99da5fbebb | ||
|
6a0d024c69 | ||
|
b24929a243 | ||
|
9a47821642 | ||
|
d69968313b | ||
|
3c377d97dc | ||
|
ea15218197 | ||
|
0eee907c88 | ||
|
c877583979 | ||
|
844cf70345 | ||
|
a0d92a167c | ||
|
d7b0d6f9f5 | ||
|
4c3b328aca | ||
|
260ffee093 | ||
|
c59cfe3371 | ||
|
0822c0c128 | ||
|
57a88f0a1b | ||
|
87393409f9 | ||
|
062f5e4712 | ||
|
aaba1e8368 | ||
|
ff2684dfee | ||
|
6b5fa201aa | ||
|
7167e443ca | ||
|
175d647e47 | ||
|
4c324e1160 | ||
|
0365b7c6a4 | ||
|
19889187a5 | ||
|
9571277c44 | ||
|
a202da9e23 | ||
|
e5a77a477d | ||
|
c05dc50f53 | ||
|
3bbdbb832c | ||
|
d9684bef6b | ||
|
db0c45c172 | ||
|
ad4393e3f7 | ||
|
f83a8a36d1 | ||
|
0e9eba8c8b | ||
|
d5c760960a | ||
|
2c6ef2bc68 | ||
|
7032ae5587 | ||
|
eba22c2d94 | ||
|
11cc9ae0c0 | ||
|
fb648db47d | ||
|
959283d333 | ||
|
385c2227e7 | ||
|
6d9f03e84b | ||
|
6a972e4b19 | ||
|
171b174ce9 | ||
|
93b7ded1e6 | ||
|
29c6b145ca | ||
|
a7a479623c | ||
|
83dff9ae6e | ||
|
6b2cc5a3ee | ||
|
5247e0d773 | ||
|
05b308b8b4 | ||
|
9621278fca | ||
|
570d6c8bf9 | ||
|
ad48e9ed0f | ||
|
f724addf9a | ||
|
aa20974703 | ||
|
a846f6c610 | ||
|
c218c34812 | ||
|
2626e66873 | ||
|
81e0e1b339 | ||
|
fd1354d00e | ||
|
071a3b2a32 | ||
|
32cfaab5ee | ||
|
d348f12a0e | ||
|
11845d9f5b | ||
|
de70fbf88a | ||
|
0b04caab78 | ||
|
4c78c5a9c9 | ||
|
73f0841f17 | ||
|
4559e85daa | ||
|
bbef332e25 | ||
|
1e950c7dbc | ||
|
f14e19a3d8 | ||
|
668d5c23dc | ||
|
fb6f96f5c3 | ||
|
6e6e34ff18 | ||
|
790146bfac | ||
|
af625930d6 | ||
|
a28ebcb401 | ||
|
77e47ddd1f | ||
|
5b620ba6cd | ||
|
d5f9b33f66 | ||
|
596c9b8691 | ||
|
d4357eb55a | ||
|
b37f0dfde3 | ||
|
624791e09a | ||
|
f9a73a9bbe | ||
|
35868dd72c | ||
|
979d010dc2 | ||
|
b34d548246 | ||
|
a87646b8cb | ||
|
a2411eef56 | ||
|
52ed8e4d75 | ||
|
24c914799d | ||
|
db53511855 | ||
|
325691e588 | ||
|
fac3cb687d | ||
|
afbf1db331 | ||
|
1aefaec297 | ||
|
f1d3fb5d40 | ||
|
ac2723f898 | ||
|
2fffaec226 | ||
|
5c54dfee3a | ||
|
967d2d78ec | ||
|
1aa5e0d4dc | ||
|
b47cf97409 | ||
|
5e802f8aa3 | ||
|
0bdeb02a31 | ||
|
b03698fadb | ||
|
39d1a09704 | ||
|
a447e4e7ef | ||
|
4eee6e7aee | ||
|
b6fde857a7 | ||
|
3c66deb5cc | ||
|
4146612a32 | ||
|
a314933557 | ||
|
c5d7e3f2bc | ||
|
c95a2881b5 | ||
|
4c3727b4a3 | ||
|
a1f304dff7 | ||
|
a8870eef0d | ||
|
afaebc6cf3 | ||
|
8f4a1f4fc2 | ||
|
0807783388 | ||
|
80d4061d14 | ||
|
dc2f8e5c85 | ||
|
aee1ea032b | ||
|
484e82fb9f | ||
|
322a08edfb | ||
|
08afc312c3 | ||
|
5571a5d8ed | ||
|
6a8c65493f | ||
|
dfdf4473ea | ||
|
8bbbff7567 | ||
|
42e37ebea1 | ||
|
632f4d5453 | ||
|
6c5e35ce5c | ||
|
4ff15f6dc2 | ||
|
ec8028aef2 | ||
|
63cbd9ef9c | ||
|
9cca64003a | ||
|
819d5e2dc8 | ||
|
3b06ab296b | ||
|
0de52c6c99 | ||
|
e3b00b59a7 | ||
|
5a390a973f | ||
|
1ee8e44912 | ||
|
86685c1cd2 | ||
|
e3feba2a2c | ||
|
0a68de6c24 | ||
|
4be8dae626 | ||
|
e4d08836e2 | ||
|
c2a324e5da | ||
|
77f95146d6 | ||
|
6cd8512bbd | ||
|
843604c9e7 | ||
|
7407b8326a | ||
|
adf47827c9 | ||
|
5471088e93 | ||
|
4e85a1dee1 | ||
|
ec60839064 | ||
|
d4bfa1a189 | ||
|
862d401077 | ||
|
255a06382d | ||
|
bbb0484d03 | ||
|
93346bc05d | ||
|
fdf50f0064 | ||
|
ccf6ee79d0 | ||
|
91dd19473d | ||
|
c06162b22f | ||
|
7a6a3e4160 |
|
@ -3,6 +3,7 @@ target
|
||||||
|
|
||||||
# Data folder
|
# Data folder
|
||||||
data
|
data
|
||||||
|
.env
|
||||||
|
|
||||||
# IDE files
|
# IDE files
|
||||||
.vscode
|
.vscode
|
||||||
|
@ -10,5 +11,15 @@ data
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
.github
|
||||||
*.md
|
*.md
|
||||||
|
*.txt
|
||||||
|
*.yml
|
||||||
|
*.yaml
|
||||||
|
|
||||||
|
# Docker folders
|
||||||
|
hooks
|
||||||
|
tools
|
||||||
|
|
||||||
|
# Web vault
|
||||||
|
web-vault
|
112
.env.template
112
.env.template
|
@ -1,14 +1,28 @@
|
||||||
## Bitwarden_RS Configuration File
|
## Bitwarden_RS 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
|
||||||
|
## in the admin interface. Those overrides are stored within DATA_FOLDER/config.json .
|
||||||
|
|
||||||
## Main data folder
|
## Main data folder
|
||||||
# DATA_FOLDER=data
|
# DATA_FOLDER=data
|
||||||
|
|
||||||
## Database URL
|
## Database URL
|
||||||
## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3
|
## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3
|
||||||
## When using MySQL, this it is the URL to the DB, including username and password:
|
|
||||||
## Format: mysql://[user[:password]@]host/database_name
|
|
||||||
# DATABASE_URL=data/db.sqlite3
|
# DATABASE_URL=data/db.sqlite3
|
||||||
|
## When using MySQL, specify an appropriate connection URI.
|
||||||
|
## Details: https://docs.diesel.rs/diesel/mysql/struct.MysqlConnection.html
|
||||||
|
# DATABASE_URL=mysql://user:password@host[:port]/database_name
|
||||||
|
## When using PostgreSQL, specify an appropriate connection URI (recommended)
|
||||||
|
## or keyword/value connection string.
|
||||||
|
## Details:
|
||||||
|
## - https://docs.diesel.rs/diesel/pg/struct.PgConnection.html
|
||||||
|
## - https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
||||||
|
# DATABASE_URL=postgresql://user:password@host[:port]/database_name
|
||||||
|
|
||||||
|
## Database max connections
|
||||||
|
## Define the size of the connection pool used for connecting to the database.
|
||||||
|
# DATABASE_MAX_CONNS=10
|
||||||
|
|
||||||
## Individual folders, these override %DATA_FOLDER%
|
## Individual folders, these override %DATA_FOLDER%
|
||||||
# RSA_KEY_FILENAME=data/rsa_key
|
# RSA_KEY_FILENAME=data/rsa_key
|
||||||
|
@ -44,6 +58,10 @@
|
||||||
## 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
|
||||||
|
|
||||||
|
## Timestamp format used in extended logging.
|
||||||
|
## Format specifiers: https://docs.rs/chrono/latest/chrono/format/strftime
|
||||||
|
# LOG_TIMESTAMP_FORMAT="%Y-%m-%d %H:%M:%S.%3f"
|
||||||
|
|
||||||
## Logging to file
|
## Logging to file
|
||||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||||
# LOG_FILE=/path/to/log
|
# LOG_FILE=/path/to/log
|
||||||
|
@ -56,7 +74,7 @@
|
||||||
## Log level
|
## Log level
|
||||||
## Change the verbosity of the log output
|
## Change the verbosity of the log output
|
||||||
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
||||||
## Setting it to "trace" or "debug" would also show logs for mounted
|
## Setting it to "trace" or "debug" would also show logs for mounted
|
||||||
## routes and static file, websocket and alive requests
|
## routes and static file, websocket and alive requests
|
||||||
# LOG_LEVEL=Info
|
# LOG_LEVEL=Info
|
||||||
|
|
||||||
|
@ -68,6 +86,10 @@
|
||||||
## 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
|
||||||
|
|
||||||
|
## Database connection retries
|
||||||
|
## Number of times to retry the database connection during startup, with 1 second delay between each retry, set to 0 to retry indefinitely
|
||||||
|
# DB_CONNECTION_RETRIES=15
|
||||||
|
|
||||||
## Disable icon downloading
|
## Disable icon downloading
|
||||||
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
|
## 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,
|
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||||
|
@ -82,10 +104,11 @@
|
||||||
## 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.
|
||||||
## Usefull 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
|
||||||
# ICON_BLACKLIST_NON_GLOBAL_IPS=true
|
# ICON_BLACKLIST_NON_GLOBAL_IPS=true
|
||||||
|
|
||||||
## Disable 2FA remember
|
## Disable 2FA remember
|
||||||
|
@ -93,6 +116,18 @@
|
||||||
## Note that the checkbox would still be present, but ignored.
|
## Note that the checkbox would still be present, but ignored.
|
||||||
# DISABLE_2FA_REMEMBER=false
|
# DISABLE_2FA_REMEMBER=false
|
||||||
|
|
||||||
|
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
||||||
|
# EMAIL_ATTEMPTS_LIMIT=3
|
||||||
|
|
||||||
|
## Token expiration time
|
||||||
|
## Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||||
|
# EMAIL_EXPIRATION_TIME=600
|
||||||
|
|
||||||
|
## Email token size
|
||||||
|
## Number of digits in an email token (min: 6, max: 19).
|
||||||
|
## Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting!
|
||||||
|
# EMAIL_TOKEN_SIZE=6
|
||||||
|
|
||||||
## Controls if new users can register
|
## Controls if new users can register
|
||||||
# SIGNUPS_ALLOWED=true
|
# SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
|
@ -114,6 +149,14 @@
|
||||||
## even if SIGNUPS_ALLOWED is set to false
|
## even if SIGNUPS_ALLOWED is set to false
|
||||||
# SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org
|
# SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org
|
||||||
|
|
||||||
|
## Controls which users can create new orgs.
|
||||||
|
## Blank or 'all' means all users can create orgs (this is the default):
|
||||||
|
# ORG_CREATION_USERS=
|
||||||
|
## 'none' means no users can create orgs:
|
||||||
|
# ORG_CREATION_USERS=none
|
||||||
|
## A comma-separated list means only those users can create orgs:
|
||||||
|
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com
|
||||||
|
|
||||||
## Token for the admin interface, preferably use a long random string
|
## Token for the admin interface, preferably use a long random string
|
||||||
## One option is to use 'openssl rand -base64 48'
|
## One option is to use 'openssl rand -base64 48'
|
||||||
## If not set, the admin panel is disabled
|
## If not set, the admin panel is disabled
|
||||||
|
@ -125,6 +168,16 @@
|
||||||
|
|
||||||
## 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
|
||||||
|
# INVITATION_ORG_NAME=Bitwarden_RS
|
||||||
|
|
||||||
|
## 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=
|
||||||
|
## Per-user attachment limit (KB).
|
||||||
|
## Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more
|
||||||
|
# USER_ATTACHMENT_LIMIT=
|
||||||
|
|
||||||
|
|
||||||
## Controls the PBBKDF password iterations to apply on the server
|
## Controls the PBBKDF password iterations to apply on the server
|
||||||
## The change only applies when the password is changed
|
## The change only applies when the password is changed
|
||||||
|
@ -140,6 +193,13 @@
|
||||||
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
|
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
|
||||||
# DOMAIN=https://bw.domain.tld:8443
|
# DOMAIN=https://bw.domain.tld:8443
|
||||||
|
|
||||||
|
## Allowed iframe ancestors (Know the risks!)
|
||||||
|
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
|
||||||
|
## Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
|
||||||
|
## This adds the configured value to the 'Content-Security-Policy' headers 'frame-ancestors' value.
|
||||||
|
## Multiple values must be separated with a whitespace.
|
||||||
|
# ALLOWED_IFRAME_ANCESTORS=
|
||||||
|
|
||||||
## Yubico (Yubikey) Settings
|
## Yubico (Yubikey) Settings
|
||||||
## Set your Client ID and Secret Key for Yubikey OTP
|
## Set your Client ID and Secret Key for Yubikey OTP
|
||||||
## You can generate it here: https://upgrade.yubico.com/getapikey/
|
## You can generate it here: https://upgrade.yubico.com/getapikey/
|
||||||
|
@ -162,7 +222,7 @@
|
||||||
## Authenticator Settings
|
## Authenticator Settings
|
||||||
## Disable authenticator time drifted codes to be valid.
|
## Disable authenticator time drifted codes to be valid.
|
||||||
## TOTP codes of the previous and next 30 seconds will be invalid
|
## TOTP codes of the previous and next 30 seconds will be invalid
|
||||||
##
|
##
|
||||||
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
||||||
## we allow by default the TOTP code which was valid one step back and one in the future.
|
## we allow by default the TOTP code which was valid one step back and one in the future.
|
||||||
## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes.
|
## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes.
|
||||||
|
@ -183,11 +243,45 @@
|
||||||
# SMTP_HOST=smtp.domain.tld
|
# SMTP_HOST=smtp.domain.tld
|
||||||
# SMTP_FROM=bitwarden-rs@domain.tld
|
# SMTP_FROM=bitwarden-rs@domain.tld
|
||||||
# SMTP_FROM_NAME=Bitwarden_RS
|
# SMTP_FROM_NAME=Bitwarden_RS
|
||||||
# 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
|
# 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. Usually port 465 is used here.
|
||||||
# SMTP_USERNAME=username
|
# SMTP_USERNAME=username
|
||||||
# SMTP_PASSWORD=password
|
# SMTP_PASSWORD=password
|
||||||
# SMTP_AUTH_MECHANISM="Plain"
|
|
||||||
# SMTP_TIMEOUT=15
|
# SMTP_TIMEOUT=15
|
||||||
|
|
||||||
|
## 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="Plain"
|
||||||
|
|
||||||
|
## Server name sent during the SMTP 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=
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
## If sending the email fails the login attempt will fail!!
|
||||||
|
# REQUIRE_DEVICE_EMAIL=false
|
||||||
|
|
||||||
|
## HIBP Api Key
|
||||||
|
## HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
|
||||||
|
# HIBP_API_KEY=
|
||||||
|
|
||||||
# vim: syntax=ini
|
# vim: syntax=ini
|
||||||
|
|
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -6,27 +6,36 @@ labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!--
|
||||||
|
# ###
|
||||||
|
NOTE: Please update to the latest version of bitwarden_rs before reporting an issue!
|
||||||
|
This saves you and us a lot of time and troubleshooting.
|
||||||
|
See: https://github.com/dani-garcia/bitwarden_rs/issues/1180
|
||||||
|
# ###
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
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/obfuscate personal and confidential information,
|
||||||
such as names, global IP/DNS adresses and especially passwords, if neccessary.
|
such as names, global IP/DNS addresses and especially passwords, if necessary.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Subject of the issue
|
### Subject of the issue
|
||||||
<!-- Describe your issue here.-->
|
<!-- Describe your issue here.-->
|
||||||
|
|
||||||
### Your environment
|
### Your environment
|
||||||
<!-- The version number, obtained from the logs or the admin page -->
|
<!-- The version number, obtained from the logs or the admin diagnostics page -->
|
||||||
* Bitwarden_rs version:
|
<!-- Remember to check your issue on the latest version first! -->
|
||||||
|
* Bitwarden_rs version:
|
||||||
<!-- How the server was installed: Docker image / package / built from source -->
|
<!-- How the server was installed: Docker image / package / built from source -->
|
||||||
* Install method:
|
* Install method:
|
||||||
* Clients used: <!-- if applicable -->
|
* Clients used: <!-- if applicable -->
|
||||||
* Reverse proxy and version: <!-- if applicable -->
|
* Reverse proxy and version: <!-- if applicable -->
|
||||||
* Version of mysql/postgresql: <!-- if applicable -->
|
* Version of mysql/postgresql: <!-- if applicable -->
|
||||||
* Other relevant information:
|
* Other relevant information:
|
||||||
|
|
||||||
### 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)
|
||||||
|
|
125
.github/workflows/build.yml
vendored
Normal file
125
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Ignore when there are only changes done too one of these paths
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- "azure-pipelines.yml"
|
||||||
|
- "docker/**"
|
||||||
|
- "hooks/**"
|
||||||
|
- "tools/**"
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
# End Uses the rust-toolchain file to determine version
|
||||||
|
|
||||||
|
|
||||||
|
# Run cargo tests (In release mode to speed up cargo build afterwards)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
|
||||||
|
# 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: bitwarden_rs-${{ matrix.target-triple }}${{ matrix.ext }}
|
||||||
|
path: target/${{ matrix.target-triple }}/release/bitwarden_rs${{ 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: bitwarden_rs-${{ matrix.target-triple }}${{ matrix.ext }}
|
||||||
|
# path: target/${{ matrix.target-triple }}/release/bitwarden_rs${{ matrix.ext }}
|
||||||
|
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# End Upload to github actions release
|
34
.github/workflows/hadolint.yml
vendored
Normal file
34
.github/workflows/hadolint.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
name: Hadolint
|
||||||
|
|
||||||
|
on:
|
||||||
|
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: 1.19.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"
|
|
1551
Cargo.lock
generated
1551
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
80
Cargo.toml
80
Cargo.toml
|
@ -16,6 +16,12 @@ enable_syslog = []
|
||||||
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
||||||
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
||||||
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
|
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
|
||||||
|
# Enable to use a vendored and statically linked openssl
|
||||||
|
vendored_openssl = ["openssl/vendored"]
|
||||||
|
|
||||||
|
# Enable unstable features, requires nightly
|
||||||
|
# Currently only used to enable rusts official ip support
|
||||||
|
unstable = []
|
||||||
|
|
||||||
[target."cfg(not(windows))".dependencies]
|
[target."cfg(not(windows))".dependencies]
|
||||||
syslog = "4.0.1"
|
syslog = "4.0.1"
|
||||||
|
@ -26,89 +32,87 @@ 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.4", features = ["blocking", "json"] }
|
reqwest = { version = "0.10.10", features = ["blocking", "json"] }
|
||||||
|
|
||||||
# multipart/form-data support
|
# multipart/form-data support
|
||||||
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
multipart = { version = "0.17.0", 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.4"
|
rmpv = "0.4.6"
|
||||||
|
|
||||||
# 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.104"
|
serde = "1.0.118"
|
||||||
serde_derive = "1.0.104"
|
serde_derive = "1.0.118"
|
||||||
serde_json = "1.0.48"
|
serde_json = "1.0.60"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.8"
|
log = "0.4.11"
|
||||||
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.3", features = [ "chrono", "r2d2"] }
|
diesel = { version = "1.4.5", features = [ "chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled SQLite
|
||||||
libsqlite3-sys = { version = "0.16.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.18.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto library
|
# Crypto-related libraries
|
||||||
ring = "0.16.11"
|
rand = "0.7.3"
|
||||||
|
ring = "0.16.19"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "0.8.1", features = ["v4"] }
|
uuid = { version = "0.8.1", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time librar for Rust
|
# Date and time libraries
|
||||||
chrono = "0.4.11"
|
chrono = "0.4.19"
|
||||||
time = "0.2.9"
|
chrono-tz = "0.5.3"
|
||||||
|
time = "0.2.23"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
oath = "0.10.2"
|
oath = "0.10.2"
|
||||||
|
|
||||||
# Data encoding library
|
# Data encoding library
|
||||||
data-encoding = "2.2.0"
|
data-encoding = "2.3.1"
|
||||||
|
|
||||||
# JWT library
|
# JWT library
|
||||||
jsonwebtoken = "7.1.0"
|
jsonwebtoken = "7.2.0"
|
||||||
|
|
||||||
# U2F library
|
# U2F library
|
||||||
u2f = "0.2.0"
|
u2f = "0.2.0"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.9.0", features = ["online-tokio"], default-features = false }
|
yubico = { version = "0.9.1", 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.3.1"
|
once_cell = "1.5.2"
|
||||||
|
|
||||||
# More derives
|
|
||||||
derive_more = "0.99.3"
|
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.11"
|
num-traits = "0.2.14"
|
||||||
num-derive = "0.3.0"
|
num-derive = "0.3.3"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = "0.10.0-pre"
|
lettre = { version = "0.10.0-alpha.4", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
||||||
native-tls = "0.2.4"
|
newline-converter = "0.1.0"
|
||||||
quoted_printable = "0.4.2"
|
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = { version = "3.0.1", features = ["dir_source"] }
|
handlebars = { version = "3.5.1", features = ["dir_source"] }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
soup = "0.5.0"
|
soup = "0.5.0"
|
||||||
regex = "1.3.4"
|
regex = "1.4.2"
|
||||||
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.28"
|
openssl = "0.10.31"
|
||||||
|
|
||||||
# URL encoding library
|
# URL encoding library
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
|
@ -116,18 +120,18 @@ percent-encoding = "2.1.0"
|
||||||
idna = "0.2.0"
|
idna = "0.2.0"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
structopt = "0.3.11"
|
structopt = "0.3.21"
|
||||||
|
|
||||||
# Logging panics to logfile instead stderr only
|
# Logging panics to logfile instead stderr only
|
||||||
backtrace = "0.3.45"
|
backtrace = "0.3.55"
|
||||||
|
|
||||||
|
# Macro ident concatenation
|
||||||
|
paste = "1.0.4"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Use newest ring
|
# Use newest ring
|
||||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dfc9e9aab01d349da32c52db393e35b7fffea63c' }
|
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
||||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dfc9e9aab01d349da32c52db393e35b7fffea63c' }
|
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
|
||||||
|
|
||||||
# Use git version for timeout fix #706
|
|
||||||
lettre = { git = 'https://github.com/lettre/lettre', rev = '245c600c82ee18b766e8729f005ff453a55dce34' }
|
|
||||||
|
|
||||||
# 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' }
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
docker/amd64/sqlite/Dockerfile
|
docker/amd64/Dockerfile
|
|
@ -21,7 +21,6 @@ Image is based on [Rust implementation of Bitwarden API](https://github.com/dani
|
||||||
|
|
||||||
Basically full implementation of Bitwarden API is provided including:
|
Basically full implementation of Bitwarden API is provided including:
|
||||||
|
|
||||||
* Single user functionality
|
|
||||||
* Organizations support
|
* Organizations support
|
||||||
* Attachments
|
* Attachments
|
||||||
* Vault API support
|
* Vault API support
|
||||||
|
@ -59,3 +58,4 @@ If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org
|
||||||
Thanks for your contribution to the project!
|
Thanks for your contribution to the project!
|
||||||
|
|
||||||
- [@ChonoN](https://github.com/ChonoN)
|
- [@ChonoN](https://github.com/ChonoN)
|
||||||
|
- [@themightychris](https://github.com/themightychris)
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
31
build.rs
31
build.rs
|
@ -1,17 +1,24 @@
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(all(feature = "sqlite", feature = "mysql"))]
|
// This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros
|
||||||
compile_error!("Can't enable both sqlite and mysql at the same time");
|
#[cfg(feature = "sqlite")]
|
||||||
#[cfg(all(feature = "sqlite", feature = "postgresql"))]
|
println!("cargo:rustc-cfg=sqlite");
|
||||||
compile_error!("Can't enable both sqlite and postgresql at the same time");
|
#[cfg(feature = "mysql")]
|
||||||
#[cfg(all(feature = "mysql", feature = "postgresql"))]
|
println!("cargo:rustc-cfg=mysql");
|
||||||
compile_error!("Can't enable both mysql and postgresql at the same time");
|
#[cfg(feature = "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");
|
||||||
|
|
||||||
read_git_info().ok();
|
if let Ok(version) = env::var("BWRS_VERSION") {
|
||||||
|
println!("cargo:rustc-env=BWRS_VERSION={}", version);
|
||||||
|
println!("cargo:rustc-env=CARGO_PKG_VERSION={}", version);
|
||||||
|
} else {
|
||||||
|
read_git_info().ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
||||||
|
@ -54,14 +61,16 @@ fn read_git_info() -> Result<(), std::io::Error> {
|
||||||
} else {
|
} else {
|
||||||
format!("{}-{}", last_tag, rev_short)
|
format!("{}-{}", last_tag, rev_short)
|
||||||
};
|
};
|
||||||
println!("cargo:rustc-env=GIT_VERSION={}", version);
|
|
||||||
|
println!("cargo:rustc-env=BWRS_VERSION={}", version);
|
||||||
|
println!("cargo:rustc-env=CARGO_PKG_VERSION={}", version);
|
||||||
|
|
||||||
// To access these values, use:
|
// To access these values, use:
|
||||||
// env!("GIT_EXACT_TAG")
|
// env!("GIT_EXACT_TAG")
|
||||||
// env!("GIT_LAST_TAG")
|
// env!("GIT_LAST_TAG")
|
||||||
// env!("GIT_BRANCH")
|
// env!("GIT_BRANCH")
|
||||||
// env!("GIT_REV")
|
// env!("GIT_REV")
|
||||||
// env!("GIT_VERSION")
|
// env!("BWRS_VERSION")
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,82 @@
|
||||||
# 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 Dockerfile's.
|
||||||
|
|
||||||
{% set build_stage_base_image = "rust:1.40" %}
|
{% set build_stage_base_image = "rust:1.48" %}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% set build_stage_base_image = "clux/muslrust:nightly-2020-03-09" %}
|
{% if "amd64" in target_file %}
|
||||||
{% set runtime_stage_base_image = "alpine:3.11" %}
|
{% set build_stage_base_image = "clux/muslrust:nightly-2020-11-22" %}
|
||||||
{% set package_arch_name = "" %}
|
{% set runtime_stage_base_image = "alpine:3.12" %}
|
||||||
|
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||||
|
{% elif "arm32v7" in target_file %}
|
||||||
|
{% set build_stage_base_image = "messense/rust-musl-cross:armv7-musleabihf" %}
|
||||||
|
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.12" %}
|
||||||
|
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
||||||
|
{% 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 "arm64v8" in target_file %}
|
||||||
{% elif "aarch64" 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 "armv6" in target_file %}
|
{% set package_arch_target = "aarch64-unknown-linux-gnu" %}
|
||||||
|
{% set package_cross_compiler = "aarch64-linux-gnu" %}
|
||||||
|
{% elif "arm32v6" 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 "armv7" in target_file %}
|
{% set package_arch_target = "arm-unknown-linux-gnueabi" %}
|
||||||
|
{% set package_cross_compiler = "arm-linux-gnueabi" %}
|
||||||
|
{% elif "arm32v7" 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:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c" %}
|
{% set vault_image_hash = "sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0" %}
|
||||||
{% raw %}
|
{% raw %}
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
FROM bitwardenrs/web-vault@{{ vault_image_hash }} as vault
|
FROM bitwardenrs/web-vault@{{ vault_image_hash }} as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
{% if "musl" in build_stage_base_image %}
|
|
||||||
# Musl build image for statically compiled binary
|
|
||||||
{% else %}
|
|
||||||
# We need to use the Rust build image, because
|
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
{% endif %}
|
|
||||||
FROM {{ build_stage_base_image }} as build
|
FROM {{ build_stage_base_image }} as build
|
||||||
|
|
||||||
{% if "sqlite" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
# set sqlite as default for DB ARG for backward compatibility
|
{% 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" %}
|
||||||
{% elif "mysql" in target_file %}
|
{% endif %}
|
||||||
# set mysql backend
|
{% else %}
|
||||||
ARG DB=mysql
|
# Debian-based builds support multidb
|
||||||
|
ARG DB=sqlite,mysql,postgresql
|
||||||
{% elif "postgresql" in target_file %}
|
{% set features = "sqlite,mysql,postgresql" %}
|
||||||
# set postgresql backend
|
|
||||||
ARG DB=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
|
||||||
|
|
||||||
|
@ -72,9 +86,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'
|
||||||
|
{% elif "arm" in target_file %}
|
||||||
{% elif "aarch64" in target_file or "armv" 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
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
/etc/apt/sources.list.d/deb-src.list \
|
||||||
&& dpkg --add-architecture {{ package_arch_name }} \
|
&& dpkg --add-architecture {{ package_arch_name }} \
|
||||||
|
@ -82,77 +96,34 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev{{ package_arch_prefix }} \
|
libssl-dev{{ package_arch_prefix }} \
|
||||||
libc6-dev{{ package_arch_prefix }}
|
libc6-dev{{ package_arch_prefix }} \
|
||||||
|
libpq5{{ package_arch_prefix }} \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev{{ 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 "aarch64" 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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
{% if "amd64" in target_file and "alpine" not in target_file %}
|
||||||
ENV USER "root"
|
# Install DB packages
|
||||||
|
|
||||||
{% elif "armv6" 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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
{% elif "armv6" 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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
{% elif "armv7" 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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% if "mysql" in target_file %}
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
{% if "musl" in build_stage_base_image %}
|
|
||||||
libmysqlclient-dev{{ package_arch_prefix }} \
|
|
||||||
{% else %}
|
|
||||||
libmariadb-dev{{ package_arch_prefix }} \
|
libmariadb-dev{{ package_arch_prefix }} \
|
||||||
{% endif %}
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
{% elif "postgresql" in target_file %}
|
|
||||||
# Install PostgreSQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libpq-dev{{ package_arch_prefix }} \
|
libpq-dev{{ package_arch_prefix }} \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -162,39 +133,38 @@ COPY ./Cargo.* ./
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
COPY ./rust-toolchain ./rust-toolchain
|
||||||
COPY ./build.rs ./build.rs
|
COPY ./build.rs ./build.rs
|
||||||
|
|
||||||
{% if "aarch64" in target_file %}
|
{% if "alpine" not in target_file %}
|
||||||
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
{% if "arm" in target_file %}
|
||||||
|
# NOTE: This should be the last apt-get/dpkg for this stage, since after this it will fail because of broken dependencies.
|
||||||
|
# For Diesel-RS migrations_macros to compile with MySQL/MariaDB we need to do some magic.
|
||||||
|
# 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.
|
||||||
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# 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.
|
||||||
|
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 CROSS_COMPILE="1"
|
||||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
|
ENV OPENSSL_INCLUDE_DIR="/usr/include/{{ package_cross_compiler }}"
|
||||||
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
|
ENV OPENSSL_LIB_DIR="/usr/lib/{{ package_cross_compiler }}"
|
||||||
{% elif "armv6" in target_file %}
|
{% endif -%}
|
||||||
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"
|
|
||||||
{% elif "armv7" in target_file %}
|
|
||||||
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"
|
|
||||||
{% endif -%}
|
|
||||||
|
|
||||||
{% if "alpine" in target_file %}
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
{% elif "aarch64" in target_file %}
|
|
||||||
RUN rustup target add aarch64-unknown-linux-gnu
|
|
||||||
|
|
||||||
{% elif "armv6" in target_file %}
|
|
||||||
RUN rustup target add arm-unknown-linux-gnueabi
|
|
||||||
|
|
||||||
{% elif "armv7" in target_file %}
|
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if package_arch_target is defined %}
|
||||||
|
RUN rustup target add {{ package_arch_target }}
|
||||||
|
{% 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
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release{{ package_arch_target_param }}
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
|
@ -206,14 +176,11 @@ 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
|
||||||
{% if "amd64" in target_file %}
|
RUN cargo build --features ${DB} --release{{ package_arch_target_param }}
|
||||||
RUN cargo build --features ${DB} --release
|
{% if "alpine" in target_file %}
|
||||||
{% elif "aarch64" in target_file %}
|
{% if "arm32v7" in target_file %}
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
RUN musl-strip target/{{ package_arch_target }}/release/bitwarden_rs
|
||||||
{% elif "armv6" in target_file %}
|
{% endif %}
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
|
||||||
{% elif "armv7" in target_file %}
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
|
@ -237,11 +204,13 @@ RUN [ "cross-build-start" ]
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
{% if "sqlite" in target_file %}
|
{% if "sqlite" in features %}
|
||||||
sqlite \
|
sqlite \
|
||||||
{% elif "mysql" in target_file %}
|
{% endif %}
|
||||||
|
{% if "mysql" in features %}
|
||||||
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
|
||||||
|
@ -251,15 +220,14 @@ RUN apt-get update && apt-get install -y \
|
||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
{% if "sqlite" in target_file %}
|
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
{% elif "mysql" in target_file %}
|
libmariadb-dev-compat \
|
||||||
libmariadbclient-dev \
|
|
||||||
{% elif "postgresql" in target_file %}
|
|
||||||
libpq5 \
|
libpq5 \
|
||||||
{% endif %}
|
|
||||||
&& 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 %}
|
||||||
|
@ -275,22 +243,21 @@ EXPOSE 3012
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
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 %}
|
||||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
COPY --from=build /app/target/{{ package_arch_target }}/release/bitwarden_rs .
|
||||||
{% elif "aarch64" in target_file %}
|
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
|
|
||||||
{% elif "armv6" in target_file %}
|
|
||||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
|
|
||||||
{% elif "armv7" in target_file %}
|
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
|
|
||||||
{% else %}
|
{% else %}
|
||||||
COPY --from=build app/target/release/bitwarden_rs .
|
COPY --from=build /app/target/release/bitwarden_rs .
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
{% if "alpine" in target_file and "arm32v7" in target_file %}
|
||||||
|
CMD ["catatonit", "/start.sh"]
|
||||||
|
{% else %}
|
||||||
|
CMD ["/start.sh"]
|
||||||
|
{% endif %}
|
||||||
|
|
3
docker/README.md
Normal file
3
docker/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
The arch-specific directory names follow the arch identifiers used by the Docker official images:
|
||||||
|
|
||||||
|
https://github.com/docker-library/official-images/blob/master/README.md#architectures-other-than-amd64
|
|
@ -1,133 +0,0 @@
|
||||||
# This file was generated using a Jinja2 template.
|
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
|
||||||
|
|
||||||
# Using multistage build:
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
|
||||||
####################### VAULT BUILD IMAGE #######################
|
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
|
||||||
# It can be viewed in multiple ways:
|
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
|
||||||
#
|
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
|
||||||
# We need to use the Rust build image, because
|
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set mysql backend
|
|
||||||
ARG DB=mysql
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Don't download rust docs
|
|
||||||
RUN rustup set profile minimal
|
|
||||||
|
|
||||||
# Install required build libs for arm64 architecture.
|
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
|
||||||
&& dpkg --add-architecture arm64 \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libssl-dev:arm64 \
|
|
||||||
libc6-dev:arm64
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmariadb-dev:arm64 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
|
||||||
RUN USER=root cargo new --bin /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
|
||||||
COPY ./Cargo.* ./
|
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
|
||||||
COPY ./build.rs ./build.rs
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
|
||||||
# dummy project, except the target folder
|
|
||||||
# This folder contains the compiled dependencies
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
RUN find . -not -path "./target*" -delete
|
|
||||||
|
|
||||||
# Copies the complete project
|
|
||||||
# To avoid copying unneeded files, use .dockerignore
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Make sure that we actually build the project
|
|
||||||
RUN touch src/main.rs
|
|
||||||
|
|
||||||
# Builds again, this time it'll just be
|
|
||||||
# your actual source files being built
|
|
||||||
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
|
||||||
# Create a new stage with a minimal image
|
|
||||||
# because we already have a binary built
|
|
||||||
FROM balenalib/aarch64-debian:buster
|
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
|
||||||
ENV ROCKET_PORT=80
|
|
||||||
ENV ROCKET_WORKERS=10
|
|
||||||
|
|
||||||
RUN [ "cross-build-start" ]
|
|
||||||
|
|
||||||
# Install needed libraries
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
libmariadbclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
|
||||||
|
|
||||||
VOLUME /data
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
|
||||||
# and the binary from the "build" stage to the current stage
|
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
|
||||||
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
|
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
WORKDIR /
|
|
||||||
CMD ["/bitwarden_rs"]
|
|
|
@ -6,24 +6,22 @@
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
FROM bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
FROM rust:1.48 as build
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set postgresql backend
|
# Debian-based builds support multidb
|
||||||
ARG DB=postgresql
|
ARG DB=sqlite,mysql,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
|
||||||
|
@ -31,9 +29,10 @@ ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
# Don't download rust docs
|
# Don't download rust docs
|
||||||
RUN rustup set profile minimal
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
# Install PostgreSQL package
|
# Install DB packages
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
|
libmariadb-dev \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
@ -46,6 +45,7 @@ COPY ./Cargo.* ./
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
COPY ./rust-toolchain ./rust-toolchain
|
||||||
COPY ./build.rs ./build.rs
|
COPY ./build.rs ./build.rs
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -78,6 +78,8 @@ RUN apt-get update && apt-get install -y \
|
||||||
openssl \
|
openssl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
|
sqlite3 \
|
||||||
|
libmariadb-dev-compat \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
@ -90,12 +92,14 @@ EXPOSE 3012
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
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/bitwarden_rs .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
CMD ["/start.sh"]
|
||||||
|
|
|
@ -6,23 +6,22 @@
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
FROM bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
FROM clux/muslrust:nightly-2020-11-22 as build
|
||||||
FROM clux/muslrust:nightly-2019-12-19 as build
|
|
||||||
|
|
||||||
# set postgresql backend
|
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
||||||
ARG DB=postgresql
|
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
|
||||||
|
@ -31,12 +30,7 @@ ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
RUN rustup set profile minimal
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
ENV RUSTFLAGS='-C link-arg=-s'
|
||||||
# Install PostgreSQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libpq-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -52,7 +46,7 @@ RUN rustup target add x86_64-unknown-linux-musl
|
||||||
# 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
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
|
@ -64,12 +58,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
|
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.11
|
FROM alpine:3.12
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
|
@ -80,6 +74,7 @@ ENV SSL_CERT_DIR=/etc/ssl/certs
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
|
sqlite \
|
||||||
postgresql-libs \
|
postgresql-libs \
|
||||||
ca-certificates
|
ca-certificates
|
||||||
|
|
||||||
|
@ -94,10 +89,12 @@ 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/bitwarden_rs .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
CMD ["/start.sh"]
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
# This file was generated using a Jinja2 template.
|
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
|
||||||
|
|
||||||
# Using multistage build:
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
|
||||||
####################### VAULT BUILD IMAGE #######################
|
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
|
||||||
# It can be viewed in multiple ways:
|
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
|
||||||
#
|
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
|
||||||
# We need to use the Rust build image, because
|
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set mysql backend
|
|
||||||
ARG DB=mysql
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Don't download rust docs
|
|
||||||
RUN rustup set profile minimal
|
|
||||||
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmariadb-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
|
||||||
RUN USER=root cargo new --bin /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
|
||||||
COPY ./Cargo.* ./
|
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
|
||||||
COPY ./build.rs ./build.rs
|
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
|
||||||
# dummy project, except the target folder
|
|
||||||
# This folder contains the compiled dependencies
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
RUN find . -not -path "./target*" -delete
|
|
||||||
|
|
||||||
# Copies the complete project
|
|
||||||
# To avoid copying unneeded files, use .dockerignore
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Make sure that we actually build the project
|
|
||||||
RUN touch src/main.rs
|
|
||||||
|
|
||||||
# Builds again, this time it'll just be
|
|
||||||
# your actual source files being built
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
|
||||||
# Create a new stage with a minimal image
|
|
||||||
# because we already have a binary built
|
|
||||||
FROM debian:buster-slim
|
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
|
||||||
ENV ROCKET_PORT=80
|
|
||||||
ENV ROCKET_WORKERS=10
|
|
||||||
|
|
||||||
# Install needed libraries
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
libmariadbclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
VOLUME /data
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
|
||||||
# and the binary from the "build" stage to the current stage
|
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
|
||||||
COPY --from=build app/target/release/bitwarden_rs .
|
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
WORKDIR /
|
|
||||||
CMD ["/bitwarden_rs"]
|
|
|
@ -1,103 +0,0 @@
|
||||||
# This file was generated using a Jinja2 template.
|
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
|
||||||
|
|
||||||
# Using multistage build:
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
|
||||||
####################### VAULT BUILD IMAGE #######################
|
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
|
||||||
# It can be viewed in multiple ways:
|
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
|
||||||
#
|
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
|
||||||
# Musl build image for statically compiled binary
|
|
||||||
FROM clux/muslrust:nightly-2019-12-19 as build
|
|
||||||
|
|
||||||
# set mysql backend
|
|
||||||
ARG DB=mysql
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Don't download rust docs
|
|
||||||
RUN rustup set profile minimal
|
|
||||||
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmysqlclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
|
||||||
RUN USER=root cargo new --bin /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
|
||||||
COPY ./Cargo.* ./
|
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
|
||||||
COPY ./build.rs ./build.rs
|
|
||||||
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
|
||||||
# dummy project, except the target folder
|
|
||||||
# This folder contains the compiled dependencies
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
RUN find . -not -path "./target*" -delete
|
|
||||||
|
|
||||||
# Copies the complete project
|
|
||||||
# To avoid copying unneeded files, use .dockerignore
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Make sure that we actually build the project
|
|
||||||
RUN touch src/main.rs
|
|
||||||
|
|
||||||
# Builds again, this time it'll just be
|
|
||||||
# your actual source files being built
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
|
||||||
# Create a new stage with a minimal image
|
|
||||||
# because we already have a binary built
|
|
||||||
FROM alpine:3.11
|
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
|
||||||
ENV ROCKET_PORT=80
|
|
||||||
ENV ROCKET_WORKERS=10
|
|
||||||
ENV SSL_CERT_DIR=/etc/ssl/certs
|
|
||||||
|
|
||||||
# Install needed libraries
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
openssl \
|
|
||||||
curl \
|
|
||||||
mariadb-connector-c \
|
|
||||||
ca-certificates
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
VOLUME /data
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
|
||||||
# and the binary from the "build" stage to the current stage
|
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
|
||||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
|
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
WORKDIR /
|
|
||||||
CMD ["/bitwarden_rs"]
|
|
|
@ -1,95 +0,0 @@
|
||||||
# This file was generated using a Jinja2 template.
|
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
|
||||||
|
|
||||||
# Using multistage build:
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
|
||||||
####################### VAULT BUILD IMAGE #######################
|
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
|
||||||
# It can be viewed in multiple ways:
|
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
|
||||||
#
|
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
|
||||||
# We need to use the Rust build image, because
|
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward compatibility
|
|
||||||
ARG DB=sqlite
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Don't download rust docs
|
|
||||||
RUN rustup set profile minimal
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
|
||||||
RUN USER=root cargo new --bin /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
|
||||||
COPY ./Cargo.* ./
|
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
|
||||||
COPY ./build.rs ./build.rs
|
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
|
||||||
# dummy project, except the target folder
|
|
||||||
# This folder contains the compiled dependencies
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
RUN find . -not -path "./target*" -delete
|
|
||||||
|
|
||||||
# Copies the complete project
|
|
||||||
# To avoid copying unneeded files, use .dockerignore
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Make sure that we actually build the project
|
|
||||||
RUN touch src/main.rs
|
|
||||||
|
|
||||||
# Builds again, this time it'll just be
|
|
||||||
# your actual source files being built
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
|
||||||
# Create a new stage with a minimal image
|
|
||||||
# because we already have a binary built
|
|
||||||
FROM debian:buster-slim
|
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
|
||||||
ENV ROCKET_PORT=80
|
|
||||||
ENV ROCKET_WORKERS=10
|
|
||||||
|
|
||||||
# Install needed libraries
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
sqlite3 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
VOLUME /data
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
|
||||||
# and the binary from the "build" stage to the current stage
|
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
|
||||||
COPY --from=build app/target/release/bitwarden_rs .
|
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
WORKDIR /
|
|
||||||
CMD ["/bitwarden_rs"]
|
|
|
@ -6,24 +6,22 @@
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
FROM bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
FROM rust:1.48 as build
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward compatibility
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite,mysql,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,6 +30,7 @@ ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
RUN rustup set profile minimal
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
# Install required build libs for armel architecture.
|
# Install required build libs for armel architecture.
|
||||||
|
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
/etc/apt/sources.list.d/deb-src.list \
|
||||||
&& dpkg --add-architecture armel \
|
&& dpkg --add-architecture armel \
|
||||||
|
@ -39,7 +38,11 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armel \
|
libssl-dev:armel \
|
||||||
libc6-dev:armel
|
libc6-dev:armel \
|
||||||
|
libpq5:armel \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev:armel \
|
||||||
|
libmariadb-dev-compat:armel
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
|
@ -47,7 +50,8 @@ RUN apt-get update \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
&& mkdir -p ~/.cargo \
|
&& mkdir -p ~/.cargo \
|
||||||
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
|
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
|
||||||
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.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 CARGO_HOME "/root/.cargo"
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
@ -61,6 +65,22 @@ COPY ./Cargo.* ./
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
COPY ./rust-toolchain ./rust-toolchain
|
||||||
COPY ./build.rs ./build.rs
|
COPY ./build.rs ./build.rs
|
||||||
|
|
||||||
|
# NOTE: This should be the last apt-get/dpkg for this stage, since after this it will fail because of broken dependencies.
|
||||||
|
# For Diesel-RS migrations_macros to compile with MySQL/MariaDB we need to do some magic.
|
||||||
|
# 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.
|
||||||
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# 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.
|
||||||
|
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 CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
|
||||||
ENV CROSS_COMPILE="1"
|
ENV CROSS_COMPILE="1"
|
||||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
|
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
|
||||||
|
@ -70,7 +90,7 @@ RUN rustup target add arm-unknown-linux-gnueabi
|
||||||
# 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
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
|
@ -102,6 +122,8 @@ RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
|
@ -118,10 +140,12 @@ 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/bitwarden_rs .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
CMD ["/start.sh"]
|
||||||
|
|
|
@ -6,24 +6,22 @@
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
FROM bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
FROM rust:1.48 as build
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward compatibility
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite,mysql,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,6 +30,7 @@ ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
RUN rustup set profile minimal
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
# Install required build libs for armhf architecture.
|
# Install required build libs for armhf architecture.
|
||||||
|
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
/etc/apt/sources.list.d/deb-src.list \
|
||||||
&& dpkg --add-architecture armhf \
|
&& dpkg --add-architecture armhf \
|
||||||
|
@ -39,7 +38,11 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:armhf \
|
libssl-dev:armhf \
|
||||||
libc6-dev:armhf
|
libc6-dev:armhf \
|
||||||
|
libpq5:armhf \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev:armhf \
|
||||||
|
libmariadb-dev-compat:armhf
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
|
@ -47,7 +50,8 @@ RUN apt-get update \
|
||||||
gcc-arm-linux-gnueabihf \
|
gcc-arm-linux-gnueabihf \
|
||||||
&& mkdir -p ~/.cargo \
|
&& mkdir -p ~/.cargo \
|
||||||
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
|
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
|
||||||
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.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 CARGO_HOME "/root/.cargo"
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
@ -61,15 +65,32 @@ COPY ./Cargo.* ./
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
COPY ./rust-toolchain ./rust-toolchain
|
||||||
COPY ./build.rs ./build.rs
|
COPY ./build.rs ./build.rs
|
||||||
|
|
||||||
|
# NOTE: This should be the last apt-get/dpkg for this stage, since after this it will fail because of broken dependencies.
|
||||||
|
# For Diesel-RS migrations_macros to compile with MySQL/MariaDB we need to do some magic.
|
||||||
|
# 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.
|
||||||
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# 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.
|
||||||
|
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 CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
|
||||||
ENV CROSS_COMPILE="1"
|
ENV CROSS_COMPILE="1"
|
||||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
|
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
|
||||||
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
# 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
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
|
@ -101,6 +122,8 @@ RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
|
@ -117,10 +140,12 @@ 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/bitwarden_rs .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
CMD ["/start.sh"]
|
||||||
|
|
|
@ -6,22 +6,21 @@
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
FROM bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# Musl build image for statically compiled binary
|
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
||||||
FROM clux/muslrust:nightly-2019-12-19 as build
|
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward compatibility
|
# 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,6 +30,7 @@ ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
RUN rustup set profile minimal
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
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
|
||||||
|
@ -41,12 +41,12 @@ COPY ./Cargo.* ./
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
COPY ./rust-toolchain ./rust-toolchain
|
||||||
COPY ./build.rs ./build.rs
|
COPY ./build.rs ./build.rs
|
||||||
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
RUN rustup target add armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
# 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
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
|
@ -58,26 +58,33 @@ 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
|
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabihf
|
||||||
|
RUN musl-strip target/armv7-unknown-linux-musleabihf/release/bitwarden_rs
|
||||||
|
|
||||||
######################## 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.11
|
FROM balenalib/armv7hf-alpine:3.12
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
ENV ROCKET_WORKERS=10
|
ENV ROCKET_WORKERS=10
|
||||||
ENV SSL_CERT_DIR=/etc/ssl/certs
|
ENV SSL_CERT_DIR=/etc/ssl/certs
|
||||||
|
|
||||||
|
RUN [ "cross-build-start" ]
|
||||||
|
|
||||||
# Install needed libraries
|
# Install needed libraries
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
sqlite \
|
sqlite \
|
||||||
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
|
||||||
|
|
||||||
|
RUN [ "cross-build-end" ]
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 3012
|
EXPOSE 3012
|
||||||
|
@ -86,12 +93,14 @@ EXPOSE 3012
|
||||||
# and the binary from the "build" stage to the current stage
|
# and the binary from the "build" stage to the current stage
|
||||||
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/armv7-unknown-linux-musleabihf/release/bitwarden_rs .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
CMD ["catatonit", "/start.sh"]
|
||||||
|
|
|
@ -6,24 +6,22 @@
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
# This hash is extracted from the docker web-vault builds and it's preferred over a simple tag because it's immutable.
|
||||||
# It can be viewed in multiple ways:
|
# It can be viewed in multiple ways:
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
||||||
# - From the console, with the following commands:
|
# - From the console, with the following commands:
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
# docker pull bitwardenrs/web-vault:v2.17.1
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.17.1
|
||||||
#
|
#
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
# - To do the opposite, and get the tag from the hash, you can do:
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
FROM bitwardenrs/web-vault@sha256:dcb7884dc5845b3842ff2204fe77482000b771495c6c359297ec3c03330d65e0 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# We need to use the Rust build image, because
|
FROM rust:1.48 as build
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set sqlite as default for DB ARG for backward compatibility
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite
|
ARG DB=sqlite,mysql,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,6 +30,7 @@ ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
|
||||||
RUN rustup set profile minimal
|
RUN rustup set profile minimal
|
||||||
|
|
||||||
# Install required build libs for arm64 architecture.
|
# Install required build libs for arm64 architecture.
|
||||||
|
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
/etc/apt/sources.list.d/deb-src.list \
|
||||||
&& dpkg --add-architecture arm64 \
|
&& dpkg --add-architecture arm64 \
|
||||||
|
@ -39,7 +38,11 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
libssl-dev:arm64 \
|
libssl-dev:arm64 \
|
||||||
libc6-dev:arm64
|
libc6-dev:arm64 \
|
||||||
|
libpq5:arm64 \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev:arm64 \
|
||||||
|
libmariadb-dev-compat:arm64
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
|
@ -47,7 +50,8 @@ RUN apt-get update \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
&& mkdir -p ~/.cargo \
|
&& mkdir -p ~/.cargo \
|
||||||
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
|
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
|
||||||
&& echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.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 CARGO_HOME "/root/.cargo"
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
|
@ -61,6 +65,22 @@ COPY ./Cargo.* ./
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
COPY ./rust-toolchain ./rust-toolchain
|
||||||
COPY ./build.rs ./build.rs
|
COPY ./build.rs ./build.rs
|
||||||
|
|
||||||
|
# NOTE: This should be the last apt-get/dpkg for this stage, since after this it will fail because of broken dependencies.
|
||||||
|
# For Diesel-RS migrations_macros to compile with MySQL/MariaDB we need to do some magic.
|
||||||
|
# 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.
|
||||||
|
# What we can do is a force install, because nothing important is overlapping each other.
|
||||||
|
RUN apt-get install -y --no-install-recommends libmariadb3:amd64 && \
|
||||||
|
apt-get download libmariadb-dev-compat:amd64 && \
|
||||||
|
dpkg --force-all -i ./libmariadb-dev-compat*.deb && \
|
||||||
|
rm -rvf ./libmariadb-dev-compat*.deb
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# 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.
|
||||||
|
RUN ln -sfnr /usr/lib/aarch64-linux-gnu/libpq.so.5 /usr/lib/aarch64-linux-gnu/libpq.so
|
||||||
|
|
||||||
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
|
||||||
ENV CROSS_COMPILE="1"
|
ENV CROSS_COMPILE="1"
|
||||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
|
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
|
||||||
|
@ -70,7 +90,7 @@ RUN rustup target add aarch64-unknown-linux-gnu
|
||||||
# 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
|
||||||
RUN cargo build --features ${DB} --release
|
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
|
||||||
RUN find . -not -path "./target*" -delete
|
RUN find . -not -path "./target*" -delete
|
||||||
|
|
||||||
# Copies the complete project
|
# Copies the complete project
|
||||||
|
@ -102,6 +122,8 @@ RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
|
@ -118,10 +140,12 @@ 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/bitwarden_rs .
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
COPY docker/healthcheck.sh /healthcheck.sh
|
||||||
|
COPY docker/start.sh /start.sh
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
|
||||||
|
|
||||||
# Configures the startup!
|
# Configures the startup!
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
CMD ["/bitwarden_rs"]
|
CMD ["/start.sh"]
|
||||||
|
|
|
@ -1,133 +0,0 @@
|
||||||
# This file was generated using a Jinja2 template.
|
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
|
||||||
|
|
||||||
# Using multistage build:
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
|
||||||
####################### VAULT BUILD IMAGE #######################
|
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
|
||||||
# It can be viewed in multiple ways:
|
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
|
||||||
#
|
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
|
||||||
# We need to use the Rust build image, because
|
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set mysql backend
|
|
||||||
ARG DB=mysql
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Don't download rust docs
|
|
||||||
RUN rustup set profile minimal
|
|
||||||
|
|
||||||
# Install required build libs for armel architecture.
|
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
|
||||||
&& dpkg --add-architecture armel \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libssl-dev:armel \
|
|
||||||
libc6-dev:armel
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmariadb-dev:armel \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
|
||||||
RUN USER=root cargo new --bin /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
|
||||||
COPY ./Cargo.* ./
|
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
|
||||||
COPY ./build.rs ./build.rs
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Builds your dependencies and removes the
|
|
||||||
# dummy project, except the target folder
|
|
||||||
# This folder contains the compiled dependencies
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
RUN find . -not -path "./target*" -delete
|
|
||||||
|
|
||||||
# Copies the complete project
|
|
||||||
# To avoid copying unneeded files, use .dockerignore
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Make sure that we actually build the project
|
|
||||||
RUN touch src/main.rs
|
|
||||||
|
|
||||||
# Builds again, this time it'll just be
|
|
||||||
# your actual source files being built
|
|
||||||
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
|
||||||
# Create a new stage with a minimal image
|
|
||||||
# because we already have a binary built
|
|
||||||
FROM balenalib/rpi-debian:buster
|
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
|
||||||
ENV ROCKET_PORT=80
|
|
||||||
ENV ROCKET_WORKERS=10
|
|
||||||
|
|
||||||
RUN [ "cross-build-start" ]
|
|
||||||
|
|
||||||
# Install needed libraries
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
libmariadbclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
|
||||||
|
|
||||||
VOLUME /data
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
|
||||||
# and the binary from the "build" stage to the current stage
|
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
|
||||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
|
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
WORKDIR /
|
|
||||||
CMD ["/bitwarden_rs"]
|
|
|
@ -1,132 +0,0 @@
|
||||||
# This file was generated using a Jinja2 template.
|
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
|
|
||||||
|
|
||||||
# Using multistage build:
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
|
||||||
####################### VAULT BUILD IMAGE #######################
|
|
||||||
|
|
||||||
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
|
|
||||||
# It can be viewed in multiple ways:
|
|
||||||
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
|
|
||||||
# - From the console, with the following commands:
|
|
||||||
# docker pull bitwardenrs/web-vault:v2.12.0e
|
|
||||||
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
|
|
||||||
#
|
|
||||||
# - To do the opposite, and get the tag from the hash, you can do:
|
|
||||||
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c
|
|
||||||
FROM bitwardenrs/web-vault@sha256:feb3f46d15738191b9043be4cdb1be2c0078ed411e7b7be73a2f4fcbca01e13c as vault
|
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
|
||||||
# We need to use the Rust build image, because
|
|
||||||
# we need the Rust compiler and Cargo tooling
|
|
||||||
FROM rust:1.40 as build
|
|
||||||
|
|
||||||
# set mysql backend
|
|
||||||
ARG DB=mysql
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Don't download rust docs
|
|
||||||
RUN rustup set profile minimal
|
|
||||||
|
|
||||||
# Install required build libs for armhf architecture.
|
|
||||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
|
||||||
/etc/apt/sources.list.d/deb-src.list \
|
|
||||||
&& dpkg --add-architecture armhf \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libssl-dev:armhf \
|
|
||||||
libc6-dev:armhf
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
ENV CARGO_HOME "/root/.cargo"
|
|
||||||
ENV USER "root"
|
|
||||||
|
|
||||||
# Install MySQL package
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
libmariadb-dev:armhf \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
|
||||||
RUN USER=root cargo new --bin /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copies over *only* your manifests and build files
|
|
||||||
COPY ./Cargo.* ./
|
|
||||||
COPY ./rust-toolchain ./rust-toolchain
|
|
||||||
COPY ./build.rs ./build.rs
|
|
||||||
|
|
||||||
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
|
|
||||||
# Builds your dependencies and removes the
|
|
||||||
# dummy project, except the target folder
|
|
||||||
# This folder contains the compiled dependencies
|
|
||||||
RUN cargo build --features ${DB} --release
|
|
||||||
RUN find . -not -path "./target*" -delete
|
|
||||||
|
|
||||||
# Copies the complete project
|
|
||||||
# To avoid copying unneeded files, use .dockerignore
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Make sure that we actually build the project
|
|
||||||
RUN touch src/main.rs
|
|
||||||
|
|
||||||
# Builds again, this time it'll just be
|
|
||||||
# your actual source files being built
|
|
||||||
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
|
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
|
||||||
# Create a new stage with a minimal image
|
|
||||||
# because we already have a binary built
|
|
||||||
FROM balenalib/armv7hf-debian:buster
|
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
|
||||||
ENV ROCKET_PORT=80
|
|
||||||
ENV ROCKET_WORKERS=10
|
|
||||||
|
|
||||||
RUN [ "cross-build-start" ]
|
|
||||||
|
|
||||||
# Install needed libraries
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
--no-install-recommends \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
libmariadbclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir /data
|
|
||||||
|
|
||||||
RUN [ "cross-build-end" ]
|
|
||||||
|
|
||||||
VOLUME /data
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 3012
|
|
||||||
|
|
||||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
|
||||||
# and the binary from the "build" stage to the current stage
|
|
||||||
COPY Rocket.toml .
|
|
||||||
COPY --from=vault /web-vault ./web-vault
|
|
||||||
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
|
|
||||||
|
|
||||||
COPY docker/healthcheck.sh ./healthcheck.sh
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
|
|
||||||
|
|
||||||
# Configures the startup!
|
|
||||||
WORKDIR /
|
|
||||||
CMD ["/bitwarden_rs"]
|
|
59
docker/healthcheck.sh
Normal file → Executable file
59
docker/healthcheck.sh
Normal file → Executable file
|
@ -1,8 +1,53 @@
|
||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
if [ -z "$ROCKET_TLS"]
|
# Use the value of the corresponding env var (if present),
|
||||||
then
|
# or a default value otherwise.
|
||||||
curl --fail http://localhost:${ROCKET_PORT:-"80"}/alive || exit 1
|
: ${DATA_FOLDER:="data"}
|
||||||
else
|
: ${ROCKET_PORT:="80"}
|
||||||
curl --insecure --fail https://localhost:${ROCKET_PORT:-"80"}/alive || exit 1
|
|
||||||
fi
|
CONFIG_FILE="${DATA_FOLDER}"/config.json
|
||||||
|
|
||||||
|
# Given a config key, return the corresponding config value from the
|
||||||
|
# config file. If the key doesn't exist, return an empty string.
|
||||||
|
get_config_val() {
|
||||||
|
local key="$1"
|
||||||
|
# Extract a line of the form:
|
||||||
|
# "domain": "https://bw.example.com/path",
|
||||||
|
grep "\"${key}\":" "${CONFIG_FILE}" |
|
||||||
|
# To extract just the value (https://bw.example.com/path), delete:
|
||||||
|
# (1) everything up to and including the first ':',
|
||||||
|
# (2) whitespace and '"' from the front,
|
||||||
|
# (3) ',' and '"' from the back.
|
||||||
|
sed -e 's/[^:]\+://' -e 's/^[ "]\+//' -e 's/[,"]\+$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the base path from a domain URL. For example:
|
||||||
|
# - `` -> ``
|
||||||
|
# - `https://bw.example.com` -> ``
|
||||||
|
# - `https://bw.example.com/` -> ``
|
||||||
|
# - `https://bw.example.com/path` -> `/path`
|
||||||
|
# - `https://bw.example.com/multi/path` -> `/multi/path`
|
||||||
|
get_base_path() {
|
||||||
|
echo "$1" |
|
||||||
|
# Delete:
|
||||||
|
# (1) everything up to and including '://',
|
||||||
|
# (2) everything up to '/',
|
||||||
|
# (3) trailing '/' from the back.
|
||||||
|
sed -e 's|.*://||' -e 's|[^/]\+||' -e 's|/*$||'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read domain URL from config.json, if present.
|
||||||
|
if [ -r "${CONFIG_FILE}" ]; then
|
||||||
|
domain="$(get_config_val 'domain')"
|
||||||
|
if [ -n "${domain}" ]; then
|
||||||
|
# config.json 'domain' overrides the DOMAIN env var.
|
||||||
|
DOMAIN="${domain}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
base_path="$(get_base_path "${DOMAIN}")"
|
||||||
|
if [ -n "${ROCKET_TLS}" ]; then
|
||||||
|
s='s'
|
||||||
|
fi
|
||||||
|
curl --insecure --fail --silent --show-error \
|
||||||
|
"http${s}://localhost:${ROCKET_PORT}${base_path}/alive" || exit 1
|
||||||
|
|
15
docker/start.sh
Executable file
15
docker/start.sh
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ -r /etc/bitwarden_rs.sh ]; then
|
||||||
|
. /etc/bitwarden_rs.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /etc/bitwarden_rs.d ]; then
|
||||||
|
for f in /etc/bitwarden_rs.d/*.sh; do
|
||||||
|
if [ -r $f ]; then
|
||||||
|
. $f
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /bitwarden_rs "${@}"
|
20
hooks/README.md
Normal file
20
hooks/README.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
The hooks in this directory are used to create multi-arch images using Docker Hub automated builds.
|
||||||
|
|
||||||
|
Docker Hub hooks provide these predefined [environment variables](https://docs.docker.com/docker-hub/builds/advanced/#environment-variables-for-building-and-testing):
|
||||||
|
|
||||||
|
* `SOURCE_BRANCH`: the name of the branch or the tag that is currently being tested.
|
||||||
|
* `SOURCE_COMMIT`: the SHA1 hash of the commit being tested.
|
||||||
|
* `COMMIT_MSG`: the message from the commit being tested and built.
|
||||||
|
* `DOCKER_REPO`: the name of the Docker repository being built.
|
||||||
|
* `DOCKERFILE_PATH`: the dockerfile currently 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`.)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
* https://docs.docker.com/docker-hub/builds/advanced/
|
||||||
|
* https://docs.docker.com/engine/reference/commandline/manifest/
|
||||||
|
* https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/
|
||||||
|
* https://success.docker.com/article/how-do-i-authenticate-with-the-v2-api
|
19
hooks/arches.sh
Normal file
19
hooks/arches.sh
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# The default Debian-based images support these arches for all database connections
|
||||||
|
#
|
||||||
|
# Other images (Alpine-based) currently
|
||||||
|
# support only a subset of these.
|
||||||
|
arches=(
|
||||||
|
amd64
|
||||||
|
arm32v6
|
||||||
|
arm32v7
|
||||||
|
arm64v8
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
||||||
|
# The Alpine build currently only works for amd64.
|
||||||
|
os_suffix=.alpine
|
||||||
|
arches=(
|
||||||
|
amd64
|
||||||
|
arm32v7
|
||||||
|
)
|
||||||
|
fi
|
14
hooks/build
Executable file
14
hooks/build
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo ">>> Building images..."
|
||||||
|
|
||||||
|
source ./hooks/arches.sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
for arch in "${arches[@]}"; do
|
||||||
|
docker build \
|
||||||
|
-t "${DOCKER_REPO}:${DOCKER_TAG}-${arch}" \
|
||||||
|
-f docker/${arch}/Dockerfile${os_suffix} \
|
||||||
|
.
|
||||||
|
done
|
117
hooks/push
Executable file
117
hooks/push
Executable file
|
@ -0,0 +1,117 @@
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
declare -A images
|
||||||
|
for arch in ${arches[@]}; do
|
||||||
|
images[$arch]="${DOCKER_REPO}:${DOCKER_TAG}-${arch}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Push the images that were just built; manifest list creation fails if the
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
# If the Docker tag starts with a version number, assume the latest release is
|
||||||
|
# being pushed. Add an extra manifest (`latest` or `alpine`, as appropriate)
|
||||||
|
# to make it easier for users to track the latest release.
|
||||||
|
if [[ "${DOCKER_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
|
if [[ "${DOCKER_TAG}" == *alpine ]]; then
|
||||||
|
manifest_lists+=(${DOCKER_REPO}:alpine)
|
||||||
|
else
|
||||||
|
manifest_lists+=(${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
|
||||||
|
|
||||||
|
for manifest_list in "${manifest_lists[@]}"; do
|
||||||
|
# Create the (multi-arch) manifest list of arch-specific images.
|
||||||
|
docker manifest create ${manifest_list} ${images[@]}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Avoid logging credentials and tokens.
|
||||||
|
set +ex
|
||||||
|
|
||||||
|
# Delete the arch-specific tags, if credentials for doing so are available.
|
||||||
|
# Note that `DOCKER_PASSWORD` must be the actual user password. Passing a JWT
|
||||||
|
# obtained using a personal access token results in a 403 error with
|
||||||
|
# {"detail": "access to the resource is forbidden with personal access token"}
|
||||||
|
if [[ -z "${DOCKER_USERNAME}" || -z "${DOCKER_PASSWORD}" ]]; then
|
||||||
|
exit 0
|
||||||
|
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
|
||||||
|
# Don't delete the `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).
|
||||||
|
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
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN
|
||||||
|
deleted_at DATETIME;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Transfer favorite status for user-owned ciphers.
|
||||||
|
UPDATE ciphers
|
||||||
|
SET favorite = TRUE
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT * FROM favorites
|
||||||
|
WHERE favorites.user_uuid = ciphers.user_uuid
|
||||||
|
AND favorites.cipher_uuid = ciphers.uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP TABLE favorites;
|
|
@ -0,0 +1,16 @@
|
||||||
|
CREATE TABLE favorites (
|
||||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users(uuid),
|
||||||
|
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers(uuid),
|
||||||
|
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Transfer favorite status for user-owned ciphers.
|
||||||
|
INSERT INTO favorites(user_uuid, cipher_uuid)
|
||||||
|
SELECT user_uuid, uuid
|
||||||
|
FROM ciphers
|
||||||
|
WHERE favorite = TRUE
|
||||||
|
AND user_uuid IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
DROP COLUMN favorite;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN
|
||||||
|
deleted_at TIMESTAMP;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Transfer favorite status for user-owned ciphers.
|
||||||
|
UPDATE ciphers
|
||||||
|
SET favorite = TRUE
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT * FROM favorites
|
||||||
|
WHERE favorites.user_uuid = ciphers.user_uuid
|
||||||
|
AND favorites.cipher_uuid = ciphers.uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP TABLE favorites;
|
|
@ -0,0 +1,16 @@
|
||||||
|
CREATE TABLE favorites (
|
||||||
|
user_uuid VARCHAR(40) NOT NULL REFERENCES users(uuid),
|
||||||
|
cipher_uuid VARCHAR(40) NOT NULL REFERENCES ciphers(uuid),
|
||||||
|
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Transfer favorite status for user-owned ciphers.
|
||||||
|
INSERT INTO favorites(user_uuid, cipher_uuid)
|
||||||
|
SELECT user_uuid, uuid
|
||||||
|
FROM ciphers
|
||||||
|
WHERE favorite = TRUE
|
||||||
|
AND user_uuid IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
DROP COLUMN favorite;
|
|
@ -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 @@
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN
|
||||||
|
deleted_at DATETIME;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE ciphers
|
||||||
|
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
||||||
|
|
||||||
|
-- Transfer favorite status for user-owned ciphers.
|
||||||
|
UPDATE ciphers
|
||||||
|
SET favorite = 1
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT * FROM favorites
|
||||||
|
WHERE favorites.user_uuid = ciphers.user_uuid
|
||||||
|
AND favorites.cipher_uuid = ciphers.uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP TABLE favorites;
|
|
@ -0,0 +1,71 @@
|
||||||
|
CREATE TABLE favorites (
|
||||||
|
user_uuid TEXT NOT NULL REFERENCES users(uuid),
|
||||||
|
cipher_uuid TEXT NOT NULL REFERENCES ciphers(uuid),
|
||||||
|
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Transfer favorite status for user-owned ciphers.
|
||||||
|
INSERT INTO favorites(user_uuid, cipher_uuid)
|
||||||
|
SELECT user_uuid, uuid
|
||||||
|
FROM ciphers
|
||||||
|
WHERE favorite = 1
|
||||||
|
AND user_uuid IS NOT NULL;
|
||||||
|
|
||||||
|
-- Drop the `favorite` column from the `ciphers` table, using the 12-step
|
||||||
|
-- procedure from <https://www.sqlite.org/lang_altertable.html#altertabrename>.
|
||||||
|
-- Note that some steps aren't applicable and are omitted.
|
||||||
|
|
||||||
|
-- 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF.
|
||||||
|
--
|
||||||
|
-- Diesel runs each migration in its own transaction. `PRAGMA foreign_keys`
|
||||||
|
-- is a no-op within a transaction, so this step must be done outside of this
|
||||||
|
-- file, before starting the Diesel migrations.
|
||||||
|
|
||||||
|
-- 2. Start a transaction.
|
||||||
|
--
|
||||||
|
-- Diesel already runs each migration in its own transaction.
|
||||||
|
|
||||||
|
-- 4. Use CREATE TABLE to construct a new table "new_X" that is in the
|
||||||
|
-- desired revised format of table X. Make sure that the name "new_X" does
|
||||||
|
-- not collide with any existing table name, of course.
|
||||||
|
|
||||||
|
CREATE TABLE new_ciphers(
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
user_uuid TEXT REFERENCES users(uuid),
|
||||||
|
organization_uuid TEXT REFERENCES organizations(uuid),
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
fields TEXT,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
password_history TEXT,
|
||||||
|
deleted_at DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 5. Transfer content from X into new_X using a statement like:
|
||||||
|
-- INSERT INTO new_X SELECT ... FROM X.
|
||||||
|
|
||||||
|
INSERT INTO new_ciphers(uuid, created_at, updated_at, user_uuid, organization_uuid, atype,
|
||||||
|
name, notes, fields, data, password_history, deleted_at)
|
||||||
|
SELECT uuid, created_at, updated_at, user_uuid, organization_uuid, atype,
|
||||||
|
name, notes, fields, data, password_history, deleted_at
|
||||||
|
FROM ciphers;
|
||||||
|
|
||||||
|
-- 6. Drop the old table X: DROP TABLE X.
|
||||||
|
|
||||||
|
DROP TABLE ciphers;
|
||||||
|
|
||||||
|
-- 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X.
|
||||||
|
|
||||||
|
ALTER TABLE new_ciphers RENAME TO ciphers;
|
||||||
|
|
||||||
|
-- 11. Commit the transaction started in step 2.
|
||||||
|
|
||||||
|
-- 12. If foreign keys constraints were originally enabled, reenable them now.
|
||||||
|
--
|
||||||
|
-- `PRAGMA foreign_keys` is scoped to a database connection, and Diesel
|
||||||
|
-- migrations are run in a separate database connection that is closed once
|
||||||
|
-- the migrations finish.
|
|
@ -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 +1 @@
|
||||||
nightly-2020-03-09
|
nightly-2020-11-22
|
321
src/api/admin.rs
321
src/api/admin.rs
|
@ -1,46 +1,61 @@
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use rocket::http::{Cookie, Cookies, SameSite};
|
use rocket::{
|
||||||
use rocket::request::{self, FlashMessage, Form, FromRequest, Request};
|
http::{Cookie, Cookies, SameSite},
|
||||||
use rocket::response::{content::Html, Flash, Redirect};
|
request::{self, FlashMessage, Form, FromRequest, Outcome, Request},
|
||||||
use rocket::{Outcome, Route};
|
response::{content::Html, Flash, Redirect},
|
||||||
|
Route,
|
||||||
|
};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
use crate::{
|
||||||
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
|
api::{ApiResult, EmptyResult, JsonResult},
|
||||||
use crate::config::ConfigBuilder;
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||||
use crate::db::{backup_database, models::*, DbConn};
|
config::ConfigBuilder,
|
||||||
use crate::error::Error;
|
db::{backup_database, models::*, DbConn, DbConnType},
|
||||||
use crate::mail;
|
error::{Error, MapResult},
|
||||||
use crate::CONFIG;
|
mail,
|
||||||
|
util::{get_display_size, format_naive_datetime_local},
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
if CONFIG.admin_token().is_none() && !CONFIG.disable_admin_token() {
|
if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {
|
||||||
return routes![admin_disabled];
|
return routes![admin_disabled];
|
||||||
}
|
}
|
||||||
|
|
||||||
routes![
|
routes![
|
||||||
admin_login,
|
admin_login,
|
||||||
get_users,
|
get_users_json,
|
||||||
post_admin_login,
|
post_admin_login,
|
||||||
admin_page,
|
admin_page,
|
||||||
invite_user,
|
invite_user,
|
||||||
logout,
|
logout,
|
||||||
delete_user,
|
delete_user,
|
||||||
deauth_user,
|
deauth_user,
|
||||||
|
disable_user,
|
||||||
|
enable_user,
|
||||||
remove_2fa,
|
remove_2fa,
|
||||||
update_revision_users,
|
update_revision_users,
|
||||||
post_config,
|
post_config,
|
||||||
delete_config,
|
delete_config,
|
||||||
backup_db,
|
backup_db,
|
||||||
test_smtp,
|
test_smtp,
|
||||||
|
users_overview,
|
||||||
|
organizations_overview,
|
||||||
|
diagnostics,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
static CAN_BACKUP: Lazy<bool> =
|
static CAN_BACKUP: Lazy<bool> = Lazy::new(|| {
|
||||||
Lazy::new(|| cfg!(feature = "sqlite") && Command::new("sqlite3").arg("-version").status().is_ok());
|
DbConnType::from_url(&CONFIG.database_url())
|
||||||
|
.map(|t| t == DbConnType::sqlite)
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& Command::new("sqlite3").arg("-version").status().is_ok()
|
||||||
|
});
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn admin_disabled() -> &'static str {
|
fn admin_disabled() -> &'static str {
|
||||||
|
@ -51,12 +66,43 @@ const COOKIE_NAME: &str = "BWRS_ADMIN";
|
||||||
const ADMIN_PATH: &str = "/admin";
|
const ADMIN_PATH: &str = "/admin";
|
||||||
|
|
||||||
const BASE_TEMPLATE: &str = "admin/base";
|
const BASE_TEMPLATE: &str = "admin/base";
|
||||||
const VERSION: Option<&str> = option_env!("GIT_VERSION");
|
const VERSION: Option<&str> = option_env!("BWRS_VERSION");
|
||||||
|
|
||||||
fn admin_path() -> String {
|
fn admin_path() -> String {
|
||||||
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Referer(Option<String>);
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for Referer {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
Outcome::Success(Referer(request.headers().get_one("Referer").map(str::to_string)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for `Location` response headers, which must specify an absolute URI
|
||||||
|
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
||||||
|
fn admin_url(referer: Referer) -> String {
|
||||||
|
// If we get a referer use that to make it work when, DOMAIN is not set
|
||||||
|
if let Some(mut referer) = referer.0 {
|
||||||
|
if let Some(start_index) = referer.find(ADMIN_PATH) {
|
||||||
|
referer.truncate(start_index + ADMIN_PATH.len());
|
||||||
|
return referer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.domain_set() {
|
||||||
|
// Don't use CONFIG.domain() directly, since the user may want to keep a
|
||||||
|
// trailing slash there, particularly when running under a subpath.
|
||||||
|
format!("{}{}{}", CONFIG.domain_origin(), CONFIG.domain_path(), ADMIN_PATH)
|
||||||
|
} else {
|
||||||
|
// Last case, when no referer or domain set, technically invalid but better than nothing
|
||||||
|
ADMIN_PATH.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/", rank = 2)]
|
#[get("/", rank = 2)]
|
||||||
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
|
||||||
|
@ -74,14 +120,19 @@ struct LoginForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<data>")]
|
#[post("/", data = "<data>")]
|
||||||
fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -> Result<Redirect, Flash<Redirect>> {
|
fn post_admin_login(
|
||||||
|
data: Form<LoginForm>,
|
||||||
|
mut cookies: Cookies,
|
||||||
|
ip: ClientIp,
|
||||||
|
referer: Referer,
|
||||||
|
) -> Result<Redirect, Flash<Redirect>> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
// 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_path()),
|
Redirect::to(admin_url(referer)),
|
||||||
"Invalid admin token, please try again.",
|
"Invalid admin token, please try again.",
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
@ -97,7 +148,7 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
cookies.add(cookie);
|
cookies.add(cookie);
|
||||||
Ok(Redirect::to(admin_path()))
|
Ok(Redirect::to(admin_url(referer)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +163,9 @@ fn _validate_token(token: &str) -> bool {
|
||||||
struct AdminTemplateData {
|
struct AdminTemplateData {
|
||||||
page_content: String,
|
page_content: String,
|
||||||
version: Option<&'static str>,
|
version: Option<&'static str>,
|
||||||
users: Vec<Value>,
|
users: Option<Vec<Value>>,
|
||||||
|
organizations: Option<Vec<Value>>,
|
||||||
|
diagnostics: Option<Value>,
|
||||||
config: Value,
|
config: Value,
|
||||||
can_backup: bool,
|
can_backup: bool,
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
|
@ -120,15 +173,59 @@ struct AdminTemplateData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminTemplateData {
|
impl AdminTemplateData {
|
||||||
fn new(users: Vec<Value>) -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
page_content: String::from("admin/page"),
|
page_content: String::from("admin/settings"),
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
users,
|
|
||||||
config: CONFIG.prepare_json(),
|
config: CONFIG.prepare_json(),
|
||||||
can_backup: *CAN_BACKUP,
|
can_backup: *CAN_BACKUP,
|
||||||
logged_in: true,
|
logged_in: true,
|
||||||
urlpath: CONFIG.domain_path(),
|
urlpath: CONFIG.domain_path(),
|
||||||
|
users: None,
|
||||||
|
organizations: None,
|
||||||
|
diagnostics: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn users(users: Vec<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
page_content: String::from("admin/users"),
|
||||||
|
version: VERSION,
|
||||||
|
users: Some(users),
|
||||||
|
config: CONFIG.prepare_json(),
|
||||||
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
|
urlpath: CONFIG.domain_path(),
|
||||||
|
organizations: None,
|
||||||
|
diagnostics: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn organizations(organizations: Vec<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
page_content: String::from("admin/organizations"),
|
||||||
|
version: VERSION,
|
||||||
|
organizations: Some(organizations),
|
||||||
|
config: CONFIG.prepare_json(),
|
||||||
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
|
urlpath: CONFIG.domain_path(),
|
||||||
|
users: None,
|
||||||
|
diagnostics: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostics(diagnostics: Value) -> Self {
|
||||||
|
Self {
|
||||||
|
page_content: String::from("admin/diagnostics"),
|
||||||
|
version: VERSION,
|
||||||
|
organizations: None,
|
||||||
|
config: CONFIG.prepare_json(),
|
||||||
|
can_backup: *CAN_BACKUP,
|
||||||
|
logged_in: true,
|
||||||
|
urlpath: CONFIG.domain_path(),
|
||||||
|
users: None,
|
||||||
|
diagnostics: Some(diagnostics),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,11 +235,8 @@ impl AdminTemplateData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/", rank = 1)]
|
#[get("/", rank = 1)]
|
||||||
fn admin_page(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
fn admin_page(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
let users = User::get_all(&conn);
|
let text = AdminTemplateData::new().render()?;
|
||||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
|
||||||
|
|
||||||
let text = AdminTemplateData::new(users_json).render()?;
|
|
||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,59 +268,87 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
||||||
#[post("/test/smtp", data = "<data>")]
|
#[post("/test/smtp", data = "<data>")]
|
||||||
fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: InviteData = data.into_inner();
|
let data: InviteData = data.into_inner();
|
||||||
let email = data.email.clone();
|
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_test(&email)
|
mail::send_test(&data.email)
|
||||||
} else {
|
} else {
|
||||||
err!("Mail is not enabled")
|
err!("Mail is not enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
fn logout(mut cookies: Cookies) -> Result<Redirect, ()> {
|
fn logout(mut cookies: Cookies, referer: Referer) -> Result<Redirect, ()> {
|
||||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||||
Ok(Redirect::to(admin_path()))
|
Ok(Redirect::to(admin_url(referer)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
|
fn get_users_json(_token: AdminToken, conn: DbConn) -> JsonResult {
|
||||||
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)))
|
Ok(Json(Value::Array(users_json)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/users/overview")]
|
||||||
|
fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
let users = User::get_all(&conn);
|
||||||
|
let dt_fmt = "%Y-%m-%d %H:%M:%S %Z";
|
||||||
|
let users_json: Vec<Value> = users.iter()
|
||||||
|
.map(|u| {
|
||||||
|
let mut usr = u.to_json(&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_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
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let text = AdminTemplateData::users(users_json).render()?;
|
||||||
|
Ok(Html(text))
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/delete")]
|
#[post("/users/<uuid>/delete")]
|
||||||
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
let user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
||||||
Some(user) => user,
|
|
||||||
None => err!("User doesn't exist"),
|
|
||||||
};
|
|
||||||
|
|
||||||
user.delete(&conn)
|
user.delete(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/deauth")]
|
#[post("/users/<uuid>/deauth")]
|
||||||
fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
let mut user = match User::find_by_uuid(&uuid, &conn) {
|
let mut user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
||||||
Some(user) => user,
|
|
||||||
None => err!("User doesn't exist"),
|
|
||||||
};
|
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
Device::delete_all_by_user(&user.uuid, &conn)?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
|
|
||||||
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 = match User::find_by_uuid(&uuid, &conn) {
|
let mut user = User::find_by_uuid(&uuid, &conn).map_res("User doesn't exist")?;
|
||||||
Some(user) => user,
|
|
||||||
None => err!("User doesn't exist"),
|
|
||||||
};
|
|
||||||
|
|
||||||
TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
|
TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
|
||||||
user.totp_recover = None;
|
user.totp_recover = None;
|
||||||
user.save(&conn)
|
user.save(&conn)
|
||||||
|
@ -237,6 +359,109 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||||
User::update_all_revisions(&conn)
|
User::update_all_revisions(&conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/organizations/overview")]
|
||||||
|
fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
let organizations = Organization::get_all(&conn);
|
||||||
|
let organizations_json: Vec<Value> = organizations.iter().map(|o| {
|
||||||
|
let mut org = o.to_json();
|
||||||
|
org["user_count"] = json!(UserOrganization::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_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32));
|
||||||
|
org
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let text = AdminTemplateData::organizations(organizations_json).render()?;
|
||||||
|
Ok(Html(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct WebVaultVersion {
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct GitRelease {
|
||||||
|
tag_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct GitCommit {
|
||||||
|
sha: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
|
use reqwest::{blocking::Client, header::USER_AGENT};
|
||||||
|
use std::time::Duration;
|
||||||
|
let github_api = Client::builder().build()?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
github_api.get(url)
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.header(USER_AGENT, "Bitwarden_RS")
|
||||||
|
.send()?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<T>()?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/diagnostics")]
|
||||||
|
fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use crate::util::read_file_string;
|
||||||
|
|
||||||
|
let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json");
|
||||||
|
let vault_version_str = read_file_string(&vault_version_path)?;
|
||||||
|
let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?;
|
||||||
|
|
||||||
|
let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next());
|
||||||
|
let (dns_resolved, dns_ok) = match github_ips {
|
||||||
|
Ok(Some(a)) => (a.ip().to_string(), true),
|
||||||
|
_ => ("Could not resolve domain name.".to_string(), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the DNS Check failed, do not even attempt to check for new versions since we were not able to resolve github.com
|
||||||
|
let (latest_release, latest_commit, latest_web_build) = if dns_ok {
|
||||||
|
(
|
||||||
|
match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") {
|
||||||
|
Ok(r) => r.tag_name,
|
||||||
|
_ => "-".to_string()
|
||||||
|
},
|
||||||
|
match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") {
|
||||||
|
Ok(mut c) => {
|
||||||
|
c.sha.truncate(8);
|
||||||
|
c.sha
|
||||||
|
},
|
||||||
|
_ => "-".to_string()
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
("-".to_string(), "-".to_string(), "-".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the date check as the last item right before filling the json.
|
||||||
|
// This should ensure that the time difference between the browser and the server is as minimal as possible.
|
||||||
|
let dt = Utc::now();
|
||||||
|
let server_time = dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
|
||||||
|
|
||||||
|
let diagnostics_json = json!({
|
||||||
|
"dns_resolved": dns_resolved,
|
||||||
|
"server_time": server_time,
|
||||||
|
"web_vault_version": web_vault_version.version,
|
||||||
|
"latest_release": latest_release,
|
||||||
|
"latest_commit": latest_commit,
|
||||||
|
"latest_web_build": latest_web_build,
|
||||||
|
});
|
||||||
|
|
||||||
|
let text = AdminTemplateData::diagnostics(diagnostics_json).render()?;
|
||||||
|
Ok(Html(text))
|
||||||
|
}
|
||||||
|
|
||||||
#[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();
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::db::models::*;
|
use crate::{
|
||||||
use crate::db::DbConn;
|
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
|
||||||
|
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
|
||||||
|
crypto,
|
||||||
|
db::{models::*, DbConn},
|
||||||
|
mail, CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
use crate::auth::{decode_delete, decode_invite, decode_verify_email, Headers};
|
|
||||||
use crate::crypto;
|
|
||||||
use crate::mail;
|
|
||||||
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
use rocket::Route;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
|
||||||
routes![
|
routes![
|
||||||
register,
|
register,
|
||||||
profile,
|
profile,
|
||||||
|
@ -36,6 +32,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
revision_date,
|
revision_date,
|
||||||
password_hint,
|
password_hint,
|
||||||
prelogin,
|
prelogin,
|
||||||
|
verify_password,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +65,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||||
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
if !user.password_hash.is_empty() {
|
if !user.password_hash.is_empty() {
|
||||||
if CONFIG.signups_allowed() {
|
if CONFIG.is_signup_allowed(&data.Email) {
|
||||||
err!("User already exists")
|
err!("User already exists")
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
|
@ -89,14 +86,17 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
user
|
user
|
||||||
} else if CONFIG.signups_allowed() {
|
} else if CONFIG.is_signup_allowed(&data.Email) {
|
||||||
err!("Account with this email already exists")
|
err!("Account with this email already exists")
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if CONFIG.signups_allowed() || Invitation::take(&data.Email, &conn) || CONFIG.can_signup_user(&data.Email) {
|
// Order is important here; the invitation check must come first
|
||||||
|
// because the bitwarden_rs admin can invite anyone, regardless
|
||||||
|
// of other signup restrictions.
|
||||||
|
if Invitation::take(&data.Email, &conn) || CONFIG.is_signup_allowed(&data.Email) {
|
||||||
User::new(data.Email.clone())
|
User::new(data.Email.clone())
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
|
@ -115,7 +115,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
|
||||||
|
@ -207,7 +207,12 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
|
||||||
user.public_key = Some(data.PublicKey);
|
user.public_key = Some(data.PublicKey);
|
||||||
|
|
||||||
user.save(&conn)?;
|
user.save(&conn)?;
|
||||||
Ok(Json(user.to_json(&conn)))
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"PrivateKey": user.private_key,
|
||||||
|
"PublicKey": user.public_key,
|
||||||
|
"Object":"keys"
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -227,7 +232,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)
|
||||||
}
|
}
|
||||||
|
@ -254,7 +259,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)
|
||||||
}
|
}
|
||||||
|
@ -333,6 +338,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)
|
||||||
}
|
}
|
||||||
|
@ -371,8 +377,8 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
|
||||||
err!("Email already in use");
|
err!("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.signups_allowed() && !CONFIG.can_signup_user(&data.NewEmail) {
|
if !CONFIG.is_email_domain_allowed(&data.NewEmail) {
|
||||||
err!("Email cannot be changed to this address");
|
err!("Email domain not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = crypto::generate_token(6)?;
|
let token = crypto::generate_token(6)?;
|
||||||
|
@ -440,7 +446,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)
|
||||||
|
@ -455,7 +461,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(())
|
||||||
|
@ -619,3 +625,20 @@ fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
||||||
"KdfIterations": kdf_iter
|
"KdfIterations": kdf_iter
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct VerifyPasswordData {
|
||||||
|
MasterPasswordHash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/verify-password", data = "<data>")]
|
||||||
|
fn verify_password(data: JsonUpcase<VerifyPasswordData>, headers: Headers, _conn: DbConn) -> EmptyResult {
|
||||||
|
let data: VerifyPasswordData = data.into_inner().data;
|
||||||
|
let user = headers.user;
|
||||||
|
|
||||||
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
err!("Invalid password")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,28 +1,33 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use rocket::http::ContentType;
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use rocket::{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;
|
||||||
|
|
||||||
use multipart::server::save::SavedData;
|
|
||||||
use multipart::server::{Multipart, SaveResult};
|
|
||||||
|
|
||||||
use data_encoding::HEXLOWER;
|
use data_encoding::HEXLOWER;
|
||||||
|
use multipart::server::{save::SavedData, Multipart, SaveResult};
|
||||||
|
|
||||||
use crate::db::models::*;
|
use crate::{
|
||||||
use crate::db::DbConn;
|
api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
|
||||||
|
auth::Headers,
|
||||||
use crate::crypto;
|
crypto,
|
||||||
|
db::{models::*, DbConn},
|
||||||
use crate::api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType};
|
CONFIG,
|
||||||
use crate::auth::Headers;
|
};
|
||||||
|
|
||||||
use crate::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.
|
||||||
|
//
|
||||||
|
// bitwarden_rs 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,
|
||||||
|
@ -44,15 +49,24 @@ 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,
|
||||||
delete_cipher_post_admin,
|
delete_cipher_post_admin,
|
||||||
|
delete_cipher_put,
|
||||||
|
delete_cipher_put_admin,
|
||||||
delete_cipher,
|
delete_cipher,
|
||||||
delete_cipher_admin,
|
delete_cipher_admin,
|
||||||
delete_cipher_selected,
|
delete_cipher_selected,
|
||||||
delete_cipher_selected_post,
|
delete_cipher_selected_post,
|
||||||
|
delete_cipher_selected_put,
|
||||||
|
delete_cipher_selected_admin,
|
||||||
|
delete_cipher_selected_post_admin,
|
||||||
|
delete_cipher_selected_put_admin,
|
||||||
|
restore_cipher_put,
|
||||||
|
restore_cipher_put_admin,
|
||||||
|
restore_cipher_selected,
|
||||||
delete_all,
|
delete_all,
|
||||||
move_cipher_selected,
|
move_cipher_selected,
|
||||||
move_cipher_selected_put,
|
move_cipher_selected_put,
|
||||||
|
@ -82,7 +96,7 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
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(&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> = ciphers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
||||||
|
@ -107,7 +121,7 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
|
||||||
#[get("/ciphers")]
|
#[get("/ciphers")]
|
||||||
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let ciphers = Cipher::find_by_user(&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> = ciphers
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -181,6 +195,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 +212,35 @@ 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;
|
||||||
|
|
||||||
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;
|
||||||
|
@ -225,6 +260,17 @@ pub fn update_cipher_from_data(
|
||||||
nt: &Notify,
|
nt: &Notify,
|
||||||
ut: UpdateType,
|
ut: UpdateType,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
// 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")
|
||||||
}
|
}
|
||||||
|
@ -268,7 +314,10 @@ pub fn update_cipher_from_data(
|
||||||
};
|
};
|
||||||
|
|
||||||
if saved_att.cipher_uuid != cipher.uuid {
|
if saved_att.cipher_uuid != cipher.uuid {
|
||||||
err!("Attachment is not owned by the cipher")
|
// Warn and break here since cloning ciphers provides attachment data but will not be cloned.
|
||||||
|
// If we error out here it will break the whole cloning and causes empty ciphers to appear.
|
||||||
|
warn!("Attachment is not owned by the cipher");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
saved_att.akey = Some(attachment.Key);
|
saved_att.akey = Some(attachment.Key);
|
||||||
|
@ -300,7 +349,6 @@ pub fn update_cipher_from_data(
|
||||||
type_data["PasswordHistory"] = data.PasswordHistory.clone().unwrap_or(Value::Null);
|
type_data["PasswordHistory"] = data.PasswordHistory.clone().unwrap_or(Value::Null);
|
||||||
// TODO: ******* Backwards compat end **********
|
// TODO: ******* Backwards compat end **********
|
||||||
|
|
||||||
cipher.favorite = data.Favorite.unwrap_or(false);
|
|
||||||
cipher.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| f.to_string());
|
||||||
|
@ -309,6 +357,7 @@ pub fn update_cipher_from_data(
|
||||||
|
|
||||||
cipher.save(&conn)?;
|
cipher.save(&conn)?;
|
||||||
cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn)?;
|
cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn)?;
|
||||||
|
cipher.set_favorite(data.Favorite, &headers.user.uuid, &conn)?;
|
||||||
|
|
||||||
if ut != UpdateType::None {
|
if ut != UpdateType::None {
|
||||||
nt.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
|
nt.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
@ -371,6 +420,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,
|
||||||
|
@ -407,6 +457,11 @@ fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Check if only the folder ID or favorite status is being changed.
|
||||||
|
// These are per-user properties that technically aren't part of the
|
||||||
|
// cipher itself, so the user shouldn't need write access to change these.
|
||||||
|
// Interestingly, upstream Bitwarden doesn't properly handle this either.
|
||||||
|
|
||||||
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
||||||
err!("Cipher is not write accessible")
|
err!("Cipher is not write accessible")
|
||||||
}
|
}
|
||||||
|
@ -540,7 +595,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,
|
||||||
|
@ -608,9 +663,8 @@ fn share_cipher_by_uuid(
|
||||||
match data.Cipher.OrganizationId.clone() {
|
match data.Cipher.OrganizationId.clone() {
|
||||||
// If we don't get an organization ID, we don't do anything
|
// If we don't get an organization ID, we don't do anything
|
||||||
// No error because this is used when using the Clone functionality
|
// No error because this is used when using the Clone functionality
|
||||||
None => {},
|
None => {}
|
||||||
Some(organization_uuid) => {
|
Some(organization_uuid) => {
|
||||||
|
|
||||||
for uuid in &data.CollectionIds {
|
for uuid in &data.CollectionIds {
|
||||||
match Collection::find_by_uuid_and_org(uuid, &organization_uuid, &conn) {
|
match Collection::find_by_uuid_and_org(uuid, &organization_uuid, &conn) {
|
||||||
None => err!("Invalid collection ID provided"),
|
None => err!("Invalid collection ID provided"),
|
||||||
|
@ -665,8 +719,8 @@ fn post_attachment(
|
||||||
let size_limit = if let Some(ref user_uuid) = cipher.user_uuid {
|
let size_limit = if let Some(ref user_uuid) = cipher.user_uuid {
|
||||||
match CONFIG.user_attachment_limit() {
|
match CONFIG.user_attachment_limit() {
|
||||||
Some(0) => err_discard!("Attachments are disabled", data),
|
Some(0) => err_discard!("Attachments are disabled", data),
|
||||||
Some(limit) => {
|
Some(limit_kb) => {
|
||||||
let left = limit - Attachment::size_by_user(user_uuid, &conn);
|
let left = (limit_kb * 1024) - Attachment::size_by_user(user_uuid, &conn);
|
||||||
if left <= 0 {
|
if left <= 0 {
|
||||||
err_discard!("Attachment size limit reached! Delete some files to open space", data)
|
err_discard!("Attachment size limit reached! Delete some files to open space", data)
|
||||||
}
|
}
|
||||||
|
@ -677,8 +731,8 @@ fn post_attachment(
|
||||||
} else if let Some(ref org_uuid) = cipher.organization_uuid {
|
} else if let Some(ref org_uuid) = cipher.organization_uuid {
|
||||||
match CONFIG.org_attachment_limit() {
|
match CONFIG.org_attachment_limit() {
|
||||||
Some(0) => err_discard!("Attachments are disabled", data),
|
Some(0) => err_discard!("Attachments are disabled", data),
|
||||||
Some(limit) => {
|
Some(limit_kb) => {
|
||||||
let left = limit - Attachment::size_by_org(org_uuid, &conn);
|
let left = (limit_kb * 1024) - Attachment::size_by_org(org_uuid, &conn);
|
||||||
if left <= 0 {
|
if left <= 0 {
|
||||||
err_discard!("Attachment size limit reached! Delete some files to open space", data)
|
err_discard!("Attachment size limit reached! Delete some files to open space", data)
|
||||||
}
|
}
|
||||||
|
@ -761,11 +815,7 @@ fn post_attachment_admin(
|
||||||
post_attachment(uuid, data, content_type, headers, conn, nt)
|
post_attachment(uuid, data, content_type, headers, conn, nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post(
|
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
|
||||||
"/ciphers/<uuid>/attachment/<attachment_id>/share",
|
|
||||||
format = "multipart/form-data",
|
|
||||||
data = "<data>"
|
|
||||||
)]
|
|
||||||
fn post_attachment_share(
|
fn post_attachment_share(
|
||||||
uuid: String,
|
uuid: String,
|
||||||
attachment_id: String,
|
attachment_id: String,
|
||||||
|
@ -819,50 +869,79 @@ fn delete_attachment_admin(
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete")]
|
#[post("/ciphers/<uuid>/delete")]
|
||||||
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/delete-admin")]
|
#[post("/ciphers/<uuid>/delete-admin")]
|
||||||
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/delete")]
|
||||||
|
fn delete_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, true, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/delete-admin")]
|
||||||
|
fn delete_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, true, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>")]
|
#[delete("/ciphers/<uuid>")]
|
||||||
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers/<uuid>/admin")]
|
#[delete("/ciphers/<uuid>/admin")]
|
||||||
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
_delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/ciphers", data = "<data>")]
|
#[delete("/ciphers", data = "<data>")]
|
||||||
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
let data: Value = data.into_inner().data;
|
_delete_multiple_ciphers(data, headers, conn, false, nt)
|
||||||
|
|
||||||
let uuids = match data.get("Ids") {
|
|
||||||
Some(ids) => match ids.as_array() {
|
|
||||||
Some(ids) => ids.iter().filter_map(Value::as_str),
|
|
||||||
None => err!("Posted ids field is not an array"),
|
|
||||||
},
|
|
||||||
None => err!("Request missing ids field"),
|
|
||||||
};
|
|
||||||
|
|
||||||
for uuid in uuids {
|
|
||||||
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &nt) {
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/delete", data = "<data>")]
|
#[post("/ciphers/delete", data = "<data>")]
|
||||||
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_delete_multiple_ciphers(data, headers, conn, false, nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/delete", data = "<data>")]
|
||||||
|
fn delete_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_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)
|
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")]
|
||||||
|
fn restore_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<uuid>/restore-admin")]
|
||||||
|
fn restore_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/restore", data = "<data>")]
|
||||||
|
fn restore_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
_restore_multiple_ciphers(data, headers, conn, nt)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct MoveCipherData {
|
struct MoveCipherData {
|
||||||
|
@ -974,8 +1053,8 @@ fn delete_all(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_delete: bool, nt: &Notify) -> EmptyResult {
|
||||||
let 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"),
|
||||||
};
|
};
|
||||||
|
@ -984,8 +1063,72 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Not
|
||||||
err!("Cipher can't be deleted by user")
|
err!("Cipher can't be deleted by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
cipher.delete(&conn)?;
|
if soft_delete {
|
||||||
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
cipher.deleted_at = Some(Utc::now().naive_utc());
|
||||||
|
cipher.save(&conn)?;
|
||||||
|
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
} else {
|
||||||
|
cipher.delete(&conn)?;
|
||||||
|
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 uuids = match data.get("Ids") {
|
||||||
|
Some(ids) => match ids.as_array() {
|
||||||
|
Some(ids) => ids.iter().filter_map(Value::as_str),
|
||||||
|
None => err!("Posted ids field is not an array"),
|
||||||
|
},
|
||||||
|
None => err!("Request missing ids field"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, soft_delete, &nt) {
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
||||||
|
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
|
||||||
|
Some(cipher) => cipher,
|
||||||
|
None => err!("Cipher doesn't exist"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
||||||
|
err!("Cipher can't be restored by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.deleted_at = None;
|
||||||
|
cipher.save(&conn)?;
|
||||||
|
|
||||||
|
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
let data: Value = data.into_inner().data;
|
||||||
|
|
||||||
|
let uuids = match data.get("Ids") {
|
||||||
|
Some(ids) => match ids.as_array() {
|
||||||
|
Some(ids) => ids.iter().filter_map(Value::as_str),
|
||||||
|
None => err!("Posted ids field is not an array"),
|
||||||
|
},
|
||||||
|
None => err!("Request missing ids field"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
if let error @ Err(_) = _restore_cipher_by_uuid(uuid, &headers, &conn, &nt) {
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::db::models::*;
|
use crate::{
|
||||||
use crate::db::DbConn;
|
api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
|
||||||
|
auth::Headers,
|
||||||
|
db::{models::*, DbConn},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType};
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
use crate::auth::Headers;
|
|
||||||
|
|
||||||
use rocket::Route;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
|
||||||
routes![
|
routes![
|
||||||
get_folders,
|
get_folders,
|
||||||
get_folder,
|
get_folder,
|
||||||
|
@ -50,7 +48,6 @@ fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|
||||||
pub struct FolderData {
|
pub struct FolderData {
|
||||||
pub Name: String,
|
pub Name: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ mod accounts;
|
||||||
mod ciphers;
|
mod ciphers;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
pub(crate) mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut mod_routes = routes![
|
let mut mod_routes = routes![
|
||||||
|
@ -29,14 +29,15 @@ pub fn routes() -> Vec<Route> {
|
||||||
// Move this somewhere else
|
// Move this somewhere else
|
||||||
//
|
//
|
||||||
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::api::{EmptyResult, JsonResult, JsonUpcase};
|
use crate::{
|
||||||
use crate::auth::Headers;
|
api::{EmptyResult, JsonResult, JsonUpcase},
|
||||||
use crate::db::DbConn;
|
auth::Headers,
|
||||||
use crate::error::Error;
|
db::DbConn,
|
||||||
|
error::Error,
|
||||||
|
};
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
fn clear_device_token(uuid: String) -> EmptyResult {
|
||||||
|
@ -146,7 +147,7 @@ fn hibp_breach(username: String) -> JsonResult {
|
||||||
username
|
username
|
||||||
);
|
);
|
||||||
|
|
||||||
use reqwest::{header::USER_AGENT, blocking::Client};
|
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 = Client::builder().build()?;
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
use rocket::request::Form;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::Route;
|
use rocket::{request::Form, Route};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use num_traits::FromPrimitive;
|
|
||||||
|
|
||||||
use crate::api::{
|
use crate::{
|
||||||
EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType,
|
api::{EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType},
|
||||||
|
auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders, ManagerHeaders, ManagerHeadersLoose},
|
||||||
|
db::{models::*, DbConn},
|
||||||
|
mail, CONFIG,
|
||||||
};
|
};
|
||||||
use crate::auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders};
|
|
||||||
use crate::db::models::*;
|
|
||||||
use crate::db::DbConn;
|
|
||||||
use crate::mail;
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
|
@ -50,6 +47,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
list_policies_token,
|
list_policies_token,
|
||||||
get_policy,
|
get_policy,
|
||||||
put_policy,
|
put_policy,
|
||||||
|
get_plans,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +77,10 @@ struct NewCollectionData {
|
||||||
|
|
||||||
#[post("/organizations", data = "<data>")]
|
#[post("/organizations", data = "<data>")]
|
||||||
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
|
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
|
||||||
|
if !CONFIG.is_org_creation_allowed(&headers.user.email) {
|
||||||
|
err!("User not allowed to create organizations")
|
||||||
|
}
|
||||||
|
|
||||||
let data: OrgData = data.into_inner().data;
|
let data: OrgData = data.into_inner().data;
|
||||||
|
|
||||||
let org = Organization::new(data.Name, data.BillingEmail);
|
let org = Organization::new(data.Name, data.BillingEmail);
|
||||||
|
@ -215,7 +217,7 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
|
||||||
#[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 {
|
||||||
|
@ -226,9 +228,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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +251,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 {
|
||||||
|
@ -247,7 +262,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 {
|
||||||
|
@ -315,7 +330,7 @@ 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) => {
|
||||||
|
@ -339,7 +354,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 {
|
||||||
|
@ -347,7 +362,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) => {
|
||||||
|
@ -361,7 +376,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"),
|
||||||
|
@ -374,7 +389,7 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
|
||||||
.map(|col_user| {
|
.map(|col_user| {
|
||||||
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_json_collection_user_details(col_user.read_only)
|
.to_json_user_access_restrictions(&col_user)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -386,7 +401,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
|
||||||
|
@ -408,7 +423,9 @@ fn put_collection_users(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::save(&user.user_uuid, &coll_id, d.ReadOnly, &conn)?;
|
CollectionUser::save(&user.user_uuid, &coll_id,
|
||||||
|
d.ReadOnly, d.HidePasswords,
|
||||||
|
&conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -436,7 +453,7 @@ fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> Jso
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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) -> JsonResult {
|
||||||
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();
|
||||||
|
|
||||||
|
@ -452,6 +469,7 @@ fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonRe
|
||||||
struct CollectionData {
|
struct CollectionData {
|
||||||
Id: String,
|
Id: String,
|
||||||
ReadOnly: bool,
|
ReadOnly: bool,
|
||||||
|
HidePasswords: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -485,7 +503,11 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||||
let user = match User::find_by_mail(&email, &conn) {
|
let user = match User::find_by_mail(&email, &conn) {
|
||||||
None => {
|
None => {
|
||||||
if !CONFIG.invitations_allowed() {
|
if !CONFIG.invitations_allowed() {
|
||||||
err!(format!("User email does not exist: {}", email))
|
err!(format!("User does not exist: {}", email))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !CONFIG.is_email_domain_allowed(&email) {
|
||||||
|
err!("Email domain not eligible for invitations")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
if !CONFIG.mail_enabled() {
|
||||||
|
@ -519,7 +541,9 @@ 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, col.ReadOnly, &conn)?;
|
CollectionUser::save(&user.uuid, &collection.uuid,
|
||||||
|
col.ReadOnly, col.HidePasswords,
|
||||||
|
&conn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -774,7 +798,9 @@ 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, col.ReadOnly, &conn)?;
|
CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid,
|
||||||
|
col.ReadOnly, col.HidePasswords,
|
||||||
|
&conn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -924,7 +950,7 @@ fn list_policies_token(org_id: String, token: String, conn: DbConn) -> JsonResul
|
||||||
if invite_org_id != org_id {
|
if invite_org_id != org_id {
|
||||||
err!("Token doesn't match request organization");
|
err!("Token doesn't match request organization");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We receive the invite token as ?token=<>, validate it contains the org id
|
// TODO: We receive the invite token as ?token=<>, validate it contains the org id
|
||||||
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();
|
||||||
|
@ -978,4 +1004,56 @@ fn put_policy(org_id: String, pol_type: i32, data: Json<PolicyData>, _headers: A
|
||||||
policy.save(&conn)?;
|
policy.save(&conn)?;
|
||||||
|
|
||||||
Ok(Json(policy.to_json()))
|
Ok(Json(policy.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/plans")]
|
||||||
|
fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Object": "list",
|
||||||
|
"Data": [
|
||||||
|
{
|
||||||
|
"Object": "plan",
|
||||||
|
"Type": 0,
|
||||||
|
"Product": 0,
|
||||||
|
"Name": "Free",
|
||||||
|
"IsAnnual": false,
|
||||||
|
"NameLocalizationKey": "planNameFree",
|
||||||
|
"DescriptionLocalizationKey": "planDescFree",
|
||||||
|
"CanBeUsedByBusiness": false,
|
||||||
|
"BaseSeats": 2,
|
||||||
|
"BaseStorageGb": null,
|
||||||
|
"MaxCollections": 2,
|
||||||
|
"MaxUsers": 2,
|
||||||
|
"HasAdditionalSeatsOption": false,
|
||||||
|
"MaxAdditionalSeats": null,
|
||||||
|
"HasAdditionalStorageOption": false,
|
||||||
|
"MaxAdditionalStorage": null,
|
||||||
|
"HasPremiumAccessOption": false,
|
||||||
|
"TrialPeriodDays": null,
|
||||||
|
"HasSelfHost": false,
|
||||||
|
"HasPolicies": false,
|
||||||
|
"HasGroups": false,
|
||||||
|
"HasDirectory": false,
|
||||||
|
"HasEvents": false,
|
||||||
|
"HasTotp": false,
|
||||||
|
"Has2fa": false,
|
||||||
|
"HasApi": false,
|
||||||
|
"HasSso": false,
|
||||||
|
"UsersGetPremium": false,
|
||||||
|
"UpgradeSortOrder": -1,
|
||||||
|
"DisplaySortOrder": -1,
|
||||||
|
"LegacyYear": null,
|
||||||
|
"Disabled": false,
|
||||||
|
"StripePlanId": null,
|
||||||
|
"StripeSeatPlanId": null,
|
||||||
|
"StripeStoragePlanId": null,
|
||||||
|
"StripePremiumAccessPlanId": null,
|
||||||
|
"BasePrice": 0.0,
|
||||||
|
"SeatPrice": 0.0,
|
||||||
|
"AdditionalStoragePricePerGb": 0.0,
|
||||||
|
"PremiumAccessOptionPrice": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ContinuationToken": null
|
||||||
|
})))
|
||||||
}
|
}
|
|
@ -2,13 +2,16 @@ use data_encoding::BASE32;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::{
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
api::{
|
||||||
use crate::auth::Headers;
|
core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
|
||||||
use crate::crypto;
|
},
|
||||||
use crate::db::{
|
auth::{ClientIp, Headers},
|
||||||
models::{TwoFactor, TwoFactorType},
|
crypto,
|
||||||
DbConn,
|
db::{
|
||||||
|
models::{TwoFactor, TwoFactorType},
|
||||||
|
DbConn,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::config::CONFIG;
|
pub use crate::config::CONFIG;
|
||||||
|
@ -20,6 +23,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
activate_authenticator_put,
|
activate_authenticator_put,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-authenticator", data = "<data>")]
|
#[post("/two-factor/get-authenticator", data = "<data>")]
|
||||||
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
@ -53,7 +57,12 @@ struct EnableAuthenticatorData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/authenticator", data = "<data>")]
|
#[post("/two-factor/authenticator", data = "<data>")]
|
||||||
fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_authenticator(
|
||||||
|
data: JsonUpcase<EnableAuthenticatorData>,
|
||||||
|
headers: Headers,
|
||||||
|
ip: ClientIp,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
let data: EnableAuthenticatorData = data.into_inner().data;
|
let data: EnableAuthenticatorData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
let password_hash = data.MasterPasswordHash;
|
||||||
let key = data.Key;
|
let key = data.Key;
|
||||||
|
@ -76,7 +85,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the token provided with the key, and save new twofactor
|
// Validate the token provided with the key, and save new twofactor
|
||||||
validate_totp_code(&user.uuid, token, &key.to_uppercase(), &conn)?;
|
validate_totp_code(&user.uuid, token, &key.to_uppercase(), &ip, &conn)?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
|
|
||||||
|
@ -88,20 +97,31 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/authenticator", data = "<data>")]
|
#[put("/two-factor/authenticator", data = "<data>")]
|
||||||
fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_authenticator_put(
|
||||||
activate_authenticator(data, headers, conn)
|
data: JsonUpcase<EnableAuthenticatorData>,
|
||||||
|
headers: Headers,
|
||||||
|
ip: ClientIp,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
activate_authenticator(data, headers, ip, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code_str(user_uuid: &str, totp_code: &str, secret: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_totp_code_str(
|
||||||
|
user_uuid: &str,
|
||||||
|
totp_code: &str,
|
||||||
|
secret: &str,
|
||||||
|
ip: &ClientIp,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
let totp_code: u64 = match totp_code.parse() {
|
let totp_code: u64 = match totp_code.parse() {
|
||||||
Ok(code) => code,
|
Ok(code) => code,
|
||||||
_ => err!("TOTP code is not a number"),
|
_ => err!("TOTP code is not a number"),
|
||||||
};
|
};
|
||||||
|
|
||||||
validate_totp_code(user_uuid, totp_code, secret, &conn)
|
validate_totp_code(user_uuid, totp_code, secret, ip, &conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, ip: &ClientIp, conn: &DbConn) -> EmptyResult {
|
||||||
use oath::{totp_raw_custom_time, HashType};
|
use oath::{totp_raw_custom_time, HashType};
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||||
|
@ -143,11 +163,22 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &
|
||||||
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!("This or a TOTP code within {} steps back and forward has already been used!", steps);
|
warn!(
|
||||||
err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
|
"This or a TOTP code within {} steps back and forward has already been used!",
|
||||||
|
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!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
|
err!(format!(
|
||||||
|
"Invalid TOTP code! Server time: {} IP: {}",
|
||||||
|
current_time.format("%F %T UTC"),
|
||||||
|
ip.ip
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,18 @@ use chrono::Utc;
|
||||||
use data_encoding::BASE64;
|
use data_encoding::BASE64;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::{
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
api::{core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
||||||
use crate::auth::Headers;
|
auth::Headers,
|
||||||
use crate::crypto;
|
crypto,
|
||||||
use crate::db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType, User},
|
models::{TwoFactor, TwoFactorType, User},
|
||||||
DbConn,
|
DbConn,
|
||||||
|
},
|
||||||
|
error::MapResult,
|
||||||
|
CONFIG,
|
||||||
};
|
};
|
||||||
use crate::error::MapResult;
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![get_duo, activate_duo, activate_duo_put,]
|
routes![get_duo, activate_duo, activate_duo_put,]
|
||||||
|
@ -21,9 +21,9 @@ pub fn routes() -> Vec<Route> {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct DuoData {
|
struct DuoData {
|
||||||
host: String,
|
host: String, // Duo API hostname
|
||||||
ik: String,
|
ik: String, // integration key
|
||||||
sk: String,
|
sk: String, // secret key
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DuoData {
|
impl DuoData {
|
||||||
|
@ -187,9 +187,10 @@ 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)";
|
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
|
||||||
|
|
||||||
use reqwest::{header::*, Method, blocking::Client};
|
use reqwest::{blocking::Client, header::*, Method};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// https://duo.com/docs/authapi#api-details
|
||||||
let url = format!("https://{}{}", &data.host, path);
|
let url = format!("https://{}{}", &data.host, path);
|
||||||
let date = Utc::now().to_rfc2822();
|
let date = Utc::now().to_rfc2822();
|
||||||
let username = &data.ik;
|
let username = &data.ik;
|
||||||
|
@ -268,6 +269,10 @@ fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
// email is as entered by the user, so it needs to be normalized before
|
||||||
|
// comparison with auth_user below.
|
||||||
|
let email = &email.to_lowercase();
|
||||||
|
|
||||||
let split: Vec<&str> = response.split(':').collect();
|
let split: Vec<&str> = response.split(':').collect();
|
||||||
if split.len() != 2 {
|
if split.len() != 2 {
|
||||||
err!("Invalid response length");
|
err!("Invalid response length");
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::{
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
||||||
use crate::auth::Headers;
|
auth::Headers,
|
||||||
use crate::crypto;
|
crypto,
|
||||||
use crate::db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
|
},
|
||||||
|
error::{Error, MapResult},
|
||||||
|
mail, CONFIG,
|
||||||
};
|
};
|
||||||
use crate::error::Error;
|
|
||||||
use crate::mail;
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
use chrono::{Duration, NaiveDateTime, Utc};
|
|
||||||
use std::ops::Add;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![get_email, send_email_login, send_email, email,]
|
routes![get_email, send_email_login, send_email, email,]
|
||||||
|
@ -59,7 +56,7 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> Empty
|
||||||
/// Generate the token, save the data for later verification and send email to user
|
/// Generate the token, save the data for later verification and send email to user
|
||||||
pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
let type_ = TwoFactorType::Email as i32;
|
let type_ = TwoFactorType::Email as i32;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?;
|
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn).map_res("Two factor not found")?;
|
||||||
|
|
||||||
let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
|
let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
|
||||||
|
|
||||||
|
@ -68,7 +65,7 @@ pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
twofactor.data = twofactor_data.to_json();
|
twofactor.data = twofactor_data.to_json();
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?;
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -135,7 +132,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
||||||
);
|
);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?;
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -159,7 +156,7 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).map_res("Two factor not found")?;
|
||||||
|
|
||||||
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
|
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
|
|
||||||
|
@ -189,7 +186,7 @@ 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)?;
|
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"),
|
||||||
|
@ -212,7 +209,7 @@ pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &
|
||||||
|
|
||||||
let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0);
|
let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0);
|
||||||
let max_time = CONFIG.email_expiration_time() as i64;
|
let max_time = CONFIG.email_expiration_time() as i64;
|
||||||
if date.add(Duration::seconds(max_time)) < Utc::now().naive_utc() {
|
if date + Duration::seconds(max_time) < Utc::now().naive_utc() {
|
||||||
err!("Token has expired")
|
err!("Token has expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
use data_encoding::BASE32;
|
use data_encoding::BASE32;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
use crate::{
|
||||||
use crate::auth::Headers;
|
api::{JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
||||||
use crate::crypto;
|
auth::Headers,
|
||||||
use crate::db::{
|
crypto,
|
||||||
models::{TwoFactor, User},
|
db::{
|
||||||
DbConn,
|
models::{TwoFactor, User},
|
||||||
|
DbConn,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod authenticator;
|
pub mod authenticator;
|
||||||
pub(crate) mod duo;
|
pub mod duo;
|
||||||
pub(crate) mod email;
|
pub mod email;
|
||||||
pub(crate) mod u2f;
|
pub mod u2f;
|
||||||
pub(crate) mod yubikey;
|
pub mod yubikey;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = routes![
|
let mut routes = routes![
|
||||||
|
@ -39,7 +40,7 @@ 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) -> JsonResult {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
||||||
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": twofactors_json,
|
"Data": twofactors_json,
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
use u2f::{
|
||||||
use u2f::protocol::{Challenge, U2f};
|
messages::{RegisterResponse, SignResponse, U2fSignRequest},
|
||||||
use u2f::register::Registration;
|
protocol::{Challenge, U2f},
|
||||||
|
register::Registration,
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
};
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
|
||||||
use crate::auth::Headers;
|
use crate::{
|
||||||
use crate::db::{
|
api::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString,
|
||||||
DbConn,
|
PasswordData,
|
||||||
|
},
|
||||||
|
auth::Headers,
|
||||||
|
db::{
|
||||||
|
models::{TwoFactor, TwoFactorType},
|
||||||
|
DbConn,
|
||||||
|
},
|
||||||
|
error::Error,
|
||||||
|
CONFIG,
|
||||||
};
|
};
|
||||||
use crate::error::Error;
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
const U2F_VERSION: &str = "U2F_V2";
|
const U2F_VERSION: &str = "U2F_V2";
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use yubico::config::Config;
|
use yubico::{config::Config, verify};
|
||||||
use yubico::verify;
|
|
||||||
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::{
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
||||||
use crate::auth::Headers;
|
auth::Headers,
|
||||||
use crate::db::{
|
db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
|
},
|
||||||
|
error::{Error, MapResult},
|
||||||
|
CONFIG,
|
||||||
};
|
};
|
||||||
use crate::error::{Error, MapResult};
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
|
routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
|
||||||
|
|
385
src/api/icons.rs
385
src/api/icons.rs
|
@ -1,50 +1,85 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::{create_dir_all, remove_file, symlink_metadata, File},
|
||||||
|
io::prelude::*,
|
||||||
|
net::{IpAddr, ToSocketAddrs},
|
||||||
|
sync::RwLock,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::fs::{create_dir_all, remove_file, symlink_metadata, File};
|
|
||||||
use std::io::prelude::*;
|
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
use std::time::{Duration, SystemTime};
|
|
||||||
|
|
||||||
use rocket::http::ContentType;
|
|
||||||
use rocket::response::Content;
|
|
||||||
use rocket::Route;
|
|
||||||
|
|
||||||
use reqwest::{Url, header::HeaderMap, blocking::Client, blocking::Response};
|
|
||||||
|
|
||||||
use rocket::http::Cookie;
|
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use reqwest::{blocking::Client, blocking::Response, header, Url};
|
||||||
|
use rocket::{http::ContentType, http::Cookie, response::Content, Route};
|
||||||
use soup::prelude::*;
|
use soup::prelude::*;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::{error::Error, util::Cached, CONFIG};
|
||||||
use crate::CONFIG;
|
|
||||||
use crate::util::Cached;
|
|
||||||
|
|
||||||
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 = "_-.";
|
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()
|
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()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build Regex only once since this takes a lot of time.
|
||||||
|
static ICON_REL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)icon$|apple.*icon").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) -> Option<Cached<Content<Vec<u8>>>> {
|
||||||
|
if !is_valid_domain(&domain) {
|
||||||
|
warn!("Invalid domain: {}", domain);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_icon(&domain).map(|icon| Cached::long(Content(ContentType::new("image", "x-icon"), icon)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
// If parsing the domain fails using Url, it will not work with reqwest.
|
||||||
if domain.is_empty() || domain.len() > 255 || domain.contains("..") {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,25 +87,112 @@ fn is_valid_domain(domain: &str) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<domain>/icon.png")]
|
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
||||||
fn icon(domain: String) -> Cached<Content<Vec<u8>>> {
|
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
||||||
let icon_type = ContentType::new("image", "x-icon");
|
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
||||||
|
#[allow(clippy::nonminimal_bool)]
|
||||||
if !is_valid_domain(&domain) {
|
#[cfg(not(feature = "unstable"))]
|
||||||
warn!("Invalid domain: {:#?}", domain);
|
fn is_global(ip: IpAddr) -> bool {
|
||||||
return Cached::long(Content(icon_type, FALLBACK_ICON.to_vec()));
|
match ip {
|
||||||
|
IpAddr::V4(ip) => {
|
||||||
|
// check if this address is 192.0.0.9 or 192.0.0.10. These addresses are the only two
|
||||||
|
// globally routable addresses in the 192.0.0.0/24 range.
|
||||||
|
if u32::from(ip) == 0xc0000009 || u32::from(ip) == 0xc000000a {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
!ip.is_private()
|
||||||
|
&& !ip.is_loopback()
|
||||||
|
&& !ip.is_link_local()
|
||||||
|
&& !ip.is_broadcast()
|
||||||
|
&& !ip.is_documentation()
|
||||||
|
&& !(ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000))
|
||||||
|
&& !(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0)
|
||||||
|
&& !(ip.octets()[0] & 240 == 240 && !ip.is_broadcast())
|
||||||
|
&& !(ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18)
|
||||||
|
// Make sure the address is not in 0.0.0.0/8
|
||||||
|
&& ip.octets()[0] != 0
|
||||||
|
}
|
||||||
|
IpAddr::V6(ip) => {
|
||||||
|
if ip.is_multicast() && ip.segments()[0] & 0x000f == 14 {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
!ip.is_multicast()
|
||||||
|
&& !ip.is_loopback()
|
||||||
|
&& !((ip.segments()[0] & 0xffc0) == 0xfe80)
|
||||||
|
&& !((ip.segments()[0] & 0xfe00) == 0xfc00)
|
||||||
|
&& !ip.is_unspecified()
|
||||||
|
&& !((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cached::long(Content(icon_type, get_icon(&domain)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
|
#[cfg(feature = "unstable")]
|
||||||
|
fn is_global(ip: IpAddr) -> bool {
|
||||||
|
ip.is_global()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// These are some tests to check that the implementations match
|
||||||
|
/// The IPv4 can be all checked in 5 mins or so and they are correct as of nightly 2020-07-11
|
||||||
|
/// The IPV6 can't be checked in a reasonable time, so we check about ten billion random ones, so far correct
|
||||||
|
/// Note that the is_global implementation is subject to change as new IP RFCs are created
|
||||||
|
///
|
||||||
|
/// To run while showing progress output:
|
||||||
|
/// cargo test --features sqlite,unstable -- --nocapture --ignored
|
||||||
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_ipv4_global() {
|
||||||
|
for a in 0..u8::MAX {
|
||||||
|
println!("Iter: {}/255", a);
|
||||||
|
for b in 0..u8::MAX {
|
||||||
|
for c in 0..u8::MAX {
|
||||||
|
for d in 0..u8::MAX {
|
||||||
|
let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d));
|
||||||
|
assert_eq!(ip.is_global(), is_global(ip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_ipv6_global() {
|
||||||
|
use ring::rand::{SecureRandom, SystemRandom};
|
||||||
|
let mut v = [0u8; 16];
|
||||||
|
let rand = SystemRandom::new();
|
||||||
|
for i in 0..1_000 {
|
||||||
|
println!("Iter: {}/1_000", i);
|
||||||
|
for _ in 0..10_000_000 {
|
||||||
|
rand.fill(&mut v).expect("Error generating random values");
|
||||||
|
let ip = IpAddr::V6(std::net::Ipv6Addr::new(
|
||||||
|
(v[14] as u16) << 8 | v[15] as u16,
|
||||||
|
(v[12] as u16) << 8 | v[13] as u16,
|
||||||
|
(v[10] as u16) << 8 | v[11] as u16,
|
||||||
|
(v[8] as u16) << 8 | v[9] as u16,
|
||||||
|
(v[6] as u16) << 8 | v[7] as u16,
|
||||||
|
(v[4] as u16) << 8 | v[5] as u16,
|
||||||
|
(v[2] as u16) << 8 | v[3] as u16,
|
||||||
|
(v[0] as u16) << 8 | v[1] as u16,
|
||||||
|
));
|
||||||
|
assert_eq!(ip.is_global(), is_global(ip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
for ip_port in x {
|
for ip_port in x {
|
||||||
if !ip_port.ip().is_global() {
|
if !is_global(ip_port.ip()) {
|
||||||
warn!("IP {} for domain '{}' is not a global IP!", ip_port.ip(), domain);
|
warn!("IP {} for domain '{}' is not a global IP!", ip_port.ip(), domain);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +204,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;
|
||||||
|
@ -93,39 +239,38 @@ 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>> {
|
||||||
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;
|
return Some(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
||||||
save_icon(&path, &icon);
|
save_icon(&path, &icon);
|
||||||
icon
|
Some(icon)
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
@ -182,11 +327,17 @@ struct Icon {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
fn new(priority: u8, href: String) -> Self {
|
const fn new(priority: u8, href: String) -> Self {
|
||||||
Self { href, priority }
|
Self { href, priority }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -199,24 +350,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(mut 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()
|
||||||
|
@ -230,26 +422,34 @@ 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 = crate::util::LimitedReader::new(&mut content, 512 * 1024);
|
let limited_reader = content.take(512 * 1024);
|
||||||
|
|
||||||
let soup = Soup::from_reader(limited_reader)?;
|
let soup = Soup::from_reader(limited_reader)?;
|
||||||
// Search for and filter
|
// Search for and filter
|
||||||
let favicons = soup
|
let favicons = soup
|
||||||
.tag("link")
|
.tag("link")
|
||||||
.attr("rel", Regex::new(r"icon$|apple.*icon")?) // Only use icon rels
|
.attr("rel", ICON_REL_REGEX.clone()) // Only use icon rels
|
||||||
.attr("href", Regex::new(r"(?i)\w+\.(jpg|jpeg|png|ico)(\?.*)?$|^data:image.*base64")?) // Only allow specific extensions
|
.attr_name("href") // Make sure there is a href
|
||||||
.find_all();
|
.find_all();
|
||||||
|
|
||||||
// Loop through all the found icons and determine it's priority
|
// Loop through all the found icons and determine it's priority
|
||||||
for favicon in favicons {
|
for favicon in favicons {
|
||||||
let sizes = favicon.get("sizes");
|
let sizes = favicon.get("sizes");
|
||||||
let href = favicon.get("href").expect("Missing href");
|
let href = favicon.get("href").unwrap();
|
||||||
let full_href = url.join(&href).unwrap().into_string();
|
// Skip invalid url's
|
||||||
|
let full_href = match url.join(&href) {
|
||||||
|
Ok(h) => h.into_string(),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
let priority = get_icon_priority(&full_href, sizes);
|
let priority = get_icon_priority(&full_href, sizes);
|
||||||
|
|
||||||
|
@ -265,28 +465,33 @@ 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.
|
||||||
|
@ -314,7 +519,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
|
||||||
|
@ -354,7 +559,7 @@ fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
||||||
let mut height: u16 = 0;
|
let mut height: u16 = 0;
|
||||||
|
|
||||||
if let Some(sizes) = sizes {
|
if let Some(sizes) = sizes {
|
||||||
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
|
match ICON_SIZE_REGEX.captures(sizes.trim()) {
|
||||||
None => {}
|
None => {}
|
||||||
Some(dimensions) => {
|
Some(dimensions) => {
|
||||||
if dimensions.len() >= 3 {
|
if dimensions.len() >= 3 {
|
||||||
|
@ -369,17 +574,17 @@ fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
fn download_icon(domain: &str) -> Result<Vec<u8>, 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();
|
||||||
|
|
||||||
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
|
||||||
|
@ -394,13 +599,13 @@ fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
||||||
_ => warn!("data uri is invalid"),
|
_ => warn!("data 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);
|
info!("Downloaded icon from {}", icon.href);
|
||||||
res.copy_to(&mut buffer)?;
|
res.copy_to(&mut buffer)?;
|
||||||
break;
|
break;
|
||||||
}
|
},
|
||||||
Err(_) => info!("Download failed for {}", icon.href),
|
_ => warn!("Download failed for {}", icon.href),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -425,25 +630,3 @@ fn save_icon(path: &str, icon: &[u8]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _header_map() -> HeaderMap {
|
|
||||||
// Set some default headers for the request.
|
|
||||||
// Use a browser like user-agent to make sure most websites will return there correct website.
|
|
||||||
use reqwest::header::*;
|
|
||||||
|
|
||||||
macro_rules! headers {
|
|
||||||
($( $name:ident : $value:literal),+ $(,)? ) => {
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
$( headers.insert($name, HeaderValue::from_static($value)); )+
|
|
||||||
headers
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
headers! {
|
|
||||||
USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299",
|
|
||||||
ACCEPT_LANGUAGE: "en-US,en;q=0.8",
|
|
||||||
CACHE_CONTROL: "no-cache",
|
|
||||||
PRAGMA: "no-cache",
|
|
||||||
ACCEPT: "text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
use chrono::Utc;
|
use chrono::Local;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::request::{Form, FormItems, FromForm};
|
use rocket::{
|
||||||
use rocket::Route;
|
request::{Form, FormItems, FromForm},
|
||||||
|
Route,
|
||||||
|
};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::core::two_factor::email::EmailTokenData;
|
use crate::{
|
||||||
use crate::api::core::two_factor::{duo, email, yubikey};
|
api::{
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
||||||
use crate::auth::ClientIp;
|
ApiResult, EmptyResult, JsonResult,
|
||||||
use crate::db::models::*;
|
},
|
||||||
use crate::db::DbConn;
|
auth::ClientIp,
|
||||||
use crate::mail;
|
db::{models::*, DbConn},
|
||||||
use crate::util;
|
error::MapResult,
|
||||||
use crate::CONFIG;
|
mail, util, CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![login]
|
routes![login]
|
||||||
|
@ -38,7 +41,7 @@ fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
|
||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, conn, ip)
|
_password_login(data, conn, &ip)
|
||||||
}
|
}
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
}
|
}
|
||||||
|
@ -49,10 +52,7 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||||
let token = data.refresh_token.unwrap();
|
let token = data.refresh_token.unwrap();
|
||||||
|
|
||||||
// Get device by refresh token
|
// Get device by refresh token
|
||||||
let mut device = match Device::find_by_refresh_token(&token, &conn) {
|
let mut device = Device::find_by_refresh_token(&token, &conn).map_res("Invalid refresh token")?;
|
||||||
Some(device) => device,
|
|
||||||
None => err!("Invalid refresh token"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// COMMON
|
// COMMON
|
||||||
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
||||||
|
@ -68,10 +68,15 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||||
"refresh_token": device.refresh_token,
|
"refresh_token": device.refresh_token,
|
||||||
"Key": user.akey,
|
"Key": user.akey,
|
||||||
"PrivateKey": user.private_key,
|
"PrivateKey": user.private_key,
|
||||||
|
|
||||||
|
"Kdf": user.client_kdf_type,
|
||||||
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||||
|
"scope": "api offline_access"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
|
fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
let scope = data.scope.as_ref().unwrap();
|
||||||
if scope != "api offline_access" {
|
if scope != "api offline_access" {
|
||||||
|
@ -97,8 +102,18 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
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 = Utc::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 {
|
||||||
|
@ -127,10 +142,10 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, &conn, &user);
|
let (mut device, new_device) = get_device(&data, &conn, &user);
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
|
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &ip, &conn)?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name) {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name) {
|
||||||
error!("Error sending new device email: {:#?}", e);
|
error!("Error sending new device email: {:#?}", e);
|
||||||
|
|
||||||
if CONFIG.require_device_email() {
|
if CONFIG.require_device_email() {
|
||||||
|
@ -140,7 +155,6 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
|
||||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
||||||
|
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||||
|
@ -154,6 +168,11 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
||||||
"Key": user.akey,
|
"Key": user.akey,
|
||||||
"PrivateKey": user.private_key,
|
"PrivateKey": user.private_key,
|
||||||
//"TwoFactorToken": "11122233333444555666777888999"
|
//"TwoFactorToken": "11122233333444555666777888999"
|
||||||
|
|
||||||
|
"Kdf": user.client_kdf_type,
|
||||||
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"ResetMasterPassword": false,// TODO: Same as above
|
||||||
|
"scope": "api offline_access"
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(token) = twofactor_token {
|
if let Some(token) = twofactor_token {
|
||||||
|
@ -197,6 +216,7 @@ fn twofactor_auth(
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
|
ip: &ClientIp,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
||||||
|
@ -216,8 +236,7 @@ fn twofactor_auth(
|
||||||
|
|
||||||
let selected_twofactor = twofactors
|
let selected_twofactor = twofactors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|tf| tf.atype == selected_id && tf.enabled)
|
.find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
.nth(0);
|
|
||||||
|
|
||||||
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;
|
||||||
|
@ -226,7 +245,7 @@ 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?, 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) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
||||||
|
@ -252,10 +271,7 @@ fn twofactor_auth(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
||||||
match tf {
|
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
||||||
Some(tf) => Ok(tf.data),
|
|
||||||
None => err!("Two factor doesn't exist"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
||||||
|
@ -347,6 +363,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct ConnectData {
|
struct ConnectData {
|
||||||
|
@ -364,6 +381,7 @@ struct ConnectData {
|
||||||
device_identifier: Option<String>,
|
device_identifier: Option<String>,
|
||||||
device_name: Option<String>,
|
device_name: Option<String>,
|
||||||
device_type: Option<String>,
|
device_type: Option<String>,
|
||||||
|
device_push_token: Option<String>, // Unused; mobile device push not yet supported.
|
||||||
|
|
||||||
// Needed for two-factor auth
|
// Needed for two-factor auth
|
||||||
two_factor_provider: Option<i32>,
|
two_factor_provider: Option<i32>,
|
||||||
|
@ -391,6 +409,7 @@ impl<'f> FromForm<'f> for ConnectData {
|
||||||
"deviceidentifier" => form.device_identifier = Some(value),
|
"deviceidentifier" => form.device_identifier = Some(value),
|
||||||
"devicename" => form.device_name = Some(value),
|
"devicename" => form.device_name = Some(value),
|
||||||
"devicetype" => form.device_type = Some(value),
|
"devicetype" => form.device_type = Some(value),
|
||||||
|
"devicepushtoken" => form.device_push_token = Some(value),
|
||||||
"twofactorprovider" => form.two_factor_provider = value.parse().ok(),
|
"twofactorprovider" => form.two_factor_provider = value.parse().ok(),
|
||||||
"twofactortoken" => form.two_factor_token = Some(value),
|
"twofactortoken" => form.two_factor_token = Some(value),
|
||||||
"twofactorremember" => form.two_factor_remember = value.parse().ok(),
|
"twofactorremember" => form.two_factor_remember = value.parse().ok(),
|
||||||
|
|
|
@ -1,27 +1,29 @@
|
||||||
mod admin;
|
mod admin;
|
||||||
pub(crate) mod core;
|
pub mod core;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
pub use self::admin::routes as admin_routes;
|
|
||||||
pub use self::core::routes as core_routes;
|
|
||||||
pub use self::icons::routes as icons_routes;
|
|
||||||
pub use self::identity::routes as identity_routes;
|
|
||||||
pub use self::notifications::routes as notifications_routes;
|
|
||||||
pub use self::notifications::{start_notification_server, Notify, UpdateType};
|
|
||||||
pub use self::web::routes as web_routes;
|
|
||||||
|
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub use crate::api::{
|
||||||
|
admin::routes as admin_routes,
|
||||||
|
core::routes as core_routes,
|
||||||
|
icons::routes as icons_routes,
|
||||||
|
identity::routes as identity_routes,
|
||||||
|
notifications::routes as notifications_routes,
|
||||||
|
notifications::{start_notification_server, Notify, UpdateType},
|
||||||
|
web::routes as web_routes,
|
||||||
|
};
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
// Type aliases for API methods results
|
// Type aliases for API methods results
|
||||||
type ApiResult<T> = Result<T, crate::error::Error>;
|
type ApiResult<T> = Result<T, crate::error::Error>;
|
||||||
pub type JsonResult = ApiResult<Json<Value>>;
|
pub type JsonResult = ApiResult<Json<Value>>;
|
||||||
pub type EmptyResult = ApiResult<()>;
|
pub type EmptyResult = ApiResult<()>;
|
||||||
|
|
||||||
use crate::util;
|
|
||||||
type JsonUpcase<T> = Json<util::UpCase<T>>;
|
type JsonUpcase<T> = Json<util::UpCase<T>>;
|
||||||
type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>;
|
type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>;
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ 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::api::{EmptyResult, JsonResult};
|
use crate::{
|
||||||
use crate::auth::Headers;
|
api::{EmptyResult, JsonResult},
|
||||||
use crate::db::DbConn;
|
auth::Headers,
|
||||||
|
db::DbConn,
|
||||||
use crate::{Error, CONFIG};
|
Error, CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![negotiate, websockets_err]
|
routes![negotiate, websockets_err]
|
||||||
|
@ -19,10 +20,12 @@ 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_and_swap(true, false, Ordering::Relaxed) {
|
||||||
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.
|
||||||
###########################################################################################")
|
###########################################################################################"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::empty())
|
Err(Error::empty())
|
||||||
}
|
}
|
||||||
|
@ -136,7 +139,6 @@ struct InitialMessage {
|
||||||
const PING_MS: u64 = 15_000;
|
const PING_MS: u64 = 15_000;
|
||||||
const PING: Token = Token(1);
|
const PING: Token = Token(1);
|
||||||
|
|
||||||
const ID_KEY: &str = "id=";
|
|
||||||
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
||||||
|
|
||||||
impl WSHandler {
|
impl WSHandler {
|
||||||
|
@ -147,38 +149,50 @@ impl WSHandler {
|
||||||
let io_error = io::Error::from(io::ErrorKind::InvalidData);
|
let io_error = io::Error::from(io::ErrorKind::InvalidData);
|
||||||
Err(ws::Error::new(ws::ErrorKind::Io(io_error), msg))
|
Err(ws::Error::new(ws::ErrorKind::Io(io_error), msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_request_token(&self, hs: Handshake) -> Option<String> {
|
||||||
|
use std::str::from_utf8;
|
||||||
|
|
||||||
|
// Verify we have a token header
|
||||||
|
if let Some(header_value) = hs.request.header("Authorization") {
|
||||||
|
if let Ok(converted) = from_utf8(header_value) {
|
||||||
|
if let Some(token_part) = converted.split("Bearer ").nth(1) {
|
||||||
|
return Some(token_part.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Otherwise verify the query parameter value
|
||||||
|
let path = hs.request.resource();
|
||||||
|
if let Some(params) = path.split('?').nth(1) {
|
||||||
|
let params_iter = params.split('&').take(1);
|
||||||
|
for val in params_iter {
|
||||||
|
if val.starts_with(ACCESS_TOKEN_KEY) {
|
||||||
|
return Some(val[ACCESS_TOKEN_KEY.len()..].into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>"
|
||||||
let path = hs.request.resource();
|
//
|
||||||
|
// We don't use `id`, and as of around 2020-03-25, the official clients
|
||||||
|
// no longer seem to pass `id` (only `access_token`).
|
||||||
|
|
||||||
let (_id, access_token) = match path.split('?').nth(1) {
|
// Get user token from header or query parameter
|
||||||
Some(params) => {
|
let access_token = match self.get_request_token(hs) {
|
||||||
let mut params_iter = params.split('&').take(2);
|
Some(token) => token,
|
||||||
|
_ => return self.err("Missing access token"),
|
||||||
let mut id = None;
|
|
||||||
let mut access_token = None;
|
|
||||||
while let Some(val) = params_iter.next() {
|
|
||||||
if val.starts_with(ID_KEY) {
|
|
||||||
id = Some(&val[ID_KEY.len()..]);
|
|
||||||
} else if val.starts_with(ACCESS_TOKEN_KEY) {
|
|
||||||
access_token = Some(&val[ACCESS_TOKEN_KEY.len()..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match (id, access_token) {
|
|
||||||
(Some(a), Some(b)) => (a, b),
|
|
||||||
_ => return self.err("Missing id or access token"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => return self.err("Missing query path"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the user
|
// Validate the user
|
||||||
use crate::auth;
|
use crate::auth;
|
||||||
let claims = match auth::decode_login(access_token) {
|
let claims = match auth::decode_login(access_token.as_str()) {
|
||||||
Ok(claims) => claims,
|
Ok(claims) => claims,
|
||||||
Err(_) => return self.err("Invalid access token provided"),
|
Err(_) => return self.err("Invalid access token provided"),
|
||||||
};
|
};
|
||||||
|
@ -256,7 +270,9 @@ impl Factory for WSFactory {
|
||||||
// Remove handler
|
// Remove handler
|
||||||
if let Some(user_uuid) = &handler.user_uuid {
|
if let Some(user_uuid) = &handler.user_uuid {
|
||||||
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
|
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
|
||||||
user_conn.remove_item(&handler.out);
|
if let Some(pos) = user_conn.iter().position(|x| x == &handler.out) {
|
||||||
|
user_conn.remove(pos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,7 +343,7 @@ impl WebSocketUsers {
|
||||||
/* Message Structure
|
/* Message Structure
|
||||||
[
|
[
|
||||||
1, // MessageType.Invocation
|
1, // MessageType.Invocation
|
||||||
{}, // Headers
|
{}, // Headers (map)
|
||||||
null, // InvocationId
|
null, // InvocationId
|
||||||
"ReceiveMessage", // Target
|
"ReceiveMessage", // Target
|
||||||
[ // Arguments
|
[ // Arguments
|
||||||
|
@ -344,7 +360,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType) -> Vec<u8> {
|
||||||
|
|
||||||
let value = V::Array(vec![
|
let value = V::Array(vec![
|
||||||
1.into(),
|
1.into(),
|
||||||
V::Array(vec![]),
|
V::Map(vec![]),
|
||||||
V::Nil,
|
V::Nil,
|
||||||
"ReceiveMessage".into(),
|
"ReceiveMessage".into(),
|
||||||
V::Array(vec![V::Map(vec![
|
V::Array(vec![V::Map(vec![
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use rocket::http::ContentType;
|
use rocket::{http::ContentType, response::content::Content, response::NamedFile, Route};
|
||||||
use rocket::response::content::Content;
|
|
||||||
use rocket::response::NamedFile;
|
|
||||||
use rocket::Route;
|
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::{error::Error, util::Cached, CONFIG};
|
||||||
use crate::util::Cached;
|
|
||||||
use crate::CONFIG;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
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
|
||||||
|
@ -78,13 +73,17 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
|
||||||
match filename.as_ref() {
|
match filename.as_ref() {
|
||||||
"mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
|
"mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
|
||||||
"logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
"logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
|
||||||
|
"shield-white.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/shield-white.png"))),
|
||||||
"error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
|
"error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
|
||||||
"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-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))),
|
"bootstrap-native.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
||||||
"md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.js"))),
|
"md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.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.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"))),
|
||||||
_ => err!(format!("Static file not found: {}", filename)),
|
_ => err!(format!("Static file not found: {}", filename)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
186
src/auth.rs
186
src/auth.rs
|
@ -1,17 +1,19 @@
|
||||||
//
|
//
|
||||||
// JWT Handling
|
// JWT Handling
|
||||||
//
|
//
|
||||||
use crate::util::read_file;
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use jsonwebtoken::{self, Algorithm, Header, EncodingKey, DecodingKey};
|
use jsonwebtoken::{self, Algorithm, DecodingKey, EncodingKey, Header};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::ser::Serialize;
|
use serde::ser::Serialize;
|
||||||
|
|
||||||
use crate::error::{Error, MapResult};
|
use crate::{
|
||||||
use crate::CONFIG;
|
error::{Error, MapResult},
|
||||||
|
util::read_file,
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||||
|
|
||||||
|
@ -213,11 +215,12 @@ pub fn generate_admin_claims() -> AdminJWTClaims {
|
||||||
//
|
//
|
||||||
// Bearer token authentication
|
// Bearer token authentication
|
||||||
//
|
//
|
||||||
use rocket::request::{self, FromRequest, Request};
|
use rocket::request::{FromRequest, Outcome, Request};
|
||||||
use rocket::Outcome;
|
|
||||||
|
|
||||||
use crate::db::models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization};
|
use crate::db::{
|
||||||
use crate::db::DbConn;
|
models::{CollectionUser, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
|
||||||
|
DbConn,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Headers {
|
pub struct Headers {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
@ -228,7 +231,7 @@ pub struct Headers {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
let headers = request.headers();
|
let headers = request.headers();
|
||||||
|
|
||||||
// Get host
|
// Get host
|
||||||
|
@ -293,7 +296,25 @@ 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 })
|
||||||
|
@ -305,6 +326,8 @@ 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 param ("/organizations/<org_id>")
|
||||||
|
@ -329,7 +352,7 @@ fn get_org_id(request: &Request) -> Option<String> {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
match request.guard::<Headers>() {
|
match request.guard::<Headers>() {
|
||||||
Outcome::Forward(_) => Outcome::Forward(()),
|
Outcome::Forward(_) => Outcome::Forward(()),
|
||||||
Outcome::Failure(f) => Outcome::Failure(f),
|
Outcome::Failure(f) => Outcome::Failure(f),
|
||||||
|
@ -365,8 +388,10 @@ 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,7 +409,7 @@ pub struct AdminHeaders {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
match request.guard::<OrgHeaders>() {
|
match request.guard::<OrgHeaders>() {
|
||||||
Outcome::Forward(_) => Outcome::Forward(()),
|
Outcome::Forward(_) => Outcome::Forward(()),
|
||||||
Outcome::Failure(f) => Outcome::Failure(f),
|
Outcome::Failure(f) => Outcome::Failure(f),
|
||||||
|
@ -404,14 +429,135 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Headers> for AdminHeaders {
|
impl Into<Headers> for AdminHeaders {
|
||||||
fn into(self) -> Headers {
|
fn into(self) -> Headers {
|
||||||
Headers {
|
Headers {
|
||||||
host: self.host,
|
host: self.host,
|
||||||
device: self.device,
|
device: self.device,
|
||||||
user: self.user
|
user: self.user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// col_id is usually the forth param ("/organizations/<org_id>/collections/<col_id>")
|
||||||
|
// But there cloud be cases where it is located in a query value.
|
||||||
|
// First check the param, if this is not a valid uuid, we will try the query value.
|
||||||
|
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.access_all {
|
||||||
|
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 Into<Headers> for ManagerHeaders {
|
||||||
|
fn into(self) -> Headers {
|
||||||
|
Headers {
|
||||||
|
host: self.host,
|
||||||
|
device: self.device,
|
||||||
|
user: self.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 Into<Headers> for ManagerHeadersLoose {
|
||||||
|
fn into(self) -> Headers {
|
||||||
|
Headers {
|
||||||
|
host: self.host,
|
||||||
|
device: self.device,
|
||||||
|
user: self.user,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OwnerHeaders {
|
pub struct OwnerHeaders {
|
||||||
|
@ -423,7 +569,7 @@ pub struct OwnerHeaders {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
|
impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
match request.guard::<OrgHeaders>() {
|
match request.guard::<OrgHeaders>() {
|
||||||
Outcome::Forward(_) => Outcome::Forward(()),
|
Outcome::Forward(_) => Outcome::Forward(()),
|
||||||
Outcome::Failure(f) => Outcome::Failure(f),
|
Outcome::Failure(f) => Outcome::Failure(f),
|
||||||
|
@ -454,7 +600,7 @@ pub struct ClientIp {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
|
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
let ip = if CONFIG._ip_header_enabled() {
|
let ip = if CONFIG._ip_header_enabled() {
|
||||||
req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| {
|
req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| {
|
||||||
match ip.find(',') {
|
match ip.find(',') {
|
||||||
|
|
187
src/config.rs
187
src/config.rs
|
@ -1,11 +1,14 @@
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::{
|
||||||
use crate::util::{get_env, get_env_bool};
|
db::DbConnType,
|
||||||
|
error::Error,
|
||||||
|
util::{get_env, get_env_bool},
|
||||||
|
};
|
||||||
|
|
||||||
static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
|
static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
|
||||||
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
|
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
|
||||||
|
@ -50,7 +53,32 @@ macro_rules! make_config {
|
||||||
|
|
||||||
impl ConfigBuilder {
|
impl ConfigBuilder {
|
||||||
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();
|
||||||
$($(
|
$($(
|
||||||
|
@ -112,6 +140,9 @@ macro_rules! make_config {
|
||||||
)+)+
|
)+)+
|
||||||
config.domain_set = _domain_set;
|
config.domain_set = _domain_set;
|
||||||
|
|
||||||
|
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
|
||||||
|
config.org_creation_users = config.org_creation_users.trim().to_lowercase();
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +164,6 @@ macro_rules! make_config {
|
||||||
(inner._env.build(), inner.config.clone())
|
(inner._env.build(), inner.config.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
fn _get_form_type(rust_type: &str) -> &'static str {
|
fn _get_form_type(rust_type: &str) -> &'static str {
|
||||||
match rust_type {
|
match rust_type {
|
||||||
"Pass" => "password",
|
"Pass" => "password",
|
||||||
|
@ -217,6 +247,8 @@ make_config! {
|
||||||
data_folder: String, false, def, "data".to_string();
|
data_folder: String, false, def, "data".to_string();
|
||||||
/// Database URL
|
/// Database URL
|
||||||
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
|
||||||
|
/// Database connection pool size
|
||||||
|
database_max_conns: u32, false, def, 10;
|
||||||
/// Icon cache folder
|
/// Icon cache folder
|
||||||
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
|
||||||
|
@ -263,7 +295,7 @@ make_config! {
|
||||||
/// $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 if new users can register. Note that while this is disabled, users could still be invited
|
/// Allow new signups |> Controls whether new users can register. Users can be invited by the bitwarden_rs 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;
|
||||||
|
@ -271,9 +303,12 @@ make_config! {
|
||||||
signups_verify_resend_time: u64, true, def, 3_600;
|
signups_verify_resend_time: u64, true, def, 3_600;
|
||||||
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
|
/// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)
|
||||||
signups_verify_resend_limit: u32, true, def, 6;
|
signups_verify_resend_limit: u32, true, def, 6;
|
||||||
/// Allow signups only from this list of comma-separated domains
|
/// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled
|
||||||
signups_domains_whitelist: String, true, def, "".to_string();
|
signups_domains_whitelist: String, true, def, "".to_string();
|
||||||
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
/// Org creation users |> Allow org creation only by this list of comma-separated user emails.
|
||||||
|
/// Blank or 'all' means all users can create orgs; 'none' means no users can create orgs.
|
||||||
|
org_creation_users: String, true, def, "".to_string();
|
||||||
|
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled
|
||||||
invitations_allowed: bool, true, def, true;
|
invitations_allowed: bool, true, def, true;
|
||||||
/// Password iterations |> Number of server-side passwords hashing iterations.
|
/// Password iterations |> Number of server-side passwords hashing iterations.
|
||||||
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
||||||
|
@ -326,6 +361,8 @@ make_config! {
|
||||||
reload_templates: bool, true, def, false;
|
reload_templates: bool, true, def, false;
|
||||||
/// Enable extended logging
|
/// Enable extended logging
|
||||||
extended_logging: bool, false, def, true;
|
extended_logging: bool, false, def, true;
|
||||||
|
/// Log timestamp format
|
||||||
|
log_timestamp_format: String, true, def, "%Y-%m-%d %H:%M:%S.%3f".to_string();
|
||||||
/// Enable the log to output to Syslog
|
/// Enable the log to output to Syslog
|
||||||
use_syslog: bool, false, def, false;
|
use_syslog: bool, false, def, false;
|
||||||
/// Log file path
|
/// Log file path
|
||||||
|
@ -337,6 +374,9 @@ make_config! {
|
||||||
/// 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;
|
||||||
|
|
||||||
|
/// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely
|
||||||
|
db_connection_retries: u32, false, def, 15;
|
||||||
|
|
||||||
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||||
disable_admin_token: bool, true, def, false;
|
disable_admin_token: bool, true, def, false;
|
||||||
|
|
||||||
|
@ -373,34 +413,42 @@ 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 SSL
|
/// 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;
|
||||||
/// Use explicit TLS |> Enabling this would force the use of an explicit TLS connection, instead of upgrading an insecure one with STARTTLS
|
/// 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, "Bitwarden_RS".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;
|
||||||
/// Json form auth mechanism |> Defaults for ssl is "Plain" and "Login" and nothing for non-ssl connections. Possible values: ["Plain", "Login", "Xoauth2"]
|
/// 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
|
||||||
|
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
|
||||||
email_2fa: _enable_email_2fa {
|
email_2fa: _enable_email_2fa {
|
||||||
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
||||||
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
|
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
|
||||||
/// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19.
|
/// Email token size |> Number of digits in an email token (min: 6, max: 19). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
|
||||||
email_token_size: u32, true, def, 6;
|
email_token_size: u32, true, def, 6;
|
||||||
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||||
email_expiration_time: u64, true, def, 600;
|
email_expiration_time: u64, true, def, 600;
|
||||||
|
@ -410,27 +458,39 @@ make_config! {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
let db_url = cfg.database_url.to_lowercase();
|
|
||||||
if cfg!(feature = "sqlite") && (db_url.starts_with("mysql:") || db_url.starts_with("postgresql:")) {
|
|
||||||
err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg!(feature = "mysql") && !db_url.starts_with("mysql:") {
|
// Validate connection URL is valid and DB feature is enabled
|
||||||
err!("`DATABASE_URL` should start with mysql: when using the MySQL server")
|
DbConnType::from_url(&cfg.database_url)?;
|
||||||
}
|
|
||||||
|
|
||||||
if cfg!(feature = "postgresql") && !db_url.starts_with("postgresql:") {
|
let limit = 256;
|
||||||
err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server")
|
if cfg.database_max_conns < 1 || cfg.database_max_conns > limit {
|
||||||
|
err!(format!(
|
||||||
|
"`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;
|
||||||
|
if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) {
|
||||||
|
err!("`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.split(',').any(|u| !u.contains('@')) {
|
||||||
|
err!("`ORG_CREATION_USERS` contains invalid email addresses");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref token) = cfg.admin_token {
|
if let Some(ref token) = cfg.admin_token {
|
||||||
if token.trim().is_empty() && !cfg.disable_admin_token {
|
if token.trim().is_empty() && !cfg.disable_admin_token {
|
||||||
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
|
println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled.");
|
||||||
|
println!("[WARNING] To enable the admin page without a token, use `DISABLE_ADMIN_TOKEN`.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,6 +527,16 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the icon blacklist regex is valid
|
||||||
|
if let Some(ref r) = cfg.icon_blacklist_regex {
|
||||||
|
use regex::Regex;
|
||||||
|
let validate_regex = Regex::new(&r);
|
||||||
|
match validate_regex {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {:#?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,18 +621,43 @@ impl Config {
|
||||||
self.update_config(builder)
|
self.update_config(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_signup_user(&self, email: &str) -> bool {
|
/// Tests whether an email's domain is allowed. A domain is allowed if it
|
||||||
|
/// is in signups_domains_whitelist, or if no whitelist is set (so there
|
||||||
|
/// are no domain restrictions in effect).
|
||||||
|
pub fn is_email_domain_allowed(&self, email: &str) -> bool {
|
||||||
let e: Vec<&str> = email.rsplitn(2, '@').collect();
|
let e: Vec<&str> = email.rsplitn(2, '@').collect();
|
||||||
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
|
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
|
||||||
warn!("Failed to parse email address '{}'", email);
|
warn!("Failed to parse email address '{}'", email);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let email_domain = e[0].to_lowercase();
|
||||||
|
let whitelist = self.signups_domains_whitelist();
|
||||||
|
|
||||||
// Allow signups if the whitelist is empty/not configured
|
whitelist.is_empty() || whitelist.split(',').any(|d| d.trim() == email_domain)
|
||||||
// (it doesn't contain any domains), or if it matches at least
|
}
|
||||||
// one domain.
|
|
||||||
let whitelist_str = self.signups_domains_whitelist();
|
/// Tests whether signup is allowed for an email address, taking into
|
||||||
( whitelist_str.is_empty() && CONFIG.signups_allowed() )|| whitelist_str.split(',').filter(|s| !s.is_empty()).any(|d| d == e[0])
|
/// account the signups_allowed and signups_domains_whitelist settings.
|
||||||
|
pub fn is_signup_allowed(&self, email: &str) -> bool {
|
||||||
|
if !self.signups_domains_whitelist().is_empty() {
|
||||||
|
// The whitelist setting overrides the signups_allowed setting.
|
||||||
|
self.is_email_domain_allowed(email)
|
||||||
|
} else {
|
||||||
|
self.signups_allowed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests whether the specified user is allowed to create an organization.
|
||||||
|
pub fn is_org_creation_allowed(&self, email: &str) -> bool {
|
||||||
|
let users = self.org_creation_users();
|
||||||
|
if users == "" || users == "all" {
|
||||||
|
true
|
||||||
|
} else if users == "none" {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let email = email.to_lowercase();
|
||||||
|
users.split(',').any(|u| u.trim() == email)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||||
|
@ -617,6 +712,13 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests whether the admin token is set to a non-empty value.
|
||||||
|
pub fn is_admin_token_set(&self) -> bool {
|
||||||
|
let token = self.admin_token();
|
||||||
|
|
||||||
|
token.is_some() && !token.unwrap().trim().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_template<T: serde::ser::Serialize>(
|
pub fn render_template<T: serde::ser::Serialize>(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
@ -674,7 +776,10 @@ where
|
||||||
|
|
||||||
reg!("admin/base");
|
reg!("admin/base");
|
||||||
reg!("admin/login");
|
reg!("admin/login");
|
||||||
reg!("admin/page");
|
reg!("admin/settings");
|
||||||
|
reg!("admin/users");
|
||||||
|
reg!("admin/organizations");
|
||||||
|
reg!("admin/diagnostics");
|
||||||
|
|
||||||
// And then load user templates to overwrite the defaults
|
// And then load user templates to overwrite the defaults
|
||||||
// Use .hbs extension for the files
|
// Use .hbs extension for the files
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
//
|
//
|
||||||
// PBKDF2 derivation
|
// PBKDF2 derivation
|
||||||
//
|
//
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
|
use ring::{digest, hmac, pbkdf2};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use ring::{digest, hmac, pbkdf2};
|
|
||||||
use std::num::NonZeroU32;
|
|
||||||
|
|
||||||
static DIGEST_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
|
static DIGEST_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
|
||||||
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||||
|
@ -54,17 +55,21 @@ pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_token(token_size: u32) -> Result<String, Error> {
|
pub fn generate_token(token_size: u32) -> Result<String, Error> {
|
||||||
|
// A u64 can represent all whole numbers up to 19 digits long.
|
||||||
if token_size > 19 {
|
if token_size > 19 {
|
||||||
err!("Generating token failed")
|
err!("Token size is limited to 19 digits")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8 bytes to create an u64 for up to 19 token digits
|
let low: u64 = 0;
|
||||||
let bytes = get_random(vec![0; 8]);
|
let high: u64 = 10u64.pow(token_size);
|
||||||
let mut bytes_array = [0u8; 8];
|
|
||||||
bytes_array.copy_from_slice(&bytes);
|
|
||||||
|
|
||||||
let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size);
|
// Generate a random number in the range [low, high), then format it as a
|
||||||
|
// token of fixed width, left-padding with 0 as needed.
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
330
src/db/mod.rs
330
src/db/mod.rs
|
@ -1,55 +1,207 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use diesel::r2d2;
|
|
||||||
use diesel::r2d2::ConnectionManager;
|
|
||||||
use diesel::{Connection as DieselConnection, ConnectionError};
|
|
||||||
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::request::{self, FromRequest};
|
|
||||||
use rocket::{Outcome, Request, State};
|
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use crate::CONFIG;
|
use chrono::prelude::*;
|
||||||
|
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
request::{FromRequest, Outcome},
|
||||||
|
Request, State,
|
||||||
|
};
|
||||||
|
|
||||||
/// An alias to the database connection used
|
use crate::{
|
||||||
#[cfg(feature = "sqlite")]
|
error::{Error, MapResult},
|
||||||
type Connection = diesel::sqlite::SqliteConnection;
|
CONFIG,
|
||||||
#[cfg(feature = "mysql")]
|
};
|
||||||
type Connection = diesel::mysql::MysqlConnection;
|
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
type Connection = diesel::pg::PgConnection;
|
|
||||||
|
|
||||||
/// An alias to the type for a pool of Diesel connections.
|
#[cfg(sqlite)]
|
||||||
type Pool = r2d2::Pool<ConnectionManager<Connection>>;
|
|
||||||
|
|
||||||
/// Connection request guard type: a wrapper around an r2d2 pooled connection.
|
|
||||||
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>);
|
|
||||||
|
|
||||||
pub mod models;
|
|
||||||
#[cfg(feature = "sqlite")]
|
|
||||||
#[path = "schemas/sqlite/schema.rs"]
|
#[path = "schemas/sqlite/schema.rs"]
|
||||||
pub mod schema;
|
pub mod __sqlite_schema;
|
||||||
#[cfg(feature = "mysql")]
|
|
||||||
|
#[cfg(mysql)]
|
||||||
#[path = "schemas/mysql/schema.rs"]
|
#[path = "schemas/mysql/schema.rs"]
|
||||||
pub mod schema;
|
pub mod __mysql_schema;
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
|
#[cfg(postgresql)]
|
||||||
#[path = "schemas/postgresql/schema.rs"]
|
#[path = "schemas/postgresql/schema.rs"]
|
||||||
pub mod schema;
|
pub mod __postgresql_schema;
|
||||||
|
|
||||||
/// Initializes a database pool.
|
|
||||||
pub fn init_pool() -> Pool {
|
|
||||||
let manager = ConnectionManager::new(CONFIG.database_url());
|
|
||||||
|
|
||||||
r2d2::Pool::builder().build(manager).expect("Failed to create pool")
|
// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported
|
||||||
|
macro_rules! generate_connections {
|
||||||
|
( $( $name:ident: $ty:ty ),+ ) => {
|
||||||
|
#[allow(non_camel_case_types, dead_code)]
|
||||||
|
#[derive(Eq, PartialEq)]
|
||||||
|
pub enum DbConnType { $( $name, )+ }
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
|
||||||
|
|
||||||
|
impl DbPool {
|
||||||
|
// For the given database URL, guess it's type, run migrations create pool and return it
|
||||||
|
pub fn from_config() -> Result<Self, Error> {
|
||||||
|
let url = CONFIG.database_url();
|
||||||
|
let conn_type = DbConnType::from_url(&url)?;
|
||||||
|
|
||||||
|
match conn_type { $(
|
||||||
|
DbConnType::$name => {
|
||||||
|
#[cfg($name)]
|
||||||
|
{
|
||||||
|
paste::paste!{ [< $name _migrations >]::run_migrations()?; }
|
||||||
|
let manager = ConnectionManager::new(&url);
|
||||||
|
let pool = Pool::builder()
|
||||||
|
.max_size(CONFIG.database_max_conns())
|
||||||
|
.build(manager)
|
||||||
|
.map_res("Failed to create pool")?;
|
||||||
|
return Ok(Self::$name(pool));
|
||||||
|
}
|
||||||
|
#[cfg(not($name))]
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
return unreachable!("Trying to use a DB backend when it's feature is disabled");
|
||||||
|
},
|
||||||
|
)+ }
|
||||||
|
}
|
||||||
|
// Get a connection from the pool
|
||||||
|
pub fn get(&self) -> Result<DbConn, Error> {
|
||||||
|
match self { $(
|
||||||
|
#[cfg($name)]
|
||||||
|
Self::$name(p) => Ok(DbConn::$name(p.get().map_res("Error retrieving connection from pool")?)),
|
||||||
|
)+ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connection() -> Result<Connection, ConnectionError> {
|
generate_connections! {
|
||||||
Connection::establish(&CONFIG.database_url())
|
sqlite: diesel::sqlite::SqliteConnection,
|
||||||
|
mysql: diesel::mysql::MysqlConnection,
|
||||||
|
postgresql: diesel::pg::PgConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DbConnType {
|
||||||
|
pub fn from_url(url: &str) -> Result<DbConnType, Error> {
|
||||||
|
// Mysql
|
||||||
|
if url.starts_with("mysql:") {
|
||||||
|
#[cfg(mysql)]
|
||||||
|
return Ok(DbConnType::mysql);
|
||||||
|
|
||||||
|
#[cfg(not(mysql))]
|
||||||
|
err!("`DATABASE_URL` is a MySQL URL, but the 'mysql' feature is not enabled")
|
||||||
|
|
||||||
|
// Postgres
|
||||||
|
} else if url.starts_with("postgresql:") || url.starts_with("postgres:") {
|
||||||
|
#[cfg(postgresql)]
|
||||||
|
return Ok(DbConnType::postgresql);
|
||||||
|
|
||||||
|
#[cfg(not(postgresql))]
|
||||||
|
err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled")
|
||||||
|
|
||||||
|
//Sqlite
|
||||||
|
} else {
|
||||||
|
#[cfg(sqlite)]
|
||||||
|
return Ok(DbConnType::sqlite);
|
||||||
|
|
||||||
|
#[cfg(not(sqlite))]
|
||||||
|
err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! db_run {
|
||||||
|
// Same for all dbs
|
||||||
|
( $conn:ident: $body:block ) => {
|
||||||
|
db_run! { $conn: sqlite, mysql, postgresql $body }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Different code for each db
|
||||||
|
( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {
|
||||||
|
#[allow(unused)] use diesel::prelude::*;
|
||||||
|
match $conn {
|
||||||
|
$($(
|
||||||
|
#[cfg($db)]
|
||||||
|
crate::db::DbConn::$db(ref $conn) => {
|
||||||
|
paste::paste! {
|
||||||
|
#[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *};
|
||||||
|
#[allow(unused)] use [<__ $db _model>]::*;
|
||||||
|
#[allow(unused)] use crate::db::FromDb;
|
||||||
|
}
|
||||||
|
$body
|
||||||
|
},
|
||||||
|
)+)+
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub trait FromDb {
|
||||||
|
type Output;
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
fn from_db(self) -> Self::Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql),
|
||||||
|
// to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run!
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! db_object {
|
||||||
|
( $(
|
||||||
|
$( #[$attr:meta] )*
|
||||||
|
pub struct $name:ident {
|
||||||
|
$( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+
|
||||||
|
$(,)?
|
||||||
|
}
|
||||||
|
)+ ) => {
|
||||||
|
// Create the normal struct, without attributes
|
||||||
|
$( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+
|
||||||
|
|
||||||
|
#[cfg(sqlite)]
|
||||||
|
pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
|
||||||
|
#[cfg(mysql)]
|
||||||
|
pub mod __mysql_model { $( db_object! { @db mysql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
|
||||||
|
#[cfg(postgresql)]
|
||||||
|
pub mod __postgresql_model { $( db_object! { @db postgresql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
|
||||||
|
};
|
||||||
|
|
||||||
|
( @db $db:ident | $( #[$attr:meta] )* | $name:ident | $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty),+) => {
|
||||||
|
paste::paste! {
|
||||||
|
#[allow(unused)] use super::*;
|
||||||
|
#[allow(unused)] use diesel::prelude::*;
|
||||||
|
#[allow(unused)] use crate::db::[<__ $db _schema>]::*;
|
||||||
|
|
||||||
|
$( #[$attr] )*
|
||||||
|
pub struct [<$name Db>] { $(
|
||||||
|
$( #[$field_attr] )* $vis $field : $typ,
|
||||||
|
)+ }
|
||||||
|
|
||||||
|
impl [<$name Db>] {
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
#[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::db::FromDb for [<$name Db>] {
|
||||||
|
type Output = super::$name;
|
||||||
|
#[inline(always)] fn from_db(self) -> Self::Output { super::$name { $( $field: self.$field, )+ } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::db::FromDb for Vec<[<$name Db>]> {
|
||||||
|
type Output = Vec<super::$name>;
|
||||||
|
#[inline(always)] fn from_db(self) -> Self::Output { self.into_iter().map(crate::db::FromDb::from_db).collect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::db::FromDb for Option<[<$name Db>]> {
|
||||||
|
type Output = Option<super::$name>;
|
||||||
|
#[inline(always)] fn from_db(self) -> Self::Output { self.map(crate::db::FromDb::from_db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reexport the models, needs to be after the macros are defined so it can access them
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
/// Creates a back-up of the database using sqlite3
|
/// Creates a back-up of the database using sqlite3
|
||||||
pub fn backup_database() -> Result<(), Error> {
|
pub fn backup_database() -> Result<(), Error> {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -75,20 +227,104 @@ pub fn backup_database() -> Result<(), Error> {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
|
impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<DbConn, ()> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<DbConn, ()> {
|
||||||
// https://github.com/SergioBenitez/Rocket/commit/e3c1a4ad3ab9b840482ec6de4200d30df43e357c
|
// https://github.com/SergioBenitez/Rocket/commit/e3c1a4ad3ab9b840482ec6de4200d30df43e357c
|
||||||
let pool = try_outcome!(request.guard::<State<Pool>>());
|
let pool = try_outcome!(request.guard::<State<DbPool>>());
|
||||||
match pool.get() {
|
match pool.get() {
|
||||||
Ok(conn) => Outcome::Success(DbConn(conn)),
|
Ok(conn) => Outcome::Success(conn),
|
||||||
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
|
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the convenience of using an &DbConn as a &Database.
|
// Embed the migrations from the migrations folder into the application
|
||||||
impl Deref for DbConn {
|
// This way, the program automatically migrates the database to the latest version
|
||||||
type Target = Connection;
|
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
|
||||||
fn deref(&self) -> &Self::Target {
|
#[cfg(sqlite)]
|
||||||
&self.0
|
mod sqlite_migrations {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
embed_migrations!("migrations/sqlite");
|
||||||
|
|
||||||
|
pub fn run_migrations() -> Result<(), super::Error> {
|
||||||
|
// Make sure the directory exists
|
||||||
|
let url = crate::CONFIG.database_url();
|
||||||
|
let path = std::path::Path::new(&url);
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if std::fs::create_dir_all(parent).is_err() {
|
||||||
|
error!("Error creating database directory");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use diesel::{Connection, RunQueryDsl};
|
||||||
|
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||||
|
let connection =
|
||||||
|
diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
|
// Scoped to a connection.
|
||||||
|
diesel::sql_query("PRAGMA foreign_keys = OFF")
|
||||||
|
.execute(&connection)
|
||||||
|
.expect("Failed to disable Foreign Key Checks during migrations");
|
||||||
|
|
||||||
|
// Turn on WAL in SQLite
|
||||||
|
if crate::CONFIG.enable_db_wal() {
|
||||||
|
diesel::sql_query("PRAGMA journal_mode=wal")
|
||||||
|
.execute(&connection)
|
||||||
|
.expect("Failed to turn on WAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mysql)]
|
||||||
|
mod mysql_migrations {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
embed_migrations!("migrations/mysql");
|
||||||
|
|
||||||
|
pub fn run_migrations() -> Result<(), super::Error> {
|
||||||
|
use diesel::{Connection, RunQueryDsl};
|
||||||
|
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||||
|
let connection =
|
||||||
|
diesel::mysql::MysqlConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
|
// Scoped to a connection/session.
|
||||||
|
diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0")
|
||||||
|
.execute(&connection)
|
||||||
|
.expect("Failed to disable Foreign Key Checks during migrations");
|
||||||
|
|
||||||
|
embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(postgresql)]
|
||||||
|
mod postgresql_migrations {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
embed_migrations!("migrations/postgresql");
|
||||||
|
|
||||||
|
pub fn run_migrations() -> Result<(), super::Error> {
|
||||||
|
use diesel::{Connection, RunQueryDsl};
|
||||||
|
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||||
|
let connection =
|
||||||
|
diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
|
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
|
||||||
|
// "SET CONSTRAINTS sets the behavior of constraint checking within the
|
||||||
|
// current transaction", so this setting probably won't take effect for
|
||||||
|
// any of the migrations since it's being run outside of a transaction.
|
||||||
|
// Migrations that need to disable foreign key checks should run this
|
||||||
|
// from within the migration script itself.
|
||||||
|
diesel::sql_query("SET CONSTRAINTS ALL DEFERRED")
|
||||||
|
.execute(&connection)
|
||||||
|
.expect("Failed to disable Foreign Key Checks during migrations");
|
||||||
|
|
||||||
|
embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,24 @@ use serde_json::Value;
|
||||||
use super::Cipher;
|
use super::Cipher;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "attachments"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[table_name = "attachments"]
|
||||||
#[primary_key(id)]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
pub struct Attachment {
|
#[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]
|
||||||
pub id: String,
|
#[primary_key(id)]
|
||||||
pub cipher_uuid: String,
|
pub struct Attachment {
|
||||||
pub file_name: String,
|
pub id: String,
|
||||||
pub file_size: i32,
|
pub cipher_uuid: String,
|
||||||
pub akey: Option<String>,
|
pub file_name: String,
|
||||||
|
pub file_size: i32,
|
||||||
|
pub akey: Option<String>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl Attachment {
|
impl Attachment {
|
||||||
pub fn new(id: String, cipher_uuid: String, file_name: String, file_size: i32) -> Self {
|
pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
cipher_uuid,
|
cipher_uuid,
|
||||||
|
@ -49,44 +52,57 @@ impl Attachment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::{attachments, ciphers};
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Attachment {
|
impl Attachment {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
|
||||||
diesel::insert_into(attachments::table)
|
|
||||||
.values(self)
|
|
||||||
.on_conflict(attachments::id)
|
|
||||||
.do_update()
|
|
||||||
.set(self)
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_res("Error saving attachment")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "postgresql"))]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::replace_into(attachments::table)
|
db_run! { conn:
|
||||||
.values(self)
|
sqlite, mysql {
|
||||||
.execute(&**conn)
|
match diesel::replace_into(attachments::table)
|
||||||
.map_res("Error saving attachment")
|
.values(AttachmentDb::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(attachments::table)
|
||||||
|
.filter(attachments::id.eq(&self.id))
|
||||||
|
.set(AttachmentDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving attachment")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving attachment")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = AttachmentDb::to_db(self);
|
||||||
|
diesel::insert_into(attachments::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(attachments::id)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving attachment")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
crate::util::retry(
|
db_run! { conn: {
|
||||||
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(&**conn),
|
crate::util::retry(
|
||||||
10,
|
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn),
|
||||||
)
|
10,
|
||||||
.map_res("Error deleting attachment")?;
|
)
|
||||||
|
.map_res("Error deleting attachment")?;
|
||||||
|
|
||||||
crate::util::delete_file(&self.get_file_path())?;
|
crate::util::delete_file(&self.get_file_path())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -97,47 +113,78 @@ impl Attachment {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> {
|
||||||
let id = id.to_lowercase();
|
db_run! { conn: {
|
||||||
|
attachments::table
|
||||||
attachments::table
|
.filter(attachments::id.eq(id.to_lowercase()))
|
||||||
.filter(attachments::id.eq(id))
|
.first::<AttachmentDb>(conn)
|
||||||
.first::<Self>(&**conn)
|
.ok()
|
||||||
.ok()
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
attachments::table
|
db_run! { conn: {
|
||||||
.filter(attachments::cipher_uuid.eq(cipher_uuid))
|
attachments::table
|
||||||
.load::<Self>(&**conn)
|
.filter(attachments::cipher_uuid.eq(cipher_uuid))
|
||||||
.expect("Error loading attachments")
|
.load::<AttachmentDb>(conn)
|
||||||
|
.expect("Error loading attachments")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> {
|
||||||
attachments::table
|
db_run! { conn: {
|
||||||
.filter(attachments::cipher_uuid.eq_any(cipher_uuids))
|
attachments::table
|
||||||
.load::<Self>(&**conn)
|
.filter(attachments::cipher_uuid.eq_any(cipher_uuids))
|
||||||
.expect("Error loading attachments")
|
.load::<AttachmentDb>(conn)
|
||||||
|
.expect("Error loading attachments")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
|
pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
let result: Option<i64> = attachments::table
|
db_run! { conn: {
|
||||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
let result: Option<i64> = attachments::table
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid))
|
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||||
.select(diesel::dsl::sum(attachments::file_size))
|
.filter(ciphers::user_uuid.eq(user_uuid))
|
||||||
.first(&**conn)
|
.select(diesel::dsl::sum(attachments::file_size))
|
||||||
.expect("Error loading user attachment total size");
|
.first(conn)
|
||||||
|
.expect("Error loading user attachment total size");
|
||||||
|
result.unwrap_or(0)
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
result.unwrap_or(0)
|
pub fn count_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
|
db_run! { conn: {
|
||||||
|
attachments::table
|
||||||
|
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||||
|
.filter(ciphers::user_uuid.eq(user_uuid))
|
||||||
|
.count()
|
||||||
|
.first(conn)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
|
pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
let result: Option<i64> = attachments::table
|
db_run! { conn: {
|
||||||
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
let result: Option<i64> = attachments::table
|
||||||
.filter(ciphers::organization_uuid.eq(org_uuid))
|
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||||
.select(diesel::dsl::sum(attachments::file_size))
|
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||||
.first(&**conn)
|
.select(diesel::dsl::sum(attachments::file_size))
|
||||||
.expect("Error loading user attachment total size");
|
.first(conn)
|
||||||
|
.expect("Error loading user attachment total size");
|
||||||
|
result.unwrap_or(0)
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
result.unwrap_or(0)
|
pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
|
db_run! { conn: {
|
||||||
|
attachments::table
|
||||||
|
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
|
||||||
|
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||||
|
.count()
|
||||||
|
.first(conn)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,37 +2,48 @@ use chrono::{NaiveDateTime, Utc};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization,
|
Attachment,
|
||||||
|
CollectionCipher,
|
||||||
|
Favorite,
|
||||||
|
FolderCipher,
|
||||||
|
Organization,
|
||||||
|
User,
|
||||||
|
UserOrgStatus,
|
||||||
|
UserOrgType,
|
||||||
|
UserOrganization,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "ciphers"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[table_name = "ciphers"]
|
||||||
#[belongs_to(Organization, foreign_key = "organization_uuid")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[primary_key(uuid)]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
pub struct Cipher {
|
#[belongs_to(Organization, foreign_key = "organization_uuid")]
|
||||||
pub uuid: String,
|
#[primary_key(uuid)]
|
||||||
pub created_at: NaiveDateTime,
|
pub struct Cipher {
|
||||||
pub updated_at: NaiveDateTime,
|
pub uuid: String,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: NaiveDateTime,
|
||||||
|
|
||||||
pub user_uuid: Option<String>,
|
pub user_uuid: Option<String>,
|
||||||
pub organization_uuid: Option<String>,
|
pub organization_uuid: Option<String>,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Login = 1,
|
Login = 1,
|
||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4
|
Identity = 4
|
||||||
*/
|
*/
|
||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
pub fields: Option<String>,
|
pub fields: Option<String>,
|
||||||
|
|
||||||
pub data: String,
|
pub data: String,
|
||||||
|
|
||||||
pub favorite: bool,
|
pub password_history: Option<String>,
|
||||||
pub password_history: Option<String>,
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
|
@ -49,7 +60,6 @@ impl Cipher {
|
||||||
organization_uuid: None,
|
organization_uuid: None,
|
||||||
|
|
||||||
atype,
|
atype,
|
||||||
favorite: false,
|
|
||||||
name,
|
name,
|
||||||
|
|
||||||
notes: None,
|
notes: None,
|
||||||
|
@ -57,14 +67,12 @@ impl Cipher {
|
||||||
|
|
||||||
data: String::new(),
|
data: String::new(),
|
||||||
password_history: None,
|
password_history: None,
|
||||||
|
deleted_at: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::*;
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
@ -80,7 +88,28 @@ impl Cipher {
|
||||||
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 mut data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
|
let (read_only, hide_passwords) =
|
||||||
|
match self.get_access_restrictions(&user_uuid, conn) {
|
||||||
|
Some((ro, hp)) => (ro, hp),
|
||||||
|
None => {
|
||||||
|
error!("Cipher ownership assertion failure");
|
||||||
|
(true, true)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the data or a default empty value to avoid issues with the mobile apps
|
||||||
|
let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({
|
||||||
|
"Fields":null,
|
||||||
|
"Name": self.name,
|
||||||
|
"Notes":null,
|
||||||
|
"Password":null,
|
||||||
|
"PasswordHistory":null,
|
||||||
|
"PasswordRevisionDate":null,
|
||||||
|
"Response":null,
|
||||||
|
"Totp":null,
|
||||||
|
"Uris":null,
|
||||||
|
"Username":null
|
||||||
|
}));
|
||||||
|
|
||||||
// TODO: ******* Backwards compat start **********
|
// TODO: ******* Backwards compat start **********
|
||||||
// To remove backwards compatibility, just remove this entire section
|
// To remove backwards compatibility, just remove this entire section
|
||||||
|
@ -91,16 +120,27 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
// TODO: ******* Backwards compat end **********
|
// TODO: ******* Backwards compat end **********
|
||||||
|
|
||||||
|
// There are three types of cipher response models in upstream
|
||||||
|
// Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order
|
||||||
|
// of increasing level of detail). bitwarden_rs currently only
|
||||||
|
// supports the "cipherDetails" type, though it seems like the
|
||||||
|
// Bitwarden clients will ignore extra fields.
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs
|
||||||
let mut json_object = json!({
|
let mut json_object = json!({
|
||||||
|
"Object": "cipherDetails",
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
"RevisionDate": format_date(&self.updated_at),
|
"RevisionDate": format_date(&self.updated_at),
|
||||||
"FolderId": self.get_folder_uuid(&user_uuid, &conn),
|
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
||||||
"Favorite": self.favorite,
|
"FolderId": self.get_folder_uuid(&user_uuid, conn),
|
||||||
|
"Favorite": self.is_favorite(&user_uuid, conn),
|
||||||
"OrganizationId": self.organization_uuid,
|
"OrganizationId": self.organization_uuid,
|
||||||
"Attachments": attachments_json,
|
"Attachments": attachments_json,
|
||||||
"OrganizationUseTotp": true,
|
"OrganizationUseTotp": true,
|
||||||
"CollectionIds": self.get_collections(user_uuid, &conn),
|
|
||||||
|
// This field is specific to the cipherDetails type.
|
||||||
|
"CollectionIds": self.get_collections(user_uuid, conn),
|
||||||
|
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"Notes": self.notes,
|
"Notes": self.notes,
|
||||||
|
@ -108,8 +148,11 @@ impl Cipher {
|
||||||
|
|
||||||
"Data": data_json,
|
"Data": data_json,
|
||||||
|
|
||||||
"Object": "cipher",
|
// These values are true by default, but can be false if the
|
||||||
"Edit": true,
|
// cipher belongs to a collection where the org owner has enabled
|
||||||
|
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
||||||
|
"Edit": !read_only,
|
||||||
|
"ViewPassword": !hide_passwords,
|
||||||
|
|
||||||
"PasswordHistory": password_history_json,
|
"PasswordHistory": password_history_json,
|
||||||
});
|
});
|
||||||
|
@ -148,41 +191,54 @@ impl Cipher {
|
||||||
user_uuids
|
user_uuids
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
self.update_users_revision(conn);
|
self.update_users_revision(conn);
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
diesel::insert_into(ciphers::table)
|
db_run! { conn:
|
||||||
.values(&*self)
|
sqlite, mysql {
|
||||||
.on_conflict(ciphers::uuid)
|
match diesel::replace_into(ciphers::table)
|
||||||
.do_update()
|
.values(CipherDb::to_db(self))
|
||||||
.set(&*self)
|
.execute(conn)
|
||||||
.execute(&**conn)
|
{
|
||||||
.map_res("Error saving cipher")
|
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, _)) => {
|
||||||
#[cfg(not(feature = "postgresql"))]
|
diesel::update(ciphers::table)
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
self.update_users_revision(conn);
|
.set(CipherDb::to_db(self))
|
||||||
self.updated_at = Utc::now().naive_utc();
|
.execute(conn)
|
||||||
|
.map_res("Error saving cipher")
|
||||||
diesel::replace_into(ciphers::table)
|
}
|
||||||
.values(&*self)
|
Err(e) => Err(e.into()),
|
||||||
.execute(&**conn)
|
}.map_res("Error saving cipher")
|
||||||
.map_res("Error saving cipher")
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = CipherDb::to_db(self);
|
||||||
|
diesel::insert_into(ciphers::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(ciphers::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving cipher")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||||
self.update_users_revision(conn);
|
self.update_users_revision(conn);
|
||||||
|
|
||||||
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
FolderCipher::delete_all_by_cipher(&self.uuid, conn)?;
|
||||||
CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?;
|
CollectionCipher::delete_all_by_cipher(&self.uuid, conn)?;
|
||||||
Attachment::delete_all_by_cipher(&self.uuid, &conn)?;
|
Attachment::delete_all_by_cipher(&self.uuid, conn)?;
|
||||||
|
Favorite::delete_all_by_cipher(&self.uuid, conn)?;
|
||||||
|
|
||||||
diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid)))
|
||||||
.map_res("Error deleting cipher")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting cipher")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -200,181 +256,266 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
match (self.get_folder_uuid(&user_uuid, &conn), folder_uuid) {
|
match (self.get_folder_uuid(&user_uuid, conn), folder_uuid) {
|
||||||
// No changes
|
// No changes
|
||||||
(None, None) => Ok(()),
|
(None, None) => Ok(()),
|
||||||
(Some(ref old), Some(ref new)) if old == new => Ok(()),
|
(Some(ref old), Some(ref new)) if old == new => Ok(()),
|
||||||
|
|
||||||
// Add to folder
|
// Add to folder
|
||||||
(None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(&conn),
|
(None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn),
|
||||||
|
|
||||||
// Remove from folder
|
// Remove from folder
|
||||||
(Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) {
|
(Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn) {
|
||||||
Some(old) => old.delete(&conn),
|
Some(old) => old.delete(conn),
|
||||||
None => err!("Couldn't move from previous folder"),
|
None => err!("Couldn't move from previous folder"),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Move to another folder
|
// Move to another folder
|
||||||
(Some(old), Some(new)) => {
|
(Some(old), Some(new)) => {
|
||||||
if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) {
|
if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn) {
|
||||||
old.delete(&conn)?;
|
old.delete(conn)?;
|
||||||
}
|
}
|
||||||
FolderCipher::new(&new, &self.uuid).save(&conn)
|
FolderCipher::new(&new, &self.uuid).save(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether this cipher is directly owned by the user.
|
||||||
|
pub fn is_owned_by_user(&self, user_uuid: &str) -> bool {
|
||||||
|
self.user_uuid.is_some() && self.user_uuid.as_ref().unwrap() == user_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this cipher is owned by an org in which the user has full access.
|
||||||
|
pub fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
|
if let Some(ref org_uuid) = self.organization_uuid {
|
||||||
|
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user_uuid, &org_uuid, conn) {
|
||||||
|
return user_org.has_full_access();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the user's access restrictions to this cipher. A return value
|
||||||
|
/// of None means that this cipher does not belong to the user, and is
|
||||||
|
/// not in any collection the user has access to. Otherwise, the user has
|
||||||
|
/// access to this cipher, and Some(read_only, hide_passwords) represents
|
||||||
|
/// the access restrictions.
|
||||||
|
pub fn get_access_restrictions(&self, user_uuid: &str, conn: &DbConn) -> Option<(bool, bool)> {
|
||||||
|
// Check whether this cipher is directly owned by the user, or is in
|
||||||
|
// a collection that the user has full access to. If so, there are no
|
||||||
|
// access restrictions.
|
||||||
|
if self.is_owned_by_user(&user_uuid) || self.is_in_full_access_org(&user_uuid, &conn) {
|
||||||
|
return Some((false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
db_run! {conn: {
|
||||||
|
// Check whether this cipher is in any collections accessible to the
|
||||||
|
// user. If so, retrieve the access flags for each collection.
|
||||||
|
let query = ciphers::table
|
||||||
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
|
.inner_join(ciphers_collections::table.on(
|
||||||
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
|
||||||
|
.inner_join(users_collections::table.on(
|
||||||
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||||
|
.and(users_collections::user_uuid.eq(user_uuid))))
|
||||||
|
.select((users_collections::read_only, users_collections::hide_passwords));
|
||||||
|
|
||||||
|
// There's an edge case where a cipher can be in multiple collections
|
||||||
|
// with inconsistent access flags. For example, a cipher could be in
|
||||||
|
// one collection where the user has read-only access, but also in
|
||||||
|
// another collection where the user has read/write access. To handle
|
||||||
|
// this, we do a boolean OR of all values in each of the `read_only`
|
||||||
|
// and `hide_passwords` columns. This could ideally be done as part
|
||||||
|
// of the query, but Diesel doesn't support a max() or bool_or()
|
||||||
|
// function on booleans and this behavior isn't portable anyway.
|
||||||
|
if let Ok(vec) = query.load::<(bool, bool)>(conn) {
|
||||||
|
let mut read_only = false;
|
||||||
|
let mut hide_passwords = false;
|
||||||
|
for (ro, hp) in vec.iter() {
|
||||||
|
read_only |= ro;
|
||||||
|
hide_passwords |= hp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((read_only, hide_passwords))
|
||||||
|
} else {
|
||||||
|
// This cipher isn't in any collections accessible to the user.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
ciphers::table
|
match self.get_access_restrictions(&user_uuid, &conn) {
|
||||||
.filter(ciphers::uuid.eq(&self.uuid))
|
Some((read_only, _hide_passwords)) => !read_only,
|
||||||
.left_join(
|
None => false,
|
||||||
users_organizations::table.on(ciphers::organization_uuid
|
}
|
||||||
.eq(users_organizations::org_uuid.nullable())
|
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
|
||||||
)
|
|
||||||
.left_join(ciphers_collections::table)
|
|
||||||
.left_join(
|
|
||||||
users_collections::table
|
|
||||||
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
|
|
||||||
)
|
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid).or(
|
|
||||||
// Cipher owner
|
|
||||||
users_organizations::access_all.eq(true).or(
|
|
||||||
// access_all in Organization
|
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32).or(
|
|
||||||
// Org admin or owner
|
|
||||||
users_collections::user_uuid.eq(user_uuid).and(
|
|
||||||
users_collections::read_only.eq(false), //R/W access to collection
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.select(ciphers::all_columns)
|
|
||||||
.first::<Self>(&**conn)
|
|
||||||
.ok()
|
|
||||||
.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
ciphers::table
|
self.get_access_restrictions(&user_uuid, &conn).is_some()
|
||||||
.filter(ciphers::uuid.eq(&self.uuid))
|
}
|
||||||
.left_join(
|
|
||||||
users_organizations::table.on(ciphers::organization_uuid
|
// Returns whether this cipher is a favorite of the specified user.
|
||||||
.eq(users_organizations::org_uuid.nullable())
|
pub fn is_favorite(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
Favorite::is_favorite(&self.uuid, user_uuid, conn)
|
||||||
)
|
}
|
||||||
.left_join(ciphers_collections::table)
|
|
||||||
.left_join(
|
// Sets whether this cipher is a favorite of the specified user.
|
||||||
users_collections::table
|
pub fn set_favorite(&self, favorite: Option<bool>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
|
match favorite {
|
||||||
)
|
None => Ok(()), // No change requested.
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid).or(
|
Some(status) => Favorite::set_favorite(status, &self.uuid, user_uuid, conn),
|
||||||
// Cipher owner
|
}
|
||||||
users_organizations::access_all.eq(true).or(
|
|
||||||
// access_all in Organization
|
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32).or(
|
|
||||||
// Org admin or owner
|
|
||||||
users_collections::user_uuid.eq(user_uuid), // Access to Collection
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.select(ciphers::all_columns)
|
|
||||||
.first::<Self>(&**conn)
|
|
||||||
.ok()
|
|
||||||
.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
||||||
folders_ciphers::table
|
db_run! {conn: {
|
||||||
.inner_join(folders::table)
|
folders_ciphers::table
|
||||||
.filter(folders::user_uuid.eq(&user_uuid))
|
.inner_join(folders::table)
|
||||||
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
|
.filter(folders::user_uuid.eq(&user_uuid))
|
||||||
.select(folders_ciphers::folder_uuid)
|
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
|
||||||
.first::<String>(&**conn)
|
.select(folders_ciphers::folder_uuid)
|
||||||
.ok()
|
.first::<String>(conn)
|
||||||
|
.ok()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
ciphers::table
|
db_run! {conn: {
|
||||||
.filter(ciphers::uuid.eq(uuid))
|
ciphers::table
|
||||||
.first::<Self>(&**conn)
|
.filter(ciphers::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<CipherDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all ciphers accessible to user
|
// Find all ciphers accessible or visible to the specified user.
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
//
|
||||||
ciphers::table
|
// "Accessible" means the user has read access to the cipher, either via
|
||||||
.left_join(users_organizations::table.on(
|
// direct ownership or via collection access.
|
||||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
//
|
||||||
users_organizations::user_uuid.eq(user_uuid).and(
|
// "Visible" usually means the same as accessible, except when an org
|
||||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
// owner/admin sets their account to have access to only selected
|
||||||
)
|
// collections in the org (presumably because they aren't interested in
|
||||||
)
|
// the other collections in the org). In this case, if `visible_only` is
|
||||||
))
|
// true, then the non-interesting ciphers will not be returned. As a
|
||||||
.left_join(ciphers_collections::table.on(
|
// result, those ciphers will not appear in "My Vault" for the org
|
||||||
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
|
// owner/admin, but they can still be accessed via the org vault view.
|
||||||
))
|
pub fn find_by_user(user_uuid: &str, visible_only: bool, conn: &DbConn) -> Vec<Self> {
|
||||||
.left_join(users_collections::table.on(
|
db_run! {conn: {
|
||||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
let mut query = ciphers::table
|
||||||
))
|
.left_join(ciphers_collections::table.on(
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
|
||||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
))
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32).or( // Org admin or owner
|
.left_join(users_organizations::table.on(
|
||||||
users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
|
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
|
||||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
.and(users_organizations::user_uuid.eq(user_uuid))
|
||||||
)
|
.and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
||||||
)
|
))
|
||||||
)
|
.left_join(users_collections::table.on(
|
||||||
))
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||||
.select(ciphers::all_columns)
|
// Ensure that users_collections::user_uuid is NULL for unconfirmed users.
|
||||||
.distinct()
|
.and(users_organizations::user_uuid.eq(users_collections::user_uuid))
|
||||||
.load::<Self>(&**conn).expect("Error loading ciphers")
|
))
|
||||||
|
.filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner
|
||||||
|
.or_filter(users_organizations::access_all.eq(true)) // access_all in org
|
||||||
|
.or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection
|
||||||
|
.into_boxed();
|
||||||
|
|
||||||
|
if !visible_only {
|
||||||
|
query = query.or_filter(
|
||||||
|
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
query
|
||||||
|
.select(ciphers::all_columns)
|
||||||
|
.distinct()
|
||||||
|
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all ciphers directly owned by user
|
// Find all ciphers visible to the specified user.
|
||||||
|
pub fn find_by_user_visible(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
|
Self::find_by_user(user_uuid, true, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all ciphers directly owned by the specified user.
|
||||||
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> {
|
||||||
ciphers::table
|
db_run! {conn: {
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid))
|
ciphers::table
|
||||||
.load::<Self>(&**conn).expect("Error loading ciphers")
|
.filter(ciphers::user_uuid.eq(user_uuid))
|
||||||
|
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
|
db_run! {conn: {
|
||||||
|
ciphers::table
|
||||||
|
.filter(ciphers::user_uuid.eq(user_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
ciphers::table
|
db_run! {conn: {
|
||||||
.filter(ciphers::organization_uuid.eq(org_uuid))
|
ciphers::table
|
||||||
.load::<Self>(&**conn).expect("Error loading ciphers")
|
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||||
|
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
|
db_run! {conn: {
|
||||||
|
ciphers::table
|
||||||
|
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
folders_ciphers::table.inner_join(ciphers::table)
|
db_run! {conn: {
|
||||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
folders_ciphers::table.inner_join(ciphers::table)
|
||||||
.select(ciphers::all_columns)
|
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||||
.load::<Self>(&**conn).expect("Error loading ciphers")
|
.select(ciphers::all_columns)
|
||||||
|
.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> {
|
||||||
ciphers_collections::table
|
db_run! {conn: {
|
||||||
.inner_join(collections::table.on(
|
ciphers_collections::table
|
||||||
collections::uuid.eq(ciphers_collections::collection_uuid)
|
.inner_join(collections::table.on(
|
||||||
))
|
collections::uuid.eq(ciphers_collections::collection_uuid)
|
||||||
.inner_join(users_organizations::table.on(
|
))
|
||||||
users_organizations::org_uuid.eq(collections::org_uuid).and(
|
.inner_join(users_organizations::table.on(
|
||||||
users_organizations::user_uuid.eq(user_id)
|
users_organizations::org_uuid.eq(collections::org_uuid).and(
|
||||||
)
|
users_organizations::user_uuid.eq(user_id)
|
||||||
))
|
)
|
||||||
.left_join(users_collections::table.on(
|
))
|
||||||
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
|
.left_join(users_collections::table.on(
|
||||||
users_collections::user_uuid.eq(user_id)
|
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
|
||||||
)
|
users_collections::user_uuid.eq(user_id)
|
||||||
))
|
)
|
||||||
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
|
))
|
||||||
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection
|
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
|
||||||
users_organizations::access_all.eq(true).or( // User has access all
|
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner
|
users_organizations::access_all.eq(true).or( // User has access all
|
||||||
)
|
users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner
|
||||||
))
|
)
|
||||||
.select(ciphers_collections::collection_uuid)
|
))
|
||||||
.load::<String>(&**conn).unwrap_or_default()
|
.select(ciphers_collections::collection_uuid)
|
||||||
|
.load::<String>(conn).unwrap_or_default()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,39 @@
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher};
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "collections"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
#[table_name = "collections"]
|
||||||
#[primary_key(uuid)]
|
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
||||||
pub struct Collection {
|
#[primary_key(uuid)]
|
||||||
pub uuid: String,
|
pub struct Collection {
|
||||||
pub org_uuid: String,
|
pub uuid: String,
|
||||||
pub name: String,
|
pub org_uuid: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||||
|
#[table_name = "users_collections"]
|
||||||
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
|
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
||||||
|
#[primary_key(user_uuid, collection_uuid)]
|
||||||
|
pub struct CollectionUser {
|
||||||
|
pub user_uuid: String,
|
||||||
|
pub collection_uuid: String,
|
||||||
|
pub read_only: bool,
|
||||||
|
pub hide_passwords: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||||
|
#[table_name = "ciphers_collections"]
|
||||||
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
|
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
||||||
|
#[primary_key(cipher_uuid, collection_uuid)]
|
||||||
|
pub struct CollectionCipher {
|
||||||
|
pub cipher_uuid: String,
|
||||||
|
pub collection_uuid: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
|
@ -33,37 +57,45 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::*;
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Collection {
|
impl Collection {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
self.update_users_revision(conn);
|
self.update_users_revision(conn);
|
||||||
|
|
||||||
diesel::insert_into(collections::table)
|
db_run! { conn:
|
||||||
.values(self)
|
sqlite, mysql {
|
||||||
.on_conflict(collections::uuid)
|
match diesel::replace_into(collections::table)
|
||||||
.do_update()
|
.values(CollectionDb::to_db(self))
|
||||||
.set(self)
|
.execute(conn)
|
||||||
.execute(&**conn)
|
{
|
||||||
.map_res("Error saving collection")
|
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, _)) => {
|
||||||
#[cfg(not(feature = "postgresql"))]
|
diesel::update(collections::table)
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
.filter(collections::uuid.eq(&self.uuid))
|
||||||
self.update_users_revision(conn);
|
.set(CollectionDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
diesel::replace_into(collections::table)
|
.map_res("Error saving collection")
|
||||||
.values(self)
|
}
|
||||||
.execute(&**conn)
|
Err(e) => Err(e.into()),
|
||||||
.map_res("Error saving collection")
|
}.map_res("Error saving collection")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = CollectionDb::to_db(self);
|
||||||
|
diesel::insert_into(collections::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(collections::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving collection")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -71,9 +103,11 @@ impl Collection {
|
||||||
CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?;
|
CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?;
|
||||||
CollectionUser::delete_all_by_collection(&self.uuid, &conn)?;
|
CollectionUser::delete_all_by_collection(&self.uuid, &conn)?;
|
||||||
|
|
||||||
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
|
||||||
.map_res("Error deleting collection")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting collection")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -92,33 +126,38 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
collections::table
|
db_run! { conn: {
|
||||||
.filter(collections::uuid.eq(uuid))
|
collections::table
|
||||||
.first::<Self>(&**conn)
|
.filter(collections::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<CollectionDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
collections::table
|
db_run! { conn: {
|
||||||
.left_join(users_collections::table.on(
|
collections::table
|
||||||
users_collections::collection_uuid.eq(collections::uuid).and(
|
.left_join(users_collections::table.on(
|
||||||
users_collections::user_uuid.eq(user_uuid)
|
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||||
|
users_collections::user_uuid.eq(user_uuid)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.left_join(users_organizations::table.on(
|
||||||
|
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||||
|
users_organizations::user_uuid.eq(user_uuid)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.filter(
|
||||||
|
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||||
)
|
)
|
||||||
))
|
.filter(
|
||||||
.left_join(users_organizations::table.on(
|
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
|
||||||
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
users_organizations::access_all.eq(true) // access_all in Organization
|
||||||
users_organizations::user_uuid.eq(user_uuid)
|
)
|
||||||
)
|
).select(collections::all_columns)
|
||||||
))
|
.load::<CollectionDb>(conn).expect("Error loading collections").from_db()
|
||||||
.filter(
|
}}
|
||||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
|
|
||||||
users_organizations::access_all.eq(true) // access_all in Organization
|
|
||||||
)
|
|
||||||
).select(collections::all_columns)
|
|
||||||
.load::<Self>(&**conn).expect("Error loading collections")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
|
@ -129,149 +168,178 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
collections::table
|
db_run! { conn: {
|
||||||
.filter(collections::org_uuid.eq(org_uuid))
|
collections::table
|
||||||
.load::<Self>(&**conn)
|
.filter(collections::org_uuid.eq(org_uuid))
|
||||||
.expect("Error loading collections")
|
.load::<CollectionDb>(conn)
|
||||||
|
.expect("Error loading collections")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
collections::table
|
db_run! { conn: {
|
||||||
.filter(collections::uuid.eq(uuid))
|
collections::table
|
||||||
.filter(collections::org_uuid.eq(org_uuid))
|
.filter(collections::uuid.eq(uuid))
|
||||||
.select(collections::all_columns)
|
.filter(collections::org_uuid.eq(org_uuid))
|
||||||
.first::<Self>(&**conn)
|
.select(collections::all_columns)
|
||||||
.ok()
|
.first::<CollectionDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
collections::table
|
db_run! { conn: {
|
||||||
.left_join(users_collections::table.on(
|
collections::table
|
||||||
users_collections::collection_uuid.eq(collections::uuid).and(
|
.left_join(users_collections::table.on(
|
||||||
users_collections::user_uuid.eq(user_uuid)
|
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||||
)
|
users_collections::user_uuid.eq(user_uuid)
|
||||||
))
|
|
||||||
.left_join(users_organizations::table.on(
|
|
||||||
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
|
||||||
users_organizations::user_uuid.eq(user_uuid)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
.filter(collections::uuid.eq(uuid))
|
|
||||||
.filter(
|
|
||||||
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
|
|
||||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
).select(collections::all_columns)
|
.left_join(users_organizations::table.on(
|
||||||
.first::<Self>(&**conn).ok()
|
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||||
|
users_organizations::user_uuid.eq(user_uuid)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.filter(collections::uuid.eq(uuid))
|
||||||
|
.filter(
|
||||||
|
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
|
||||||
|
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||||
|
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).select(collections::all_columns)
|
||||||
|
.first::<CollectionDb>(conn).ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
pub fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
match UserOrganization::find_by_user_and_org(&user_uuid, &self.org_uuid, &conn) {
|
match UserOrganization::find_by_user_and_org(&user_uuid, &self.org_uuid, &conn) {
|
||||||
None => false, // Not in Org
|
None => false, // Not in Org
|
||||||
Some(user_org) => {
|
Some(user_org) => {
|
||||||
if user_org.access_all {
|
if user_org.has_full_access() {
|
||||||
true
|
return true;
|
||||||
} else {
|
|
||||||
users_collections::table
|
|
||||||
.inner_join(collections::table)
|
|
||||||
.filter(users_collections::collection_uuid.eq(&self.uuid))
|
|
||||||
.filter(users_collections::user_uuid.eq(&user_uuid))
|
|
||||||
.filter(users_collections::read_only.eq(false))
|
|
||||||
.select(collections::all_columns)
|
|
||||||
.first::<Self>(&**conn)
|
|
||||||
.ok()
|
|
||||||
.is_some() // Read only or no access to collection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::read_only.eq(false))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0) != 0
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::User;
|
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
|
||||||
#[table_name = "users_collections"]
|
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
|
||||||
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
|
||||||
#[primary_key(user_uuid, collection_uuid)]
|
|
||||||
pub struct CollectionUser {
|
|
||||||
pub user_uuid: String,
|
|
||||||
pub collection_uuid: String,
|
|
||||||
pub read_only: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl CollectionUser {
|
impl CollectionUser {
|
||||||
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> {
|
||||||
users_collections::table
|
db_run! { conn: {
|
||||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
users_collections::table
|
||||||
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||||
.filter(collections::org_uuid.eq(org_uuid))
|
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
||||||
.select(users_collections::all_columns)
|
.filter(collections::org_uuid.eq(org_uuid))
|
||||||
.load::<Self>(&**conn)
|
.select(users_collections::all_columns)
|
||||||
.expect("Error loading users_collections")
|
.load::<CollectionUserDb>(conn)
|
||||||
|
.expect("Error loading users_collections")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgresql")]
|
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, conn: &DbConn) -> EmptyResult {
|
|
||||||
User::update_uuid_revision(&user_uuid, conn);
|
User::update_uuid_revision(&user_uuid, conn);
|
||||||
|
|
||||||
diesel::insert_into(users_collections::table)
|
db_run! { conn:
|
||||||
.values((
|
sqlite, mysql {
|
||||||
users_collections::user_uuid.eq(user_uuid),
|
match diesel::replace_into(users_collections::table)
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
.values((
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::user_uuid.eq(user_uuid),
|
||||||
))
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
users_collections::read_only.eq(read_only),
|
||||||
.do_update()
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
.set(users_collections::read_only.eq(read_only))
|
))
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to collection")
|
{
|
||||||
}
|
Ok(_) => Ok(()),
|
||||||
|
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
||||||
#[cfg(not(feature = "postgresql"))]
|
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
||||||
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
|
diesel::update(users_collections::table)
|
||||||
User::update_uuid_revision(&user_uuid, conn);
|
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||||
diesel::replace_into(users_collections::table)
|
.set((
|
||||||
.values((
|
users_collections::user_uuid.eq(user_uuid),
|
||||||
users_collections::user_uuid.eq(user_uuid),
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
users_collections::read_only.eq(read_only),
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
))
|
))
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to collection")
|
.map_res("Error adding user to collection")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error adding user to collection")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
diesel::insert_into(users_collections::table)
|
||||||
|
.values((
|
||||||
|
users_collections::user_uuid.eq(user_uuid),
|
||||||
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
|
users_collections::read_only.eq(read_only),
|
||||||
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
|
))
|
||||||
|
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
||||||
|
.do_update()
|
||||||
|
.set((
|
||||||
|
users_collections::read_only.eq(read_only),
|
||||||
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding user to collection")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
diesel::delete(
|
db_run! { conn: {
|
||||||
users_collections::table
|
diesel::delete(
|
||||||
.filter(users_collections::user_uuid.eq(&self.user_uuid))
|
users_collections::table
|
||||||
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)),
|
.filter(users_collections::user_uuid.eq(&self.user_uuid))
|
||||||
)
|
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)),
|
||||||
.execute(&**conn)
|
)
|
||||||
.map_res("Error removing user from collection")
|
.execute(conn)
|
||||||
|
.map_res("Error removing user from collection")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_collections::table
|
db_run! { conn: {
|
||||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
users_collections::table
|
||||||
.select(users_collections::all_columns)
|
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||||
.load::<Self>(&**conn)
|
.select(users_collections::all_columns)
|
||||||
.expect("Error loading users_collections")
|
.load::<CollectionUserDb>(conn)
|
||||||
|
.expect("Error loading users_collections")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
users_collections::table
|
db_run! { conn: {
|
||||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
users_collections::table
|
||||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||||
.select(users_collections::all_columns)
|
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||||
.first::<Self>(&**conn)
|
.select(users_collections::all_columns)
|
||||||
.ok()
|
.first::<CollectionUserDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -281,81 +349,91 @@ impl CollectionUser {
|
||||||
User::update_uuid_revision(&collection.user_uuid, conn);
|
User::update_uuid_revision(&collection.user_uuid, conn);
|
||||||
});
|
});
|
||||||
|
|
||||||
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
|
||||||
.map_res("Error deleting users from collection")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting users from collection")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
for user in collectionusers {
|
||||||
.map_res("Error removing user from collections")
|
diesel::delete(users_collections::table.filter(
|
||||||
|
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(())
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::Cipher;
|
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
|
||||||
#[table_name = "ciphers_collections"]
|
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
|
||||||
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
|
||||||
#[primary_key(cipher_uuid, collection_uuid)]
|
|
||||||
pub struct CollectionCipher {
|
|
||||||
pub cipher_uuid: String,
|
|
||||||
pub collection_uuid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl CollectionCipher {
|
impl CollectionCipher {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
Self::update_users_revision(&collection_uuid, conn);
|
Self::update_users_revision(&collection_uuid, conn);
|
||||||
diesel::insert_into(ciphers_collections::table)
|
|
||||||
.values((
|
|
||||||
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
|
||||||
ciphers_collections::collection_uuid.eq(collection_uuid),
|
|
||||||
))
|
|
||||||
.on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))
|
|
||||||
.do_nothing()
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_res("Error adding cipher to collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "postgresql"))]
|
db_run! { conn:
|
||||||
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
sqlite, mysql {
|
||||||
Self::update_users_revision(&collection_uuid, conn);
|
// Not checking for ForeignKey Constraints here.
|
||||||
diesel::replace_into(ciphers_collections::table)
|
// Table ciphers_collections does not have ForeignKey Constraints which would cause conflicts.
|
||||||
.values((
|
// This table has no constraints pointing to itself, but only to others.
|
||||||
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
diesel::replace_into(ciphers_collections::table)
|
||||||
ciphers_collections::collection_uuid.eq(collection_uuid),
|
.values((
|
||||||
))
|
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
||||||
.execute(&**conn)
|
ciphers_collections::collection_uuid.eq(collection_uuid),
|
||||||
.map_res("Error adding cipher to collection")
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding cipher to collection")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
diesel::insert_into(ciphers_collections::table)
|
||||||
|
.values((
|
||||||
|
ciphers_collections::cipher_uuid.eq(cipher_uuid),
|
||||||
|
ciphers_collections::collection_uuid.eq(collection_uuid),
|
||||||
|
))
|
||||||
|
.on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))
|
||||||
|
.do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding cipher to collection")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
Self::update_users_revision(&collection_uuid, conn);
|
Self::update_users_revision(&collection_uuid, conn);
|
||||||
diesel::delete(
|
|
||||||
ciphers_collections::table
|
db_run! { conn: {
|
||||||
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
diesel::delete(
|
||||||
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)),
|
ciphers_collections::table
|
||||||
)
|
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
|
||||||
.execute(&**conn)
|
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)),
|
||||||
.map_res("Error deleting cipher from collection")
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting cipher from collection")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
|
||||||
.map_res("Error removing cipher from collections")
|
.execute(conn)
|
||||||
|
.map_res("Error removing cipher from collections")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
|
||||||
.map_res("Error removing ciphers from collection")
|
.execute(conn)
|
||||||
|
.map_res("Error removing ciphers from collection")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_users_revision(collection_uuid: &str, conn: &DbConn) {
|
pub fn update_users_revision(collection_uuid: &str, conn: &DbConn) {
|
||||||
|
|
|
@ -3,25 +3,28 @@ use chrono::{NaiveDateTime, Utc};
|
||||||
use super::User;
|
use super::User;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "devices"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[table_name = "devices"]
|
||||||
#[primary_key(uuid)]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
pub struct Device {
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
pub uuid: String,
|
#[primary_key(uuid)]
|
||||||
pub created_at: NaiveDateTime,
|
pub struct Device {
|
||||||
pub updated_at: NaiveDateTime,
|
pub uuid: String,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: NaiveDateTime,
|
||||||
|
|
||||||
pub user_uuid: String,
|
pub user_uuid: String,
|
||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// https://github.com/bitwarden/core/tree/master/src/Core/Enums
|
// https://github.com/bitwarden/core/tree/master/src/Core/Enums
|
||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub push_token: Option<String>,
|
pub push_token: Option<String>,
|
||||||
|
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
|
|
||||||
pub twofactor_remember: Option<String>,
|
pub twofactor_remember: Option<String>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
|
@ -76,7 +79,6 @@ impl Device {
|
||||||
let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
|
||||||
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 {
|
||||||
|
@ -105,42 +107,39 @@ impl Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::devices;
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Device {
|
impl Device {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
crate::util::retry(
|
db_run! { conn:
|
||||||
|| diesel::insert_into(devices::table).values(&*self).on_conflict(devices::uuid).do_update().set(&*self).execute(&**conn),
|
sqlite, mysql {
|
||||||
10,
|
crate::util::retry(
|
||||||
)
|
|| diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),
|
||||||
.map_res("Error saving device")
|
10,
|
||||||
}
|
).map_res("Error saving device")
|
||||||
|
}
|
||||||
#[cfg(not(feature = "postgresql"))]
|
postgresql {
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
let value = DeviceDb::to_db(self);
|
||||||
self.updated_at = Utc::now().naive_utc();
|
crate::util::retry(
|
||||||
|
|| diesel::insert_into(devices::table).values(&value).on_conflict(devices::uuid).do_update().set(&value).execute(conn),
|
||||||
crate::util::retry(
|
10,
|
||||||
|| diesel::replace_into(devices::table).values(&*self).execute(&**conn),
|
).map_res("Error saving device")
|
||||||
10,
|
}
|
||||||
)
|
}
|
||||||
.map_res("Error saving device")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid)))
|
||||||
.map_res("Error removing device")
|
.execute(conn)
|
||||||
|
.map_res("Error removing device")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -151,23 +150,43 @@ impl Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
devices::table
|
db_run! { conn: {
|
||||||
.filter(devices::uuid.eq(uuid))
|
devices::table
|
||||||
.first::<Self>(&**conn)
|
.filter(devices::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<DeviceDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {
|
||||||
devices::table
|
db_run! { conn: {
|
||||||
.filter(devices::refresh_token.eq(refresh_token))
|
devices::table
|
||||||
.first::<Self>(&**conn)
|
.filter(devices::refresh_token.eq(refresh_token))
|
||||||
.ok()
|
.first::<DeviceDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
devices::table
|
db_run! { conn: {
|
||||||
.filter(devices::user_uuid.eq(user_uuid))
|
devices::table
|
||||||
.load::<Self>(&**conn)
|
.filter(devices::user_uuid.eq(user_uuid))
|
||||||
.expect("Error loading devices")
|
.load::<DeviceDb>(conn)
|
||||||
|
.expect("Error loading devices")
|
||||||
|
.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()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
83
src/db/models/favorite.rs
Normal file
83
src/db/models/favorite.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use super::{Cipher, User};
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||||
|
#[table_name = "favorites"]
|
||||||
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
|
#[primary_key(user_uuid, cipher_uuid)]
|
||||||
|
pub struct Favorite {
|
||||||
|
pub user_uuid: String,
|
||||||
|
pub cipher_uuid: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::db::DbConn;
|
||||||
|
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::error::MapResult;
|
||||||
|
|
||||||
|
impl Favorite {
|
||||||
|
// 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 {
|
||||||
|
db_run!{ conn: {
|
||||||
|
let query = favorites::table
|
||||||
|
.filter(favorites::cipher_uuid.eq(cipher_uuid))
|
||||||
|
.filter(favorites::user_uuid.eq(user_uuid))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
query.first::<i64>(conn).ok().unwrap_or(0) != 0
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets whether the specified cipher is a favorite of the specified user.
|
||||||
|
pub fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, &conn), favorite);
|
||||||
|
match (old, new) {
|
||||||
|
(false, true) => {
|
||||||
|
User::update_uuid_revision(user_uuid, &conn);
|
||||||
|
db_run!{ conn: {
|
||||||
|
diesel::insert_into(favorites::table)
|
||||||
|
.values((
|
||||||
|
favorites::user_uuid.eq(user_uuid),
|
||||||
|
favorites::cipher_uuid.eq(cipher_uuid),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding favorite")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
(true, false) => {
|
||||||
|
User::update_uuid_revision(user_uuid, &conn);
|
||||||
|
db_run!{ conn: {
|
||||||
|
diesel::delete(
|
||||||
|
favorites::table
|
||||||
|
.filter(favorites::user_uuid.eq(user_uuid))
|
||||||
|
.filter(favorites::cipher_uuid.eq(cipher_uuid))
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error removing favorite")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
// Otherwise, the favorite status is already what it should be.
|
||||||
|
_ => Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all favorite entries associated with the specified cipher.
|
||||||
|
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error removing favorites by cipher")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all favorite entries associated with the specified user.
|
||||||
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error removing favorites by user")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,26 +3,28 @@ use serde_json::Value;
|
||||||
|
|
||||||
use super::{Cipher, User};
|
use super::{Cipher, User};
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "folders"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[table_name = "folders"]
|
||||||
#[primary_key(uuid)]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
pub struct Folder {
|
#[primary_key(uuid)]
|
||||||
pub uuid: String,
|
pub struct Folder {
|
||||||
pub created_at: NaiveDateTime,
|
pub uuid: String,
|
||||||
pub updated_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub user_uuid: String,
|
pub updated_at: NaiveDateTime,
|
||||||
pub name: String,
|
pub user_uuid: String,
|
||||||
}
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Debug, 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")]
|
||||||
#[primary_key(cipher_uuid, folder_uuid)]
|
#[primary_key(cipher_uuid, folder_uuid)]
|
||||||
pub struct FolderCipher {
|
pub struct FolderCipher {
|
||||||
pub cipher_uuid: String,
|
pub cipher_uuid: String,
|
||||||
pub folder_uuid: String,
|
pub folder_uuid: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
|
@ -61,48 +63,58 @@ impl FolderCipher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::{folders, folders_ciphers};
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Folder {
|
impl Folder {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
diesel::insert_into(folders::table)
|
db_run! { conn:
|
||||||
.values(&*self)
|
sqlite, mysql {
|
||||||
.on_conflict(folders::uuid)
|
match diesel::replace_into(folders::table)
|
||||||
.do_update()
|
.values(FolderDb::to_db(self))
|
||||||
.set(&*self)
|
.execute(conn)
|
||||||
.execute(&**conn)
|
{
|
||||||
.map_res("Error saving folder")
|
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, _)) => {
|
||||||
#[cfg(not(feature = "postgresql"))]
|
diesel::update(folders::table)
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
.filter(folders::uuid.eq(&self.uuid))
|
||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
.set(FolderDb::to_db(self))
|
||||||
self.updated_at = Utc::now().naive_utc();
|
.execute(conn)
|
||||||
|
.map_res("Error saving folder")
|
||||||
diesel::replace_into(folders::table)
|
}
|
||||||
.values(&*self)
|
Err(e) => Err(e.into()),
|
||||||
.execute(&**conn)
|
}.map_res("Error saving folder")
|
||||||
.map_res("Error saving folder")
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = FolderDb::to_db(self);
|
||||||
|
diesel::insert_into(folders::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(folders::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
|
||||||
|
|
||||||
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
|
|
||||||
.execute(&**conn)
|
db_run! { conn: {
|
||||||
.map_res("Error deleting folder")
|
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting folder")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -113,73 +125,95 @@ impl Folder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
folders::table
|
db_run! { conn: {
|
||||||
.filter(folders::uuid.eq(uuid))
|
folders::table
|
||||||
.first::<Self>(&**conn)
|
.filter(folders::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<FolderDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
folders::table
|
db_run! { conn: {
|
||||||
.filter(folders::user_uuid.eq(user_uuid))
|
folders::table
|
||||||
.load::<Self>(&**conn)
|
.filter(folders::user_uuid.eq(user_uuid))
|
||||||
.expect("Error loading folders")
|
.load::<FolderDb>(conn)
|
||||||
|
.expect("Error loading folders")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FolderCipher {
|
impl FolderCipher {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::insert_into(folders_ciphers::table)
|
db_run! { conn:
|
||||||
.values(&*self)
|
sqlite, mysql {
|
||||||
.on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
|
// Not checking for ForeignKey Constraints here.
|
||||||
.do_nothing()
|
// Table folders_ciphers does not have ForeignKey Constraints which would cause conflicts.
|
||||||
.execute(&**conn)
|
// This table has no constraints pointing to itself, but only to others.
|
||||||
.map_res("Error adding cipher to folder")
|
diesel::replace_into(folders_ciphers::table)
|
||||||
}
|
.values(FolderCipherDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
#[cfg(not(feature = "postgresql"))]
|
.map_res("Error adding cipher to folder")
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
}
|
||||||
diesel::replace_into(folders_ciphers::table)
|
postgresql {
|
||||||
.values(&*self)
|
diesel::insert_into(folders_ciphers::table)
|
||||||
.execute(&**conn)
|
.values(FolderCipherDb::to_db(self))
|
||||||
.map_res("Error adding cipher to folder")
|
.on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
|
||||||
|
.do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding cipher to folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(
|
db_run! { conn: {
|
||||||
folders_ciphers::table
|
diesel::delete(
|
||||||
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
|
folders_ciphers::table
|
||||||
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)),
|
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
|
||||||
)
|
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)),
|
||||||
.execute(&**conn)
|
)
|
||||||
.map_res("Error removing cipher from folder")
|
.execute(conn)
|
||||||
|
.map_res("Error removing cipher from folder")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
|
||||||
.map_res("Error removing cipher from folders")
|
.execute(conn)
|
||||||
|
.map_res("Error removing cipher from folders")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
|
||||||
.map_res("Error removing ciphers from folder")
|
.execute(conn)
|
||||||
|
.map_res("Error removing ciphers from folder")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
folders_ciphers::table
|
db_run! { conn: {
|
||||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
folders_ciphers::table
|
||||||
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
|
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||||
.first::<Self>(&**conn)
|
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
|
||||||
.ok()
|
.first::<FolderCipherDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
folders_ciphers::table
|
db_run! { conn: {
|
||||||
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
folders_ciphers::table
|
||||||
.load::<Self>(&**conn)
|
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
|
||||||
.expect("Error loading folders")
|
.load::<FolderCipherDb>(conn)
|
||||||
|
.expect("Error loading folders")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
mod attachment;
|
mod attachment;
|
||||||
mod cipher;
|
mod cipher;
|
||||||
mod device;
|
|
||||||
mod folder;
|
|
||||||
mod user;
|
|
||||||
|
|
||||||
mod collection;
|
mod collection;
|
||||||
|
mod device;
|
||||||
|
mod favorite;
|
||||||
|
mod folder;
|
||||||
|
mod org_policy;
|
||||||
mod organization;
|
mod organization;
|
||||||
mod two_factor;
|
mod two_factor;
|
||||||
mod org_policy;
|
mod user;
|
||||||
|
|
||||||
pub use self::attachment::Attachment;
|
pub use self::attachment::Attachment;
|
||||||
pub use self::cipher::Cipher;
|
pub use self::cipher::Cipher;
|
||||||
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
||||||
pub use self::device::Device;
|
pub use self::device::Device;
|
||||||
|
pub use self::favorite::Favorite;
|
||||||
pub use self::folder::{Folder, FolderCipher};
|
pub use self::folder::{Folder, FolderCipher};
|
||||||
pub use self::organization::Organization;
|
pub use self::org_policy::{OrgPolicy, OrgPolicyType};
|
||||||
pub use self::organization::{UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
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};
|
||||||
pub use self::org_policy::{OrgPolicy, OrgPolicyType};
|
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::db::schema::org_policies;
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
use super::Organization;
|
use super::{Organization, UserOrgStatus};
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "org_policies"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
#[table_name = "org_policies"]
|
||||||
#[primary_key(uuid)]
|
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
||||||
pub struct OrgPolicy {
|
#[primary_key(uuid)]
|
||||||
pub uuid: String,
|
pub struct OrgPolicy {
|
||||||
pub org_uuid: String,
|
pub uuid: String,
|
||||||
pub atype: i32,
|
pub org_uuid: String,
|
||||||
pub enabled: bool,
|
pub atype: i32,
|
||||||
pub data: String,
|
pub enabled: bool,
|
||||||
|
pub data: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum OrgPolicyType {
|
pub enum OrgPolicyType {
|
||||||
TwoFactorAuthentication = 0,
|
TwoFactorAuthentication = 0,
|
||||||
MasterPassword = 1,
|
MasterPassword = 1,
|
||||||
|
@ -56,87 +55,119 @@ impl OrgPolicy {
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl OrgPolicy {
|
impl OrgPolicy {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
// We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
|
db_run! { conn:
|
||||||
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
|
sqlite, mysql {
|
||||||
// not support multiple constraints on ON CONFLICT clauses.
|
match diesel::replace_into(org_policies::table)
|
||||||
diesel::delete(
|
.values(OrgPolicyDb::to_db(self))
|
||||||
org_policies::table
|
.execute(conn)
|
||||||
.filter(org_policies::org_uuid.eq(&self.org_uuid))
|
{
|
||||||
.filter(org_policies::atype.eq(&self.atype)),
|
Ok(_) => Ok(()),
|
||||||
)
|
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
||||||
.execute(&**conn)
|
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
||||||
.map_res("Error deleting org_policy for insert")?;
|
diesel::update(org_policies::table)
|
||||||
|
.filter(org_policies::uuid.eq(&self.uuid))
|
||||||
|
.set(OrgPolicyDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving org_policy")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving org_policy")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = OrgPolicyDb::to_db(self);
|
||||||
|
// We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
|
||||||
|
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
|
||||||
|
// not support multiple constraints on ON CONFLICT clauses.
|
||||||
|
diesel::delete(
|
||||||
|
org_policies::table
|
||||||
|
.filter(org_policies::org_uuid.eq(&self.org_uuid))
|
||||||
|
.filter(org_policies::atype.eq(&self.atype)),
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting org_policy for insert")?;
|
||||||
|
|
||||||
diesel::insert_into(org_policies::table)
|
diesel::insert_into(org_policies::table)
|
||||||
.values(self)
|
.values(&value)
|
||||||
.on_conflict(org_policies::uuid)
|
.on_conflict(org_policies::uuid)
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(self)
|
.set(&value)
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
.map_res("Error saving org_policy")
|
.map_res("Error saving org_policy")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#[cfg(not(feature = "postgresql"))]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
|
||||||
diesel::replace_into(org_policies::table)
|
|
||||||
.values(&*self)
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_res("Error saving org_policy")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))
|
||||||
.map_res("Error deleting org_policy")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting org_policy")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
org_policies::table
|
db_run! { conn: {
|
||||||
.filter(org_policies::uuid.eq(uuid))
|
org_policies::table
|
||||||
.first::<Self>(&**conn)
|
.filter(org_policies::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<OrgPolicyDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
org_policies::table
|
db_run! { conn: {
|
||||||
.filter(org_policies::org_uuid.eq(org_uuid))
|
org_policies::table
|
||||||
.load::<Self>(&**conn)
|
.filter(org_policies::org_uuid.eq(org_uuid))
|
||||||
.expect("Error loading org_policy")
|
.load::<OrgPolicyDb>(conn)
|
||||||
|
.expect("Error loading org_policy")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
use crate::db::schema::users_organizations;
|
db_run! { conn: {
|
||||||
|
org_policies::table
|
||||||
org_policies::table
|
.inner_join(
|
||||||
.left_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(
|
||||||
.select(org_policies::all_columns)
|
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||||
.load::<Self>(&**conn)
|
)
|
||||||
.expect("Error loading org_policy")
|
.select(org_policies::all_columns)
|
||||||
|
.load::<OrgPolicyDb>(conn)
|
||||||
|
.expect("Error loading org_policy")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
|
||||||
org_policies::table
|
db_run! { conn: {
|
||||||
.filter(org_policies::org_uuid.eq(org_uuid))
|
org_policies::table
|
||||||
.filter(org_policies::atype.eq(atype))
|
.filter(org_policies::org_uuid.eq(org_uuid))
|
||||||
.first::<Self>(&**conn)
|
.filter(org_policies::atype.eq(atype))
|
||||||
.ok()
|
.first::<OrgPolicyDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
|
||||||
.map_res("Error deleting org_policy")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting org_policy")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
||||||
.map_res("Error deleting twofactors")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting twofactors")
|
||||||
|
}}
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,29 @@ use num_traits::FromPrimitive;
|
||||||
|
|
||||||
use super::{CollectionUser, User, OrgPolicy};
|
use super::{CollectionUser, User, OrgPolicy};
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "organizations"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[primary_key(uuid)]
|
#[table_name = "organizations"]
|
||||||
pub struct Organization {
|
#[primary_key(uuid)]
|
||||||
pub uuid: String,
|
pub struct Organization {
|
||||||
pub name: String,
|
pub uuid: String,
|
||||||
pub billing_email: String,
|
pub name: String,
|
||||||
}
|
pub billing_email: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "users_organizations"]
|
#[table_name = "users_organizations"]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct UserOrganization {
|
pub struct UserOrganization {
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
pub user_uuid: String,
|
pub user_uuid: String,
|
||||||
pub org_uuid: String,
|
pub org_uuid: String,
|
||||||
|
|
||||||
pub access_all: bool,
|
pub access_all: bool,
|
||||||
pub akey: String,
|
pub akey: String,
|
||||||
pub status: i32,
|
pub status: i32,
|
||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum UserOrgStatus {
|
pub enum UserOrgStatus {
|
||||||
|
@ -34,7 +36,7 @@ pub enum UserOrgStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
#[derive(FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum UserOrgType {
|
pub enum UserOrgType {
|
||||||
Owner = 0,
|
Owner = 0,
|
||||||
Admin = 1,
|
Admin = 1,
|
||||||
|
@ -42,24 +44,28 @@ pub enum UserOrgType {
|
||||||
Manager = 3,
|
Manager = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UserOrgType {
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"0" | "Owner" => Some(UserOrgType::Owner),
|
||||||
|
"1" | "Admin" => Some(UserOrgType::Admin),
|
||||||
|
"2" | "User" => Some(UserOrgType::User),
|
||||||
|
"3" | "Manager" => Some(UserOrgType::Manager),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Ord for UserOrgType {
|
impl Ord for UserOrgType {
|
||||||
fn cmp(&self, other: &UserOrgType) -> Ordering {
|
fn cmp(&self, other: &UserOrgType) -> Ordering {
|
||||||
if self == other {
|
// For easy comparison, map each variant to an access level (where 0 is lowest).
|
||||||
Ordering::Equal
|
static ACCESS_LEVEL: [i32; 4] = [
|
||||||
} else {
|
3, // Owner
|
||||||
match self {
|
2, // Admin
|
||||||
UserOrgType::Owner => Ordering::Greater,
|
0, // User
|
||||||
UserOrgType::Admin => match other {
|
1, // Manager
|
||||||
UserOrgType::Owner => Ordering::Less,
|
];
|
||||||
_ => Ordering::Greater,
|
ACCESS_LEVEL[*self as usize].cmp(&ACCESS_LEVEL[*other as usize])
|
||||||
},
|
|
||||||
UserOrgType::Manager => match other {
|
|
||||||
UserOrgType::Owner | UserOrgType::Admin => Ordering::Less,
|
|
||||||
_ => Ordering::Greater,
|
|
||||||
},
|
|
||||||
UserOrgType::User => Ordering::Less,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,18 +133,6 @@ impl PartialOrd<UserOrgType> for i32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserOrgType {
|
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
|
||||||
match s {
|
|
||||||
"0" | "Owner" => Some(UserOrgType::Owner),
|
|
||||||
"1" | "Admin" => Some(UserOrgType::Admin),
|
|
||||||
"2" | "User" => Some(UserOrgType::User),
|
|
||||||
"3" | "Manager" => Some(UserOrgType::Manager),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl Organization {
|
impl Organization {
|
||||||
pub fn new(name: String, billing_email: String) -> Self {
|
pub fn new(name: String, billing_email: String) -> Self {
|
||||||
|
@ -165,9 +159,9 @@ impl Organization {
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
|
|
||||||
"BusinessName": null,
|
"BusinessName": null,
|
||||||
"BusinessAddress1": null,
|
"BusinessAddress1": null,
|
||||||
"BusinessAddress2": null,
|
"BusinessAddress2": null,
|
||||||
"BusinessAddress3": null,
|
"BusinessAddress3": null,
|
||||||
"BusinessCountry": null,
|
"BusinessCountry": null,
|
||||||
"BusinessTaxNumber": null,
|
"BusinessTaxNumber": null,
|
||||||
|
|
||||||
|
@ -196,17 +190,13 @@ impl UserOrganization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations};
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl Organization {
|
impl Organization {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
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()
|
.iter()
|
||||||
|
@ -214,27 +204,36 @@ impl Organization {
|
||||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
User::update_uuid_revision(&user_org.user_uuid, conn);
|
||||||
});
|
});
|
||||||
|
|
||||||
diesel::insert_into(organizations::table)
|
db_run! { conn:
|
||||||
.values(self)
|
sqlite, mysql {
|
||||||
.on_conflict(organizations::uuid)
|
match diesel::replace_into(organizations::table)
|
||||||
.do_update()
|
.values(OrganizationDb::to_db(self))
|
||||||
.set(self)
|
.execute(conn)
|
||||||
.execute(&**conn)
|
{
|
||||||
.map_res("Error saving organization")
|
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(organizations::table)
|
||||||
|
.filter(organizations::uuid.eq(&self.uuid))
|
||||||
|
.set(OrganizationDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving organization")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving organization")
|
||||||
|
|
||||||
#[cfg(not(feature = "postgresql"))]
|
}
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
postgresql {
|
||||||
UserOrganization::find_by_org(&self.uuid, conn)
|
let value = OrganizationDb::to_db(self);
|
||||||
.iter()
|
diesel::insert_into(organizations::table)
|
||||||
.for_each(|user_org| {
|
.values(&value)
|
||||||
User::update_uuid_revision(&user_org.user_uuid, conn);
|
.on_conflict(organizations::uuid)
|
||||||
});
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
diesel::replace_into(organizations::table)
|
.execute(conn)
|
||||||
.values(self)
|
.map_res("Error saving organization")
|
||||||
.execute(&**conn)
|
}
|
||||||
.map_res("Error saving organization")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -245,23 +244,34 @@ 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)?;
|
||||||
|
|
||||||
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
|
|
||||||
.execute(&**conn)
|
db_run! { conn: {
|
||||||
.map_res("Error saving organization")
|
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving organization")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
organizations::table
|
db_run! { conn: {
|
||||||
.filter(organizations::uuid.eq(uuid))
|
organizations::table
|
||||||
.first::<Self>(&**conn)
|
.filter(organizations::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<OrganizationDb>(conn)
|
||||||
|
.ok().from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all(conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
organizations::table.load::<OrganizationDb>(conn).expect("Error loading organizations").from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserOrganization {
|
impl UserOrganization {
|
||||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||||
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
|
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"Id": self.org_uuid,
|
"Id": self.org_uuid,
|
||||||
"Name": org.name,
|
"Name": org.name,
|
||||||
|
@ -275,6 +285,8 @@ impl UserOrganization {
|
||||||
"UseGroups": false,
|
"UseGroups": false,
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
|
"UseApi": false,
|
||||||
|
"SelfHost": true,
|
||||||
|
|
||||||
"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
|
||||||
|
|
||||||
|
@ -305,10 +317,11 @@ impl UserOrganization {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json_collection_user_details(&self, read_only: bool) -> Value {
|
pub fn to_json_user_access_restrictions(&self, col_user: &CollectionUser) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"ReadOnly": read_only
|
"ReadOnly": col_user.read_only,
|
||||||
|
"HidePasswords": col_user.hide_passwords,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +332,11 @@ 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!({"Id": c.collection_uuid, "ReadOnly": c.read_only}))
|
.map(|c| json!({
|
||||||
|
"Id": c.collection_uuid,
|
||||||
|
"ReadOnly": c.read_only,
|
||||||
|
"HidePasswords": c.hide_passwords,
|
||||||
|
}))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -335,38 +352,50 @@ impl UserOrganization {
|
||||||
"Object": "organizationUserDetails",
|
"Object": "organizationUserDetails",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
User::update_uuid_revision(&self.user_uuid, conn);
|
||||||
|
|
||||||
diesel::insert_into(users_organizations::table)
|
db_run! { conn:
|
||||||
.values(self)
|
sqlite, mysql {
|
||||||
.on_conflict(users_organizations::uuid)
|
match diesel::replace_into(users_organizations::table)
|
||||||
.do_update()
|
.values(UserOrganizationDb::to_db(self))
|
||||||
.set(self)
|
.execute(conn)
|
||||||
.execute(&**conn)
|
{
|
||||||
.map_res("Error adding user to organization")
|
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, _)) => {
|
||||||
#[cfg(not(feature = "postgresql"))]
|
diesel::update(users_organizations::table)
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
.filter(users_organizations::uuid.eq(&self.uuid))
|
||||||
User::update_uuid_revision(&self.user_uuid, conn);
|
.set(UserOrganizationDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
diesel::replace_into(users_organizations::table)
|
.map_res("Error adding user to organization")
|
||||||
.values(self)
|
}
|
||||||
.execute(&**conn)
|
Err(e) => Err(e.into()),
|
||||||
.map_res("Error adding user to organization")
|
}.map_res("Error adding user to organization")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = UserOrganizationDb::to_db(self);
|
||||||
|
diesel::insert_into(users_organizations::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(users_organizations::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error adding user to organization")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)?;
|
||||||
|
|
||||||
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
|
||||||
.map_res("Error removing user from organization")
|
.execute(conn)
|
||||||
|
.map_res("Error removing user from organization")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -383,103 +412,152 @@ impl UserOrganization {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_status(self, status: UserOrgStatus) -> bool {
|
||||||
|
self.status == status as i32
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_full_access(self) -> bool {
|
pub fn has_full_access(self) -> bool {
|
||||||
self.access_all || self.atype >= UserOrgType::Admin
|
(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> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::uuid.eq(uuid))
|
users_organizations::table
|
||||||
.first::<Self>(&**conn)
|
.filter(users_organizations::uuid.eq(uuid))
|
||||||
.ok()
|
.first::<UserOrganizationDb>(conn)
|
||||||
|
.ok().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::uuid.eq(uuid))
|
users_organizations::table
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
.filter(users_organizations::uuid.eq(uuid))
|
||||||
.first::<Self>(&**conn)
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
.ok()
|
.first::<UserOrganizationDb>(conn)
|
||||||
|
.ok().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
users_organizations::table
|
||||||
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.load::<Self>(&**conn)
|
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
|
||||||
.unwrap_or_default()
|
.load::<UserOrganizationDb>(conn)
|
||||||
|
.unwrap_or_default().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
users_organizations::table
|
||||||
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.load::<Self>(&**conn)
|
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
|
||||||
.unwrap_or_default()
|
.load::<UserOrganizationDb>(conn)
|
||||||
|
.unwrap_or_default().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
users_organizations::table
|
||||||
.load::<Self>(&**conn)
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.unwrap_or_default()
|
.load::<UserOrganizationDb>(conn)
|
||||||
|
.unwrap_or_default().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
users_organizations::table
|
||||||
.load::<Self>(&**conn)
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
.expect("Error loading user organizations")
|
.load::<UserOrganizationDb>(conn)
|
||||||
|
.expect("Error loading user organizations").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
|
||||||
|
db_run! { conn: {
|
||||||
|
users_organizations::table
|
||||||
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
users_organizations::table
|
||||||
.filter(users_organizations::atype.eq(atype))
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
.load::<Self>(&**conn)
|
.filter(users_organizations::atype.eq(atype))
|
||||||
.expect("Error loading user organizations")
|
.load::<UserOrganizationDb>(conn)
|
||||||
|
.expect("Error loading user organizations").from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
users_organizations::table
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.first::<Self>(&**conn)
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
.ok()
|
.first::<UserOrganizationDb>(conn)
|
||||||
|
.ok().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
users_organizations::table
|
||||||
.left_join(users_collections::table.on(
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
.left_join(users_collections::table.on(
|
||||||
))
|
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
||||||
.left_join(ciphers_collections::table.on(
|
))
|
||||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and(
|
.left_join(ciphers_collections::table.on(
|
||||||
ciphers_collections::cipher_uuid.eq(&cipher_uuid)
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and(
|
||||||
|
ciphers_collections::cipher_uuid.eq(&cipher_uuid)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.filter(
|
||||||
|
users_organizations::access_all.eq(true).or( // AccessAll..
|
||||||
|
ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher
|
||||||
|
)
|
||||||
)
|
)
|
||||||
))
|
.select(users_organizations::all_columns)
|
||||||
.filter(
|
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
|
||||||
users_organizations::access_all.eq(true).or( // AccessAll..
|
}}
|
||||||
ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.select(users_organizations::all_columns)
|
|
||||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
db_run! { conn: {
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
users_organizations::table
|
||||||
.left_join(users_collections::table.on(
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
.left_join(users_collections::table.on(
|
||||||
))
|
users_collections::user_uuid.eq(users_organizations::user_uuid)
|
||||||
.filter(
|
))
|
||||||
users_organizations::access_all.eq(true).or( // AccessAll..
|
.filter(
|
||||||
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher
|
users_organizations::access_all.eq(true).or( // AccessAll..
|
||||||
|
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
.select(users_organizations::all_columns)
|
||||||
.select(users_organizations::all_columns)
|
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
|
||||||
.load::<Self>(&**conn).expect("Error loading user organizations")
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn partial_cmp_UserOrgType() {
|
||||||
|
assert!(UserOrgType::Owner > UserOrgType::Admin);
|
||||||
|
assert!(UserOrgType::Admin > UserOrgType::Manager);
|
||||||
|
assert!(UserOrgType::Manager > UserOrgType::User);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,28 @@
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::db::schema::twofactor;
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
use super::User;
|
use super::User;
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "twofactor"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[table_name = "twofactor"]
|
||||||
#[primary_key(uuid)]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
pub struct TwoFactor {
|
#[primary_key(uuid)]
|
||||||
pub uuid: String,
|
pub struct TwoFactor {
|
||||||
pub user_uuid: String,
|
pub uuid: String,
|
||||||
pub atype: i32,
|
pub user_uuid: String,
|
||||||
pub enabled: bool,
|
pub atype: i32,
|
||||||
pub data: String,
|
pub enabled: bool,
|
||||||
pub last_used: i32,
|
pub data: String,
|
||||||
|
pub last_used: i32,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum TwoFactorType {
|
pub enum TwoFactorType {
|
||||||
Authenticator = 0,
|
Authenticator = 0,
|
||||||
Email = 1,
|
Email = 1,
|
||||||
|
@ -60,7 +59,7 @@ impl TwoFactor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json_list(&self) -> Value {
|
pub fn to_json_provider(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Enabled": self.enabled,
|
"Enabled": self.enabled,
|
||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
|
@ -71,57 +70,80 @@ impl TwoFactor {
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl TwoFactor {
|
impl TwoFactor {
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
// We need to make sure we're not going to violate the unique constraint on user_uuid and atype.
|
db_run! { conn:
|
||||||
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
|
sqlite, mysql {
|
||||||
// not support multiple constraints on ON CONFLICT clauses.
|
match diesel::replace_into(twofactor::table)
|
||||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype)))
|
.values(TwoFactorDb::to_db(self))
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
.map_res("Error deleting twofactor for insert")?;
|
{
|
||||||
|
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(twofactor::table)
|
||||||
|
.filter(twofactor::uuid.eq(&self.uuid))
|
||||||
|
.set(TwoFactorDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving twofactor")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving twofactor")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = TwoFactorDb::to_db(self);
|
||||||
|
// We need to make sure we're not going to violate the unique constraint on user_uuid and atype.
|
||||||
|
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
|
||||||
|
// not support multiple constraints on ON CONFLICT clauses.
|
||||||
|
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting twofactor for insert")?;
|
||||||
|
|
||||||
diesel::insert_into(twofactor::table)
|
diesel::insert_into(twofactor::table)
|
||||||
.values(self)
|
.values(&value)
|
||||||
.on_conflict(twofactor::uuid)
|
.on_conflict(twofactor::uuid)
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(self)
|
.set(&value)
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
.map_res("Error saving twofactor")
|
.map_res("Error saving twofactor")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#[cfg(not(feature = "postgresql"))]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
|
||||||
diesel::replace_into(twofactor::table)
|
|
||||||
.values(self)
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_res("Error saving twofactor")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
|
||||||
.map_res("Error deleting twofactor")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting twofactor")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
twofactor::table
|
db_run! { conn: {
|
||||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
twofactor::table
|
||||||
.filter(twofactor::atype.lt(1000)) // Filter implementation types
|
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||||
.load::<Self>(&**conn)
|
.filter(twofactor::atype.lt(1000)) // Filter implementation types
|
||||||
.expect("Error loading twofactor")
|
.load::<TwoFactorDb>(conn)
|
||||||
|
.expect("Error loading twofactor")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
|
||||||
twofactor::table
|
db_run! { conn: {
|
||||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
twofactor::table
|
||||||
.filter(twofactor::atype.eq(atype))
|
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||||
.first::<Self>(&**conn)
|
.filter(twofactor::atype.eq(atype))
|
||||||
.ok()
|
.first::<TwoFactorDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
db_run! { conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
||||||
.map_res("Error deleting twofactors")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting twofactors")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,42 +4,55 @@ use serde_json::Value;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
db_object! {
|
||||||
#[table_name = "users"]
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[primary_key(uuid)]
|
#[table_name = "users"]
|
||||||
pub struct User {
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
pub uuid: String,
|
#[primary_key(uuid)]
|
||||||
pub created_at: NaiveDateTime,
|
pub struct User {
|
||||||
pub updated_at: NaiveDateTime,
|
pub uuid: String,
|
||||||
pub verified_at: Option<NaiveDateTime>,
|
pub enabled: bool,
|
||||||
pub last_verifying_at: Option<NaiveDateTime>,
|
pub created_at: NaiveDateTime,
|
||||||
pub login_verify_count: i32,
|
pub updated_at: NaiveDateTime,
|
||||||
|
pub verified_at: Option<NaiveDateTime>,
|
||||||
|
pub last_verifying_at: Option<NaiveDateTime>,
|
||||||
|
pub login_verify_count: i32,
|
||||||
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub email_new: Option<String>,
|
pub email_new: Option<String>,
|
||||||
pub email_new_token: Option<String>,
|
pub email_new_token: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
pub password_hash: Vec<u8>,
|
pub password_hash: Vec<u8>,
|
||||||
pub salt: Vec<u8>,
|
pub salt: Vec<u8>,
|
||||||
pub password_iterations: i32,
|
pub password_iterations: i32,
|
||||||
pub password_hint: Option<String>,
|
pub password_hint: Option<String>,
|
||||||
|
|
||||||
pub akey: String,
|
pub akey: String,
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
|
|
||||||
#[column_name = "totp_secret"]
|
#[column_name = "totp_secret"] // Note, this is only added to the UserDb structs, not to User
|
||||||
_totp_secret: Option<String>,
|
_totp_secret: Option<String>,
|
||||||
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,
|
||||||
|
|
||||||
pub client_kdf_type: i32,
|
pub client_kdf_type: i32,
|
||||||
pub client_kdf_iter: i32,
|
pub client_kdf_iter: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||||
|
#[table_name = "invitations"]
|
||||||
|
#[primary_key(email)]
|
||||||
|
pub struct Invitation {
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserStatus {
|
enum UserStatus {
|
||||||
|
@ -48,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
|
||||||
|
@ -59,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,
|
||||||
|
@ -75,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,
|
||||||
|
@ -108,20 +129,56 @@ 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);
|
||||||
|
|
||||||
|
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, Folder, TwoFactor, UserOrgType, UserOrganization};
|
use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization};
|
||||||
use crate::db::schema::{invitations, users};
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use diesel;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
@ -158,7 +215,6 @@ impl User {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
if self.email.trim().is_empty() {
|
if self.email.trim().is_empty() {
|
||||||
err!("User email can't be empty")
|
err!("User email can't be empty")
|
||||||
|
@ -166,49 +222,60 @@ impl User {
|
||||||
|
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
diesel::insert_into(users::table) // Insert or update
|
db_run! {conn:
|
||||||
.values(&*self)
|
sqlite, mysql {
|
||||||
.on_conflict(users::uuid)
|
match diesel::replace_into(users::table)
|
||||||
.do_update()
|
.values(UserDb::to_db(self))
|
||||||
.set(&*self)
|
.execute(conn)
|
||||||
.execute(&**conn)
|
{
|
||||||
.map_res("Error saving user")
|
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, _)) => {
|
||||||
#[cfg(not(feature = "postgresql"))]
|
diesel::update(users::table)
|
||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
.filter(users::uuid.eq(&self.uuid))
|
||||||
if self.email.trim().is_empty() {
|
.set(UserDb::to_db(self))
|
||||||
err!("User email can't be empty")
|
.execute(conn)
|
||||||
|
.map_res("Error saving user")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving user")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = UserDb::to_db(self);
|
||||||
|
diesel::insert_into(users::table) // Insert or update
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(users::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving user")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updated_at = Utc::now().naive_utc();
|
|
||||||
|
|
||||||
diesel::replace_into(users::table) // Insert or update
|
|
||||||
.values(&*self)
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_res("Error saving user")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
|
for user_org in UserOrganization::find_by_user(&self.uuid, conn) {
|
||||||
if user_org.atype == UserOrgType::Owner {
|
if user_org.atype == UserOrgType::Owner {
|
||||||
let owner_type = UserOrgType::Owner as i32;
|
let owner_type = UserOrgType::Owner as i32;
|
||||||
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 {
|
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).len() <= 1 {
|
||||||
err!("Can't delete last owner")
|
err!("Can't delete last owner")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)?;
|
||||||
Folder::delete_all_by_user(&self.uuid, &*conn)?;
|
Favorite::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Device::delete_all_by_user(&self.uuid, &*conn)?;
|
Folder::delete_all_by_user(&self.uuid, conn)?;
|
||||||
TwoFactor::delete_all_by_user(&self.uuid, &*conn)?;
|
Device::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Invitation::take(&self.email, &*conn); // Delete invitation if any
|
TwoFactor::delete_all_by_user(&self.uuid, conn)?;
|
||||||
|
Invitation::take(&self.email, conn); // Delete invitation if any
|
||||||
|
|
||||||
diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
|
db_run! {conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
|
||||||
.map_res("Error deleting user")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting user")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
|
||||||
|
@ -220,15 +287,14 @@ impl User {
|
||||||
pub fn update_all_revisions(conn: &DbConn) -> EmptyResult {
|
pub fn update_all_revisions(conn: &DbConn) -> EmptyResult {
|
||||||
let updated_at = Utc::now().naive_utc();
|
let updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
crate::util::retry(
|
db_run! {conn: {
|
||||||
|| {
|
crate::util::retry(|| {
|
||||||
diesel::update(users::table)
|
diesel::update(users::table)
|
||||||
.set(users::updated_at.eq(updated_at))
|
.set(users::updated_at.eq(updated_at))
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
},
|
}, 10)
|
||||||
10,
|
.map_res("Error updating revision date for all users")
|
||||||
)
|
}}
|
||||||
.map_res("Error updating revision date for all users")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
|
@ -238,84 +304,94 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
|
fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
|
||||||
crate::util::retry(
|
db_run! {conn: {
|
||||||
|| {
|
crate::util::retry(|| {
|
||||||
diesel::update(users::table.filter(users::uuid.eq(uuid)))
|
diesel::update(users::table.filter(users::uuid.eq(uuid)))
|
||||||
.set(users::updated_at.eq(date))
|
.set(users::updated_at.eq(date))
|
||||||
.execute(&**conn)
|
.execute(conn)
|
||||||
},
|
}, 10)
|
||||||
10,
|
.map_res("Error updating user revision")
|
||||||
)
|
}}
|
||||||
.map_res("Error updating user revision")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
||||||
let lower_mail = mail.to_lowercase();
|
let lower_mail = mail.to_lowercase();
|
||||||
users::table
|
db_run! {conn: {
|
||||||
.filter(users::email.eq(lower_mail))
|
users::table
|
||||||
.first::<Self>(&**conn)
|
.filter(users::email.eq(lower_mail))
|
||||||
.ok()
|
.first::<UserDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
users::table.filter(users::uuid.eq(uuid)).first::<Self>(&**conn).ok()
|
db_run! {conn: {
|
||||||
|
users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all(conn: &DbConn) -> Vec<Self> {
|
pub fn get_all(conn: &DbConn) -> Vec<Self> {
|
||||||
users::table.load::<Self>(&**conn).expect("Error loading users")
|
db_run! {conn: {
|
||||||
|
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
pub fn last_active(&self, conn: &DbConn) -> Option<NaiveDateTime> {
|
||||||
#[table_name = "invitations"]
|
match Device::find_latest_active_by_user(&self.uuid, conn) {
|
||||||
#[primary_key(email)]
|
Some(device) => Some(device.updated_at),
|
||||||
pub struct Invitation {
|
None => None
|
||||||
pub email: String,
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Invitation {
|
impl Invitation {
|
||||||
pub fn new(email: String) -> Self {
|
pub const fn new(email: String) -> Self {
|
||||||
Self { email }
|
Self { email }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgresql")]
|
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||||
if self.email.trim().is_empty() {
|
if self.email.trim().is_empty() {
|
||||||
err!("Invitation email can't be empty")
|
err!("Invitation email can't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::insert_into(invitations::table)
|
db_run! {conn:
|
||||||
.values(self)
|
sqlite, mysql {
|
||||||
.on_conflict(invitations::email)
|
// Not checking for ForeignKey Constraints here
|
||||||
.do_nothing()
|
// Table invitations does not have any ForeignKey Constraints.
|
||||||
.execute(&**conn)
|
diesel::replace_into(invitations::table)
|
||||||
.map_res("Error saving invitation")
|
.values(InvitationDb::to_db(self))
|
||||||
}
|
.execute(conn)
|
||||||
|
.map_res("Error saving invitation")
|
||||||
#[cfg(not(feature = "postgresql"))]
|
}
|
||||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
postgresql {
|
||||||
if self.email.trim().is_empty() {
|
diesel::insert_into(invitations::table)
|
||||||
err!("Invitation email can't be empty")
|
.values(InvitationDb::to_db(self))
|
||||||
|
.on_conflict(invitations::email)
|
||||||
|
.do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving invitation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::replace_into(invitations::table)
|
|
||||||
.values(self)
|
|
||||||
.execute(&**conn)
|
|
||||||
.map_res("Error saving invitation")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))
|
db_run! {conn: {
|
||||||
.execute(&**conn)
|
diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))
|
||||||
.map_res("Error deleting invitation")
|
.execute(conn)
|
||||||
|
.map_res("Error deleting invitation")
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
||||||
let lower_mail = mail.to_lowercase();
|
let lower_mail = mail.to_lowercase();
|
||||||
invitations::table
|
db_run! {conn: {
|
||||||
.filter(invitations::email.eq(lower_mail))
|
invitations::table
|
||||||
.first::<Self>(&**conn)
|
.filter(invitations::email.eq(lower_mail))
|
||||||
.ok()
|
.first::<InvitationDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn take(mail: &str, conn: &DbConn) -> bool {
|
pub fn take(mail: &str, conn: &DbConn) -> bool {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
table! {
|
table! {
|
||||||
attachments (id) {
|
attachments (id) {
|
||||||
id -> Varchar,
|
id -> Text,
|
||||||
cipher_uuid -> Varchar,
|
cipher_uuid -> Text,
|
||||||
file_name -> Text,
|
file_name -> Text,
|
||||||
file_size -> Integer,
|
file_size -> Integer,
|
||||||
akey -> Nullable<Text>,
|
akey -> Nullable<Text>,
|
||||||
|
@ -10,42 +10,42 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
ciphers (uuid) {
|
ciphers (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
created_at -> Datetime,
|
created_at -> Datetime,
|
||||||
updated_at -> Datetime,
|
updated_at -> Datetime,
|
||||||
user_uuid -> Nullable<Varchar>,
|
user_uuid -> Nullable<Text>,
|
||||||
organization_uuid -> Nullable<Varchar>,
|
organization_uuid -> Nullable<Text>,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
notes -> Nullable<Text>,
|
notes -> Nullable<Text>,
|
||||||
fields -> Nullable<Text>,
|
fields -> Nullable<Text>,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
favorite -> Bool,
|
|
||||||
password_history -> Nullable<Text>,
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Datetime>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
ciphers_collections (cipher_uuid, collection_uuid) {
|
ciphers_collections (cipher_uuid, collection_uuid) {
|
||||||
cipher_uuid -> Varchar,
|
cipher_uuid -> Text,
|
||||||
collection_uuid -> Varchar,
|
collection_uuid -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
collections (uuid) {
|
collections (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
org_uuid -> Varchar,
|
org_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
devices (uuid) {
|
devices (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
created_at -> Datetime,
|
created_at -> Datetime,
|
||||||
updated_at -> Datetime,
|
updated_at -> Datetime,
|
||||||
user_uuid -> Varchar,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
|
@ -54,33 +54,40 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
favorites (user_uuid, cipher_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
folders (uuid) {
|
folders (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
created_at -> Datetime,
|
created_at -> Datetime,
|
||||||
updated_at -> Datetime,
|
updated_at -> Datetime,
|
||||||
user_uuid -> Varchar,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
folders_ciphers (cipher_uuid, folder_uuid) {
|
folders_ciphers (cipher_uuid, folder_uuid) {
|
||||||
cipher_uuid -> Varchar,
|
cipher_uuid -> Text,
|
||||||
folder_uuid -> Varchar,
|
folder_uuid -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
invitations (email) {
|
invitations (email) {
|
||||||
email -> Varchar,
|
email -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
org_policies (uuid) {
|
org_policies (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
org_uuid -> Varchar,
|
org_uuid -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
enabled -> Bool,
|
enabled -> Bool,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
|
@ -89,7 +96,7 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
organizations (uuid) {
|
organizations (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
billing_email -> Text,
|
billing_email -> Text,
|
||||||
}
|
}
|
||||||
|
@ -97,8 +104,8 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
user_uuid -> Varchar,
|
user_uuid -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
enabled -> Bool,
|
enabled -> Bool,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
|
@ -108,18 +115,19 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
|
enabled -> Bool,
|
||||||
created_at -> Datetime,
|
created_at -> Datetime,
|
||||||
updated_at -> Datetime,
|
updated_at -> Datetime,
|
||||||
verified_at -> Nullable<Datetime>,
|
verified_at -> Nullable<Datetime>,
|
||||||
last_verifying_at -> Nullable<Datetime>,
|
last_verifying_at -> Nullable<Datetime>,
|
||||||
login_verify_count -> Integer,
|
login_verify_count -> Integer,
|
||||||
email -> Varchar,
|
email -> Text,
|
||||||
email_new -> Nullable<Varchar>,
|
email_new -> Nullable<Text>,
|
||||||
email_new_token -> Nullable<Varchar>,
|
email_new_token -> Nullable<Text>,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
password_hash -> Blob,
|
password_hash -> Binary,
|
||||||
salt -> Blob,
|
salt -> Binary,
|
||||||
password_iterations -> Integer,
|
password_iterations -> Integer,
|
||||||
password_hint -> Nullable<Text>,
|
password_hint -> Nullable<Text>,
|
||||||
akey -> Text,
|
akey -> Text,
|
||||||
|
@ -128,6 +136,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,
|
||||||
|
@ -137,17 +146,18 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users_collections (user_uuid, collection_uuid) {
|
users_collections (user_uuid, collection_uuid) {
|
||||||
user_uuid -> Varchar,
|
user_uuid -> Text,
|
||||||
collection_uuid -> Varchar,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users_organizations (uuid) {
|
users_organizations (uuid) {
|
||||||
uuid -> Varchar,
|
uuid -> Text,
|
||||||
user_uuid -> Varchar,
|
user_uuid -> Text,
|
||||||
org_uuid -> Varchar,
|
org_uuid -> Text,
|
||||||
access_all -> Bool,
|
access_all -> Bool,
|
||||||
akey -> Text,
|
akey -> Text,
|
||||||
status -> Integer,
|
status -> Integer,
|
||||||
|
|
|
@ -20,8 +20,8 @@ table! {
|
||||||
notes -> Nullable<Text>,
|
notes -> Nullable<Text>,
|
||||||
fields -> Nullable<Text>,
|
fields -> Nullable<Text>,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
favorite -> Bool,
|
|
||||||
password_history -> Nullable<Text>,
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,13 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
favorites (user_uuid, cipher_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
folders (uuid) {
|
folders (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@ -109,6 +116,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>,
|
||||||
|
@ -128,6 +136,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,
|
||||||
|
@ -140,6 +149,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ table! {
|
||||||
notes -> Nullable<Text>,
|
notes -> Nullable<Text>,
|
||||||
fields -> Nullable<Text>,
|
fields -> Nullable<Text>,
|
||||||
data -> Text,
|
data -> Text,
|
||||||
favorite -> Bool,
|
|
||||||
password_history -> Nullable<Text>,
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,13 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
favorites (user_uuid, cipher_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
folders (uuid) {
|
folders (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@ -109,6 +116,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>,
|
||||||
|
@ -128,6 +136,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,
|
||||||
|
@ -140,6 +149,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
39
src/error.rs
39
src/error.rs
|
@ -7,7 +7,6 @@ macro_rules! make_error {
|
||||||
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
|
||||||
const BAD_REQUEST: u16 = 400;
|
const BAD_REQUEST: u16 = 400;
|
||||||
|
|
||||||
#[derive(Display)]
|
|
||||||
pub enum ErrorKind { $($name( $ty )),+ }
|
pub enum ErrorKind { $($name( $ty )),+ }
|
||||||
pub struct Error { message: String, error: ErrorKind, error_code: u16 }
|
pub struct Error { message: String, error: ErrorKind, error_code: u16 }
|
||||||
|
|
||||||
|
@ -35,6 +34,9 @@ macro_rules! make_error {
|
||||||
}
|
}
|
||||||
|
|
||||||
use diesel::result::Error as DieselErr;
|
use diesel::result::Error as DieselErr;
|
||||||
|
use diesel::ConnectionError as DieselConErr;
|
||||||
|
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;
|
||||||
|
@ -42,13 +44,16 @@ use reqwest::Error as ReqErr;
|
||||||
use serde_json::{Error as SerdeErr, Value};
|
use serde_json::{Error as SerdeErr, Value};
|
||||||
use std::io::Error as IOErr;
|
use std::io::Error as IOErr;
|
||||||
|
|
||||||
use std::option::NoneError as NoneErr;
|
|
||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use u2f::u2ferror::U2fError as U2fErr;
|
use u2f::u2ferror::U2fError as U2fErr;
|
||||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||||
use lettre::smtp::error::Error as LettreErr;
|
|
||||||
|
|
||||||
#[derive(Display, Serialize)]
|
use lettre::address::AddressError as AddrErr;
|
||||||
|
use lettre::error::Error as LettreErr;
|
||||||
|
use lettre::message::mime::FromStrError as FromStrErr;
|
||||||
|
use lettre::transport::smtp::Error as SmtpErr;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
pub struct Empty {}
|
pub struct Empty {}
|
||||||
|
|
||||||
// Error struct
|
// Error struct
|
||||||
|
@ -64,6 +69,7 @@ make_error! {
|
||||||
// Used for special return values, like 2FA errors
|
// Used for special return values, like 2FA errors
|
||||||
JsonError(Value): _no_source, _serialize,
|
JsonError(Value): _no_source, _serialize,
|
||||||
DbError(DieselErr): _has_source, _api_error,
|
DbError(DieselErr): _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,
|
||||||
|
@ -74,14 +80,14 @@ make_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,
|
||||||
YubiError(YubiErr): _has_source, _api_error,
|
YubiError(YubiErr): _has_source, _api_error,
|
||||||
LetreErr(LettreErr): _has_source, _api_error,
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
|
LettreError(LettreErr): _has_source, _api_error,
|
||||||
impl From<NoneErr> for Error {
|
AddressError(AddrErr): _has_source, _api_error,
|
||||||
fn from(_: NoneErr) -> Self {
|
SmtpError(SmtpErr): _has_source, _api_error,
|
||||||
Error::from(("NoneError", String::new()))
|
FromStrError(FromStrErr): _has_source, _api_error,
|
||||||
}
|
|
||||||
|
DieselConError(DieselConErr): _has_source, _api_error,
|
||||||
|
DieselMigError(DieselMigErr): _has_source, _api_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Error {
|
impl std::fmt::Debug for Error {
|
||||||
|
@ -118,7 +124,7 @@ impl Error {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_code(mut self, code: u16) -> Self {
|
pub const fn with_code(mut self, code: u16) -> Self {
|
||||||
self.error_code = code;
|
self.error_code = code;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -146,7 +152,7 @@ impl<S> MapResult<S> for Option<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _has_source<T>(e: T) -> Option<T> {
|
const fn _has_source<T>(e: T) -> Option<T> {
|
||||||
Some(e)
|
Some(e)
|
||||||
}
|
}
|
||||||
fn _no_source<T, S>(_: T) -> Option<S> {
|
fn _no_source<T, S>(_: T) -> Option<S> {
|
||||||
|
@ -185,6 +191,7 @@ 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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -204,9 +211,11 @@ 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));
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
@ -234,10 +243,10 @@ macro_rules! err_json {
|
||||||
macro_rules! err_handler {
|
macro_rules! err_handler {
|
||||||
($expr:expr) => {{
|
($expr:expr) => {{
|
||||||
error!(target: "auth", "Unauthorized Error: {}", $expr);
|
error!(target: "auth", "Unauthorized Error: {}", $expr);
|
||||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
|
return ::rocket::request::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
|
||||||
}};
|
}};
|
||||||
($usr_msg:expr, $log_value:expr) => {{
|
($usr_msg:expr, $log_value:expr) => {{
|
||||||
error!(target: "auth", "Unauthorized Error: {}. {}", $usr_msg, $log_value);
|
error!(target: "auth", "Unauthorized Error: {}. {}", $usr_msg, $log_value);
|
||||||
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
|
return ::rocket::request::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user