Compare commits

...

13 Commits

Author SHA1 Message Date
Bevan Hunt 34848f7860 update changelog 2021-04-09 00:05:36 -07:00
Bevan Hunt a2b872651e update readme 2021-04-09 00:03:36 -07:00
Bevan Hunt 14541291dd release 13.0.0 2021-04-09 00:01:39 -07:00
Bevan Hunt 9120532fff update readme 2021-04-08 16:54:14 -07:00
Bevan Hunt 067a83a2ad update to 12.0.1 2021-04-08 16:47:46 -07:00
Bevan Hunt ff3ee38608 update readme 2021-04-08 16:42:33 -07:00
Bevan Hunt e488cc75e0 update to 12.0.0 2021-04-08 16:40:00 -07:00
Bevan Hunt 559cc792e8 release 11.2.0 2021-04-06 20:21:05 -07:00
Bevan Hunt 597b05b6f7 update readme 2021-04-06 17:26:24 -07:00
Bevan Hunt 8eaac0fe9a update to 11.1.0 2021-04-06 17:25:09 -07:00
Bevan Hunt 5a167fba5a update readme 2021-04-05 14:31:05 -07:00
Bevan Hunt d866272f02 update to 11.0.0 2021-04-05 14:30:22 -07:00
Bevan Hunt f8ed649000 update to 10.0.0 2021-04-05 12:11:07 -07:00
5 changed files with 1433 additions and 89 deletions

View File

@ -4,25 +4,96 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [13.0.0] - 2021-04-08
### Added
- Two Factor Auth
- User password reset
### Updated
- Updated README
## [12.0.2] - 2021-04-08
### Updated
- Updated README
## [12.0.1] - 2021-04-08
### Updated
- Updated README
## [12.0.0] - 2021-04-08
### Added
- Added JWT custom scopes on user
### Updated
- Updated README
## [11.2.0] - 2021-04-06
### Added
- Added zxcvbn password strength checker
### Fixed
- Fixed JWT issued not being in JSON format
## [11.1.0] - 2021-04-06
### Added
- Added use your own SSL cert
- Added health check endpoints
### Updated
- Updated README
## [11.0.0] - 2021-04-05
### Added
- Added email field on user
- Added data field on user
- Added user email address validation
- Added update user endpoint
## [10.0.0] - 2021-04-05
### Fixed
- Fixed infinite SSE event sending
### Changed
- Changed RocksDB keys for user and event to be tenanted
- Changed default RocksDB path from tmp to db
- Changed create user endpoint URL
### Added
- Added list users endpoint
- Added revoke user endpoint
- Added get user endpoint
- Added unrevoke user endpoint
### Updated
- Updated README
## [9.1.0] - 2021-04-02
### Added
- verify optional endpoint
- Added verify endpoint
## [9.0.2] - 2021-03-25
### Fixed
- keys on event
- Fixed keys on event
## [9.0.0] - 2021-03-25
### Removed
- removed tenant_name from event - uses users event name
- Removed tenant_name from event - uses users event name
## [8.1.x] - 2021-03-25
### Added
- Adds http basic auth
- Added http basic auth
## [8.0.x] - 2021-03-23
@ -33,10 +104,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [7.0.x] - 2021-03-22
### Added
- Adds tide-acme
- Added tide-acme
## Changed
- Changes command args
- Changed command args
## [6.1.x] - 2021-03-21

543
Cargo.lock generated
View File

@ -1,5 +1,26 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "addr2line"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aead"
version = "0.3.2"
@ -90,6 +111,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "ascii_utils"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]]
name = "async-attributes"
version = "1.1.2"
@ -338,12 +365,32 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
dependencies = [
"addr2line",
"cfg-if 1.0.0",
"libc",
"miniz_oxide 0.4.4",
"object",
"rustc-demangle",
]
[[package]]
name = "base-x"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base32"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]]
name = "base64"
version = "0.12.3"
@ -390,6 +437,21 @@ dependencies = [
"which",
]
[[package]]
name = "bit-set"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.2.1"
@ -459,11 +521,12 @@ dependencies = [
[[package]]
name = "broker"
version = "9.1.0"
version = "13.0.0"
dependencies = [
"anyhow",
"async-std",
"base64 0.13.0",
"chbs",
"driftwood",
"futures",
"go-flag",
@ -471,6 +534,7 @@ dependencies = [
"json",
"jsonwebtoken",
"lazy_static",
"mailchecker",
"nippy",
"rmp-serde",
"rocksdb",
@ -481,7 +545,9 @@ dependencies = [
"tide",
"tide-acme",
"tide-rustls",
"totp-rs",
"uuid",
"zxcvbn",
]
[[package]]
@ -500,6 +566,12 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "bytemuck"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -542,17 +614,36 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chbs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3553a1f278b0090017b37d0f2e4878ee1de65439626bd51030ef2bd97ebdf7f7"
dependencies = [
"derive_builder",
"failure",
"rand 0.8.3",
]
[[package]]
name = "checked_int_cast"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"js-sys",
"libc",
"num-integer",
"num-traits",
"serde",
"time 0.1.44",
"wasm-bindgen",
"winapi",
]
@ -585,12 +676,18 @@ dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"strsim 0.8.0",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colored"
version = "2.0.0"
@ -661,6 +758,49 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
[[package]]
name = "crc32fast"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
"lazy_static",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.1"
@ -727,6 +867,41 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9"
[[package]]
name = "darling"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.9.3",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "dashmap"
version = "4.0.2"
@ -743,6 +918,16 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "der-oid-macro"
version = "0.4.0"
@ -768,6 +953,31 @@ dependencies = [
"rusticata-macros",
]
[[package]]
name = "derive_builder"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
dependencies = [
"darling",
"derive_builder_core",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.9.0"
@ -795,6 +1005,12 @@ dependencies = [
"tide",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "env_logger"
version = "0.7.1"
@ -814,6 +1030,47 @@ version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
[[package]]
name = "failure"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
dependencies = [
"backtrace",
"failure_derive",
]
[[package]]
name = "failure_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "fancy-regex"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36996e5f56f32ca51a937f325094fa450b32df871af1a89be331b7145b931bfc"
dependencies = [
"bit-set",
"regex",
]
[[package]]
name = "fast_chemail"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
dependencies = [
"ascii_utils",
]
[[package]]
name = "fastrand"
version = "1.4.0"
@ -839,6 +1096,12 @@ dependencies = [
"web-sys",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@ -1004,6 +1267,22 @@ dependencies = [
"polyval",
]
[[package]]
name = "gif"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce"
[[package]]
name = "glob"
version = "0.3.0"
@ -1118,9 +1397,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
dependencies = [
"quick-error",
"quick-error 1.2.3",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.2.2"
@ -1132,6 +1417,25 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"jpeg-decoder",
"num-iter",
"num-rational",
"num-traits",
"png",
"scoped_threadpool",
"tiff",
]
[[package]]
name = "infer"
version = "0.2.3"
@ -1147,6 +1451,15 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.7"
@ -1162,6 +1475,15 @@ dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.50"
@ -1263,6 +1585,15 @@ dependencies = [
"value-bag",
]
[[package]]
name = "mailchecker"
version = "4.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be9884d8adeab79160bc07b42bce71324406c4792fcf9e785476dbc796d3d55"
dependencies = [
"fast_chemail",
]
[[package]]
name = "matches"
version = "0.1.8"
@ -1275,6 +1606,34 @@ version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "memoffset"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "nb-connect"
version = "1.1.0"
@ -1354,6 +1713,28 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
@ -1373,6 +1754,12 @@ dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
[[package]]
name = "oid-registry"
version = "0.1.1"
@ -1461,6 +1848,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
dependencies = [
"bitflags",
"crc32fast",
"deflate",
"miniz_oxide 0.3.7",
]
[[package]]
name = "polling"
version = "2.0.3"
@ -1512,12 +1911,28 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "qrcode"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f"
dependencies = [
"checked_int_cast",
"image",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda"
[[package]]
name = "quote"
version = "1.0.9"
@ -1614,6 +2029,31 @@ dependencies = [
"rand_core 0.6.2",
]
[[package]]
name = "rayon"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
name = "rcgen"
version = "0.8.9"
@ -1707,6 +2147,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rustc-demangle"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
[[package]]
name = "rustc-hash"
version = "1.1.0"
@ -1780,6 +2226,18 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
@ -1860,6 +2318,19 @@ dependencies = [
"serde",
]
[[package]]
name = "sha-1"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
"cpuid-bool 0.1.2",
"digest",
"opaque-debug",
]
[[package]]
name = "sha1"
version = "0.6.0"
@ -2031,6 +2502,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "subtle"
version = "2.4.0"
@ -2054,6 +2531,18 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "synstructure"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "tap"
version = "1.0.1"
@ -2147,6 +2636,17 @@ dependencies = [
"tide",
]
[[package]]
name = "tiff"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
dependencies = [
"jpeg-decoder",
"miniz_oxide 0.4.4",
"weezl",
]
[[package]]
name = "time"
version = "0.1.44"
@ -2211,6 +2711,22 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "totp-rs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71aa19b1e6a6bdf8c212498da1a8e54c50ec7c22da9862dea576910616d1137e"
dependencies = [
"base32",
"base64 0.13.0",
"byteorder",
"hmac 0.8.1",
"image",
"qrcode",
"sha-1",
"sha2",
]
[[package]]
name = "typenum"
version = "1.13.0"
@ -2429,6 +2945,12 @@ dependencies = [
"webpki",
]
[[package]]
name = "weezl"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a32b378380f4e9869b22f0b5177c68a5519f03b3454fde0b291455ddbae266c"
[[package]]
name = "wepoll-sys"
version = "3.0.1"
@ -2510,3 +3032,18 @@ checksum = "0de7bff972b4f2a06c85f6d8454b09df153af7e3a4ec2aac81db1b105b684ddb"
dependencies = [
"chrono",
]
[[package]]
name = "zxcvbn"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f9db3a05b2ee81dcda4602487314ab654eca316f517be2e2e64175658f0dd0"
dependencies = [
"chrono",
"derive_builder",
"fancy-regex",
"itertools",
"lazy_static",
"quick-error 2.0.0",
"regex",
]

View File

@ -1,6 +1,6 @@
[package]
name = "broker"
version = "9.1.0"
version = "13.0.0"
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
edition = "2018"
license = "MIT"
@ -12,14 +12,14 @@ readme = "README.md"
[dependencies]
serde_json = "1"
tide = "0.16.0"
tide = "0.16"
async-std = { version = "1.9", features = ["attributes"] }
serde = { version = "1", features = ["derive"] }
serde_derive = "1"
json = "0.12"
rocksdb = "0.15"
uuid = { version = "0.8", features = ["serde", "v4"] }
jsonwebtoken = "7.0.1"
jsonwebtoken = "7"
go-flag = "0.1"
lazy_static = "1.4"
nippy = "2"
@ -32,3 +32,7 @@ tide-rustls = "0.3"
futures = "0.3"
tide-acme = "0.1.0"
base64 = "0.13"
mailchecker = "4"
zxcvbn = "2"
totp-rs = { version = "0.6", features = ["qr"] }
chbs = "0.1"

292
README.md
View File

@ -15,19 +15,25 @@ Broker follows an insert-only/publish/subscribe paradigm rather than a REST CRUD
### Features
* Very performant with almost no CPU and memory usage
* Under 500 lines of code
* Under 1000 lines of code
* Secure Real-time Event Stream via SSE - requires the use of [broker-client](https://www.npmjs.com/package/broker-client)
* Supports CORS
* JSON API
* Add users with admin token permission
* Multi-tenant
* Supports SSL - full end-to-end encryption
* Provides user authentication with JWTs or HTTP Basic
* Secure passwords with Argon2 encoding
* Uses Global NTP servers and doesn't rely on your local server time
* Insert event via JSON POST request
* Sync latest events on SSE client connection
* Auto-provision and renews SSL cert via LetsEncrypt
* Issues JWTs for authentication (username) and authorization (scopes) for external services
* Verify endpoint for external services using Broker user system like [portal](https://crates.io/crates/portal)
* Secure password storage with Argon2 encoding
* Uses Global NTP servers and doesn't rely on your local server time for JWT expiry timing and Two Factor timing
* Sync latest events on SSE client connection
* Auto-provision and renews SSL cert via LetsEncrypt or use your own SSL cert
* User Management API endpoints (create, revoke, unrevoke, list, get, update)
* User Email Address Validation (regex and blacklist check against throwaway emails) using [mailchecker](https://crates.io/crates/mailchecker)
* Password Strength Checker using [zxcvbn](https://crates.io/crates/zxcvbn)
* Two Factor Authenication with QR code generation for Google Authenticator, Authy, etc.
* Secure user password resets with a TOTP with a configurable time duration
### How it works
@ -44,9 +50,11 @@ The side-effect of this system is that the latest event is the schema. This is p
* [React Debounce Input](https://www.npmjs.com/package/react-debounce-input) - React input for Real-time Submission (Edit in Place forms)
### Broker FAQ
* Why compete against Parse Server and Firebase?
* Why compete against Parse Server, Auth0, and Firebase?
[Firebase](https://firebase.google.com/) is not open-source, is not free, and has complicated pricing. [Parse Server](https://github.com/parse-community/parse-server) doesn't have real-time features and is about 30,000 LOC of JS.
* [Firebase](https://firebase.google.com/) is not open-source, is not free, and has complicated pricing.
* [Parse Server](https://github.com/parse-community/parse-server) doesn't have real-time features and is about 30,000 LOC of JS.
* [Auth0](https://auth0.com) is not open-source, is not free, and is expensive.
* Will broker work with mobile apps?
@ -57,15 +65,26 @@ Yes with React Native. There may be native 3rd party libraries for SSE that work
#### Step 1 - create a user
```html
POST /users
POST /create_user
```
- public endpoint
- POST JSON to create a user
```json
{"username":{...}, "password":{...}, "admin_token":{...}, "tenant_name":{...}}
{
"username": "bob",
"password": "password1",
"admin_token": "letmein",
"tenant_name": "tenant_1",
"email": "bob@hotmail.com",
"two_factor": true,
"scopes": ["news:get", "news:post"],
"data": {
"name": "Robert Wieland",
"image": "https://img.com/bucket/123/123.jpg"
}
}
```
- where {...} is for username is a string, password is a string, admin_token is a string, and tenant_name is a string
- admin_token is required and can be set in the command args - it is for not allowing everyone to add a user - the default is `letmein`
- `admin_token` is required and can be set in the command args - it is for not allowing everyone to add a user - the default is `letmein`
- `email`, `scopes`, `two_factor`, and `data` are optional fields
will return `200` or `500` or `400`
@ -75,17 +94,23 @@ will return `200` or `500` or `400`
POST /login
```
- public endpoint
- POST JSON to login
```json
{"username":{...}, "password":{...}}
{
"username": "bob",
"password": "password1",
"totp": "123456",
}
```
- where {...} is for username is a string and password is a string
- `totp` is required if two factor is enabled for the user - if not the field can be omitted
will return
```json
{"jwt":{...}}
{
"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc2NzQ5MTUsImlhdCI6MTYxNzU4ODUxNSwiaXNzIjoiRGlzcGF0Y2hlciIsInN1YiI6ImZvbyJ9.OwiaZJcFUC_B0CA0ffRZVTWKRf5_vQ7vt5USNJEeKRE"
}
```
- where {...} is a JWT (string)
- note: `iat` is the issue time, `exp` is the expiry time, `sub` is the username, `iss` is `Broker`, while `aud` is the user scopes joined with a comma like in this example `news:get,news:post`
- note: if you need to debug your JWT then visit [jwt.io](https://jwt.io)
#### Step 3 - connect to SSE
@ -102,11 +127,15 @@ GET /sse
POST /insert
```
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {username:password})
- POST JSON to insert an event
```json
{"event":{...}, "data":{...}}
{
"event": "test",
"data": {
"name": "robert",
"image": "https://img.com/bucket/123/123.jpg"
}
}
```
- where {...} is for the event a string and data is any JSON you want
will return: `200` or `500` or `400` or `401`
@ -120,21 +149,221 @@ GET /verify
will return: `200` or `500` or `401`
#### Optional - revoke user
```html
POST /revoke_user
```
- public endpoint
```json
{
"admin_token": "letmein",
"username": "bob"
}
```
will return: `200` or `500` or `400` or `401`
- note: revoked users cannot login
#### Optional - unrevoke user
```html
POST /unrevoke_user
```
- public endpoint
```json
{
"admin_token": "letmein",
"username": "bob"
}
```
will return: `200` or `500` or `400` or `401`
#### Optional - list users
```html
POST /list_users
```
- public endpoint
```json
{
"admin_token": "letmein",
}
```
will return: `200` or `500` or `400` or `401`
200 - will return an array of objects
```json
[
{
"id": "69123c04-fa42-4193-a6c5-ab2fc27658b1",
"password": "***",
"totp": "***",
"revoked": false,
"tenant_name": "tenant_1",
"username": "bob",
"email": "bob@hotmail.com",
"scopes": ["news:get", "news:post"],
"data": {
"name": "Robert Wieland",
"image": "https://img.com/bucket/123/123.jpg"
}
}
]
```
- note: `email`, `scopes`, `two_factor`, and `data` can be `null`
#### Optional - get user
```html
POST /get_user
```
- public endpoint
```json
{
"admin_token": "letmein",
"username": "bob"
}
```
will return: `200` or `500` or `400` or `401`
200 - will return an array of objects
```json
{
"id": "69123c04-fa42-4193-a6c5-ab2fc27658b1",
"password": "***",
"totp": "***",
"revoked": false,
"tenant_name": "tenant_1",
"username": "bob",
"email": "bob@hotmail.com",
"scopes": ["news:get", "news:post"],
"data": {
"name": "Robert Wieland",
"image": "https://img.com/bucket/123/123.jpg"
}
}
```
- note: `email`, `scopes`, `two_factor`, and `data` can be `null`
#### Optional - update user
```html
POST /update_user
```
- public endpoint
```json
{
"admin_token": "letmein",
"username": "bob",
"tenant_name": "tenant_2",
"password": "new_password",
"email": "bober@hotmail.com",
"scopes": ["news:get", "news:post"],
"data": {
"name": "Robert Falcon",
"image": "https://img.com/bucket/123/1234.jpg"
}
}
```
- note: `tenant_name`, `password`, `email`, `scopes`, `data` are optional fields
will return: `200` or `500` or `400` or `401`
#### Optional - Health Check
```html
GET or HEAD /
```
- public endpoint
will return: `200`
#### Optional - generate two factor QR Code
```html
POST /create_qr
```
- public endpoint
```json
{
"issuer": "Broker",
"admin_token": "letmein",
"username": "bob"
}
```
- note: put the name of your application in the issuer field
- note: the ID of the QR will be the user's username and your issuer field
will return: `200` or `500` or `400` or `401`
200 - will return the qr code in PNG format in base64
```json
{
"qr": "dGhpc2lzYXN0cmluZw=="
}
```
#### Optional - create totp
```html
POST /create_totp
```
- public endpoint
```json
{
"admin_token": "letmein",
"username": "bob",
}
```
will return: `200` or `500` or `400` or `401`
200 - will return the totp
```json
{
"totp": "622346"
}
```
- note: these TOTPs can only be used with the password reset endpoint
#### Optional - user password reset
```html
POST /password_reset
```
- public endpoint
```json
{
"totp": "622346",
"username": "bob",
"password": "password1"
}
```
will return: `200` or `500` or `400` or `401`
### Install
``` cargo install broker ```
- the origin can be passed in as a flag - default `*`
- the port can be passed in as a flag - default `8080` - can only be set for unsecure connections
- the jwt_expiry (for jwts) can be passed in as a flag - default `86400`
- the jwt_secret (for jwts) should be passed in as a flag - default `secret`
- the secure flag (https) and can be true or false - default `false`
- the certs flag is the storage path of LetsEncrypt certs - default `certs`
- the db flag is the path where the embedded database will be saved - default `tmp`
- the domain flag is the domain name (e.g. api.broker.com) of the domain you want to register with LetsEncrypt - must be fully resolvable
- the admin_token flag is the password for the admin to add users - default `letmein`
- production example: `./broker --secure="true" --admin_token"23ce4234@123$" --jwt_secret="xTJEX234$##$" --domain="api.broker.com"`
- the `origin` can be passed in as a flag - default `*`
- the `port` can be passed in as a flag - default `8080` - can only be set for unsecure connections
- the `jwt_expiry` for jwts can be passed in as a flag - default `86400`
- the `jwt_secret` for jwts should be passed in as a flag - default `secret`
- the `secure` flag for https and can be true or false - default `false`
- the `auto_cert` flag for an autorenewing LetsEncrypt SSL cert can be true or false - requires a resolvable domain - default `true`
- the `key_path` flag when `auto_cert` is `false` to set the SSL key path for your own cert - default `certs/private_key.pem`
- the `cert_path` flag when `auto_cert` is `false` to set the SSL cert path for your own cert - default `certs/chain.pem`
- the `certs` flag is the storage path of LetsEncrypt certs - default `certs`
- the `db` flag is the path where the embedded database will be saved - default `db`
- the `domain` flag is the domain name (e.g. api.broker.com) of the domain you want to register with LetsEncrypt - must be fully resolvable
- the `admin_token` flag is the password for the admin to add users - default `letmein`
- the `password_checker` flag enables zxcvbn password checking - default `false`
- the `totp_duration` flag is the duration of the TOTP for user generated password reset - default 300 seconds (5 min)
- production example: `./broker --secure="true" --admin_token"23ce4234@123$" --jwt_secret="xTJEX234$##$" --domain="api.broker.com" --password_checker="true"`
### Service
@ -147,6 +376,7 @@ There is an example `systemctl` service for Ubuntu called `broker.service` in th
### Inspiration
* [Auth0](https://auth0.com)
* [React Hooks](https://reactjs.org/docs/hooks-intro.html)
* [Meteor](https://meteor.com)
* [MongoDB](https://www.mongodb.com/)

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, iter::Iterator};
use std::{collections::HashMap, convert::TryInto, iter::Iterator};
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use uuid::Uuid;
@ -14,6 +14,10 @@ use async_std::stream;
use std::time::Duration;
use futures::StreamExt;
use tide_acme::{AcmeConfig, TideRustlsExt};
use mailchecker::is_valid;
use zxcvbn::zxcvbn;
use chbs::{config::BasicConfig, prelude::*};
use totp_rs::{Algorithm, TOTP};
lazy_static! {
static ref DB : Arc<rocksdb::DB> = {
@ -41,28 +45,86 @@ pub struct EnvVarConfig {
pub certs: String,
pub domain: String,
pub admin_token: String,
pub auto_cert: bool,
pub key_path: String,
pub cert_path: String,
pub password_checker: bool,
pub totp_duration: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User {
id: uuid::Uuid,
username: String,
password: String,
tenant_name: String,
pub id: uuid::Uuid,
pub revoked: bool,
pub email: Option<String>,
pub username: String,
pub password: String,
pub tenant_name: String,
pub data: Option<serde_json::Value>,
pub scopes: Option<Vec<String>>,
pub totp: String,
pub two_factor: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserForm {
username: String,
password: String,
tenant_name: String,
admin_token: String,
pub username: String,
pub password: String,
pub tenant_name: String,
pub admin_token: String,
pub email: Option<String>,
pub data: Option<serde_json::Value>,
pub scopes: Option<Vec<String>>,
pub two_factor: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdateUserForm {
pub username: String,
pub tenant_name: Option<String>,
pub admin_token: String,
pub email: Option<String>,
pub password: Option<String>,
pub data: Option<serde_json::Value>,
pub scopes: Option<Vec<String>>,
pub two_factor: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminTokenForm {
pub admin_token: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateQRForm {
pub issuer: String,
pub admin_token: String,
pub username: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateTOTPForm {
pub admin_token: String,
pub username: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PasswordResetForm {
pub totp: String,
pub password: String,
pub username: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RevokeUserForm {
pub admin_token: String,
pub username: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LoginForm {
username: String,
password: String,
pub username: String,
pub password: String,
pub totp: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -71,6 +133,7 @@ pub struct Claims {
pub iat: i64,
pub iss: String,
pub sub: String,
pub aud: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@ -85,8 +148,8 @@ pub struct Event {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EventForm {
event: String,
data: serde_json::Value
pub event: String,
pub data: serde_json::Value,
}
fn replace(key: String, value: Vec<u8>) -> Result<()> {
@ -94,14 +157,105 @@ fn replace(key: String, value: Vec<u8>) -> Result<()> {
Ok(())
}
fn soft_delete_user(username: String) -> Result<()> {
match get_user_by_username(username)? {
Some(mut user) => {
user.revoked = true;
puts_user(user)?;
Ok(())
},
None => { Ok(()) }
}
}
fn activate_user(username: String) -> Result<()> {
match get_user_by_username(username)? {
Some(mut user) => {
user.revoked = false;
puts_user(user)?;
Ok(())
},
None => { Ok(()) }
}
}
fn modify_user(update_user_form: UpdateUserForm) -> Result<Option<String>> {
match get_user_by_username(update_user_form.clone().username)? {
Some(mut user) => {
match update_user_form.tenant_name {
Some(tn) => {
user.tenant_name = tn;
},
None => {}
}
match update_user_form.email {
Some(email) => {
if is_valid(&email) {
user.email = Some(email);
}
},
None => {}
}
user.data = update_user_form.data;
user.scopes = update_user_form.scopes;
user.two_factor = update_user_form.two_factor;
match update_user_form.password {
Some(password) => {
let configure = env_var_config();
if configure.password_checker {
let estimate = zxcvbn(&password, &[&user.username]).unwrap();
if estimate.score() < 3 {
let err: String;
match estimate.feedback() {
Some(feedback) => {
match feedback.warning() {
Some(warning) => {
err = format!("password is too weak because {}", warning);
},
None => {
err = format!("password is too weak");
}
}
},
None => {
err = format!("password is too weak");
}
}
let j = json!({"error": err}).to_string();
return Ok(Some(j));
}
}
let config = Argon2Config::default();
let uuid_string = Uuid::new_v4().to_string();
let salt = uuid_string.as_bytes();
let password = password.as_bytes();
let hashed = argon2::hash_encoded(password, salt, &config).unwrap();
user.password = hashed;
},
None => {}
}
puts_user(user)?;
Ok(None)
},
None => { Ok(None) }
}
}
fn get_user_by_username(user_username: String) -> Result<Option<User>> {
let users = get_users()?;
Ok(users.into_iter().filter(|user| user.username == user_username).last())
}
fn get_users() -> Result<Vec<User>> {
let prefix = "users".as_bytes();
let i = DB.prefix_iterator(prefix);
let prefix = "users".to_string();
let i = DB.prefix_iterator(prefix.as_bytes());
let res : Vec<User> = i.map(|(_, v)| {
let data: User = rmp_serde::from_read_ref(&v).unwrap();
data
@ -110,7 +264,7 @@ fn get_users() -> Result<Vec<User>> {
}
fn puts_user(user: User) -> Result<()> {
let key = format!("users_{}", user.id);
let key = format!("users_{}", user.username);
let value = rmp_serde::to_vec_named(&user)?;
replace(key, value)?;
Ok(())
@ -126,9 +280,20 @@ fn is_user_unique(user_username: String) -> Result<bool> {
Ok(true)
}
fn get_events() -> Result<Vec<Event>> {
let prefix = "events".as_bytes();
let i = DB.prefix_iterator(prefix);
fn get_events(tenant_name: Option<String>) -> Result<Vec<Event>> {
let prefix: String;
match tenant_name {
Some(tn) => {
prefix = format!("events_{}", tn);
},
None => {
prefix = format!("events");
}
}
let i = DB.prefix_iterator(prefix.as_bytes());
let res : Vec<Event> = i.map(|(_, v)| {
let data: Event = rmp_serde::from_read_ref(&v).unwrap();
data
@ -137,7 +302,7 @@ fn get_events() -> Result<Vec<Event>> {
}
fn puts_event(event: Event) -> Result<()> {
let key = format!("events_{}", event.id);
let key = format!("events_{}_{}", event.tenant_name, event.event);
let value = rmp_serde::to_vec_named(&event)?;
replace(key, value)?;
Ok(())
@ -148,19 +313,69 @@ fn user_create(user_form: UserForm) -> Result<Option<String>> {
let configure = env_var_config();
if configure.admin_token == user_form.clone().admin_token {
match user_form.clone().email {
Some(email) => {
if !is_valid(&email) {
let j = json!({"error": "email is invalid"}).to_string();
return Ok(Some(j));
}
},
None => {}
}
if !is_user_unique(user_form.clone().username)? {
let j = json!({"error": "username already taken"}).to_string();
return Ok(Some(j));
} else {
// set as future value
if configure.password_checker {
let estimate = zxcvbn(&user_form.password, &[&user_form.username]).unwrap();
if estimate.score() < 3 {
let err: String;
match estimate.feedback() {
Some(feedback) => {
match feedback.warning() {
Some(warning) => {
err = format!("password is too weak because {}", warning);
},
None => {
err = format!("password is too weak");
}
}
},
None => {
err = format!("password is too weak");
}
}
let j = json!({"error": err}).to_string();
return Ok(Some(j));
}
}
let mut config = BasicConfig::default();
config.words = 12;
config.separator = "-".into();
let scheme = config.to_scheme();
let totp = scheme.generate();
let uuid = Uuid::new_v4();
let config = Argon2Config::default();
let uuid_string = Uuid::new_v4().to_string();
let salt = uuid_string.as_bytes();
let password = user_form.password.as_bytes();
let hashed = argon2::hash_encoded(password, salt, &config).unwrap();
let new_user = User{id: uuid, username: user_form.clone().username, password: hashed, tenant_name: user_form.clone().tenant_name };
let new_user = User{
id: uuid,
username: user_form.clone().username,
password: hashed,
tenant_name: user_form.clone().tenant_name,
revoked: false,
email: user_form.clone().email,
data: user_form.clone().data,
scopes: user_form.clone().scopes,
totp,
two_factor: user_form.clone().two_factor,
};
puts_user(new_user).unwrap();
return Ok(None);
}
@ -172,18 +387,82 @@ fn user_create(user_form: UserForm) -> Result<Option<String>> {
async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let user_value = get_user_by_username(login.username)?;
match user_value {
match get_user_by_username(login.username)? {
Some(user) => {
let verified = argon2::verify_encoded(&user.password, login.password.as_bytes())?;
if verified {
let app = env_var_config();
let iat = nippy::get_unix_ntp_time().await?;
let exp = iat + app.jwt_expiry;
let iss = "Dispatcher".to_string();
let my_claims = Claims{sub: user.clone().username, exp, iat, iss};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?;
Ok(Some(token))
if !user.revoked {
let verified = argon2::verify_encoded(&user.password, login.password.as_bytes())?;
if verified {
match user.two_factor {
Some(two_factor) => {
if two_factor {
match login.totp {
Some(token) => {
let totp = TOTP::new(
Algorithm::SHA512,
6,
1,
30,
user.clone().totp,
);
let time = nippy::get_unix_ntp_time().await?;
if totp.check(&token, time.try_into()?) {
let app = env_var_config();
let iat = nippy::get_unix_ntp_time().await?;
let exp = iat + app.jwt_expiry;
let iss = "Broker".to_string();
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
},
None => { aud = "".to_string() }
}
let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?;
Ok(Some(token))
} else {
Ok(None)
}
},
None => { Ok(None) }
}
} else {
let app = env_var_config();
let iat = nippy::get_unix_ntp_time().await?;
let exp = iat + app.jwt_expiry;
let iss = "Broker".to_string();
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
},
None => { aud = "".to_string() }
}
let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?;
Ok(Some(token))
}
},
None => {
let app = env_var_config();
let iat = nippy::get_unix_ntp_time().await?;
let exp = iat + app.jwt_expiry;
let iss = "Broker".to_string();
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
},
None => { aud = "".to_string() }
}
let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?;
Ok(Some(token))
}
}
} else {
Ok(None)
}
} else {
Ok(None)
}
@ -197,12 +476,17 @@ fn env_var_config() -> EnvVarConfig {
let mut port : u16 = 8080;
let mut jwt_expiry : i64 = 86400;
let mut secure = false;
let mut auto_cert = true;
let mut origin = "*".to_string();
let mut jwt_secret = "secret".to_string();
let mut db: String = "tmp".to_string();
let mut db: String = "db".to_string();
let mut certs = "certs".to_string();
let mut domain = "localhost".to_string();
let mut admin_token = "letmein".to_string();
let mut key_path = "certs/private_key.pem".to_string();
let mut cert_path = "certs/chain.pem".to_string();
let mut password_checker = false;
let mut totp_duration: u64 = 300;
let _ : Vec<String> = go_flag::parse(|flags| {
flags.add_flag("port", &mut port);
flags.add_flag("origin", &mut origin);
@ -213,9 +497,14 @@ fn env_var_config() -> EnvVarConfig {
flags.add_flag("domain", &mut domain);
flags.add_flag("certs", &mut certs);
flags.add_flag("admin_token", &mut admin_token);
flags.add_flag("auto_cert", &mut auto_cert);
flags.add_flag("key_path", &mut key_path);
flags.add_flag("cert_path", &mut cert_path);
flags.add_flag("password_checker", &mut password_checker);
flags.add_flag("totp_duration", &mut totp_duration);
});
EnvVarConfig{port, origin, jwt_expiry, jwt_secret, secure, domain, certs, db, admin_token}
EnvVarConfig{port, origin, jwt_expiry, jwt_secret, secure, domain, certs, db, admin_token, auto_cert, key_path, cert_path, password_checker, totp_duration}
}
async fn jwt_verify(token: String) -> Result<Option<TokenData<Claims>>> {
@ -247,8 +536,15 @@ async fn jwt_verify(token: String) -> Result<Option<TokenData<Claims>>> {
let app = env_var_config();
let iat = nippy::get_unix_ntp_time().await?;
let exp = iat + app.jwt_expiry;
let iss = "Dispatcher".to_string();
let my_claims = Claims{sub: user.clone().username, exp, iat, iss};
let iss = "Broker".to_string();
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
},
None => { aud = "".to_string() }
}
let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud};
let my_token = TokenData{
header: Header::default(),
claims: my_claims,
@ -291,7 +587,6 @@ async fn create_user(mut req: Request<()>) -> tide::Result {
let user_form : UserForm = serde_json::from_str(&r)?;
match user_create(user_form)? {
Some(err) => {
let err = format!("error: {}", err);
Ok(tide::Response::builder(400).body(err).header("content-type", "application/json").build())
},
None => {
@ -305,7 +600,7 @@ async fn login_user(mut req: Request<()>) -> tide::Result {
let login_form : LoginForm = serde_json::from_str(&r)?;
match create_jwt(login_form).await? {
Some(jwt) => {
let msg = format!("jwt: {}", jwt);
let msg = json!({"jwt": jwt}).to_string();
Ok(tide::Response::builder(200).body(msg).header("content-type", "application/json").build())
},
None => {
@ -363,6 +658,193 @@ async fn verify_user(req: Request<()>) -> tide::Result {
}
}
async fn get_user(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let admin_token_form : AdminTokenForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if configure.admin_token == admin_token_form.admin_token {
let users = get_users()?;
let users: Vec<_> = users.iter().map(|user| {
let mut u = user.to_owned();
u.password = "***".to_string();
u.totp = "***".to_string();
u
}).collect();
Ok(tide::Response::builder(200).body(json!(users)).header("content-type", "application/json").build())
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn list_users(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let admin_token_form : AdminTokenForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if configure.admin_token == admin_token_form.admin_token {
let users = get_users()?;
let users: Vec<_> = users.iter().map(|user| {
let mut u = user.to_owned();
u.password = "***".to_string();
u.totp = "***".to_string();
u
}).collect();
Ok(tide::Response::builder(200).body(json!(users)).header("content-type", "application/json").build())
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn revoke_user(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let revoke_user_form : RevokeUserForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if configure.admin_token == revoke_user_form.admin_token {
soft_delete_user(revoke_user_form.username)?;
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn unrevoke_user(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let revoke_user_form : RevokeUserForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if configure.admin_token == revoke_user_form.admin_token {
activate_user(revoke_user_form.username)?;
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn update_user(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let update_user_form : UpdateUserForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if configure.admin_token == update_user_form.admin_token {
match modify_user(update_user_form)? {
Some(err) => {
Ok(tide::Response::builder(400).body(err).header("content-type", "application/json").build())
},
None => {
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
}
}
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn health(_: Request<()>) -> tide::Result {
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
}
async fn create_qr(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let create_qr_form : CreateQRForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if create_qr_form.admin_token == configure.admin_token {
match get_user_by_username(create_qr_form.username)? {
Some(user) => {
let totp = TOTP::new(
Algorithm::SHA512,
6,
1,
30,
user.totp,
);
let code = totp.get_qr(&user.username, &create_qr_form.issuer).unwrap();
let j = json!({"qr": code});
Ok(tide::Response::builder(200).body(j).header("content-type", "application/json").build())
},
None => {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn create_totp(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let create_totp_form : CreateTOTPForm = serde_json::from_str(&r)?;
let configure = env_var_config();
if create_totp_form.admin_token == configure.admin_token {
match get_user_by_username(create_totp_form.username)? {
Some(user) => {
let totp = TOTP::new(
Algorithm::SHA512,
6,
1,
configure.totp_duration,
user.totp,
);
let time = nippy::get_unix_ntp_time().await?;
let token = totp.generate(time.try_into()?);
let j = json!({"totp": token});
Ok(tide::Response::builder(200).body(j).header("content-type", "application/json").build())
},
None => {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
async fn password_reset(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let password_reset_form : PasswordResetForm = serde_json::from_str(&r)?;
let configure = env_var_config();
match get_user_by_username(password_reset_form.username)? {
Some(user) => {
let totp = TOTP::new(
Algorithm::SHA512,
6,
1,
configure.totp_duration,
user.totp,
);
let time = nippy::get_unix_ntp_time().await?;
let check = totp.check(&password_reset_form.totp, time.try_into()?);
if check {
let update_user_form = UpdateUserForm{
username: user.username,
password: Some(password_reset_form.password),
tenant_name: Some(user.tenant_name),
admin_token: configure.admin_token,
email: user.email,
data: user.data,
scopes: user.scopes,
two_factor: user.two_factor,
};
modify_user(update_user_form)?;
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
} else {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
},
None => {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())
}
}
}
#[async_std::main]
async fn main() -> tide::Result<()> {
@ -377,10 +859,20 @@ async fn main() -> tide::Result<()> {
let mut app = tide::new();
app.with(driftwood::DevLogger);
app.with(cors);
app.at("/").get(health);
app.at("/").head(health);
app.at("/insert").post(insert_event);
app.at("/users").post(create_user);
app.at("/create_user").post(create_user);
app.at("/login").post(login_user);
app.at("/verify").get(verify_user);
app.at("/list_users").post(list_users);
app.at("/revoke_user").post(revoke_user);
app.at("/get_user").post(get_user);
app.at("/unrevoke_user").post(unrevoke_user);
app.at("/update_user").post(update_user);
app.at("/create_qr").post(create_qr);
app.at("/create_totp").post(create_totp);
app.at("/password_reset").post(password_reset);
app.at("/sse").get(tide::sse::endpoint(|req: Request<()>, sender| async move {
@ -400,7 +892,7 @@ async fn main() -> tide::Result<()> {
let mut interval = stream::interval(Duration::from_millis(100));
while let Some(_) = interval.next().await {
let events = get_events()?;
let events = get_events(None)?;
for evt in events {
if evt.tenant_name == user.tenant_name {
@ -412,13 +904,16 @@ async fn main() -> tide::Result<()> {
let value_maybe = cache.get_key_value(&evt.event);
match value_maybe {
Some((_, v)) => {
if &evt != v {
let current_data = evt.data.to_string();
let stored_data = v.data.to_string();
if current_data != stored_data {
let id = uuid::Uuid::new_v4();
sender.send(&evt.event, evt.data.to_string(), Some(&id.to_string())).await?;
cache.insert(evt.event.clone(), evt.clone());
}
},
None => { println!("helo"); return Ok(()); }
None => { return Ok(()); }
}
}
}
@ -438,7 +933,7 @@ async fn main() -> tide::Result<()> {
let ip = format!("0.0.0.0:{}", configure.port);
if configure.secure {
if configure.secure && configure.auto_cert {
app.listen(
tide_rustls::TlsListener::build().addrs("0.0.0.0:443").acme(
AcmeConfig::new()
@ -448,10 +943,17 @@ async fn main() -> tide::Result<()> {
),
)
.await?;
} else if configure.secure && !configure.auto_cert {
app.listen(
tide_rustls::TlsListener::build()
.addrs("0.0.0.0:443")
.cert(configure.cert_path)
.key(configure.key_path)
)
.await?;
} else {
app.listen(ip).await?;
}
Ok(())
}