mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Merge branch 'permission-impossible'
This commit is contained in:
commit
ab7f17d5d9
335
Cargo.lock
generated
335
Cargo.lock
generated
|
@ -4,19 +4,13 @@ version = 3
|
|||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
|
@ -180,9 +174,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
|
@ -213,9 +207,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
|
@ -385,7 +379,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -469,17 +463,17 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.73"
|
||||
version = "0.3.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.7.4",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -788,9 +782,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.17.1"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
|
||||
checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
|
@ -800,9 +794,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
|
@ -855,9 +849,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.15"
|
||||
version = "1.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
|
||||
checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
|
@ -962,9 +956,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.16"
|
||||
version = "4.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
|
||||
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
@ -972,9 +966,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.15"
|
||||
version = "4.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
||||
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
@ -1042,7 +1036,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"arc-swap",
|
||||
|
@ -1075,12 +1069,13 @@ dependencies = [
|
|||
"privdrop",
|
||||
"prometheus",
|
||||
"proxy-header",
|
||||
"psl",
|
||||
"pwhash",
|
||||
"rcgen 0.12.1",
|
||||
"regex",
|
||||
"reqwest 0.12.7",
|
||||
"ring 0.17.8",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
|
@ -1176,9 +1171,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -1437,9 +1432,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.0.1"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
|
@ -1650,7 +1645,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "directory"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"argon2",
|
||||
|
@ -1667,9 +1662,10 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"password-hash",
|
||||
"pbkdf2",
|
||||
"proc_macros",
|
||||
"pwhash",
|
||||
"regex",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pki-types",
|
||||
"scrypt",
|
||||
"serde",
|
||||
|
@ -1931,11 +1927,11 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
|
|||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
|
||||
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.77",
|
||||
|
@ -1963,7 +1959,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2037,7 +2033,7 @@ checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
|
|||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-sys",
|
||||
"miniz_oxide 0.8.0",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2353,9 +2349,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
|
@ -2738,20 +2734,20 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.2"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tower-service",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2769,9 +2765,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
|
||||
checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
@ -2789,9 +2785,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
version = "0.1.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
|
@ -2983,7 +2979,7 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
|
|||
|
||||
[[package]]
|
||||
name = "imap"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"common",
|
||||
|
@ -2998,7 +2994,7 @@ dependencies = [
|
|||
"nlp",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"store",
|
||||
"tokio",
|
||||
|
@ -3095,9 +3091,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.9.0"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
|
@ -3195,7 +3191,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "jmap"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
@ -3548,9 +3544,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "lz4-sys"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "109de74d5d2353660401699a4174a4ff23fcc649caf553df71933c7fb45ad868"
|
||||
checksum = "fcb44a01837a858d47e5a630d2ccf304c8efcc4b83b8f9f75b7a9ee4fcc6e57d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -3574,9 +3570,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mail-auth"
|
||||
version = "0.4.3"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd9d657de66a3d5ac360c3eab8c9f5cac2565f2b97cc032d5de4c900ef470de"
|
||||
checksum = "aaee4c38f4df428c6732f3d5472a013fa248d2772f48c8932295b32c683a23c4"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"flate2",
|
||||
|
@ -3585,7 +3581,7 @@ dependencies = [
|
|||
"mail-builder",
|
||||
"mail-parser",
|
||||
"parking_lot",
|
||||
"quick-xml 0.32.0",
|
||||
"quick-xml 0.36.1",
|
||||
"rand",
|
||||
"ring 0.17.8",
|
||||
"rsa",
|
||||
|
@ -3606,9 +3602,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mail-parser"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5a1335c3a964788c90cb42ae04a34b5f2628e89566949ce3bd4ada695c0bcd"
|
||||
checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"serde",
|
||||
|
@ -3623,17 +3619,17 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"gethostname",
|
||||
"md5",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pki-types",
|
||||
"smtp-proto",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mail-server"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"common",
|
||||
"directory",
|
||||
|
@ -3652,7 +3648,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "managesieve"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
|
@ -3666,7 +3662,7 @@ dependencies = [
|
|||
"mail-send",
|
||||
"md5",
|
||||
"parking_lot",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"sieve-rs",
|
||||
"store",
|
||||
|
@ -3785,15 +3781,6 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
|
@ -3879,7 +3866,7 @@ dependencies = [
|
|||
"twox-hash",
|
||||
"url",
|
||||
"webpki",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3951,7 +3938,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nlp"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
|
@ -3961,12 +3948,12 @@ dependencies = [
|
|||
"nohash",
|
||||
"parking_lot",
|
||||
"phf",
|
||||
"psl",
|
||||
"rust-stemmers",
|
||||
"serde",
|
||||
"siphasher 1.0.1",
|
||||
"tinysegmenter",
|
||||
"tokio",
|
||||
"utils",
|
||||
"whatlang",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
@ -4299,7 +4286,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.3",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
@ -4502,15 +4489,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pop3"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"common",
|
||||
"directory",
|
||||
"imap",
|
||||
"jmap",
|
||||
"jmap_proto",
|
||||
"mail-parser",
|
||||
"mail-send",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"store",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
|
@ -4544,9 +4532,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "postgres-types"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02048d9e032fb3cc3413bbf7b83a15d84a5d419778e2628751896d856498eee9"
|
||||
checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fallible-iterator 0.2.0",
|
||||
|
@ -4659,6 +4647,15 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.13.4"
|
||||
|
@ -4706,6 +4703,21 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl"
|
||||
version = "2.1.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce9398ad066421139b2e3afe16ea46772ffda30bd9ba57554dc035df5e26edc8"
|
||||
dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta"
|
||||
version = "0.1.4"
|
||||
|
@ -4768,16 +4780,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.3"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
@ -4786,15 +4798,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.6"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand",
|
||||
"ring 0.17.8",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
|
@ -4803,15 +4815,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285"
|
||||
checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5000,7 +5012,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
|
@ -5011,23 +5023,14 @@ dependencies = [
|
|||
"tokio-rustls 0.26.0",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
@ -5141,7 +5144,7 @@ dependencies = [
|
|||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.4.1",
|
||||
"hyper-rustls 0.27.2",
|
||||
"hyper-rustls 0.27.3",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
|
@ -5152,7 +5155,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
|
@ -5166,7 +5169,7 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
|
@ -5469,9 +5472,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.35"
|
||||
version = "0.38.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
|
@ -5501,21 +5504,21 @@ dependencies = [
|
|||
"log",
|
||||
"ring 0.17.8",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.7",
|
||||
"rustls-webpki 0.102.8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.12"
|
||||
version = "0.23.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
|
||||
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring 0.17.8",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.7",
|
||||
"rustls-webpki 0.102.8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
@ -5582,9 +5585,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.7"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring 0.17.8",
|
||||
"rustls-pki-types",
|
||||
|
@ -5629,20 +5632,20 @@ checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
|
|||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.1.16"
|
||||
version = "2.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeb7ac86243095b70a7920639507b71d51a63390d1ba26c4f60a552fbb914a37"
|
||||
checksum = "0c947adb109a8afce5fc9c7bf951f87f146e9147b3a6a58413105628fb1d1e66"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
|
||||
checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5684,9 +5687,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0495e4577c672de8254beb68d01a9b62d0e8a13c099edecdbedccce3223cd29f"
|
||||
checksum = "60a7b59a5d9b0099720b417b6325d91a52cbf5b3dcb5041d864be53eefa58abc"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
|
@ -5810,9 +5813,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.209"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -5828,9 +5831,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.209"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5839,9 +5842,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.127"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
@ -6054,7 +6057,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
|||
|
||||
[[package]]
|
||||
name = "smtp"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
|
@ -6080,7 +6083,7 @@ dependencies = [
|
|||
"rayon",
|
||||
"regex",
|
||||
"reqwest 0.12.7",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
|
@ -6094,7 +6097,7 @@ dependencies = [
|
|||
"tokio-rustls 0.26.0",
|
||||
"trc",
|
||||
"utils",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
"x509-parser 0.16.0",
|
||||
]
|
||||
|
||||
|
@ -6170,7 +6173,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
|||
|
||||
[[package]]
|
||||
name = "stalwart-cli"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"console",
|
||||
|
@ -6201,7 +6204,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "store"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"arc-swap",
|
||||
|
@ -6233,7 +6236,7 @@ dependencies = [
|
|||
"rocksdb",
|
||||
"rusqlite",
|
||||
"rust-s3",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -6451,7 +6454,7 @@ dependencies = [
|
|||
"rayon",
|
||||
"reqwest 0.12.7",
|
||||
"ring 0.17.8",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
|
@ -6593,9 +6596,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-postgres"
|
||||
version = "0.7.11"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03adcf0147e203b6032c0b2d30be1415ba03bc348901f3ff1cc0df6a733e60c3"
|
||||
checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
|
@ -6644,16 +6647,16 @@ version = "0.26.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
|
||||
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
|
@ -6673,7 +6676,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tungstenite 0.21.0",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6690,9 +6693,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
||||
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
|
@ -6709,9 +6712,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
|||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.20"
|
||||
version = "0.22.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
|
||||
dependencies = [
|
||||
"indexmap 2.5.0",
|
||||
"toml_datetime",
|
||||
|
@ -6828,7 +6831,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "trc"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"base64 0.22.1",
|
||||
|
@ -6959,15 +6962,15 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
@ -6986,9 +6989,9 @@ checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-security"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee9e13753df674873f3c4693b240ae5c03245ddc157dfccf7c26db9329af3a11"
|
||||
checksum = "2e4ddba1535dd35ed8b61c52166b7155d7f4e4b8847cec6f48e71dc66d8b5e50"
|
||||
dependencies = [
|
||||
"unicode-normalization",
|
||||
"unicode-script",
|
||||
|
@ -7002,9 +7005,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
|
@ -7071,7 +7074,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||
|
||||
[[package]]
|
||||
name = "utils"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"base64 0.22.1",
|
||||
|
@ -7092,7 +7095,7 @@ dependencies = [
|
|||
"regex",
|
||||
"reqwest 0.12.7",
|
||||
"ring 0.17.8",
|
||||
"rustls 0.23.12",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
|
@ -7101,7 +7104,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"trc",
|
||||
"webpki-roots 0.26.5",
|
||||
"webpki-roots 0.26.6",
|
||||
"x509-parser 0.16.0",
|
||||
]
|
||||
|
||||
|
@ -7271,9 +7274,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
|||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.5"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
|
||||
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
@ -7302,11 +7305,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
|
||||
checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
|
||||
dependencies = [
|
||||
"redox_syscall 0.4.1",
|
||||
"redox_syscall",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
|
@ -7632,9 +7635,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601"
|
||||
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
|
|
|
@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
|||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
repository = "https://github.com/stalwartlabs/cli"
|
||||
homepage = "https://github.com/stalwartlabs/cli"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
resolver = "2"
|
||||
|
@ -29,4 +29,4 @@ human-size = "0.4.2"
|
|||
futures = "0.3.28"
|
||||
pwhash = "1.0.0"
|
||||
rand = "0.8.5"
|
||||
mail-auth = { version = "0.4" }
|
||||
mail-auth = { version = "0.5" }
|
||||
|
|
|
@ -88,7 +88,7 @@ async fn oauth(url: &str) -> Credentials {
|
|||
.danger_accept_invalid_certs(is_localhost(url))
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
.get(&format!("{}/.well-known/oauth-authorization-server", url))
|
||||
.get(format!("{}/.well-known/oauth-authorization-server", url))
|
||||
.send()
|
||||
.await
|
||||
.unwrap_result("send OAuth GET request")
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use prettytable::{Attr, Cell, Row, Table, format};
|
||||
use prettytable::{format, Attr, Cell, Row, Table};
|
||||
use reqwest::Method;
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
@ -14,7 +14,7 @@ jmap_proto = { path = "../jmap-proto" }
|
|||
sieve-rs = { version = "0.5" }
|
||||
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
|
||||
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
|
||||
mail-auth = { version = "0.4" }
|
||||
mail-auth = { version = "0.5" }
|
||||
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "ring", "tls12"] }
|
||||
smtp-proto = { version = "0.1", features = ["serde_support"] }
|
||||
dns-update = { version = "0.1" }
|
||||
|
@ -58,6 +58,7 @@ hostname = "0.4.0"
|
|||
zip = "2.1"
|
||||
pwhash = "1.0.0"
|
||||
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
|
||||
psl = "2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
privdrop = "0.5.3"
|
||||
|
|
468
crates/common/src/auth/access_token.rs
Normal file
468
crates/common/src/auth/access_token.rs
Normal file
|
@ -0,0 +1,468 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use directory::{
|
||||
backend::internal::{lookup::DirectoryStore, PrincipalField},
|
||||
Permission, Principal, QueryBy,
|
||||
};
|
||||
use jmap_proto::{
|
||||
request::RequestMethod,
|
||||
types::{acl::Acl, collection::Collection, id::Id},
|
||||
};
|
||||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use store::query::acl::AclQuery;
|
||||
use trc::AddContext;
|
||||
use utils::map::{
|
||||
bitmap::{Bitmap, BitmapItem},
|
||||
ttl_dashmap::TtlMap,
|
||||
vec_map::VecMap,
|
||||
};
|
||||
|
||||
use crate::Core;
|
||||
|
||||
use super::{roles::RolePermissions, AccessToken, ResourceToken, TenantInfo};
|
||||
|
||||
impl Core {
|
||||
pub async fn build_access_token(&self, mut principal: Principal) -> trc::Result<AccessToken> {
|
||||
let mut role_permissions = RolePermissions::default();
|
||||
|
||||
// Apply role permissions
|
||||
for role_id in principal.iter_int(PrincipalField::Roles) {
|
||||
role_permissions.union(self.get_role_permissions(role_id as u32).await?.as_ref());
|
||||
}
|
||||
|
||||
// Add principal permissions
|
||||
for (permissions, field) in [
|
||||
(
|
||||
&mut role_permissions.enabled,
|
||||
PrincipalField::EnabledPermissions,
|
||||
),
|
||||
(
|
||||
&mut role_permissions.disabled,
|
||||
PrincipalField::DisabledPermissions,
|
||||
),
|
||||
] {
|
||||
for permission in principal.iter_int(field) {
|
||||
let permission = permission as usize;
|
||||
if permission < Permission::COUNT {
|
||||
permissions.set(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply principal permissions
|
||||
let mut permissions = role_permissions.finalize();
|
||||
|
||||
let mut tenant = None;
|
||||
#[cfg(feature = "enterprise")]
|
||||
if self.is_enterprise_edition() {
|
||||
if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant).map(|v| v as u32) {
|
||||
// Limit tenant permissions
|
||||
permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled);
|
||||
|
||||
// Obtain tenant quota
|
||||
tenant = Some(TenantInfo {
|
||||
id: tenant_id,
|
||||
quota: self
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(tenant_id), false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::SecurityEvent::Unauthorized
|
||||
.into_err()
|
||||
.details("Tenant not found")
|
||||
.id(tenant_id)
|
||||
.caused_by(trc::location!())
|
||||
})?
|
||||
.get_int(PrincipalField::Quota)
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AccessToken {
|
||||
primary_id: principal.id(),
|
||||
member_of: principal
|
||||
.iter_int(PrincipalField::MemberOf)
|
||||
.map(|v| v as u32)
|
||||
.collect(),
|
||||
access_to: VecMap::new(),
|
||||
tenant,
|
||||
name: principal.take_str(PrincipalField::Name).unwrap_or_default(),
|
||||
description: principal.take_str(PrincipalField::Description),
|
||||
quota: principal.quota(),
|
||||
permissions,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> {
|
||||
let err = match self
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), true)
|
||||
.await
|
||||
{
|
||||
Ok(Some(principal)) => {
|
||||
return self
|
||||
.update_access_token(self.build_access_token(principal).await?)
|
||||
.await
|
||||
}
|
||||
Ok(None) => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Account not found.")
|
||||
.caused_by(trc::location!())),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
match &self.jmap.fallback_admin {
|
||||
Some((_, secret)) if account_id == u32::MAX => {
|
||||
self.update_access_token(
|
||||
self.build_access_token(Principal::fallback_admin(secret))
|
||||
.await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => err,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_access_token(
|
||||
&self,
|
||||
mut access_token: AccessToken,
|
||||
) -> trc::Result<AccessToken> {
|
||||
for grant_account_id in [access_token.primary_id]
|
||||
.into_iter()
|
||||
.chain(access_token.member_of.iter().copied())
|
||||
{
|
||||
for acl_item in self
|
||||
.storage
|
||||
.data
|
||||
.acl_query(AclQuery::HasAccess { grant_account_id })
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
if !access_token.is_member(acl_item.to_account_id) {
|
||||
let acl = Bitmap::<Acl>::from(acl_item.permissions);
|
||||
let collection = Collection::from(acl_item.to_collection);
|
||||
if !collection.is_valid() {
|
||||
return Err(trc::StoreEvent::DataCorruption
|
||||
.ctx(trc::Key::Reason, "Corrupted collection found in ACL key.")
|
||||
.details(format!("{acl_item:?}"))
|
||||
.account_id(grant_account_id)
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
|
||||
let mut collections: Bitmap<Collection> = Bitmap::new();
|
||||
if acl.contains(Acl::Read) || acl.contains(Acl::Administer) {
|
||||
collections.insert(collection);
|
||||
}
|
||||
if collection == Collection::Mailbox
|
||||
&& (acl.contains(Acl::ReadItems) || acl.contains(Acl::Administer))
|
||||
{
|
||||
collections.insert(Collection::Email);
|
||||
}
|
||||
|
||||
if !collections.is_empty() {
|
||||
access_token
|
||||
.access_to
|
||||
.get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new)
|
||||
.union(&collections);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(access_token)
|
||||
}
|
||||
|
||||
pub fn cache_access_token(&self, access_token: Arc<AccessToken>) {
|
||||
self.security.access_tokens.insert_with_ttl(
|
||||
access_token.primary_id(),
|
||||
access_token,
|
||||
Instant::now() + self.jmap.session_cache_ttl,
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> {
|
||||
if let Some(access_token) = self.security.access_tokens.get_with_ttl(&primary_id) {
|
||||
Ok(access_token)
|
||||
} else {
|
||||
// Refresh ACL token
|
||||
self.get_access_token(primary_id).await.map(|access_token| {
|
||||
let access_token = Arc::new(access_token);
|
||||
self.cache_access_token(access_token.clone());
|
||||
access_token
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessToken {
|
||||
pub fn from_id(primary_id: u32) -> Self {
|
||||
Self {
|
||||
primary_id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_access_to(self, access_to: VecMap<u32, Bitmap<Collection>>) -> Self {
|
||||
Self { access_to, ..self }
|
||||
}
|
||||
|
||||
pub fn with_permission(mut self, permission: Permission) -> Self {
|
||||
self.permissions.set(permission.id());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(&self) -> u32 {
|
||||
// Hash state
|
||||
let mut s = DefaultHasher::new();
|
||||
self.member_of.hash(&mut s);
|
||||
self.access_to.hash(&mut s);
|
||||
s.finish() as u32
|
||||
}
|
||||
|
||||
pub fn primary_id(&self) -> u32 {
|
||||
self.primary_id
|
||||
}
|
||||
|
||||
pub fn secondary_ids(&self) -> impl Iterator<Item = &u32> {
|
||||
self.member_of
|
||||
.iter()
|
||||
.chain(self.access_to.iter().map(|(id, _)| id))
|
||||
}
|
||||
|
||||
pub fn is_member(&self, account_id: u32) -> bool {
|
||||
self.primary_id == account_id
|
||||
|| self.member_of.contains(&account_id)
|
||||
|| self.has_permission(Permission::Impersonate)
|
||||
}
|
||||
|
||||
pub fn is_primary_id(&self, account_id: u32) -> bool {
|
||||
self.primary_id == account_id
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn has_permission(&self, permission: Permission) -> bool {
|
||||
self.permissions.get(permission.id())
|
||||
}
|
||||
|
||||
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
|
||||
if self.has_permission(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(trc::SecurityEvent::Unauthorized
|
||||
.into_err()
|
||||
.details(permission.name()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> Vec<Permission> {
|
||||
const USIZE_BITS: usize = std::mem::size_of::<usize>() * 8;
|
||||
const USIZE_MASK: u32 = USIZE_BITS as u32 - 1;
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
for (block_num, bytes) in self.permissions.inner().iter().enumerate() {
|
||||
let mut bytes = *bytes;
|
||||
|
||||
while bytes != 0 {
|
||||
let item = USIZE_MASK - bytes.leading_zeros();
|
||||
bytes ^= 1 << item;
|
||||
if let Some(permission) =
|
||||
Permission::from_id((block_num * USIZE_BITS) + item as usize)
|
||||
{
|
||||
permissions.push(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
permissions
|
||||
}
|
||||
|
||||
pub fn is_shared(&self, account_id: u32) -> bool {
|
||||
!self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id)
|
||||
}
|
||||
|
||||
pub fn shared_accounts(&self, collection: impl Into<Collection>) -> impl Iterator<Item = &u32> {
|
||||
let collection = collection.into();
|
||||
self.member_of
|
||||
.iter()
|
||||
.chain(self.access_to.iter().filter_map(move |(id, cols)| {
|
||||
if cols.contains(collection) {
|
||||
id.into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn has_access(&self, to_account_id: u32, to_collection: impl Into<Collection>) -> bool {
|
||||
let to_collection = to_collection.into();
|
||||
self.is_member(to_account_id)
|
||||
|| self.access_to.iter().any(|(id, collections)| {
|
||||
*id == to_account_id && collections.contains(to_collection)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assert_has_access(
|
||||
&self,
|
||||
to_account_id: Id,
|
||||
to_collection: Collection,
|
||||
) -> trc::Result<&Self> {
|
||||
if self.has_access(to_account_id.document_id(), to_collection) {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden.into_err().details(format!(
|
||||
"You do not have access to account {}",
|
||||
to_account_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> {
|
||||
if self.is_member(account_id.document_id()) {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details(format!("You are not an owner of account {}", account_id)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> {
|
||||
let permission = match request {
|
||||
RequestMethod::Get(m) => match &m.arguments {
|
||||
jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet,
|
||||
jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet,
|
||||
jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet,
|
||||
jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet,
|
||||
jmap_proto::method::get::RequestArguments::EmailSubmission => {
|
||||
Permission::JmapEmailSubmissionGet
|
||||
}
|
||||
jmap_proto::method::get::RequestArguments::PushSubscription => {
|
||||
Permission::JmapPushSubscriptionGet
|
||||
}
|
||||
jmap_proto::method::get::RequestArguments::SieveScript => {
|
||||
Permission::JmapSieveScriptGet
|
||||
}
|
||||
jmap_proto::method::get::RequestArguments::VacationResponse => {
|
||||
Permission::JmapVacationResponseGet
|
||||
}
|
||||
jmap_proto::method::get::RequestArguments::Principal => {
|
||||
Permission::JmapPrincipalGet
|
||||
}
|
||||
jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet,
|
||||
jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet,
|
||||
},
|
||||
RequestMethod::Set(m) => match &m.arguments {
|
||||
jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet,
|
||||
jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet,
|
||||
jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet,
|
||||
jmap_proto::method::set::RequestArguments::EmailSubmission(_) => {
|
||||
Permission::JmapEmailSubmissionSet
|
||||
}
|
||||
jmap_proto::method::set::RequestArguments::PushSubscription => {
|
||||
Permission::JmapPushSubscriptionSet
|
||||
}
|
||||
jmap_proto::method::set::RequestArguments::SieveScript(_) => {
|
||||
Permission::JmapSieveScriptSet
|
||||
}
|
||||
jmap_proto::method::set::RequestArguments::VacationResponse => {
|
||||
Permission::JmapVacationResponseSet
|
||||
}
|
||||
},
|
||||
RequestMethod::Changes(m) => match m.arguments {
|
||||
jmap_proto::method::changes::RequestArguments::Email => {
|
||||
Permission::JmapEmailChanges
|
||||
}
|
||||
jmap_proto::method::changes::RequestArguments::Mailbox => {
|
||||
Permission::JmapMailboxChanges
|
||||
}
|
||||
jmap_proto::method::changes::RequestArguments::Thread => {
|
||||
Permission::JmapThreadChanges
|
||||
}
|
||||
jmap_proto::method::changes::RequestArguments::Identity => {
|
||||
Permission::JmapIdentityChanges
|
||||
}
|
||||
jmap_proto::method::changes::RequestArguments::EmailSubmission => {
|
||||
Permission::JmapEmailSubmissionChanges
|
||||
}
|
||||
jmap_proto::method::changes::RequestArguments::Quota => {
|
||||
Permission::JmapQuotaChanges
|
||||
}
|
||||
},
|
||||
RequestMethod::Copy(m) => match m.arguments {
|
||||
jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy,
|
||||
},
|
||||
RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy,
|
||||
RequestMethod::ImportEmail(_) => Permission::JmapEmailImport,
|
||||
RequestMethod::ParseEmail(_) => Permission::JmapEmailParse,
|
||||
RequestMethod::QueryChanges(m) => match m.arguments {
|
||||
jmap_proto::method::query::RequestArguments::Email(_) => {
|
||||
Permission::JmapEmailQueryChanges
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::Mailbox(_) => {
|
||||
Permission::JmapMailboxQueryChanges
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::EmailSubmission => {
|
||||
Permission::JmapEmailSubmissionQueryChanges
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::SieveScript => {
|
||||
Permission::JmapSieveScriptQueryChanges
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::Principal => {
|
||||
Permission::JmapPrincipalQueryChanges
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::Quota => {
|
||||
Permission::JmapQuotaQueryChanges
|
||||
}
|
||||
},
|
||||
RequestMethod::Query(m) => match m.arguments {
|
||||
jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery,
|
||||
jmap_proto::method::query::RequestArguments::Mailbox(_) => {
|
||||
Permission::JmapMailboxQuery
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::EmailSubmission => {
|
||||
Permission::JmapEmailSubmissionQuery
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::SieveScript => {
|
||||
Permission::JmapSieveScriptQuery
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::Principal => {
|
||||
Permission::JmapPrincipalQuery
|
||||
}
|
||||
jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery,
|
||||
},
|
||||
RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet,
|
||||
RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate,
|
||||
RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup,
|
||||
RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload,
|
||||
RequestMethod::Echo(_) => Permission::JmapEcho,
|
||||
RequestMethod::Error(_) => return Ok(()),
|
||||
};
|
||||
|
||||
if self.has_permission(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("You are not authorized to perform this action"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_resource_token(&self) -> ResourceToken {
|
||||
ResourceToken {
|
||||
account_id: self.primary_id,
|
||||
quota: self.quota,
|
||||
tenant: self.tenant,
|
||||
}
|
||||
}
|
||||
}
|
37
crates/common/src/auth/mod.rs
Normal file
37
crates/common/src/auth/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use directory::Permissions;
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use utils::map::{bitmap::Bitmap, vec_map::VecMap};
|
||||
|
||||
pub mod access_token;
|
||||
pub mod roles;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AccessToken {
|
||||
pub primary_id: u32,
|
||||
pub member_of: Vec<u32>,
|
||||
pub access_to: VecMap<u32, Bitmap<Collection>>,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quota: u64,
|
||||
pub permissions: Permissions,
|
||||
pub tenant: Option<TenantInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct TenantInfo {
|
||||
pub id: u32,
|
||||
pub quota: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResourceToken {
|
||||
pub account_id: u32,
|
||||
pub quota: u64,
|
||||
pub tenant: Option<TenantInfo>,
|
||||
}
|
208
crates/common/src/auth/roles.rs
Normal file
208
crates/common/src/auth/roles.rs
Normal file
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use ahash::AHashSet;
|
||||
use directory::{
|
||||
backend::internal::{lookup::DirectoryStore, PrincipalField},
|
||||
Permission, Permissions, QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER,
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::Core;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RolePermissions {
|
||||
pub enabled: Permissions,
|
||||
pub disabled: Permissions,
|
||||
}
|
||||
|
||||
static USER_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(user_permissions);
|
||||
static ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(admin_permissions);
|
||||
static TENANT_ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> =
|
||||
LazyLock::new(tenant_admin_permissions);
|
||||
|
||||
impl Core {
|
||||
pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> {
|
||||
match role_id {
|
||||
ROLE_USER => Ok(USER_PERMISSIONS.clone()),
|
||||
ROLE_ADMIN => Ok(ADMIN_PERMISSIONS.clone()),
|
||||
ROLE_TENANT_ADMIN => Ok(TENANT_ADMIN_PERMISSIONS.clone()),
|
||||
role_id => {
|
||||
if let Some(role_permissions) = self.security.permissions.get(&role_id) {
|
||||
Ok(role_permissions.clone())
|
||||
} else {
|
||||
self.build_role_permissions(role_id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> {
|
||||
let mut role_ids = vec![role_id as u64].into_iter();
|
||||
let mut role_ids_stack = vec![];
|
||||
let mut fetched_role_ids = AHashSet::new();
|
||||
let mut return_permissions = RolePermissions::default();
|
||||
|
||||
'outer: loop {
|
||||
if let Some(role_id) = role_ids.next() {
|
||||
let role_id = role_id as u32;
|
||||
|
||||
// Skip if already fetched
|
||||
if !fetched_role_ids.insert(role_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match role_id {
|
||||
ROLE_USER => {
|
||||
return_permissions.enabled.union(&USER_PERMISSIONS.enabled);
|
||||
return_permissions
|
||||
.disabled
|
||||
.union(&USER_PERMISSIONS.disabled);
|
||||
}
|
||||
ROLE_ADMIN => {
|
||||
return_permissions.enabled.union(&ADMIN_PERMISSIONS.enabled);
|
||||
return_permissions
|
||||
.disabled
|
||||
.union(&ADMIN_PERMISSIONS.disabled);
|
||||
break 'outer;
|
||||
}
|
||||
ROLE_TENANT_ADMIN => {
|
||||
return_permissions
|
||||
.enabled
|
||||
.union(&TENANT_ADMIN_PERMISSIONS.enabled);
|
||||
return_permissions
|
||||
.disabled
|
||||
.union(&TENANT_ADMIN_PERMISSIONS.disabled);
|
||||
}
|
||||
role_id => {
|
||||
// Try with the cache
|
||||
if let Some(role_permissions) = self.security.permissions.get(&role_id) {
|
||||
return_permissions.union(role_permissions.as_ref());
|
||||
} else {
|
||||
let mut role_permissions = RolePermissions::default();
|
||||
|
||||
// Obtain principal
|
||||
let mut principal = self
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(role_id), true)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::SecurityEvent::Unauthorized
|
||||
.into_err()
|
||||
.details(
|
||||
"Principal not found while building role permissions",
|
||||
)
|
||||
.ctx(trc::Key::Id, role_id)
|
||||
})?;
|
||||
|
||||
// Add permissions
|
||||
for (permissions, field) in [
|
||||
(
|
||||
&mut role_permissions.enabled,
|
||||
PrincipalField::EnabledPermissions,
|
||||
),
|
||||
(
|
||||
&mut role_permissions.disabled,
|
||||
PrincipalField::DisabledPermissions,
|
||||
),
|
||||
] {
|
||||
for permission in principal.iter_int(field) {
|
||||
let permission = permission as usize;
|
||||
if permission < Permission::COUNT {
|
||||
permissions.set(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add permissions
|
||||
return_permissions.union(&role_permissions);
|
||||
|
||||
// Add parent roles
|
||||
if let Some(parent_role_ids) = principal
|
||||
.take_int_array(PrincipalField::Roles)
|
||||
.filter(|r| !r.is_empty())
|
||||
{
|
||||
role_ids_stack.push(role_ids);
|
||||
role_ids = parent_role_ids.into_iter();
|
||||
} else {
|
||||
// Cache role
|
||||
self.security
|
||||
.permissions
|
||||
.insert(role_id, Arc::new(role_permissions));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(prev_role_ids) = role_ids_stack.pop() {
|
||||
role_ids = prev_role_ids;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache role
|
||||
let return_permissions = Arc::new(return_permissions);
|
||||
self.security
|
||||
.permissions
|
||||
.insert(role_id, return_permissions.clone());
|
||||
Ok(return_permissions)
|
||||
}
|
||||
}
|
||||
|
||||
impl RolePermissions {
|
||||
pub fn union(&mut self, other: &RolePermissions) {
|
||||
self.enabled.union(&other.enabled);
|
||||
self.disabled.union(&other.disabled);
|
||||
}
|
||||
|
||||
pub fn finalize(mut self) -> Permissions {
|
||||
self.enabled.difference(&self.disabled);
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
|
||||
fn tenant_admin_permissions() -> Arc<RolePermissions> {
|
||||
let mut permissions = RolePermissions {
|
||||
enabled: Permissions::new(),
|
||||
disabled: Permissions::new(),
|
||||
};
|
||||
|
||||
for permission_id in 0..Permission::COUNT {
|
||||
let permission = Permission::from_id(permission_id).unwrap();
|
||||
if permission.is_tenant_admin_permission() {
|
||||
permissions.enabled.set(permission_id);
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(permissions)
|
||||
}
|
||||
|
||||
fn user_permissions() -> Arc<RolePermissions> {
|
||||
let mut permissions = RolePermissions {
|
||||
enabled: Permissions::new(),
|
||||
disabled: Permissions::new(),
|
||||
};
|
||||
|
||||
for permission_id in 0..Permission::COUNT {
|
||||
let permission = Permission::from_id(permission_id).unwrap();
|
||||
if permission.is_user_permission() {
|
||||
permissions.enabled.set(permission_id);
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(permissions)
|
||||
}
|
||||
|
||||
fn admin_permissions() -> Arc<RolePermissions> {
|
||||
Arc::new(RolePermissions {
|
||||
enabled: Permissions::all(),
|
||||
disabled: Permissions::new(),
|
||||
})
|
||||
}
|
|
@ -83,8 +83,6 @@ pub struct JmapConfig {
|
|||
pub encrypt: bool,
|
||||
pub encrypt_append: bool,
|
||||
|
||||
pub principal_allow_lookups: bool,
|
||||
|
||||
pub capabilities: BaseCapabilities,
|
||||
pub session_purge_frequency: SimpleCron,
|
||||
pub account_purge_frequency: SimpleCron,
|
||||
|
@ -371,9 +369,6 @@ impl JmapConfig {
|
|||
push_max_total: config
|
||||
.property_or_default("jmap.push.max-total", "100")
|
||||
.unwrap_or(100),
|
||||
principal_allow_lookups: config
|
||||
.property("jmap.principal.allow-lookups")
|
||||
.unwrap_or(true),
|
||||
encrypt: config
|
||||
.property_or_default("storage.encryption.enable", "true")
|
||||
.unwrap_or(true),
|
||||
|
|
|
@ -10,9 +10,14 @@ use arc_swap::ArcSwap;
|
|||
use directory::{Directories, Directory};
|
||||
use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores};
|
||||
use telemetry::Metrics;
|
||||
use utils::config::Config;
|
||||
use utils::{
|
||||
config::Config,
|
||||
map::ttl_dashmap::{ADashMap, TtlDashMap, TtlMap},
|
||||
};
|
||||
|
||||
use crate::{expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network};
|
||||
use crate::{
|
||||
expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network, Security,
|
||||
};
|
||||
|
||||
use self::{
|
||||
imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig,
|
||||
|
@ -162,6 +167,16 @@ impl Core {
|
|||
imap: ImapConfig::parse(config),
|
||||
tls: TlsManager::parse(config),
|
||||
metrics: Metrics::parse(config),
|
||||
security: Security {
|
||||
access_tokens: TtlDashMap::with_capacity(100, 32),
|
||||
permissions: ADashMap::with_capacity_and_hasher_and_shard_amount(
|
||||
100,
|
||||
ahash::RandomState::new(),
|
||||
32,
|
||||
),
|
||||
permissions_version: Default::default(),
|
||||
logos: Default::default(),
|
||||
},
|
||||
storage: Storage {
|
||||
data,
|
||||
blob,
|
||||
|
|
|
@ -22,10 +22,7 @@ use mail_auth::{
|
|||
Resolver,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use utils::{
|
||||
config::{utils::ParseValue, Config},
|
||||
suffixlist::PublicSuffix,
|
||||
};
|
||||
use utils::config::{utils::ParseValue, Config};
|
||||
|
||||
use crate::Core;
|
||||
|
||||
|
@ -33,7 +30,6 @@ pub struct Resolvers {
|
|||
pub dns: Resolver,
|
||||
pub dnssec: DnssecResolver,
|
||||
pub cache: DnsRecordCache,
|
||||
pub psl: PublicSuffix,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -237,7 +233,6 @@ impl Resolvers {
|
|||
.unwrap_or(1024),
|
||||
),
|
||||
},
|
||||
psl: PublicSuffix::parse(config, "resolver.public-suffix").await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +351,6 @@ impl Default for Resolvers {
|
|||
tlsa: LruCache::with_capacity(1024),
|
||||
mta_sts: LruCache::with_capacity(1024),
|
||||
},
|
||||
psl: PublicSuffix::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -402,7 +396,6 @@ impl Clone for Resolvers {
|
|||
dns: self.dns.clone(),
|
||||
dnssec: self.dnssec.clone(),
|
||||
cache: self.cache.clone(),
|
||||
psl: self.psl.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use directory::{backend::internal::manage::ManageDirectory, Type};
|
||||
use store::{Store, Stores};
|
||||
use trc::{EventType, MetricType, TOTAL_EVENT_COUNT};
|
||||
use utils::config::{
|
||||
|
@ -18,10 +19,7 @@ use utils::config::{
|
|||
Config,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
expr::{tokenizer::TokenMap, Expression},
|
||||
total_accounts,
|
||||
};
|
||||
use crate::expr::{tokenizer::TokenMap, Expression};
|
||||
|
||||
use super::{
|
||||
license::LicenseValidator, AlertContent, AlertContentToken, AlertMethod, Enterprise,
|
||||
|
@ -42,7 +40,10 @@ impl Enterprise {
|
|||
}
|
||||
};
|
||||
|
||||
match total_accounts(data).await {
|
||||
match data
|
||||
.count_principals(None, Type::Individual.into(), None)
|
||||
.await
|
||||
{
|
||||
Ok(total) if total > license.accounts as u64 => {
|
||||
config.new_build_warning(
|
||||
"enterprise.license-key",
|
||||
|
@ -116,6 +117,7 @@ impl Enterprise {
|
|||
.property_or_default::<Option<Duration>>("storage.undelete.retention", "false")
|
||||
.unwrap_or_default()
|
||||
.map(|retention| Undelete { retention }),
|
||||
logo_url: config.value("enterprise.logo-url").map(|s| s.to_string()),
|
||||
trace_store,
|
||||
metrics_store,
|
||||
metrics_alerts: parse_metric_alerts(config),
|
||||
|
|
|
@ -15,17 +15,22 @@ pub mod undelete;
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use directory::{
|
||||
backend::internal::{lookup::DirectoryStore, PrincipalField},
|
||||
QueryBy, Type,
|
||||
};
|
||||
use license::LicenseKey;
|
||||
use mail_parser::DateTime;
|
||||
use store::Store;
|
||||
use trc::{EventType, MetricType};
|
||||
use trc::{AddContext, EventType, MetricType};
|
||||
use utils::config::cron::SimpleCron;
|
||||
|
||||
use crate::{expr::Expression, Core};
|
||||
use crate::{expr::Expression, manager::webadmin::Resource, Core};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Enterprise {
|
||||
pub license: LicenseKey,
|
||||
pub logo_url: Option<String>,
|
||||
pub undelete: Option<Undelete>,
|
||||
pub trace_store: Option<TraceStore>,
|
||||
pub metrics_store: Option<MetricStore>,
|
||||
|
@ -114,4 +119,95 @@ impl Core {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn logo_resource(&self, domain: &str) -> trc::Result<Option<Resource<Vec<u8>>>> {
|
||||
if self.is_enterprise_edition() {
|
||||
let domain = psl::domain_str(domain).unwrap_or(domain);
|
||||
let logo = { self.security.logos.lock().get(domain).cloned() };
|
||||
|
||||
if let Some(logo) = logo {
|
||||
Ok(logo)
|
||||
} else {
|
||||
// Try fetching the logo for the domain
|
||||
let logo_url = if let Some(mut principal) = self
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Name(domain), false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.filter(|p| p.typ() == Type::Domain)
|
||||
{
|
||||
if let Some(logo) = principal
|
||||
.take_str(PrincipalField::Picture)
|
||||
.filter(|l| l.starts_with("http"))
|
||||
{
|
||||
logo.into()
|
||||
} else if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant) {
|
||||
if let Some(logo) = self
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(tenant_id as u32), false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Picture))
|
||||
.filter(|l| l.starts_with("http"))
|
||||
{
|
||||
logo.into()
|
||||
} else {
|
||||
self.default_logo_url()
|
||||
}
|
||||
} else {
|
||||
self.default_logo_url()
|
||||
}
|
||||
} else {
|
||||
self.default_logo_url()
|
||||
};
|
||||
|
||||
let mut logo = None;
|
||||
if let Some(logo_url) = logo_url {
|
||||
let response = reqwest::get(&logo_url).await.map_err(|err| {
|
||||
trc::ResourceEvent::DownloadExternal
|
||||
.into_err()
|
||||
.details("Failed to download logo")
|
||||
.reason(err)
|
||||
})?;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
.unwrap_or("image/svg+xml")
|
||||
.to_string();
|
||||
|
||||
let contents = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
trc::ResourceEvent::DownloadExternal
|
||||
.into_err()
|
||||
.details("Failed to download logo")
|
||||
.reason(err)
|
||||
})?
|
||||
.to_vec();
|
||||
|
||||
logo = Resource::new(content_type, contents).into();
|
||||
}
|
||||
|
||||
self.security
|
||||
.logos
|
||||
.lock()
|
||||
.insert(domain.to_string(), logo.clone());
|
||||
|
||||
Ok(logo)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_logo_url(&self) -> Option<String> {
|
||||
self.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.logo_url.as_ref().map(|l| l.to_string()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,14 +91,14 @@ impl<'x> Tokenizer<'x> {
|
|||
_ => {
|
||||
let (prev_token, ch) = if ch == b'(' && self.buf.eq(b"matches") {
|
||||
// Parse regular expressions
|
||||
let stop_ch = self.find_char(&[b'\"', b'\''])?;
|
||||
let stop_ch = self.find_char(b"\"'")?;
|
||||
let regex_str = self.parse_string(stop_ch)?;
|
||||
let regex = Regex::new(®ex_str).map_err(|e| {
|
||||
format!("Invalid regular expression {:?}: {}", regex_str, e)
|
||||
})?;
|
||||
self.has_alpha = false;
|
||||
self.buf.clear();
|
||||
self.find_char(&[b','])?;
|
||||
self.find_char(b",")?;
|
||||
(Token::Regex(regex).into(), b'(')
|
||||
} else if !self.buf.is_empty() {
|
||||
self.is_start = false;
|
||||
|
|
|
@ -4,9 +4,15 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, net::IpAddr, sync::Arc};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
net::IpAddr,
|
||||
sync::{atomic::AtomicU8, Arc},
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use auth::{roles::RolePermissions, AccessToken};
|
||||
use config::{
|
||||
imap::ImapConfig,
|
||||
jmap::settings::JmapConfig,
|
||||
|
@ -19,7 +25,10 @@ use config::{
|
|||
storage::Storage,
|
||||
telemetry::Metrics,
|
||||
};
|
||||
use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy, Type};
|
||||
use directory::{
|
||||
backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory,
|
||||
Principal, QueryBy, Type,
|
||||
};
|
||||
use expr::if_block::IfBlock;
|
||||
use listener::{
|
||||
blocked::{AllowedIps, BlockedIps},
|
||||
|
@ -27,16 +36,22 @@ use listener::{
|
|||
};
|
||||
use mail_send::Credentials;
|
||||
|
||||
use manager::webadmin::Resource;
|
||||
use parking_lot::Mutex;
|
||||
use sieve::Sieve;
|
||||
use store::{
|
||||
write::{DirectoryClass, QueueClass, ValueClass},
|
||||
write::{QueueClass, ValueClass},
|
||||
IterateParams, LookupStore, ValueKey,
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use trc::AddContext;
|
||||
use utils::BlobHash;
|
||||
use utils::{
|
||||
map::ttl_dashmap::{ADashMap, TtlDashMap},
|
||||
BlobHash,
|
||||
};
|
||||
|
||||
pub mod addresses;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub mod enterprise;
|
||||
|
@ -46,6 +61,8 @@ pub mod manager;
|
|||
pub mod scripts;
|
||||
pub mod telemetry;
|
||||
|
||||
pub use psl;
|
||||
|
||||
pub static USER_AGENT: &str = concat!("Stalwart/", env!("CARGO_PKG_VERSION"),);
|
||||
pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
|
@ -63,10 +80,20 @@ pub struct Core {
|
|||
pub jmap: JmapConfig,
|
||||
pub imap: ImapConfig,
|
||||
pub metrics: Metrics,
|
||||
pub security: Security,
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub enterprise: Option<enterprise::Enterprise>,
|
||||
}
|
||||
|
||||
//TODO: temporary hack until OIDC is implemented
|
||||
#[derive(Default)]
|
||||
pub struct Security {
|
||||
pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>,
|
||||
pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>,
|
||||
pub permissions: ADashMap<u32, Arc<RolePermissions>>,
|
||||
pub permissions_version: AtomicU8,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Network {
|
||||
pub node_id: u64,
|
||||
|
@ -98,7 +125,7 @@ pub struct IngestMessage {
|
|||
pub session_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DeliveryResult {
|
||||
Success,
|
||||
TemporaryFailure {
|
||||
|
@ -128,11 +155,13 @@ impl Core {
|
|||
|
||||
pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> {
|
||||
self.storage.directories.get(name).unwrap_or_else(|| {
|
||||
trc::event!(
|
||||
Eval(trc::EvalEvent::DirectoryNotFound),
|
||||
Id = name.to_string(),
|
||||
SpanId = session_id,
|
||||
);
|
||||
if !name.is_empty() {
|
||||
trc::event!(
|
||||
Eval(trc::EvalEvent::DirectoryNotFound),
|
||||
Id = name.to_string(),
|
||||
SpanId = session_id,
|
||||
);
|
||||
}
|
||||
|
||||
&self.storage.directory
|
||||
})
|
||||
|
@ -140,11 +169,13 @@ impl Core {
|
|||
|
||||
pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore {
|
||||
self.storage.lookups.get(name).unwrap_or_else(|| {
|
||||
trc::event!(
|
||||
Eval(trc::EvalEvent::StoreNotFound),
|
||||
Id = name.to_string(),
|
||||
SpanId = session_id,
|
||||
);
|
||||
if !name.is_empty() {
|
||||
trc::event!(
|
||||
Eval(trc::EvalEvent::StoreNotFound),
|
||||
Id = name.to_string(),
|
||||
SpanId = session_id,
|
||||
);
|
||||
}
|
||||
|
||||
&self.storage.lookup
|
||||
})
|
||||
|
@ -227,7 +258,7 @@ impl Core {
|
|||
credentials: &Credentials<String>,
|
||||
remote_ip: IpAddr,
|
||||
return_member_of: bool,
|
||||
) -> trc::Result<Principal<u32>> {
|
||||
) -> trc::Result<Principal> {
|
||||
// First try to authenticate the user against the default directory
|
||||
let result = match directory
|
||||
.query(QueryBy::Credentials(credentials), return_member_of)
|
||||
|
@ -237,9 +268,9 @@ impl Core {
|
|||
trc::event!(
|
||||
Auth(trc::AuthEvent::Success),
|
||||
AccountName = credentials.login().to_string(),
|
||||
AccountId = principal.id,
|
||||
AccountId = principal.id(),
|
||||
SpanId = session_id,
|
||||
Type = principal.typ.as_str(),
|
||||
Type = principal.typ().as_str(),
|
||||
);
|
||||
|
||||
return Ok(principal);
|
||||
|
@ -268,7 +299,6 @@ impl Core {
|
|||
Auth(trc::AuthEvent::Success),
|
||||
AccountName = username.clone(),
|
||||
SpanId = session_id,
|
||||
Type = Type::Superuser.as_str(),
|
||||
);
|
||||
|
||||
return Ok(Principal::fallback_admin(fallback_pass));
|
||||
|
@ -289,8 +319,8 @@ impl Core {
|
|||
Auth(trc::AuthEvent::Success),
|
||||
AccountName = username.to_string(),
|
||||
SpanId = session_id,
|
||||
AccountId = principal.id,
|
||||
Type = principal.typ.as_str(),
|
||||
AccountId = principal.id(),
|
||||
Type = principal.typ().as_str(),
|
||||
);
|
||||
|
||||
return Ok(principal);
|
||||
|
@ -342,58 +372,22 @@ impl Core {
|
|||
}
|
||||
|
||||
pub async fn total_accounts(&self) -> trc::Result<u64> {
|
||||
total_accounts(&self.storage.data).await
|
||||
self.storage
|
||||
.data
|
||||
.count_principals(None, Type::Individual.into(), None)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn total_domains(&self) -> trc::Result<u64> {
|
||||
let mut total = 0;
|
||||
self.storage
|
||||
.data
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![]))),
|
||||
ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![
|
||||
u8::MAX;
|
||||
10
|
||||
]))),
|
||||
)
|
||||
.no_values()
|
||||
.ascending(),
|
||||
|_, _| {
|
||||
total += 1;
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.count_principals(None, Type::Domain.into(), None)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| total)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn total_accounts(store: &store::Store) -> trc::Result<u64> {
|
||||
let mut total = 0;
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![]))),
|
||||
ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![
|
||||
u8::MAX;
|
||||
10
|
||||
]))),
|
||||
)
|
||||
.ascending(),
|
||||
|_, value| {
|
||||
if matches!(value.last(), Some(0u8 | 4u8)) {
|
||||
total += 1;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| total)
|
||||
}
|
||||
|
||||
trait CredentialsUsername {
|
||||
fn login(&self) -> &str;
|
||||
}
|
||||
|
@ -407,3 +401,17 @@ impl CredentialsUsername for Credentials<String> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Security {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
access_tokens: self.access_tokens.clone(),
|
||||
permissions: self.permissions.clone(),
|
||||
permissions_version: AtomicU8::new(
|
||||
self.permissions_version
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
),
|
||||
logos: Mutex::new(self.logos.lock().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,11 @@ pub(crate) fn sign(
|
|||
let combined = format!("{}.{}", &protected, &payload);
|
||||
let signature = key
|
||||
.sign(&SystemRandom::new(), combined.as_bytes())
|
||||
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).caused_by(trc::location!()).reason(err))?;
|
||||
.map_err(|err| {
|
||||
trc::EventType::Acme(trc::AcmeEvent::Error)
|
||||
.caused_by(trc::location!())
|
||||
.reason(err)
|
||||
})?;
|
||||
let signature = URL_SAFE_NO_PAD.encode(signature.as_ref());
|
||||
let body = Body {
|
||||
protected,
|
||||
|
@ -31,7 +35,8 @@ pub(crate) fn sign(
|
|||
signature,
|
||||
};
|
||||
|
||||
serde_json::to_string(&body).map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
|
||||
serde_json::to_string(&body)
|
||||
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
|
||||
}
|
||||
|
||||
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> {
|
||||
|
|
|
@ -9,7 +9,6 @@ use rustls::sign::CertifiedKey;
|
|||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use utils::suffixlist::DomainPart;
|
||||
use x509_parser::parse_x509_certificate;
|
||||
|
||||
use crate::listener::acme::directory::Identifier;
|
||||
|
@ -241,11 +240,10 @@ impl Core {
|
|||
let domain = domain.strip_prefix("*.").unwrap_or(&domain);
|
||||
let name = format!("_acme-challenge.{}", domain);
|
||||
let origin = origin
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
self.smtp.resolvers.psl.domain_part(domain, DomainPart::Sld)
|
||||
})
|
||||
.unwrap_or_else(|| domain.to_string());
|
||||
.as_deref()
|
||||
.or_else(|| psl::domain_str(domain))
|
||||
.unwrap_or(domain)
|
||||
.to_string();
|
||||
|
||||
// First try deleting the record
|
||||
if let Err(err) = updater.delete(&name, &origin).await {
|
||||
|
|
|
@ -195,11 +195,11 @@ async fn restore_file(store: Store, blob_store: BlobStore, path: &Path) {
|
|||
.deserialize_leb128::<u32>()
|
||||
.expect("Failed to deserialize principal id"),
|
||||
)),
|
||||
3 => DirectoryClass::Domain(
|
||||
/*3 => DirectoryClass::Domain(
|
||||
key.get(1..)
|
||||
.expect("Failed to read directory string")
|
||||
.to_vec(),
|
||||
),
|
||||
),*/
|
||||
4 => {
|
||||
batch.add(
|
||||
ValueClass::Directory(DirectoryClass::UsedQuota(
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::{self, Cursor, Read},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
@ -22,12 +23,21 @@ pub struct WebAdminManager {
|
|||
routes: ArcSwap<AHashMap<String, Resource<PathBuf>>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Resource<T> {
|
||||
pub content_type: &'static str,
|
||||
pub content_type: Cow<'static, str>,
|
||||
pub contents: T,
|
||||
}
|
||||
|
||||
impl<T> Resource<T> {
|
||||
pub fn new(content_type: impl Into<Cow<'static, str>>, contents: T) -> Self {
|
||||
Self {
|
||||
content_type: content_type.into(),
|
||||
contents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebAdminManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -42,7 +52,7 @@ impl WebAdminManager {
|
|||
tokio::fs::read(&resource.contents)
|
||||
.await
|
||||
.map(|contents| Resource {
|
||||
content_type: resource.content_type,
|
||||
content_type: resource.content_type.clone(),
|
||||
contents,
|
||||
})
|
||||
.map_err(|err| {
|
||||
|
@ -114,7 +124,8 @@ impl WebAdminManager {
|
|||
"svg" => "image/svg+xml",
|
||||
"ico" => "image/x-icon",
|
||||
_ => "application/octet-stream",
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
contents: path,
|
||||
};
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ use unicode_security::MixedScript;
|
|||
|
||||
pub fn fn_is_ascii<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
|
||||
match &v[0] {
|
||||
Variable::String(s) => s.chars().all(|c| c.is_ascii()),
|
||||
Variable::String(s) => s.is_ascii(),
|
||||
Variable::Integer(_) | Variable::Float(_) => true,
|
||||
Variable::Array(a) => a.iter().all(|v| match v {
|
||||
Variable::String(s) => s.chars().all(|c| c.is_ascii()),
|
||||
Variable::String(s) => s.is_ascii(),
|
||||
_ => true,
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -63,10 +63,7 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable>
|
|||
// Train the model
|
||||
let mut model = BayesModel::default();
|
||||
model.train(
|
||||
OsbTokenizer::new(
|
||||
BayesTokenizer::new(text.as_ref(), &ctx.core.smtp.resolvers.psl),
|
||||
5,
|
||||
),
|
||||
OsbTokenizer::new(BayesTokenizer::new(text.as_ref()), 5),
|
||||
is_spam,
|
||||
);
|
||||
if model.weights.is_empty() {
|
||||
|
@ -187,10 +184,7 @@ pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> {
|
|||
|
||||
// Classify the text
|
||||
let mut tokens = Vec::new();
|
||||
for token in OsbTokenizer::<_, TokenHash>::new(
|
||||
BayesTokenizer::new(text.as_ref(), &ctx.core.smtp.resolvers.psl),
|
||||
5,
|
||||
) {
|
||||
for token in OsbTokenizer::<_, TokenHash>::new(BayesTokenizer::new(text.as_ref()), 5) {
|
||||
let weights = bayes_cache.get_or_update(token.inner, store).await?;
|
||||
tokens.push(OsbToken {
|
||||
inner: weights,
|
||||
|
|
|
@ -18,7 +18,6 @@ use mail_parser::{decoders::html::add_html_token, Message, PartType};
|
|||
use nlp::tokenizers::types::{TokenType, TypesTokenizer};
|
||||
use sha1::{Digest, Sha1};
|
||||
use tokio::net::UdpSocket;
|
||||
use utils::suffixlist::PublicSuffix;
|
||||
|
||||
const MIN_LINE_LENGTH: usize = 8;
|
||||
const ATOMIC_NUM_LINES: usize = 4;
|
||||
|
@ -47,9 +46,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
|
|||
}
|
||||
|
||||
// Hash message
|
||||
let request = ctx
|
||||
.message
|
||||
.pyzor_check_message(&ctx.core.smtp.resolvers.psl);
|
||||
let request = ctx.message.pyzor_check_message();
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
{
|
||||
|
@ -161,15 +158,15 @@ async fn pyzor_send_message(
|
|||
}
|
||||
|
||||
trait PyzorDigest<W: Write> {
|
||||
fn pyzor_digest(&self, writer: W, psl: &PublicSuffix) -> W;
|
||||
fn pyzor_digest(&self, writer: W) -> W;
|
||||
}
|
||||
|
||||
pub trait PyzorCheck {
|
||||
fn pyzor_check_message(&self, psl: &PublicSuffix) -> String;
|
||||
fn pyzor_check_message(&self) -> String;
|
||||
}
|
||||
|
||||
impl<'x, W: Write> PyzorDigest<W> for Message<'x> {
|
||||
fn pyzor_digest(&self, writer: W, psl: &PublicSuffix) -> W {
|
||||
fn pyzor_digest(&self, writer: W) -> W {
|
||||
let parts = self
|
||||
.parts
|
||||
.iter()
|
||||
|
@ -180,33 +177,27 @@ impl<'x, W: Write> PyzorDigest<W> for Message<'x> {
|
|||
})
|
||||
.collect::<Vec<Cow<str>>>();
|
||||
|
||||
pyzor_digest(writer, parts.iter().flat_map(|text| text.lines()), psl)
|
||||
pyzor_digest(writer, parts.iter().flat_map(|text| text.lines()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> PyzorCheck for Message<'x> {
|
||||
fn pyzor_check_message(&self, psl: &PublicSuffix) -> String {
|
||||
fn pyzor_check_message(&self) -> String {
|
||||
let time = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs());
|
||||
|
||||
pyzor_create_message(
|
||||
self,
|
||||
psl,
|
||||
time,
|
||||
(time & 0xFFFF) as u16 ^ ((time >> 16) & 0xFFFF) as u16,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn pyzor_create_message(
|
||||
message: &Message<'_>,
|
||||
psl: &PublicSuffix,
|
||||
time: u64,
|
||||
thread: u16,
|
||||
) -> String {
|
||||
fn pyzor_create_message(message: &Message<'_>, time: u64, thread: u16) -> String {
|
||||
// Hash message
|
||||
let hash = message.pyzor_digest(Sha1::new(), psl).finalize();
|
||||
let hash = message.pyzor_digest(Sha1::new()).finalize();
|
||||
// Hash key
|
||||
let mut hash_key = Sha1::new();
|
||||
hash_key.update("anonymous:".as_bytes());
|
||||
|
@ -223,13 +214,13 @@ fn pyzor_create_message(
|
|||
// Sign
|
||||
let mut sig = Sha1::new();
|
||||
sig.update(msg_hash);
|
||||
sig.update(&format!(":{time}:{hash_key:x}"));
|
||||
sig.update(format!(":{time}:{hash_key:x}"));
|
||||
let sig = sig.finalize();
|
||||
|
||||
format!("{message}\nSig: {sig:x}\n")
|
||||
}
|
||||
|
||||
fn pyzor_digest<'x, I, W>(mut writer: W, lines: I, psl: &PublicSuffix) -> W
|
||||
fn pyzor_digest<'x, I, W>(mut writer: W, lines: I) -> W
|
||||
where
|
||||
I: Iterator<Item = &'x str>,
|
||||
W: Write,
|
||||
|
@ -254,7 +245,7 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
for token in TypesTokenizer::new(line, psl) {
|
||||
for token in TypesTokenizer::new(line) {
|
||||
match token.word {
|
||||
TokenType::Alphabetic(_)
|
||||
| TokenType::Alphanumeric(_)
|
||||
|
@ -448,7 +439,6 @@ mod test {
|
|||
use mail_parser::MessageParser;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
use utils::suffixlist::PublicSuffix;
|
||||
|
||||
use super::pyzor_create_message;
|
||||
use super::pyzor_send_message;
|
||||
|
@ -485,11 +475,8 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn message_pyzor() {
|
||||
let mut psl = PublicSuffix::default();
|
||||
psl.suffixes.insert("com".to_string());
|
||||
let message = pyzor_create_message(
|
||||
&MessageParser::new().parse(HTML_TEXT_STYLE_SCRIPT).unwrap(),
|
||||
&psl,
|
||||
1697468672,
|
||||
49005,
|
||||
);
|
||||
|
@ -510,9 +497,6 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn digest_pyzor() {
|
||||
let mut psl = PublicSuffix::default();
|
||||
psl.suffixes.insert("com".to_string());
|
||||
|
||||
// HTML stripping
|
||||
assert_eq!(html_to_text(HTML_RAW), HTML_RAW_STRIPED);
|
||||
|
||||
|
@ -531,7 +515,6 @@ mod test {
|
|||
String::from_utf8(pyzor_digest(
|
||||
Vec::new(),
|
||||
format!("Test {strip_me} Test2").lines(),
|
||||
&psl
|
||||
))
|
||||
.unwrap(),
|
||||
"TestTest2"
|
||||
|
@ -543,7 +526,6 @@ mod test {
|
|||
String::from_utf8(pyzor_digest(
|
||||
Vec::new(),
|
||||
concat!("This line is included\n", "not this\n", "This also").lines(),
|
||||
&psl
|
||||
))
|
||||
.unwrap(),
|
||||
"ThislineisincludedThisalso"
|
||||
|
@ -554,7 +536,6 @@ mod test {
|
|||
String::from_utf8(pyzor_digest(
|
||||
Vec::new(),
|
||||
"All this message\nShould be included\nIn the digest".lines(),
|
||||
&psl
|
||||
))
|
||||
.unwrap(),
|
||||
"AllthismessageShouldbeincludedInthedigest"
|
||||
|
@ -570,7 +551,7 @@ mod test {
|
|||
expected += format!("Line{i}testtesttest").as_str();
|
||||
}
|
||||
assert_eq!(
|
||||
String::from_utf8(pyzor_digest(Vec::new(), text.lines(), &psl)).unwrap(),
|
||||
String::from_utf8(pyzor_digest(Vec::new(), text.lines(),)).unwrap(),
|
||||
expected
|
||||
);
|
||||
|
||||
|
@ -602,7 +583,7 @@ mod test {
|
|||
MessageParser::new()
|
||||
.parse(input)
|
||||
.unwrap()
|
||||
.pyzor_digest(Vec::new(), &psl)
|
||||
.pyzor_digest(Vec::new(),)
|
||||
)
|
||||
.unwrap(),
|
||||
expected,
|
||||
|
@ -617,7 +598,7 @@ mod test {
|
|||
MessageParser::new()
|
||||
.parse(HTML_TEXT_STYLE_SCRIPT)
|
||||
.unwrap()
|
||||
.pyzor_digest(Sha1::new(), &psl)
|
||||
.pyzor_digest(Sha1::new(),)
|
||||
.finalize()
|
||||
),
|
||||
"b2c27325a034c581df0c9ef37e4a0d63208a3e7e",
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
use nlp::tokenizers::types::{TokenType, TypesTokenizer};
|
||||
use sieve::{runtime::Variable, FunctionMap};
|
||||
use utils::suffixlist::DomainPart;
|
||||
|
||||
use crate::scripts::functions::{html::html_to_tokens, text::tokenize_words, ApplyString};
|
||||
|
||||
|
@ -33,7 +32,7 @@ pub fn exec_tokenize(ctx: PluginContext<'_>) -> trc::Result<Variable> {
|
|||
|
||||
Ok(match v.remove(0) {
|
||||
v @ (Variable::String(_) | Variable::Array(_)) => {
|
||||
TypesTokenizer::new(v.to_string().as_ref(), &ctx.core.smtp.resolvers.psl)
|
||||
TypesTokenizer::new(v.to_string().as_ref())
|
||||
.tokenize_numbers(false)
|
||||
.tokenize_urls(urls)
|
||||
.tokenize_urls_without_scheme(urls_without_scheme)
|
||||
|
@ -53,6 +52,12 @@ pub fn exec_tokenize(ctx: PluginContext<'_>) -> trc::Result<Variable> {
|
|||
})
|
||||
}
|
||||
|
||||
enum DomainPart {
|
||||
Sld,
|
||||
Tld,
|
||||
Host,
|
||||
}
|
||||
|
||||
pub fn exec_domain_part(ctx: PluginContext<'_>) -> trc::Result<Variable> {
|
||||
let v = ctx.arguments;
|
||||
let part = match v[1].to_string().as_ref() {
|
||||
|
@ -63,12 +68,12 @@ pub fn exec_domain_part(ctx: PluginContext<'_>) -> trc::Result<Variable> {
|
|||
};
|
||||
|
||||
Ok(v[0].transform(|domain| {
|
||||
ctx.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.psl
|
||||
.domain_part(domain, part)
|
||||
.map(Variable::from)
|
||||
.unwrap_or_default()
|
||||
match part {
|
||||
DomainPart::Sld => psl::domain_str(domain),
|
||||
DomainPart::Tld => domain.rsplit_once('.').map(|(_, tld)| tld),
|
||||
DomainPart::Host => domain.split_once('.').map(|(host, _)| host),
|
||||
}
|
||||
.map(Variable::from)
|
||||
.unwrap_or_default()
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
[package]
|
||||
name = "directory"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
utils = { path = "../utils" }
|
||||
proc_macros = { path = "../utils/proc-macros" }
|
||||
store = { path = "../store" }
|
||||
trc = { path = "../trc" }
|
||||
jmap_proto = { path = "../jmap-proto" }
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy};
|
|||
use super::{ImapDirectory, ImapError};
|
||||
|
||||
impl ImapDirectory {
|
||||
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
|
||||
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> {
|
||||
if let QueryBy::Credentials(credentials) = query {
|
||||
let mut client = self
|
||||
.pool
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
use mail_send::Credentials;
|
||||
use store::{
|
||||
write::{DirectoryClass, ValueClass},
|
||||
IterateParams, Store, ValueKey,
|
||||
Deserialize, IterateParams, Store, ValueKey,
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{Principal, QueryBy, Type};
|
||||
|
||||
use super::{manage::ManageDirectory, PrincipalIdType};
|
||||
use super::{manage::ManageDirectory, PrincipalField, PrincipalInfo};
|
||||
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait DirectoryStore: Sync + Send {
|
||||
|
@ -20,9 +21,8 @@ pub trait DirectoryStore: Sync + Send {
|
|||
&self,
|
||||
by: QueryBy<'_>,
|
||||
return_member_of: bool,
|
||||
) -> trc::Result<Option<Principal<u32>>>;
|
||||
) -> trc::Result<Option<Principal>>;
|
||||
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>;
|
||||
|
||||
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
|
||||
async fn rcpt(&self, address: &str) -> trc::Result<bool>;
|
||||
async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>>;
|
||||
|
@ -34,62 +34,65 @@ impl DirectoryStore for Store {
|
|||
&self,
|
||||
by: QueryBy<'_>,
|
||||
return_member_of: bool,
|
||||
) -> trc::Result<Option<Principal<u32>>> {
|
||||
) -> trc::Result<Option<Principal>> {
|
||||
let (account_id, secret) = match by {
|
||||
QueryBy::Name(name) => (self.get_account_id(name).await?, None),
|
||||
QueryBy::Name(name) => (self.get_principal_id(name).await?, None),
|
||||
QueryBy::Id(account_id) => (account_id.into(), None),
|
||||
QueryBy::Credentials(credentials) => match credentials {
|
||||
Credentials::Plain { username, secret } => {
|
||||
(self.get_account_id(username).await?, secret.as_str().into())
|
||||
}
|
||||
Credentials::Plain { username, secret } => (
|
||||
self.get_principal_id(username).await?,
|
||||
secret.as_str().into(),
|
||||
),
|
||||
Credentials::OAuthBearer { token } => {
|
||||
(self.get_account_id(token).await?, token.as_str().into())
|
||||
}
|
||||
Credentials::XOauth2 { username, secret } => {
|
||||
(self.get_account_id(username).await?, secret.as_str().into())
|
||||
(self.get_principal_id(token).await?, token.as_str().into())
|
||||
}
|
||||
Credentials::XOauth2 { username, secret } => (
|
||||
self.get_principal_id(username).await?,
|
||||
secret.as_str().into(),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(account_id) = account_id {
|
||||
match (
|
||||
self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
if let Some(mut principal) = self
|
||||
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::Principal(account_id),
|
||||
)))
|
||||
.await?,
|
||||
secret,
|
||||
) {
|
||||
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => {
|
||||
if return_member_of {
|
||||
principal.member_of = self.get_member_of(principal.id).await?;
|
||||
.await?
|
||||
{
|
||||
if let Some(secret) = secret {
|
||||
if !principal.verify_secret(secret).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(principal))
|
||||
}
|
||||
(Some(mut principal), None) => {
|
||||
if return_member_of {
|
||||
principal.member_of = self.get_member_of(principal.id).await?;
|
||||
}
|
||||
|
||||
Ok(Some(principal))
|
||||
if return_member_of {
|
||||
for member in self.get_member_of(principal.id).await? {
|
||||
let field = match member.typ {
|
||||
Type::List => PrincipalField::Lists,
|
||||
Type::Role => PrincipalField::Roles,
|
||||
_ => PrincipalField::MemberOf,
|
||||
};
|
||||
principal.append_int(field, member.principal_id);
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
return Ok(Some(principal));
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>> {
|
||||
if let Some(ptype) = self
|
||||
.get_value::<PrincipalIdType>(ValueKey::from(ValueClass::Directory(
|
||||
.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::EmailToId(email.as_bytes().to_vec()),
|
||||
)))
|
||||
.await?
|
||||
{
|
||||
if ptype.typ != Type::List {
|
||||
Ok(vec![ptype.account_id])
|
||||
Ok(vec![ptype.id])
|
||||
} else {
|
||||
self.get_members(ptype.account_id).await
|
||||
self.get_members(ptype.id).await
|
||||
}
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
|
@ -97,11 +100,11 @@ impl DirectoryStore for Store {
|
|||
}
|
||||
|
||||
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
|
||||
self.get_value::<()>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::Domain(domain.as_bytes().to_vec()),
|
||||
self.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::NameToId(domain.as_bytes().to_vec()),
|
||||
)))
|
||||
.await
|
||||
.map(|ids| ids.is_some())
|
||||
.map(|p| p.map_or(false, |p| p.typ == Type::Domain))
|
||||
}
|
||||
|
||||
async fn rcpt(&self, address: &str) -> trc::Result<bool> {
|
||||
|
@ -122,12 +125,16 @@ impl DirectoryStore for Store {
|
|||
ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId(
|
||||
vec![u8::MAX; 10],
|
||||
))),
|
||||
)
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
),
|
||||
|key, value| {
|
||||
let key =
|
||||
std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default();
|
||||
if key.split('@').next().unwrap_or(key).contains(address) {
|
||||
if key.split('@').next().unwrap_or(key).contains(address)
|
||||
&& PrincipalInfo::deserialize(value)
|
||||
.caused_by(trc::location!())?
|
||||
.typ
|
||||
!= Type::List
|
||||
{
|
||||
results.push(key.to_string());
|
||||
}
|
||||
Ok(true)
|
||||
|
@ -141,15 +148,23 @@ impl DirectoryStore for Store {
|
|||
|
||||
async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
|
||||
let mut results = Vec::new();
|
||||
for account_id in self.email_to_ids(address).await? {
|
||||
if let Some(email) = self
|
||||
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::Principal(account_id),
|
||||
)))
|
||||
.await?
|
||||
.and_then(|p| p.emails.into_iter().next())
|
||||
{
|
||||
results.push(email);
|
||||
if let Some(ptype) = self
|
||||
.get_value::<PrincipalInfo>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::EmailToId(address.as_bytes().to_vec()),
|
||||
)))
|
||||
.await?
|
||||
.filter(|p| p.typ == Type::List)
|
||||
{
|
||||
for account_id in self.get_members(ptype.id).await? {
|
||||
if let Some(email) = self
|
||||
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryClass::Principal(account_id),
|
||||
)))
|
||||
.await?
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Emails))
|
||||
{
|
||||
results.push(email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,47 +7,73 @@
|
|||
pub mod lookup;
|
||||
pub mod manage;
|
||||
|
||||
use std::{fmt::Display, slice::Iter, str::FromStr};
|
||||
use std::{fmt::Display, slice::Iter};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN};
|
||||
use utils::codec::leb128::Leb128Iterator;
|
||||
|
||||
use crate::{Principal, Type};
|
||||
use crate::{Principal, Type, ROLE_ADMIN, ROLE_USER};
|
||||
|
||||
pub(super) struct PrincipalIdType {
|
||||
pub account_id: u32,
|
||||
const INT_MARKER: u8 = 1 << 7;
|
||||
|
||||
pub struct PrincipalInfo {
|
||||
pub id: u32,
|
||||
pub typ: Type,
|
||||
pub tenant: Option<u32>,
|
||||
}
|
||||
|
||||
impl Serialize for Principal<u32> {
|
||||
impl Serialize for Principal {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
(&self).serialize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for &Principal<u32> {
|
||||
impl Serialize for &Principal {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut serializer = KeySerializer::new(
|
||||
U32_LEN * 3
|
||||
U32_LEN * 2
|
||||
+ 2
|
||||
+ self.name.len()
|
||||
+ self.emails.iter().map(|s| s.len()).sum::<usize>()
|
||||
+ self.secrets.iter().map(|s| s.len()).sum::<usize>()
|
||||
+ self.description.as_ref().map(|s| s.len()).unwrap_or(0),
|
||||
+ self
|
||||
.fields
|
||||
.values()
|
||||
.map(|v| v.serialized_size() + 1)
|
||||
.sum::<usize>(),
|
||||
)
|
||||
.write(1u8)
|
||||
.write(2u8)
|
||||
.write_leb128(self.id)
|
||||
.write(self.typ as u8)
|
||||
.write_leb128(self.quota)
|
||||
.write_leb128(self.name.len())
|
||||
.write(self.name.as_bytes())
|
||||
.write_leb128(self.description.as_ref().map_or(0, |s| s.len()))
|
||||
.write(self.description.as_deref().unwrap_or_default().as_bytes());
|
||||
.write_leb128(self.fields.len());
|
||||
|
||||
for list in [&self.secrets, &self.emails] {
|
||||
serializer = serializer.write_leb128(list.len());
|
||||
for value in list {
|
||||
serializer = serializer.write_leb128(value.len()).write(value.as_bytes());
|
||||
for (k, v) in &self.fields {
|
||||
let id = k.id();
|
||||
|
||||
match v {
|
||||
PrincipalValue::String(v) => {
|
||||
serializer = serializer
|
||||
.write(id)
|
||||
.write_leb128(1usize)
|
||||
.write_leb128(v.len())
|
||||
.write(v.as_bytes());
|
||||
}
|
||||
PrincipalValue::StringList(l) => {
|
||||
serializer = serializer.write(id).write_leb128(l.len());
|
||||
for v in l {
|
||||
serializer = serializer.write_leb128(v.len()).write(v.as_bytes());
|
||||
}
|
||||
}
|
||||
PrincipalValue::Integer(v) => {
|
||||
serializer = serializer
|
||||
.write(id | INT_MARKER)
|
||||
.write_leb128(1usize)
|
||||
.write_leb128(*v);
|
||||
}
|
||||
PrincipalValue::IntegerList(l) => {
|
||||
serializer = serializer.write(id | INT_MARKER).write_leb128(l.len());
|
||||
for v in l {
|
||||
serializer = serializer.write_leb128(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +81,7 @@ impl Serialize for &Principal<u32> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Principal<u32> {
|
||||
impl Deserialize for Principal {
|
||||
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
|
||||
deserialize(bytes).ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
|
@ -65,20 +91,37 @@ impl Deserialize for Principal<u32> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Serialize for PrincipalIdType {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
KeySerializer::new(U32_LEN + 1)
|
||||
.write_leb128(self.account_id)
|
||||
.write(self.typ as u8)
|
||||
.finalize()
|
||||
impl PrincipalInfo {
|
||||
pub fn has_tenant_access(&self, tenant_id: Option<u32>) -> bool {
|
||||
tenant_id.map_or(true, |tenant_id| {
|
||||
self.tenant.map_or(false, |t| tenant_id == t)
|
||||
|| (self.typ == Type::Tenant && self.id == tenant_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for PrincipalIdType {
|
||||
impl Serialize for PrincipalInfo {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
if let Some(tenant) = self.tenant {
|
||||
KeySerializer::new((U32_LEN * 2) + 1)
|
||||
.write_leb128(self.id)
|
||||
.write(self.typ as u8)
|
||||
.write_leb128(tenant)
|
||||
.finalize()
|
||||
} else {
|
||||
KeySerializer::new(U32_LEN + 1)
|
||||
.write_leb128(self.id)
|
||||
.write(self.typ as u8)
|
||||
.finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for PrincipalInfo {
|
||||
fn deserialize(bytes_: &[u8]) -> trc::Result<Self> {
|
||||
let mut bytes = bytes_.iter();
|
||||
Ok(PrincipalIdType {
|
||||
account_id: bytes.next_leb128().ok_or_else(|| {
|
||||
Ok(PrincipalInfo {
|
||||
id: bytes.next_leb128().ok_or_else(|| {
|
||||
trc::StoreEvent::DataCorruption
|
||||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes_)
|
||||
|
@ -88,59 +131,121 @@ impl Deserialize for PrincipalIdType {
|
|||
.caused_by(trc::location!())
|
||||
.ctx(trc::Key::Value, bytes_)
|
||||
})?),
|
||||
tenant: bytes.next_leb128(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalIdType {
|
||||
pub fn new(account_id: u32, typ: Type) -> Self {
|
||||
Self { account_id, typ }
|
||||
impl PrincipalInfo {
|
||||
pub fn new(principal_id: u32, typ: Type, tenant: Option<u32>) -> Self {
|
||||
Self {
|
||||
id: principal_id,
|
||||
typ,
|
||||
tenant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize(bytes: &[u8]) -> Option<Principal<u32>> {
|
||||
fn deserialize(bytes: &[u8]) -> Option<Principal> {
|
||||
let mut bytes = bytes.iter();
|
||||
if bytes.next()? != &1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Principal {
|
||||
id: bytes.next_leb128()?,
|
||||
typ: Type::from_u8(*bytes.next()?),
|
||||
quota: bytes.next_leb128()?,
|
||||
name: deserialize_string(&mut bytes)?,
|
||||
description: deserialize_string(&mut bytes).map(|v| {
|
||||
if !v.is_empty() {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
let version = *bytes.next()?;
|
||||
let id = bytes.next_leb128()?;
|
||||
let type_id = *bytes.next()?;
|
||||
let typ = Type::from_u8(type_id);
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
// Version 1 (legacy)
|
||||
let mut principal = Principal {
|
||||
id,
|
||||
typ,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
principal.set(PrincipalField::Quota, bytes.next_leb128::<u64>()?);
|
||||
principal.set(PrincipalField::Name, deserialize_string(&mut bytes)?);
|
||||
if let Some(description) = deserialize_string(&mut bytes).filter(|s| !s.is_empty()) {
|
||||
principal.set(PrincipalField::Description, description);
|
||||
}
|
||||
})?,
|
||||
secrets: deserialize_string_list(&mut bytes)?,
|
||||
emails: deserialize_string_list(&mut bytes)?,
|
||||
member_of: Vec::new(),
|
||||
for key in [PrincipalField::Secrets, PrincipalField::Emails] {
|
||||
for _ in 0..bytes.next_leb128::<usize>()? {
|
||||
principal.append_str(key, deserialize_string(&mut bytes)?);
|
||||
}
|
||||
}
|
||||
|
||||
principal
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
if type_id != 4 { ROLE_USER } else { ROLE_ADMIN },
|
||||
)
|
||||
.into()
|
||||
}
|
||||
2 => {
|
||||
// Version 2
|
||||
let num_fields = bytes.next_leb128::<usize>()?;
|
||||
|
||||
let mut principal = Principal {
|
||||
id,
|
||||
typ,
|
||||
fields: AHashMap::with_capacity(num_fields),
|
||||
};
|
||||
|
||||
for _ in 0..num_fields {
|
||||
let id = *bytes.next()?;
|
||||
let num_values = bytes.next_leb128::<usize>()?;
|
||||
|
||||
if (id & INT_MARKER) == 0 {
|
||||
let field = PrincipalField::from_id(id)?;
|
||||
if num_values == 1 {
|
||||
principal.set(field, deserialize_string(&mut bytes)?);
|
||||
} else {
|
||||
let mut values = Vec::with_capacity(num_values);
|
||||
for _ in 0..num_values {
|
||||
values.push(deserialize_string(&mut bytes)?);
|
||||
}
|
||||
principal.set(field, values);
|
||||
}
|
||||
} else {
|
||||
let field = PrincipalField::from_id(id & !INT_MARKER)?;
|
||||
if num_values == 1 {
|
||||
principal.set(field, bytes.next_leb128::<u64>()?);
|
||||
} else {
|
||||
let mut values = Vec::with_capacity(num_values);
|
||||
for _ in 0..num_values {
|
||||
values.push(bytes.next_leb128::<u64>()?);
|
||||
}
|
||||
principal.set(field, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
principal.into()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PrincipalField {
|
||||
#[serde(rename = "name")]
|
||||
Name,
|
||||
#[serde(rename = "type")]
|
||||
Type,
|
||||
#[serde(rename = "quota")]
|
||||
Quota,
|
||||
#[serde(rename = "description")]
|
||||
UsedQuota,
|
||||
Description,
|
||||
#[serde(rename = "secrets")]
|
||||
Secrets,
|
||||
#[serde(rename = "emails")]
|
||||
Emails,
|
||||
#[serde(rename = "memberOf")]
|
||||
MemberOf,
|
||||
#[serde(rename = "members")]
|
||||
Members,
|
||||
Tenant,
|
||||
Roles,
|
||||
Lists,
|
||||
EnabledPermissions,
|
||||
DisabledPermissions,
|
||||
Picture,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
|
@ -160,12 +265,13 @@ pub enum PrincipalAction {
|
|||
RemoveItem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PrincipalValue {
|
||||
String(String),
|
||||
StringList(Vec<String>),
|
||||
Integer(u64),
|
||||
IntegerList(Vec<u64>),
|
||||
}
|
||||
|
||||
impl PrincipalUpdate {
|
||||
|
@ -201,16 +307,85 @@ impl Display for PrincipalField {
|
|||
}
|
||||
|
||||
impl PrincipalField {
|
||||
pub fn id(&self) -> u8 {
|
||||
match self {
|
||||
PrincipalField::Name => 0,
|
||||
PrincipalField::Type => 1,
|
||||
PrincipalField::Quota => 2,
|
||||
PrincipalField::Description => 3,
|
||||
PrincipalField::Secrets => 4,
|
||||
PrincipalField::Emails => 5,
|
||||
PrincipalField::MemberOf => 6,
|
||||
PrincipalField::Members => 7,
|
||||
PrincipalField::Tenant => 8,
|
||||
PrincipalField::Roles => 9,
|
||||
PrincipalField::Lists => 10,
|
||||
PrincipalField::EnabledPermissions => 11,
|
||||
PrincipalField::DisabledPermissions => 12,
|
||||
PrincipalField::UsedQuota => 13,
|
||||
PrincipalField::Picture => 14,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_id(id: u8) -> Option<Self> {
|
||||
match id {
|
||||
0 => Some(PrincipalField::Name),
|
||||
1 => Some(PrincipalField::Type),
|
||||
2 => Some(PrincipalField::Quota),
|
||||
3 => Some(PrincipalField::Description),
|
||||
4 => Some(PrincipalField::Secrets),
|
||||
5 => Some(PrincipalField::Emails),
|
||||
6 => Some(PrincipalField::MemberOf),
|
||||
7 => Some(PrincipalField::Members),
|
||||
8 => Some(PrincipalField::Tenant),
|
||||
9 => Some(PrincipalField::Roles),
|
||||
10 => Some(PrincipalField::Lists),
|
||||
11 => Some(PrincipalField::EnabledPermissions),
|
||||
12 => Some(PrincipalField::DisabledPermissions),
|
||||
13 => Some(PrincipalField::UsedQuota),
|
||||
14 => Some(PrincipalField::Picture),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PrincipalField::Name => "name",
|
||||
PrincipalField::Type => "type",
|
||||
PrincipalField::Quota => "quota",
|
||||
PrincipalField::UsedQuota => "usedQuota",
|
||||
PrincipalField::Description => "description",
|
||||
PrincipalField::Secrets => "secrets",
|
||||
PrincipalField::Emails => "emails",
|
||||
PrincipalField::MemberOf => "memberOf",
|
||||
PrincipalField::Members => "members",
|
||||
PrincipalField::Tenant => "tenant",
|
||||
PrincipalField::Roles => "roles",
|
||||
PrincipalField::Lists => "lists",
|
||||
PrincipalField::EnabledPermissions => "enabledPermissions",
|
||||
PrincipalField::DisabledPermissions => "disabledPermissions",
|
||||
PrincipalField::Picture => "picture",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"name" => Some(PrincipalField::Name),
|
||||
"type" => Some(PrincipalField::Type),
|
||||
"quota" => Some(PrincipalField::Quota),
|
||||
"usedQuota" => Some(PrincipalField::UsedQuota),
|
||||
"description" => Some(PrincipalField::Description),
|
||||
"secrets" => Some(PrincipalField::Secrets),
|
||||
"emails" => Some(PrincipalField::Emails),
|
||||
"memberOf" => Some(PrincipalField::MemberOf),
|
||||
"members" => Some(PrincipalField::Members),
|
||||
"tenant" => Some(PrincipalField::Tenant),
|
||||
"roles" => Some(PrincipalField::Roles),
|
||||
"lists" => Some(PrincipalField::Lists),
|
||||
"enabledPermissions" => Some(PrincipalField::EnabledPermissions),
|
||||
"disabledPermissions" => Some(PrincipalField::DisabledPermissions),
|
||||
"picture" => Some(PrincipalField::Picture),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,58 +399,7 @@ fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> {
|
|||
String::from_utf8(string).ok()
|
||||
}
|
||||
|
||||
fn deserialize_string_list(bytes: &mut Iter<'_, u8>) -> Option<Vec<String>> {
|
||||
let len = bytes.next_leb128()?;
|
||||
let mut list = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
list.push(deserialize_string(bytes)?);
|
||||
}
|
||||
Some(list)
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"individual" => Some(Type::Individual),
|
||||
"superuser" => Some(Type::Superuser),
|
||||
"group" => Some(Type::Group),
|
||||
"resource" => Some(Type::Resource),
|
||||
"location" => Some(Type::Location),
|
||||
"list" => Some(Type::List),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
0 => Type::Individual,
|
||||
1 => Type::Group,
|
||||
2 => Type::Resource,
|
||||
3 => Type::Location,
|
||||
4 => Type::Superuser,
|
||||
5 => Type::List,
|
||||
_ => Type::Other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_base_type(self) -> Self {
|
||||
match self {
|
||||
Type::Superuser => Type::Individual,
|
||||
any => any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Type {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Type::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SpecialSecrets {
|
||||
fn is_disabled(&self) -> bool;
|
||||
fn is_otp_auth(&self) -> bool;
|
||||
fn is_app_password(&self) -> bool;
|
||||
fn is_password(&self) -> bool;
|
||||
|
@ -285,10 +409,6 @@ impl<T> SpecialSecrets for T
|
|||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn is_disabled(&self) -> bool {
|
||||
self.as_ref() == "$disabled$"
|
||||
}
|
||||
|
||||
fn is_otp_auth(&self) -> bool {
|
||||
self.as_ref().starts_with("otpauth://")
|
||||
}
|
||||
|
@ -298,6 +418,6 @@ where
|
|||
}
|
||||
|
||||
fn is_password(&self) -> bool {
|
||||
!self.is_disabled() && !self.is_otp_auth() && !self.is_app_password()
|
||||
!self.is_otp_auth() && !self.is_app_password()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,12 @@
|
|||
|
||||
use ldap3::{Ldap, LdapConnAsync, Scope, SearchEntry};
|
||||
use mail_send::Credentials;
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{backend::internal::manage::ManageDirectory, IntoError, Principal, QueryBy, Type};
|
||||
use crate::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField},
|
||||
IntoError, Principal, QueryBy, Type, ROLE_ADMIN, ROLE_USER,
|
||||
};
|
||||
|
||||
use super::{LdapDirectory, LdapMappings};
|
||||
|
||||
|
@ -16,7 +20,7 @@ impl LdapDirectory {
|
|||
&self,
|
||||
by: QueryBy<'_>,
|
||||
return_member_of: bool,
|
||||
) -> trc::Result<Option<Principal<u32>>> {
|
||||
) -> trc::Result<Option<Principal>> {
|
||||
let mut conn = self.pool.get().await.map_err(|err| err.into_error())?;
|
||||
let mut account_id = None;
|
||||
let account_name;
|
||||
|
@ -35,7 +39,7 @@ impl LdapDirectory {
|
|||
}
|
||||
}
|
||||
QueryBy::Id(uid) => {
|
||||
if let Some(username) = self.data_store.get_account_name(uid).await? {
|
||||
if let Some(username) = self.data_store.get_principal_name(uid).await? {
|
||||
account_name = username;
|
||||
} else {
|
||||
return Ok(None);
|
||||
|
@ -122,50 +126,86 @@ impl LdapDirectory {
|
|||
} else {
|
||||
principal.id = self
|
||||
.data_store
|
||||
.get_or_create_account_id(&account_name)
|
||||
.get_or_create_principal_id(&account_name, Type::Individual)
|
||||
.await?;
|
||||
}
|
||||
principal.name = account_name;
|
||||
principal.append_str(PrincipalField::Name, account_name);
|
||||
|
||||
// Obtain groups
|
||||
if return_member_of && !principal.member_of.is_empty() {
|
||||
for member_of in principal.member_of.iter_mut() {
|
||||
if member_of.contains('=') {
|
||||
let (rs, _res) = conn
|
||||
.search(
|
||||
member_of,
|
||||
Scope::Base,
|
||||
"objectClass=*",
|
||||
&self.mappings.attr_name,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.into_error().caused_by(trc::location!()))?
|
||||
.success()
|
||||
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
|
||||
for entry in rs {
|
||||
'outer: for (attr, value) in SearchEntry::construct(entry).attrs {
|
||||
if self.mappings.attr_name.contains(&attr) {
|
||||
if let Some(group) = value.into_iter().next() {
|
||||
if !group.is_empty() {
|
||||
*member_of = group;
|
||||
break 'outer;
|
||||
if return_member_of {
|
||||
// Obtain groups
|
||||
if principal.has_field(PrincipalField::MemberOf) {
|
||||
let mut member_of = Vec::new();
|
||||
for mut name in principal
|
||||
.take_str_array(PrincipalField::MemberOf)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if name.contains('=') {
|
||||
let (rs, _res) = conn
|
||||
.search(
|
||||
&name,
|
||||
Scope::Base,
|
||||
"objectClass=*",
|
||||
&self.mappings.attr_name,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.into_error().caused_by(trc::location!()))?
|
||||
.success()
|
||||
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
|
||||
for entry in rs {
|
||||
'outer: for (attr, value) in SearchEntry::construct(entry).attrs {
|
||||
if self.mappings.attr_name.contains(&attr) {
|
||||
if let Some(group) = value.into_iter().next() {
|
||||
if !group.is_empty() {
|
||||
name = group;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
member_of.push(
|
||||
self.data_store
|
||||
.get_or_create_principal_id(&name, Type::Group)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
}
|
||||
|
||||
// Map ids
|
||||
principal.set(PrincipalField::MemberOf, member_of);
|
||||
}
|
||||
|
||||
// Map ids
|
||||
self.data_store
|
||||
.map_principal(principal, true)
|
||||
// Obtain roles
|
||||
let mut did_role_cleanup = false;
|
||||
for member in self
|
||||
.data_store
|
||||
.get_member_of(principal.id)
|
||||
.await
|
||||
.map(Some)
|
||||
} else {
|
||||
principal.member_of.clear();
|
||||
Ok(Some(principal.into()))
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
match member.typ {
|
||||
Type::List => {
|
||||
principal.append_int(PrincipalField::Lists, member.principal_id);
|
||||
}
|
||||
Type::Role => {
|
||||
if !did_role_cleanup {
|
||||
principal.remove(PrincipalField::Roles);
|
||||
did_role_cleanup = true;
|
||||
}
|
||||
principal.append_int(PrincipalField::Roles, member.principal_id);
|
||||
}
|
||||
_ => {
|
||||
principal.append_int(PrincipalField::MemberOf, member.principal_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if principal.has_field(PrincipalField::MemberOf) {
|
||||
principal.remove(PrincipalField::MemberOf);
|
||||
}
|
||||
|
||||
Ok(Some(principal))
|
||||
}
|
||||
|
||||
pub async fn email_to_ids(&self, address: &str) -> trc::Result<Vec<u32>> {
|
||||
|
@ -202,7 +242,11 @@ impl LdapDirectory {
|
|||
'outer: for attr in &self.mappings.attr_name {
|
||||
if let Some(name) = entry.attrs.get(attr).and_then(|v| v.first()) {
|
||||
if !name.is_empty() {
|
||||
ids.push(self.data_store.get_or_create_account_id(name).await?);
|
||||
ids.push(
|
||||
self.data_store
|
||||
.get_or_create_principal_id(name, Type::Individual)
|
||||
.await?,
|
||||
);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
@ -370,7 +414,7 @@ impl LdapDirectory {
|
|||
&self,
|
||||
conn: &mut Ldap,
|
||||
filter: &str,
|
||||
) -> trc::Result<Option<Principal<String>>> {
|
||||
) -> trc::Result<Option<Principal>> {
|
||||
conn.search(
|
||||
&self.mappings.base_dn,
|
||||
Scope::Subtree,
|
||||
|
@ -400,39 +444,49 @@ impl LdapDirectory {
|
|||
}
|
||||
|
||||
impl LdapMappings {
|
||||
fn entry_to_principal(&self, entry: SearchEntry) -> Principal<String> {
|
||||
fn entry_to_principal(&self, entry: SearchEntry) -> Principal {
|
||||
let mut principal = Principal::default();
|
||||
let mut role = ROLE_USER;
|
||||
|
||||
for (attr, value) in entry.attrs {
|
||||
if self.attr_name.contains(&attr) {
|
||||
principal.name = value.into_iter().next().unwrap_or_default();
|
||||
principal.set(
|
||||
PrincipalField::Name,
|
||||
value.into_iter().next().unwrap_or_default(),
|
||||
);
|
||||
} else if self.attr_secret.contains(&attr) {
|
||||
principal.secrets.extend(value);
|
||||
for item in value {
|
||||
principal.append_str(PrincipalField::Secrets, item);
|
||||
}
|
||||
} else if self.attr_email_address.contains(&attr) {
|
||||
for value in value {
|
||||
if principal.emails.is_empty() {
|
||||
principal.emails.push(value);
|
||||
} else {
|
||||
principal.emails.insert(0, value);
|
||||
}
|
||||
for item in value {
|
||||
principal.prepend_str(PrincipalField::Emails, item);
|
||||
}
|
||||
} else if self.attr_email_alias.contains(&attr) {
|
||||
principal.emails.extend(value);
|
||||
for item in value {
|
||||
principal.append_str(PrincipalField::Emails, item);
|
||||
}
|
||||
} else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) {
|
||||
if principal.description.is_none() || idx == 0 {
|
||||
principal.description = value.into_iter().next();
|
||||
if !principal.has_field(PrincipalField::Description) || idx == 0 {
|
||||
principal.set(
|
||||
PrincipalField::Description,
|
||||
value.into_iter().next().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
} else if self.attr_groups.contains(&attr) {
|
||||
principal.member_of.extend(value);
|
||||
for item in value {
|
||||
principal.append_str(PrincipalField::MemberOf, item);
|
||||
}
|
||||
} else if self.attr_quota.contains(&attr) {
|
||||
if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() {
|
||||
principal.quota = quota;
|
||||
if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse::<u64>() {
|
||||
principal.set(PrincipalField::Quota, quota);
|
||||
}
|
||||
} else if self.attr_type.contains(&attr) {
|
||||
for value in value {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"admin" | "administrator" | "root" | "superuser" => {
|
||||
principal.typ = Type::Superuser
|
||||
role = ROLE_ADMIN;
|
||||
principal.typ = Type::Individual
|
||||
}
|
||||
"posixaccount" | "individual" | "person" | "inetorgperson" => {
|
||||
principal.typ = Type::Individual
|
||||
|
@ -447,6 +501,6 @@ impl LdapMappings {
|
|||
}
|
||||
}
|
||||
|
||||
principal
|
||||
principal.with_field(PrincipalField::Roles, role)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
use store::Store;
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::{backend::internal::manage::ManageDirectory, Principal, Type};
|
||||
use crate::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField},
|
||||
Principal, Type, ROLE_ADMIN, ROLE_USER,
|
||||
};
|
||||
|
||||
use super::{EmailType, MemoryDirectory};
|
||||
|
||||
|
@ -34,17 +37,18 @@ impl MemoryDirectory {
|
|||
let name = config
|
||||
.value_require((prefix.as_str(), "principals", lookup_id, "name"))?
|
||||
.to_string();
|
||||
let typ = match config.value((prefix.as_str(), "principals", lookup_id, "class")) {
|
||||
Some("individual") => Type::Individual,
|
||||
Some("admin") => Type::Superuser,
|
||||
Some("group") => Type::Group,
|
||||
_ => Type::Individual,
|
||||
};
|
||||
let (typ, is_superuser) =
|
||||
match config.value((prefix.as_str(), "principals", lookup_id, "class")) {
|
||||
Some("individual") => (Type::Individual, false),
|
||||
Some("admin") => (Type::Individual, true),
|
||||
Some("group") => (Type::Group, false),
|
||||
_ => (Type::Individual, false),
|
||||
};
|
||||
|
||||
// Obtain id
|
||||
let id = directory
|
||||
.data_store
|
||||
.get_or_create_account_id(&name)
|
||||
.get_or_create_principal_id(&name, Type::Individual)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
config.new_build_error(
|
||||
|
@ -57,17 +61,28 @@ impl MemoryDirectory {
|
|||
})
|
||||
.ok()?;
|
||||
|
||||
// Create principal
|
||||
let mut principal = Principal {
|
||||
id,
|
||||
typ,
|
||||
..Default::default()
|
||||
}
|
||||
.with_field(
|
||||
PrincipalField::Roles,
|
||||
if is_superuser { ROLE_ADMIN } else { ROLE_USER },
|
||||
);
|
||||
|
||||
// Obtain group ids
|
||||
let mut member_of = Vec::new();
|
||||
for group in config
|
||||
.values((prefix.as_str(), "principals", lookup_id, "member-of"))
|
||||
.map(|(_, s)| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
member_of.push(
|
||||
principal.append_int(
|
||||
PrincipalField::MemberOf,
|
||||
directory
|
||||
.data_store
|
||||
.get_or_create_account_id(&group)
|
||||
.get_or_create_principal_id(&group, Type::Group)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
config.new_build_error(
|
||||
|
@ -83,7 +98,6 @@ impl MemoryDirectory {
|
|||
}
|
||||
|
||||
// Parse email addresses
|
||||
let mut emails = Vec::new();
|
||||
for (pos, (_, email)) in config
|
||||
.values((prefix.as_str(), "principals", lookup_id, "email"))
|
||||
.enumerate()
|
||||
|
@ -102,7 +116,7 @@ impl MemoryDirectory {
|
|||
directory.domains.insert(domain.to_lowercase());
|
||||
}
|
||||
|
||||
emails.push(email.to_lowercase());
|
||||
principal.append_str(PrincipalField::Emails, email.to_lowercase());
|
||||
}
|
||||
|
||||
// Parse mailing lists
|
||||
|
@ -119,23 +133,22 @@ impl MemoryDirectory {
|
|||
}
|
||||
}
|
||||
|
||||
directory.principals.push(Principal {
|
||||
name: name.clone(),
|
||||
secrets: config
|
||||
.values((prefix.as_str(), "principals", lookup_id, "secret"))
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
typ,
|
||||
description: config
|
||||
.value((prefix.as_str(), "principals", lookup_id, "description"))
|
||||
.map(|v| v.to_string()),
|
||||
quota: config
|
||||
.property((prefix.as_str(), "principals", lookup_id, "quota"))
|
||||
.unwrap_or(0),
|
||||
member_of,
|
||||
id,
|
||||
emails,
|
||||
});
|
||||
principal.set(PrincipalField::Name, name.clone());
|
||||
for (_, secret) in config.values((prefix.as_str(), "principals", lookup_id, "secret")) {
|
||||
principal.append_str(PrincipalField::Secrets, secret.to_string());
|
||||
}
|
||||
if let Some(description) =
|
||||
config.value((prefix.as_str(), "principals", lookup_id, "description"))
|
||||
{
|
||||
principal.set(PrincipalField::Description, description.to_string());
|
||||
}
|
||||
if let Some(quota) =
|
||||
config.property::<u64>((prefix.as_str(), "principals", lookup_id, "quota"))
|
||||
{
|
||||
principal.set(PrincipalField::Quota, quota);
|
||||
}
|
||||
|
||||
directory.principals.push(principal);
|
||||
}
|
||||
|
||||
Some(directory)
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
|
||||
use mail_send::Credentials;
|
||||
|
||||
use crate::{Principal, QueryBy};
|
||||
use crate::{backend::internal::PrincipalField, Principal, QueryBy};
|
||||
|
||||
use super::{EmailType, MemoryDirectory};
|
||||
|
||||
impl MemoryDirectory {
|
||||
pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
|
||||
pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal>> {
|
||||
match by {
|
||||
QueryBy::Name(name) => {
|
||||
for principal in &self.principals {
|
||||
if principal.name == name {
|
||||
if principal.name() == name {
|
||||
return Ok(Some(principal.clone()));
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ impl MemoryDirectory {
|
|||
};
|
||||
|
||||
for principal in &self.principals {
|
||||
if &principal.name == username {
|
||||
if principal.name() == username {
|
||||
return if principal.verify_secret(secret).await? {
|
||||
Ok(Some(principal.clone()))
|
||||
} else {
|
||||
|
@ -87,8 +87,10 @@ impl MemoryDirectory {
|
|||
if let EmailType::List(uid) = item {
|
||||
for principal in &self.principals {
|
||||
if principal.id == *uid {
|
||||
if let Some(addr) = principal.emails.first() {
|
||||
result.push(addr.clone())
|
||||
if let Some(addr) =
|
||||
principal.iter_str(PrincipalField::Emails).next()
|
||||
{
|
||||
result.push(addr.to_string())
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ pub mod lookup;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryDirectory {
|
||||
principals: Vec<Principal<u32>>,
|
||||
principals: Vec<Principal>,
|
||||
emails_to_ids: AHashMap<String, Vec<EmailType>>,
|
||||
pub(crate) data_store: Store,
|
||||
domains: AHashSet<String>,
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy};
|
|||
use super::{SmtpClient, SmtpDirectory};
|
||||
|
||||
impl SmtpDirectory {
|
||||
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
|
||||
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> {
|
||||
if let QueryBy::Credentials(credentials) = query {
|
||||
self.pool
|
||||
.get()
|
||||
|
@ -93,7 +93,7 @@ impl SmtpClient {
|
|||
async fn authenticate(
|
||||
&mut self,
|
||||
credentials: &Credentials<String>,
|
||||
) -> trc::Result<Option<Principal<u32>>> {
|
||||
) -> trc::Result<Option<Principal>> {
|
||||
match self
|
||||
.client
|
||||
.authenticate(credentials, &self.capabilities)
|
||||
|
|
|
@ -8,7 +8,10 @@ use mail_send::Credentials;
|
|||
use store::{NamedRows, Rows, Value};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
|
||||
use crate::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalValue},
|
||||
Principal, QueryBy, Type, ROLE_ADMIN, ROLE_USER,
|
||||
};
|
||||
|
||||
use super::{SqlDirectory, SqlMappings};
|
||||
|
||||
|
@ -17,7 +20,7 @@ impl SqlDirectory {
|
|||
&self,
|
||||
by: QueryBy<'_>,
|
||||
return_member_of: bool,
|
||||
) -> trc::Result<Option<Principal<u32>>> {
|
||||
) -> trc::Result<Option<Principal>> {
|
||||
let mut account_id = None;
|
||||
let account_name;
|
||||
let mut secret = None;
|
||||
|
@ -34,7 +37,7 @@ impl SqlDirectory {
|
|||
QueryBy::Id(uid) => {
|
||||
if let Some(username) = self
|
||||
.data_store
|
||||
.get_account_name(uid)
|
||||
.get_principal_name(uid)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
|
@ -95,46 +98,72 @@ impl SqlDirectory {
|
|||
} else {
|
||||
principal.id = self
|
||||
.data_store
|
||||
.get_or_create_account_id(&account_name)
|
||||
.get_or_create_principal_id(&account_name, Type::Individual)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
principal.name = account_name;
|
||||
principal.set(PrincipalField::Name, account_name);
|
||||
|
||||
// Obtain members
|
||||
if return_member_of && !self.mappings.query_members.is_empty() {
|
||||
for row in self
|
||||
.store
|
||||
.query::<Rows>(
|
||||
&self.mappings.query_members,
|
||||
vec![principal.name.clone().into()],
|
||||
)
|
||||
if return_member_of {
|
||||
if !self.mappings.query_members.is_empty() {
|
||||
for row in self
|
||||
.store
|
||||
.query::<Rows>(&self.mappings.query_members, vec![principal.name().into()])
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.rows
|
||||
{
|
||||
if let Some(Value::Text(account_id)) = row.values.first() {
|
||||
principal.append_int(
|
||||
PrincipalField::MemberOf,
|
||||
self.data_store
|
||||
.get_or_create_principal_id(account_id, Type::Group)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain roles
|
||||
let mut did_role_cleanup = false;
|
||||
for member in self
|
||||
.data_store
|
||||
.get_member_of(principal.id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.rows
|
||||
{
|
||||
if let Some(Value::Text(account_id)) = row.values.first() {
|
||||
principal.member_of.push(
|
||||
self.data_store
|
||||
.get_or_create_account_id(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
match member.typ {
|
||||
Type::List => {
|
||||
principal.append_int(PrincipalField::Lists, member.principal_id);
|
||||
}
|
||||
Type::Role => {
|
||||
if !did_role_cleanup {
|
||||
principal.remove(PrincipalField::Roles);
|
||||
did_role_cleanup = true;
|
||||
}
|
||||
principal.append_int(PrincipalField::Roles, member.principal_id);
|
||||
}
|
||||
_ => {
|
||||
principal.append_int(PrincipalField::MemberOf, member.principal_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain emails
|
||||
if !self.mappings.query_emails.is_empty() {
|
||||
principal.emails = self
|
||||
.store
|
||||
.query::<Rows>(
|
||||
&self.mappings.query_emails,
|
||||
vec![principal.name.clone().into()],
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into();
|
||||
principal.set(
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::StringList(
|
||||
self.store
|
||||
.query::<Rows>(&self.mappings.query_emails, vec![principal.name().into()])
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(principal))
|
||||
|
@ -153,7 +182,7 @@ impl SqlDirectory {
|
|||
if let Some(Value::Text(name)) = row.values.first() {
|
||||
ids.push(
|
||||
self.data_store
|
||||
.get_or_create_account_id(name)
|
||||
.get_or_create_principal_id(name, Type::Individual)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
);
|
||||
|
@ -204,8 +233,9 @@ impl SqlDirectory {
|
|||
}
|
||||
|
||||
impl SqlMappings {
|
||||
pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal<u32>> {
|
||||
pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal> {
|
||||
let mut principal = Principal::default();
|
||||
let mut role = ROLE_USER;
|
||||
|
||||
if let Some(row) = rows.rows.into_iter().next() {
|
||||
for (name, value) in rows.names.into_iter().zip(row.values) {
|
||||
|
@ -215,27 +245,32 @@ impl SqlMappings {
|
|||
.any(|c| name.eq_ignore_ascii_case(c))
|
||||
{
|
||||
if let Value::Text(secret) = value {
|
||||
principal.secrets.push(secret.into_owned());
|
||||
principal.append_str(PrincipalField::Secrets, secret.into_owned());
|
||||
}
|
||||
} else if name.eq_ignore_ascii_case(&self.column_type) {
|
||||
match value.to_str().as_ref() {
|
||||
"individual" | "person" | "user" => principal.typ = Type::Individual,
|
||||
"individual" | "person" | "user" => {
|
||||
principal.typ = Type::Individual;
|
||||
}
|
||||
"group" => principal.typ = Type::Group,
|
||||
"admin" | "superuser" | "administrator" => principal.typ = Type::Superuser,
|
||||
"admin" | "superuser" | "administrator" => {
|
||||
principal.typ = Type::Individual;
|
||||
role = ROLE_ADMIN;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else if name.eq_ignore_ascii_case(&self.column_description) {
|
||||
if let Value::Text(text) = value {
|
||||
principal.description = text.into_owned().into();
|
||||
principal.set(PrincipalField::Description, text.into_owned());
|
||||
}
|
||||
} else if name.eq_ignore_ascii_case(&self.column_quota) {
|
||||
if let Value::Integer(quota) = value {
|
||||
principal.quota = quota as u64;
|
||||
principal.set(PrincipalField::Quota, quota as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(principal)
|
||||
Ok(principal.with_field(PrincipalField::Roles, role))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ impl Directory {
|
|||
&self,
|
||||
by: QueryBy<'_>,
|
||||
return_member_of: bool,
|
||||
) -> trc::Result<Option<Principal<u32>>> {
|
||||
) -> trc::Result<Option<Principal>> {
|
||||
match &self.store {
|
||||
DirectoryInner::Internal(store) => store.query(by, return_member_of).await,
|
||||
DirectoryInner::Ldap(store) => store.query(by, return_member_of).await,
|
||||
|
|
|
@ -4,7 +4,184 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::Permission;
|
||||
|
||||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod dispatch;
|
||||
pub mod principal;
|
||||
pub mod secret;
|
||||
|
||||
impl Permission {
|
||||
pub fn description(&self) -> &'static str {
|
||||
match self {
|
||||
Permission::Impersonate => "Allows acting on behalf of another user",
|
||||
Permission::UnlimitedRequests => "Removes request limits or quotas",
|
||||
Permission::UnlimitedUploads => "Removes upload size or frequency limits",
|
||||
Permission::DeleteSystemFolders => "Allows deletion of critical system folders",
|
||||
Permission::MessageQueueList => "View message queue",
|
||||
Permission::MessageQueueGet => "Retrieve specific messages from the queue",
|
||||
Permission::MessageQueueUpdate => "Modify queued messages",
|
||||
Permission::MessageQueueDelete => "Remove messages from the queue",
|
||||
Permission::OutgoingReportList => "View reports for outgoing emails",
|
||||
Permission::OutgoingReportGet => "Retrieve specific outgoing email reports",
|
||||
Permission::OutgoingReportDelete => "Remove outgoing email reports",
|
||||
Permission::IncomingReportList => "View reports for incoming emails",
|
||||
Permission::IncomingReportGet => "Retrieve specific incoming email reports",
|
||||
Permission::IncomingReportDelete => "Remove incoming email reports",
|
||||
Permission::SettingsList => "View system settings",
|
||||
Permission::SettingsUpdate => "Modify system settings",
|
||||
Permission::SettingsDelete => "Remove system settings",
|
||||
Permission::SettingsReload => "Refresh system settings",
|
||||
Permission::IndividualList => "View list of individual users",
|
||||
Permission::IndividualGet => "Retrieve specific user information",
|
||||
Permission::IndividualUpdate => "Modify user information",
|
||||
Permission::IndividualDelete => "Remove user accounts",
|
||||
Permission::IndividualCreate => "Add new user accounts",
|
||||
Permission::GroupList => "View list of user groups",
|
||||
Permission::GroupGet => "Retrieve specific group information",
|
||||
Permission::GroupUpdate => "Modify group information",
|
||||
Permission::GroupDelete => "Remove user groups",
|
||||
Permission::GroupCreate => "Add new user groups",
|
||||
Permission::DomainList => "View list of email domains",
|
||||
Permission::DomainGet => "Retrieve specific domain information",
|
||||
Permission::DomainCreate => "Add new email domains",
|
||||
Permission::DomainUpdate => "Modify domain information",
|
||||
Permission::DomainDelete => "Remove email domains",
|
||||
Permission::TenantList => "View list of tenants (in multi-tenant setup)",
|
||||
Permission::TenantGet => "Retrieve specific tenant information",
|
||||
Permission::TenantCreate => "Add new tenants",
|
||||
Permission::TenantUpdate => "Modify tenant information",
|
||||
Permission::TenantDelete => "Remove tenants",
|
||||
Permission::MailingListList => "View list of mailing lists",
|
||||
Permission::MailingListGet => "Retrieve specific mailing list information",
|
||||
Permission::MailingListCreate => "Create new mailing lists",
|
||||
Permission::MailingListUpdate => "Modify mailing list information",
|
||||
Permission::MailingListDelete => "Remove mailing lists",
|
||||
Permission::RoleList => "View list of roles",
|
||||
Permission::RoleGet => "Retrieve specific role information",
|
||||
Permission::RoleCreate => "Create new roles",
|
||||
Permission::RoleUpdate => "Modify role information",
|
||||
Permission::RoleDelete => "Remove roles",
|
||||
Permission::PrincipalList => "View list of principals (users or system entities)",
|
||||
Permission::PrincipalGet => "Retrieve specific principal information",
|
||||
Permission::PrincipalCreate => "Create new principals",
|
||||
Permission::PrincipalUpdate => "Modify principal information",
|
||||
Permission::PrincipalDelete => "Remove principals",
|
||||
Permission::BlobFetch => "Retrieve binary large objects",
|
||||
Permission::PurgeBlobStore => "Clear the blob storage",
|
||||
Permission::PurgeDataStore => "Clear the data storage",
|
||||
Permission::PurgeLookupStore => "Clear the lookup storage",
|
||||
Permission::PurgeAccount => "Completely remove an account and all associated data",
|
||||
Permission::Undelete => "Restore deleted items",
|
||||
Permission::DkimSignatureCreate => "Create DKIM signatures for email authentication",
|
||||
Permission::DkimSignatureGet => "Retrieve DKIM signature information",
|
||||
Permission::UpdateSpamFilter => "Modify spam filter settings",
|
||||
Permission::UpdateWebadmin => "Modify web admin interface settings",
|
||||
Permission::LogsView => "Access system logs",
|
||||
Permission::SieveRun => "Execute Sieve scripts for email filtering",
|
||||
Permission::Restart => "Restart the email server",
|
||||
Permission::TracingList => "View list of system traces",
|
||||
Permission::TracingGet => "Retrieve specific trace information",
|
||||
Permission::TracingLive => "View real-time system traces",
|
||||
Permission::MetricsList => "View list of system metrics",
|
||||
Permission::MetricsLive => "View real-time system metrics",
|
||||
Permission::Authenticate => "Perform authentication",
|
||||
Permission::AuthenticateOauth => "Perform OAuth authentication",
|
||||
Permission::EmailSend => "Send emails",
|
||||
Permission::EmailReceive => "Receive emails",
|
||||
Permission::ManageEncryption => "Handle encryption settings and operations",
|
||||
Permission::ManagePasswords => "Manage user passwords",
|
||||
Permission::JmapEmailGet => "Retrieve emails via JMAP",
|
||||
Permission::JmapMailboxGet => "Retrieve mailboxes via JMAP",
|
||||
Permission::JmapThreadGet => "Retrieve email threads via JMAP",
|
||||
Permission::JmapIdentityGet => "Retrieve user identities via JMAP",
|
||||
Permission::JmapEmailSubmissionGet => "Retrieve email submission info via JMAP",
|
||||
Permission::JmapPushSubscriptionGet => "Retrieve push subscriptions via JMAP",
|
||||
Permission::JmapSieveScriptGet => "Retrieve Sieve scripts via JMAP",
|
||||
Permission::JmapVacationResponseGet => "Retrieve vacation responses via JMAP",
|
||||
Permission::JmapPrincipalGet => "Retrieve principal information via JMAP",
|
||||
Permission::JmapQuotaGet => "Retrieve quota information via JMAP",
|
||||
Permission::JmapBlobGet => "Retrieve blobs via JMAP",
|
||||
Permission::JmapEmailSet => "Modify emails via JMAP",
|
||||
Permission::JmapMailboxSet => "Modify mailboxes via JMAP",
|
||||
Permission::JmapIdentitySet => "Modify user identities via JMAP",
|
||||
Permission::JmapEmailSubmissionSet => "Modify email submission settings via JMAP",
|
||||
Permission::JmapPushSubscriptionSet => "Modify push subscriptions via JMAP",
|
||||
Permission::JmapSieveScriptSet => "Modify Sieve scripts via JMAP",
|
||||
Permission::JmapVacationResponseSet => "Modify vacation responses via JMAP",
|
||||
Permission::JmapEmailChanges => "Track email changes via JMAP",
|
||||
Permission::JmapMailboxChanges => "Track mailbox changes via JMAP",
|
||||
Permission::JmapThreadChanges => "Track thread changes via JMAP",
|
||||
Permission::JmapIdentityChanges => "Track identity changes via JMAP",
|
||||
Permission::JmapEmailSubmissionChanges => "Track email submission changes via JMAP",
|
||||
Permission::JmapQuotaChanges => "Track quota changes via JMAP",
|
||||
Permission::JmapEmailCopy => "Copy emails via JMAP",
|
||||
Permission::JmapBlobCopy => "Copy blobs via JMAP",
|
||||
Permission::JmapEmailImport => "Import emails via JMAP",
|
||||
Permission::JmapEmailParse => "Parse emails via JMAP",
|
||||
Permission::JmapEmailQueryChanges => "Track email query changes via JMAP",
|
||||
Permission::JmapMailboxQueryChanges => "Track mailbox query changes via JMAP",
|
||||
Permission::JmapEmailSubmissionQueryChanges => {
|
||||
"Track email submission query changes via JMAP"
|
||||
}
|
||||
Permission::JmapSieveScriptQueryChanges => "Track Sieve script query changes via JMAP",
|
||||
Permission::JmapPrincipalQueryChanges => "Track principal query changes via JMAP",
|
||||
Permission::JmapQuotaQueryChanges => "Track quota query changes via JMAP",
|
||||
Permission::JmapEmailQuery => "Perform email queries via JMAP",
|
||||
Permission::JmapMailboxQuery => "Perform mailbox queries via JMAP",
|
||||
Permission::JmapEmailSubmissionQuery => "Perform email submission queries via JMAP",
|
||||
Permission::JmapSieveScriptQuery => "Perform Sieve script queries via JMAP",
|
||||
Permission::JmapPrincipalQuery => "Perform principal queries via JMAP",
|
||||
Permission::JmapQuotaQuery => "Perform quota queries via JMAP",
|
||||
Permission::JmapSearchSnippet => "Retrieve search snippets via JMAP",
|
||||
Permission::JmapSieveScriptValidate => "Validate Sieve scripts via JMAP",
|
||||
Permission::JmapBlobLookup => "Look up blobs via JMAP",
|
||||
Permission::JmapBlobUpload => "Upload blobs via JMAP",
|
||||
Permission::JmapEcho => "Perform JMAP echo requests",
|
||||
Permission::ImapAuthenticate => "Authenticate via IMAP",
|
||||
Permission::ImapAclGet => "Retrieve ACLs via IMAP",
|
||||
Permission::ImapAclSet => "Set ACLs via IMAP",
|
||||
Permission::ImapMyRights => "Retrieve own rights via IMAP",
|
||||
Permission::ImapListRights => "List rights via IMAP",
|
||||
Permission::ImapAppend => "Append messages via IMAP",
|
||||
Permission::ImapCapability => "Retrieve server capabilities via IMAP",
|
||||
Permission::ImapId => "Retrieve server ID via IMAP",
|
||||
Permission::ImapCopy => "Copy messages via IMAP",
|
||||
Permission::ImapMove => "Move messages via IMAP",
|
||||
Permission::ImapCreate => "Create mailboxes via IMAP",
|
||||
Permission::ImapDelete => "Delete mailboxes or messages via IMAP",
|
||||
Permission::ImapEnable => "Enable IMAP extensions",
|
||||
Permission::ImapExpunge => "Expunge deleted messages via IMAP",
|
||||
Permission::ImapFetch => "Fetch messages or metadata via IMAP",
|
||||
Permission::ImapIdle => "Use IMAP IDLE command",
|
||||
Permission::ImapList => "List mailboxes via IMAP",
|
||||
Permission::ImapLsub => "List subscribed mailboxes via IMAP",
|
||||
Permission::ImapNamespace => "Retrieve namespaces via IMAP",
|
||||
Permission::ImapRename => "Rename mailboxes via IMAP",
|
||||
Permission::ImapSearch => "Search messages via IMAP",
|
||||
Permission::ImapSort => "Sort messages via IMAP",
|
||||
Permission::ImapSelect => "Select mailboxes via IMAP",
|
||||
Permission::ImapExamine => "Examine mailboxes via IMAP",
|
||||
Permission::ImapStatus => "Retrieve mailbox status via IMAP",
|
||||
Permission::ImapStore => "Modify message flags via IMAP",
|
||||
Permission::ImapSubscribe => "Subscribe to mailboxes via IMAP",
|
||||
Permission::ImapThread => "Thread messages via IMAP",
|
||||
Permission::Pop3Authenticate => "Authenticate via POP3",
|
||||
Permission::Pop3List => "List messages via POP3",
|
||||
Permission::Pop3Uidl => "Retrieve unique IDs via POP3",
|
||||
Permission::Pop3Stat => "Retrieve mailbox statistics via POP3",
|
||||
Permission::Pop3Retr => "Retrieve messages via POP3",
|
||||
Permission::Pop3Dele => "Mark messages for deletion via POP3",
|
||||
Permission::SieveAuthenticate => "Authenticate for Sieve script management",
|
||||
Permission::SieveListScripts => "List Sieve scripts",
|
||||
Permission::SieveSetActive => "Set active Sieve script",
|
||||
Permission::SieveGetScript => "Retrieve Sieve scripts",
|
||||
Permission::SievePutScript => "Upload Sieve scripts",
|
||||
Permission::SieveDeleteScript => "Delete Sieve scripts",
|
||||
Permission::SieveRenameScript => "Rename Sieve scripts",
|
||||
Permission::SieveCheckScript => "Validate Sieve scripts",
|
||||
Permission::SieveHaveSpace => "Check available space for Sieve scripts",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1014
crates/directory/src/core/principal.rs
Normal file
1014
crates/directory/src/core/principal.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -18,10 +18,11 @@ use sha2::Sha512;
|
|||
use tokio::sync::oneshot;
|
||||
use totp_rs::TOTP;
|
||||
|
||||
use crate::backend::internal::PrincipalField;
|
||||
use crate::backend::internal::SpecialSecrets;
|
||||
use crate::Principal;
|
||||
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
||||
impl Principal {
|
||||
pub async fn verify_secret(&self, mut code: &str) -> trc::Result<bool> {
|
||||
let mut totp_token = None;
|
||||
let mut is_totp_token_missing = false;
|
||||
|
@ -30,12 +31,8 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
|||
let mut is_authenticated = false;
|
||||
let mut is_app_authenticated = false;
|
||||
|
||||
for secret in &self.secrets {
|
||||
if secret.is_disabled() {
|
||||
// Account is disabled, no need to check further
|
||||
|
||||
return Ok(false);
|
||||
} else if secret.is_otp_auth() {
|
||||
for secret in self.iter_str(PrincipalField::Secrets) {
|
||||
if secret.is_otp_auth() {
|
||||
if !is_totp_verified && !is_totp_token_missing {
|
||||
is_totp_required = true;
|
||||
|
||||
|
@ -99,7 +96,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
|||
} else {
|
||||
if is_totp_verified {
|
||||
// TOTP URL appeared after password hash in secrets list
|
||||
for secret in &self.secrets {
|
||||
for secret in self.iter_str(PrincipalField::Secrets) {
|
||||
if secret.is_password() && verify_secret_hash(secret, code).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use std::{fmt::Debug, sync::Arc};
|
|||
use ahash::AHashMap;
|
||||
use backend::{
|
||||
imap::{ImapDirectory, ImapError},
|
||||
internal::{PrincipalField, PrincipalValue},
|
||||
ldap::LdapDirectory,
|
||||
memory::MemoryDirectory,
|
||||
smtp::SmtpDirectory,
|
||||
|
@ -18,7 +19,9 @@ use backend::{
|
|||
use deadpool::managed::PoolError;
|
||||
use ldap3::LdapError;
|
||||
use mail_send::Credentials;
|
||||
use proc_macros::EnumMethods;
|
||||
use store::Store;
|
||||
use trc::ipc::bitset::Bitset;
|
||||
|
||||
pub mod backend;
|
||||
pub mod core;
|
||||
|
@ -28,45 +31,222 @@ pub struct Directory {
|
|||
pub cache: Option<CachedDirectory>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Principal<T> {
|
||||
#[serde(default, skip)]
|
||||
pub id: u32,
|
||||
#[serde(rename = "type")]
|
||||
pub typ: Type,
|
||||
#[serde(default)]
|
||||
pub quota: u64,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub emails: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "memberOf")]
|
||||
pub member_of: Vec<T>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Principal {
|
||||
pub(crate) id: u32,
|
||||
pub(crate) typ: Type,
|
||||
|
||||
pub(crate) fields: AHashMap<PrincipalField, PrincipalValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Type {
|
||||
#[serde(rename = "individual")]
|
||||
#[default]
|
||||
Individual = 0,
|
||||
#[serde(rename = "group")]
|
||||
Group = 1,
|
||||
#[serde(rename = "resource")]
|
||||
Resource = 2,
|
||||
#[serde(rename = "location")]
|
||||
Location = 3,
|
||||
#[serde(rename = "superuser")]
|
||||
Superuser = 4,
|
||||
#[serde(rename = "list")]
|
||||
List = 5,
|
||||
#[serde(rename = "other")]
|
||||
Other = 6,
|
||||
Domain = 7,
|
||||
Tenant = 8,
|
||||
Role = 9,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Permission {
|
||||
// Admin
|
||||
Impersonate,
|
||||
UnlimitedRequests,
|
||||
UnlimitedUploads,
|
||||
DeleteSystemFolders,
|
||||
MessageQueueList,
|
||||
MessageQueueGet,
|
||||
MessageQueueUpdate,
|
||||
MessageQueueDelete,
|
||||
OutgoingReportList,
|
||||
OutgoingReportGet,
|
||||
OutgoingReportDelete,
|
||||
IncomingReportList,
|
||||
IncomingReportGet,
|
||||
IncomingReportDelete,
|
||||
SettingsList,
|
||||
SettingsUpdate,
|
||||
SettingsDelete,
|
||||
SettingsReload,
|
||||
IndividualList,
|
||||
IndividualGet,
|
||||
IndividualUpdate,
|
||||
IndividualDelete,
|
||||
IndividualCreate,
|
||||
GroupList,
|
||||
GroupGet,
|
||||
GroupUpdate,
|
||||
GroupDelete,
|
||||
GroupCreate,
|
||||
DomainList,
|
||||
DomainGet,
|
||||
DomainCreate,
|
||||
DomainUpdate,
|
||||
DomainDelete,
|
||||
TenantList,
|
||||
TenantGet,
|
||||
TenantCreate,
|
||||
TenantUpdate,
|
||||
TenantDelete,
|
||||
MailingListList,
|
||||
MailingListGet,
|
||||
MailingListCreate,
|
||||
MailingListUpdate,
|
||||
MailingListDelete,
|
||||
RoleList,
|
||||
RoleGet,
|
||||
RoleCreate,
|
||||
RoleUpdate,
|
||||
RoleDelete,
|
||||
PrincipalList,
|
||||
PrincipalGet,
|
||||
PrincipalCreate,
|
||||
PrincipalUpdate,
|
||||
PrincipalDelete,
|
||||
BlobFetch,
|
||||
PurgeBlobStore,
|
||||
PurgeDataStore,
|
||||
PurgeLookupStore,
|
||||
PurgeAccount,
|
||||
Undelete,
|
||||
DkimSignatureCreate,
|
||||
DkimSignatureGet,
|
||||
UpdateSpamFilter,
|
||||
UpdateWebadmin,
|
||||
LogsView,
|
||||
SieveRun,
|
||||
Restart,
|
||||
TracingList,
|
||||
TracingGet,
|
||||
TracingLive,
|
||||
MetricsList,
|
||||
MetricsLive,
|
||||
|
||||
// Generic
|
||||
Authenticate,
|
||||
AuthenticateOauth,
|
||||
EmailSend,
|
||||
EmailReceive,
|
||||
|
||||
// Account Management
|
||||
ManageEncryption,
|
||||
ManagePasswords,
|
||||
|
||||
// JMAP
|
||||
JmapEmailGet,
|
||||
JmapMailboxGet,
|
||||
JmapThreadGet,
|
||||
JmapIdentityGet,
|
||||
JmapEmailSubmissionGet,
|
||||
JmapPushSubscriptionGet,
|
||||
JmapSieveScriptGet,
|
||||
JmapVacationResponseGet,
|
||||
JmapPrincipalGet,
|
||||
JmapQuotaGet,
|
||||
JmapBlobGet,
|
||||
JmapEmailSet,
|
||||
JmapMailboxSet,
|
||||
JmapIdentitySet,
|
||||
JmapEmailSubmissionSet,
|
||||
JmapPushSubscriptionSet,
|
||||
JmapSieveScriptSet,
|
||||
JmapVacationResponseSet,
|
||||
JmapEmailChanges,
|
||||
JmapMailboxChanges,
|
||||
JmapThreadChanges,
|
||||
JmapIdentityChanges,
|
||||
JmapEmailSubmissionChanges,
|
||||
JmapQuotaChanges,
|
||||
JmapEmailCopy,
|
||||
JmapBlobCopy,
|
||||
JmapEmailImport,
|
||||
JmapEmailParse,
|
||||
JmapEmailQueryChanges,
|
||||
JmapMailboxQueryChanges,
|
||||
JmapEmailSubmissionQueryChanges,
|
||||
JmapSieveScriptQueryChanges,
|
||||
JmapPrincipalQueryChanges,
|
||||
JmapQuotaQueryChanges,
|
||||
JmapEmailQuery,
|
||||
JmapMailboxQuery,
|
||||
JmapEmailSubmissionQuery,
|
||||
JmapSieveScriptQuery,
|
||||
JmapPrincipalQuery,
|
||||
JmapQuotaQuery,
|
||||
JmapSearchSnippet,
|
||||
JmapSieveScriptValidate,
|
||||
JmapBlobLookup,
|
||||
JmapBlobUpload,
|
||||
JmapEcho,
|
||||
|
||||
// IMAP
|
||||
ImapAuthenticate,
|
||||
ImapAclGet,
|
||||
ImapAclSet,
|
||||
ImapMyRights,
|
||||
ImapListRights,
|
||||
ImapAppend,
|
||||
ImapCapability,
|
||||
ImapId,
|
||||
ImapCopy,
|
||||
ImapMove,
|
||||
ImapCreate,
|
||||
ImapDelete,
|
||||
ImapEnable,
|
||||
ImapExpunge,
|
||||
ImapFetch,
|
||||
ImapIdle,
|
||||
ImapList,
|
||||
ImapLsub,
|
||||
ImapNamespace,
|
||||
ImapRename,
|
||||
ImapSearch,
|
||||
ImapSort,
|
||||
ImapSelect,
|
||||
ImapExamine,
|
||||
ImapStatus,
|
||||
ImapStore,
|
||||
ImapSubscribe,
|
||||
ImapThread,
|
||||
|
||||
// POP3
|
||||
Pop3Authenticate,
|
||||
Pop3List,
|
||||
Pop3Uidl,
|
||||
Pop3Stat,
|
||||
Pop3Retr,
|
||||
Pop3Dele,
|
||||
|
||||
// ManageSieve
|
||||
SieveAuthenticate,
|
||||
SieveListScripts,
|
||||
SieveSetActive,
|
||||
SieveGetScript,
|
||||
SievePutScript,
|
||||
SieveDeleteScript,
|
||||
SieveRenameScript,
|
||||
SieveCheckScript,
|
||||
SieveHaveSpace,
|
||||
}
|
||||
|
||||
pub type Permissions = Bitset<
|
||||
{ (Permission::COUNT + std::mem::size_of::<usize>() - 1) / std::mem::size_of::<usize>() },
|
||||
>;
|
||||
|
||||
pub const ROLE_ADMIN: u32 = u32::MAX;
|
||||
pub const ROLE_TENANT_ADMIN: u32 = u32::MAX - 1;
|
||||
pub const ROLE_USER: u32 = u32::MAX - 2;
|
||||
|
||||
pub enum DirectoryInner {
|
||||
Internal(Store),
|
||||
Ldap(LdapDirectory),
|
||||
|
@ -82,20 +262,6 @@ pub enum QueryBy<'x> {
|
|||
Credentials(&'x Credentials<String>),
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn has_name(&self) -> bool {
|
||||
!self.name.is_empty()
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.description.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Directory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -111,57 +277,11 @@ impl Debug for Directory {
|
|||
}
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn to_jmap(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Individual | Self::Superuser => "individual",
|
||||
Self::Group => "group",
|
||||
Self::Resource => "resource",
|
||||
Self::Location => "location",
|
||||
Self::Other => "other",
|
||||
Self::List => "list",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Individual => "Individual",
|
||||
Self::Group => "Group",
|
||||
Self::Resource => "Resource",
|
||||
Self::Location => "Location",
|
||||
Self::Superuser => "Superuser",
|
||||
Self::List => "List",
|
||||
Self::Other => "Other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Directories {
|
||||
pub directories: AHashMap<String, Arc<Directory>>,
|
||||
}
|
||||
|
||||
impl Principal<u32> {
|
||||
pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
|
||||
Principal {
|
||||
id: u32::MAX,
|
||||
typ: Type::Superuser,
|
||||
quota: 0,
|
||||
name: "Fallback Administrator".to_string(),
|
||||
secrets: vec![fallback_pass.into()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Principal<T> {
|
||||
pub fn into_sorted(mut self) -> Self {
|
||||
self.member_of.sort_unstable();
|
||||
self.emails.sort_unstable();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoError {
|
||||
fn into_error(self) -> trc::Error;
|
||||
}
|
||||
|
|
|
@ -501,9 +501,12 @@ impl SerializeResponse for trc::Error {
|
|||
Some(ResponseCode::NonExistent.as_str())
|
||||
}
|
||||
trc::EventType::Store(_) => Some(ResponseCode::ContactAdmin.as_str()),
|
||||
trc::EventType::Limit(trc::LimitEvent::Quota) => Some(ResponseCode::OverQuota.as_str()),
|
||||
trc::EventType::Limit(trc::LimitEvent::Quota) => {
|
||||
Some(ResponseCode::OverQuota.as_str())
|
||||
}
|
||||
trc::EventType::Limit(_) => Some(ResponseCode::Limit.as_str()),
|
||||
trc::EventType::Auth(_) => Some(ResponseCode::AuthenticationFailed.as_str()),
|
||||
trc::EventType::Security(_) => Some(ResponseCode::AuthorizationFailed.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "imap"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -5,15 +5,13 @@ use std::{
|
|||
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
config::jmap::settings::SpecialUse,
|
||||
listener::{limiter::InFlight, SessionStream},
|
||||
};
|
||||
use directory::QueryBy;
|
||||
use directory::{backend::internal::PrincipalField, QueryBy};
|
||||
use imap_proto::protocol::list::Attribute;
|
||||
use jmap::{
|
||||
auth::{acl::EffectiveAcl, AccessToken},
|
||||
mailbox::INBOX_ID,
|
||||
};
|
||||
use jmap::{auth::acl::EffectiveAcl, mailbox::INBOX_ID};
|
||||
use jmap_proto::{
|
||||
object::Object,
|
||||
types::{acl::Acl, collection::Collection, id::Id, property::Property, value::Value},
|
||||
|
@ -28,7 +26,7 @@ use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, Sessio
|
|||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn new(
|
||||
session: &Session<T>,
|
||||
access_token: &AccessToken,
|
||||
access_token: Arc<AccessToken>,
|
||||
in_flight: Option<InFlight>,
|
||||
) -> trc::Result<Self> {
|
||||
let mut session = SessionData {
|
||||
|
@ -39,12 +37,14 @@ impl<T: SessionStream> SessionData<T> {
|
|||
session_id: session.session_id,
|
||||
mailboxes: Mutex::new(vec![]),
|
||||
state: access_token.state().into(),
|
||||
access_token,
|
||||
in_flight,
|
||||
};
|
||||
let access_token = session.access_token.clone();
|
||||
|
||||
// Fetch mailboxes for the main account
|
||||
let mut mailboxes = vec![session
|
||||
.fetch_account_mailboxes(session.account_id, None, access_token)
|
||||
.fetch_account_mailboxes(session.account_id, None, &access_token)
|
||||
.await
|
||||
.caused_by(trc::location!())?];
|
||||
|
||||
|
@ -65,11 +65,11 @@ impl<T: SessionStream> SessionData<T> {
|
|||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.map(|p| p.name)
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Name))
|
||||
.unwrap_or_else(|| Id::from(account_id).to_string())
|
||||
)
|
||||
.into(),
|
||||
access_token,
|
||||
&access_token,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
|
@ -333,6 +333,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
// Obtain access token
|
||||
let access_token = self
|
||||
.jmap
|
||||
.core
|
||||
.get_cached_access_token(self.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
@ -389,8 +390,8 @@ impl<T: SessionStream> SessionData<T> {
|
|||
.directory
|
||||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.map(|p| p.name)
|
||||
.caused_by(trc::location!())?
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Name))
|
||||
.unwrap_or_else(|| Id::from(account_id).to_string())
|
||||
);
|
||||
added_accounts.push(
|
||||
|
@ -495,7 +496,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
.query(QueryBy::Id(account_id), false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.map(|p| p.name)
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Name))
|
||||
.unwrap_or_else(|| Id::from(account_id).to_string())
|
||||
)
|
||||
.into()
|
||||
|
|
|
@ -11,17 +11,17 @@ use std::{
|
|||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::listener::{limiter::InFlight, ServerInstance, SessionStream};
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
listener::{limiter::InFlight, ServerInstance, SessionStream},
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use imap_proto::{
|
||||
protocol::{list::Attribute, ProtocolVersion},
|
||||
receiver::Receiver,
|
||||
Command,
|
||||
};
|
||||
use jmap::{
|
||||
auth::{rate_limit::ConcurrencyLimiters, AccessToken},
|
||||
JmapInstance, JMAP,
|
||||
};
|
||||
use jmap::{auth::rate_limit::ConcurrencyLimiters, JmapInstance, JMAP};
|
||||
use tokio::{
|
||||
io::{ReadHalf, WriteHalf},
|
||||
sync::watch,
|
||||
|
@ -82,6 +82,7 @@ pub struct Session<T: SessionStream> {
|
|||
|
||||
pub struct SessionData<T: SessionStream> {
|
||||
pub account_id: u32,
|
||||
pub access_token: Arc<AccessToken>,
|
||||
pub jmap: JMAP,
|
||||
pub imap: Arc<Inner>,
|
||||
pub session_id: u64,
|
||||
|
@ -221,6 +222,7 @@ impl<T: SessionStream> State<T> {
|
|||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn get_access_token(&self) -> trc::Result<Arc<AccessToken>> {
|
||||
self.jmap
|
||||
.core
|
||||
.get_cached_access_token(self.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
|
@ -239,6 +241,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
stream_tx: new_stream,
|
||||
state: self.state,
|
||||
in_flight: self.in_flight,
|
||||
access_token: self.access_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use directory::QueryBy;
|
||||
use common::{auth::AccessToken, listener::SessionStream};
|
||||
use directory::{backend::internal::PrincipalField, Permission, QueryBy};
|
||||
use imap_proto::{
|
||||
protocol::acl::{
|
||||
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
|
||||
|
@ -16,10 +16,7 @@ use imap_proto::{
|
|||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
||||
use jmap::{
|
||||
auth::{acl::EffectiveAcl, AccessToken},
|
||||
mailbox::set::SCHEMA,
|
||||
};
|
||||
use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA};
|
||||
use jmap_proto::{
|
||||
object::{index::ObjectIndexBuilder, Object},
|
||||
types::{
|
||||
|
@ -36,13 +33,16 @@ use trc::AddContext;
|
|||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
use crate::{
|
||||
core::{MailboxId, Session, SessionData},
|
||||
core::{MailboxId, Session, SessionData, State},
|
||||
op::ImapContext,
|
||||
spawn_op,
|
||||
};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapAuthenticate)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_acl(self.version)?;
|
||||
let is_rev2 = self.version.is_rev2();
|
||||
|
@ -69,7 +69,7 @@ impl<T: SessionStream> Session<T> {
|
|||
.query(QueryBy::Id(item.account_id), false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.map(|p| p.name)
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Name))
|
||||
{
|
||||
let mut rights = Vec::new();
|
||||
|
||||
|
@ -142,6 +142,9 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapMyRights)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_acl(self.version)?;
|
||||
let data = self.state.session_data();
|
||||
|
@ -224,6 +227,9 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapAclSet)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let command = request.command;
|
||||
let arguments = request.parse_acl(self.version)?;
|
||||
|
@ -252,7 +258,7 @@ impl<T: SessionStream> Session<T> {
|
|||
.id(arguments.tag.to_string())
|
||||
.caused_by(trc::location!())
|
||||
})?
|
||||
.id;
|
||||
.id();
|
||||
|
||||
// Prepare changes
|
||||
let mut changes = Object::with_capacity(1);
|
||||
|
@ -359,7 +365,11 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
// Invalidate ACLs
|
||||
data.jmap.inner.access_tokens.remove(&acl_account_id);
|
||||
data.jmap
|
||||
.core
|
||||
.security
|
||||
.access_tokens
|
||||
.remove(&acl_account_id);
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::SetAcl),
|
||||
|
@ -381,6 +391,9 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapListRights)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_acl(self.version)?;
|
||||
|
||||
|
@ -415,6 +428,15 @@ impl<T: SessionStream> Session<T> {
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
|
||||
match &self.state {
|
||||
State::Authenticated { data } | State::Selected { data, .. } => {
|
||||
data.access_token.assert_has_permission(permission)
|
||||
}
|
||||
State::NotAuthenticated { .. } => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{append::Arguments, select::HighestModSeq},
|
||||
receiver::Request,
|
||||
|
@ -25,6 +26,9 @@ use super::{ImapContext, ToModSeq};
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_append(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapAppend)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_append(self.version)?;
|
||||
let (data, selected_mailbox) = self.state.session_mailbox_state();
|
||||
|
@ -84,11 +88,13 @@ impl<T: SessionStream> SessionData<T> {
|
|||
}
|
||||
|
||||
// Obtain quota
|
||||
let account_quota = self
|
||||
.get_access_token()
|
||||
let resource_token = self
|
||||
.jmap
|
||||
.core
|
||||
.get_cached_access_token(mailbox.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.quota as i64;
|
||||
.as_resource_token();
|
||||
|
||||
// Append messages
|
||||
let mut response = StatusResponse::completed(Command::Append);
|
||||
|
@ -100,8 +106,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
.email_ingest(IngestEmail {
|
||||
raw_message: &message.message,
|
||||
message: MessageParser::new().parse(&message.message),
|
||||
account_id,
|
||||
account_quota,
|
||||
resource: resource_token.clone(),
|
||||
mailbox_ids: vec![mailbox_id],
|
||||
keywords: message.flags.into_iter().map(Keyword::from).collect(),
|
||||
received_at: message.received_at.map(|d| d as u64),
|
||||
|
@ -123,6 +128,9 @@ impl<T: SessionStream> SessionData<T> {
|
|||
if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) {
|
||||
err.details("Disk quota exceeded.")
|
||||
.code(ResponseCode::OverQuota)
|
||||
} else if err.matches(trc::EventType::Limit(trc::LimitEvent::TenantQuota)) {
|
||||
err.details("Organization disk quota exceeded.")
|
||||
.code(ResponseCode::OverQuota)
|
||||
} else {
|
||||
err
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{authenticate::Mechanism, capability::Capability},
|
||||
receiver::{self, Request},
|
||||
|
@ -87,7 +88,7 @@ impl<T: SessionStream> Session<T> {
|
|||
.validate_access_token("access_token", &token)
|
||||
.await
|
||||
{
|
||||
Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await,
|
||||
Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await,
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
@ -121,14 +122,17 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
};
|
||||
|
||||
// Validate access
|
||||
access_token.assert_has_permission(Permission::ImapAuthenticate)?;
|
||||
|
||||
// Cache access token
|
||||
let access_token = Arc::new(access_token);
|
||||
self.jmap.cache_access_token(access_token.clone());
|
||||
self.jmap.core.cache_access_token(access_token.clone());
|
||||
|
||||
// Create session
|
||||
self.state = State::Authenticated {
|
||||
data: Arc::new(
|
||||
SessionData::new(self, &access_token, in_flight)
|
||||
SessionData::new(self, access_token, in_flight)
|
||||
.await
|
||||
.map_err(|err| err.id(tag.clone()))?,
|
||||
),
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::time::Instant;
|
|||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
capability::{Capability, Response},
|
||||
|
@ -19,6 +20,9 @@ use imap_proto::{
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_capability(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapCapability)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Capabilities),
|
||||
|
@ -45,6 +49,9 @@ impl<T: SessionStream> Session<T> {
|
|||
}
|
||||
|
||||
pub async fn handle_id(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapId)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Id),
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::copy_move::Arguments, receiver::Request, Command, ResponseCode, ResponseType,
|
||||
StatusResponse,
|
||||
|
@ -38,6 +39,13 @@ impl<T: SessionStream> Session<T> {
|
|||
is_move: bool,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(if is_move {
|
||||
Permission::ImapMove
|
||||
} else {
|
||||
Permission::ImapCopy
|
||||
})?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_copy_move(self.version)?;
|
||||
let (data, src_mailbox) = self.state.mailbox_state();
|
||||
|
@ -233,12 +241,13 @@ impl<T: SessionStream> SessionData<T> {
|
|||
let src_account_id = src_mailbox.id.account_id;
|
||||
let mut dest_change_id = None;
|
||||
let dest_account_id = dest_mailbox.account_id;
|
||||
let dest_quota = self
|
||||
let resource_token = self
|
||||
.jmap
|
||||
.core
|
||||
.get_cached_access_token(dest_account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.quota as i64;
|
||||
.as_resource_token();
|
||||
let mut destroy_ids = RoaringBitmap::new();
|
||||
for (id, imap_id) in ids {
|
||||
match self
|
||||
|
@ -246,8 +255,7 @@ impl<T: SessionStream> SessionData<T> {
|
|||
.copy_message(
|
||||
src_account_id,
|
||||
id,
|
||||
dest_account_id,
|
||||
dest_quota,
|
||||
&resource_token,
|
||||
vec![dest_mailbox_id],
|
||||
Vec::new(),
|
||||
None,
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
spawn_op,
|
||||
};
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{create::Arguments, list::Attribute},
|
||||
receiver::Request,
|
||||
|
@ -30,6 +31,9 @@ use trc::AddContext;
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapCreate)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
let version = self.version;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
spawn_op,
|
||||
};
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
@ -21,6 +22,9 @@ use super::ImapContext;
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_delete(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapDelete)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
let version = self.version;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::time::Instant;
|
|||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{capability::Capability, enable, ImapResponse, ProtocolVersion},
|
||||
receiver::Request,
|
||||
|
@ -16,6 +17,9 @@ use imap_proto::{
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_enable(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapEnable)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
|
||||
let arguments = request.parse_enable()?;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
parser::parse_sequence_set,
|
||||
receiver::{Request, Token},
|
||||
|
@ -34,6 +35,9 @@ impl<T: SessionStream> Session<T> {
|
|||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapExpunge)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let (data, mailbox) = self.state.select_data();
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
};
|
||||
use ahash::AHashMap;
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
parser::PushUnique,
|
||||
protocol::{
|
||||
|
@ -44,6 +45,9 @@ impl<T: SessionStream> Session<T> {
|
|||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapFetch)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_fetch()?;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use ahash::AHashSet;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
fetch,
|
||||
|
@ -32,6 +33,9 @@ use crate::{
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_idle(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapIdle)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let (data, mailbox, types) = match &self.state {
|
||||
State::Authenticated { data, .. } => {
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
spawn_op,
|
||||
};
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
list::{
|
||||
|
@ -30,8 +31,14 @@ impl<T: SessionStream> Session<T> {
|
|||
let command = request.command;
|
||||
let is_lsub = command == Command::Lsub;
|
||||
let arguments = if !is_lsub {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapList)?;
|
||||
|
||||
request.parse_list(self.version)
|
||||
} else {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapLsub)?;
|
||||
|
||||
request.parse_lsub()
|
||||
}?;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use crate::core::Session;
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{namespace::Response, ImapResponse},
|
||||
receiver::Request,
|
||||
|
@ -14,6 +15,9 @@ use imap_proto::{
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_namespace(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapNamespace)?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Namespace),
|
||||
SpanId = self.session_id,
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
spawn_op,
|
||||
};
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
@ -29,6 +30,9 @@ use super::ImapContext;
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_rename(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapRename)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_rename(self.version)?;
|
||||
let data = self.state.session_data();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
search::{self, Arguments, Filter, Response, ResultOption},
|
||||
|
@ -43,8 +44,14 @@ impl<T: SessionStream> Session<T> {
|
|||
) -> trc::Result<()> {
|
||||
let op_start = Instant::now();
|
||||
let mut arguments = if !is_sort {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSearch)?;
|
||||
|
||||
request.parse_search(self.version)
|
||||
} else {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSort)?;
|
||||
|
||||
request.parse_sort()
|
||||
}?;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
fetch,
|
||||
|
@ -26,6 +27,13 @@ use super::{ImapContext, ToModSeq};
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_select(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(if request.command == Command::Select {
|
||||
Permission::ImapSelect
|
||||
} else {
|
||||
Permission::ImapExamine
|
||||
})?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let is_select = request.command == Command::Select;
|
||||
let command = request.command;
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
spawn_op,
|
||||
};
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
parser::PushUnique,
|
||||
protocol::status::{Status, StatusItem, StatusItemType},
|
||||
|
@ -34,6 +35,9 @@ use super::ToModSeq;
|
|||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_status(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapStatus)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_status(self.version)?;
|
||||
let version = self.version;
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
};
|
||||
use ahash::AHashSet;
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
fetch::{DataItem, FetchItem},
|
||||
|
@ -39,6 +40,9 @@ impl<T: SessionStream> Session<T> {
|
|||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapStore)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_store()?;
|
||||
let (data, mailbox) = self.state.select_data();
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
spawn_op,
|
||||
};
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
|
||||
use jmap::mailbox::set::{MailboxSubscribe, SCHEMA};
|
||||
use jmap_proto::{
|
||||
|
@ -30,6 +31,9 @@ impl<T: SessionStream> Session<T> {
|
|||
request: Request<Command>,
|
||||
is_subscribe: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSubscribe)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_subscribe(self.version)?;
|
||||
let data = self.state.session_data();
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
};
|
||||
use ahash::AHashMap;
|
||||
use common::listener::SessionStream;
|
||||
use directory::Permission;
|
||||
use imap_proto::{
|
||||
protocol::{
|
||||
thread::{Arguments, Response},
|
||||
|
@ -28,6 +29,9 @@ impl<T: SessionStream> Session<T> {
|
|||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapThread)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let command = request.command;
|
||||
let mut arguments = request.parse_thread()?;
|
||||
|
|
|
@ -118,6 +118,14 @@ impl<'x> RequestError<'x> {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn tenant_over_quota() -> Self {
|
||||
RequestError::blank(
|
||||
403,
|
||||
"Tenant quota exceeded",
|
||||
"Your organization has exceeded its quota.",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn too_many_requests() -> Self {
|
||||
RequestError::blank(
|
||||
429,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "jmap"
|
||||
version = "0.9.4"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
@ -17,7 +17,7 @@ smtp-proto = { version = "0.1" }
|
|||
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
|
||||
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
|
||||
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "ring", "tls12"] }
|
||||
mail-auth = { version = "0.4", features = ["generate"] }
|
||||
mail-auth = { version = "0.5", features = ["generate"] }
|
||||
sieve-rs = { version = "0.5" }
|
||||
serde = { version = "1.0", features = ["derive"]}
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use common::manager::webadmin::Resource;
|
||||
use directory::QueryBy;
|
||||
use directory::{backend::internal::PrincipalField, QueryBy};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader;
|
||||
use utils::url_params::UrlParams;
|
||||
|
@ -67,11 +67,10 @@ impl JMAP {
|
|||
);
|
||||
config.push_str("</clientConfig>\n");
|
||||
|
||||
Ok(Resource {
|
||||
content_type: "application/xml; charset=utf-8",
|
||||
contents: config.into_bytes(),
|
||||
}
|
||||
.into_http_response())
|
||||
Ok(
|
||||
Resource::new("application/xml; charset=utf-8", config.into_bytes())
|
||||
.into_http_response(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn handle_autodiscover_request(
|
||||
|
@ -147,11 +146,10 @@ impl JMAP {
|
|||
let _ = writeln!(&mut config, "\t</Response>");
|
||||
let _ = writeln!(&mut config, "</Autodiscover>");
|
||||
|
||||
Ok(Resource {
|
||||
content_type: "application/xml; charset=utf-8",
|
||||
contents: config.into_bytes(),
|
||||
}
|
||||
.into_http_response())
|
||||
Ok(
|
||||
Resource::new("application/xml; charset=utf-8", config.into_bytes())
|
||||
.into_http_response(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn autoconfig_parameters<'x>(
|
||||
|
@ -187,14 +185,14 @@ impl JMAP {
|
|||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Ok(Some(principal)) = self
|
||||
if let Ok(Some(mut principal)) = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(id), false)
|
||||
.await
|
||||
{
|
||||
account_name = principal.name;
|
||||
account_name = principal.take_str(PrincipalField::Name).unwrap_or_default();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use http_body_util::{combinators::BoxBody, StreamBody};
|
||||
use hyper::{
|
||||
body::{Bytes, Frame},
|
||||
|
@ -17,7 +18,7 @@ use hyper::{
|
|||
use jmap_proto::types::type_state::DataType;
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP, LONG_SLUMBER};
|
||||
use crate::{JMAP, LONG_SLUMBER};
|
||||
|
||||
use super::{HttpRequest, HttpResponse, HttpResponseBody, StateChangeResponse};
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
use std::{borrow::Cow, net::IpAddr, sync::Arc};
|
||||
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
expr::{functions::ResolveVariable, *},
|
||||
listener::{ServerInstance, SessionData, SessionManager, SessionStream},
|
||||
manager::webadmin::Resource,
|
||||
Core,
|
||||
};
|
||||
use directory::Permission;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{
|
||||
body::{self, Bytes},
|
||||
|
@ -81,7 +83,7 @@ impl JMAP {
|
|||
|
||||
let request = fetch_body(
|
||||
&mut req,
|
||||
if !access_token.is_super_user() {
|
||||
if !access_token.has_permission(Permission::UnlimitedUploads) {
|
||||
self.core.jmap.upload_max_size
|
||||
} else {
|
||||
0
|
||||
|
@ -142,7 +144,7 @@ impl JMAP {
|
|||
{
|
||||
return match fetch_body(
|
||||
&mut req,
|
||||
if !access_token.is_super_user() {
|
||||
if !access_token.has_permission(Permission::UnlimitedUploads) {
|
||||
self.core.jmap.upload_max_size
|
||||
} else {
|
||||
0
|
||||
|
@ -221,22 +223,16 @@ impl JMAP {
|
|||
.key_get::<String>(format!("acme:{token}").into_bytes())
|
||||
.await?
|
||||
{
|
||||
Some(proof) => Ok(Resource {
|
||||
content_type: "text/plain",
|
||||
contents: proof.into_bytes(),
|
||||
}
|
||||
.into_http_response()),
|
||||
Some(proof) => Ok(Resource::new("text/plain", proof.into_bytes())
|
||||
.into_http_response()),
|
||||
None => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
};
|
||||
}
|
||||
}
|
||||
("mta-sts.txt", &Method::GET) => {
|
||||
if let Some(policy) = self.core.build_mta_sts_policy() {
|
||||
return Ok(Resource {
|
||||
content_type: "text/plain",
|
||||
contents: policy.to_string().into_bytes(),
|
||||
}
|
||||
.into_http_response());
|
||||
return Ok(Resource::new("text/plain", policy.to_string().into_bytes())
|
||||
.into_http_response());
|
||||
} else {
|
||||
return Err(trc::ResourceEvent::NotFound.into_err());
|
||||
}
|
||||
|
@ -302,27 +298,29 @@ impl JMAP {
|
|||
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed))
|
||||
&& self.core.is_enterprise_edition()
|
||||
{
|
||||
if let Some((live_path, token)) = req
|
||||
if let Some((live_path, grant_type, token)) = req
|
||||
.uri()
|
||||
.path()
|
||||
.strip_prefix("/api/telemetry/")
|
||||
.and_then(|p| {
|
||||
p.strip_prefix("traces/live/")
|
||||
.map(|t| ("traces", t))
|
||||
.map(|t| ("traces", "live_tracing", t))
|
||||
.or_else(|| {
|
||||
p.strip_prefix("metrics/live/")
|
||||
.map(|t| ("metrics", t))
|
||||
.map(|t| ("metrics", "live_metrics", t))
|
||||
})
|
||||
})
|
||||
{
|
||||
let (account_id, _, _) =
|
||||
self.validate_access_token("live_telemetry", token).await?;
|
||||
self.validate_access_token(grant_type, token).await?;
|
||||
|
||||
return self
|
||||
.handle_telemetry_api_request(
|
||||
&req,
|
||||
vec!["", live_path, "live"],
|
||||
account_id,
|
||||
&AccessToken::from_id(account_id)
|
||||
.with_permission(Permission::MetricsLive)
|
||||
.with_permission(Permission::TracingLive),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
@ -353,11 +351,10 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
"robots.txt" => {
|
||||
return Ok(Resource {
|
||||
content_type: "text/plain",
|
||||
contents: b"User-agent: *\nDisallow: /\n".to_vec(),
|
||||
}
|
||||
.into_http_response());
|
||||
return Ok(
|
||||
Resource::new("text/plain", b"User-agent: *\nDisallow: /\n".to_vec())
|
||||
.into_http_response(),
|
||||
);
|
||||
}
|
||||
"healthz" => match path.next().unwrap_or_default() {
|
||||
"live" => {
|
||||
|
@ -390,10 +387,10 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
|
||||
return Ok(Resource {
|
||||
content_type: "text/plain; version=0.0.4",
|
||||
contents: self.core.export_prometheus_metrics().await?.into_bytes(),
|
||||
}
|
||||
return Ok(Resource::new(
|
||||
"text/plain; version=0.0.4",
|
||||
self.core.export_prometheus_metrics().await?.into_bytes(),
|
||||
)
|
||||
.into_http_response());
|
||||
}
|
||||
}
|
||||
|
@ -402,6 +399,42 @@ impl JMAP {
|
|||
}
|
||||
_ => (),
|
||||
},
|
||||
#[cfg(feature = "enterprise")]
|
||||
"logo.svg" if self.core.is_enterprise_edition() => {
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
|
||||
match self
|
||||
.core
|
||||
.logo_resource(
|
||||
req.headers()
|
||||
.get(header::HOST)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|h| h.rsplit_once(':').map_or(h, |(h, _)| h))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(resource)) => {
|
||||
return Ok(resource.into_http_response());
|
||||
}
|
||||
Ok(None) => (),
|
||||
Err(err) => {
|
||||
trc::error!(err.span_id(session.session_id));
|
||||
}
|
||||
}
|
||||
|
||||
let resource = self.inner.webadmin.get("logo.svg").await?;
|
||||
|
||||
return if !resource.is_empty() {
|
||||
Ok(resource.into_http_response())
|
||||
} else {
|
||||
Err(trc::ResourceEvent::NotFound.into_err())
|
||||
};
|
||||
|
||||
// SPDX-SnippetEnd
|
||||
}
|
||||
_ => {
|
||||
let path = req.uri().path();
|
||||
let resource = self
|
||||
|
@ -814,11 +847,6 @@ impl ToHttpResponse for &trc::Error {
|
|||
fn into_http_response(self) -> HttpResponse {
|
||||
match self.as_ref() {
|
||||
trc::EventType::Manage(cause) => {
|
||||
let details_or_reason = self
|
||||
.value(trc::Key::Details)
|
||||
.or_else(|| self.value(trc::Key::Reason))
|
||||
.and_then(|v| v.as_str());
|
||||
|
||||
match cause {
|
||||
trc::ManageEvent::MissingParameter => ManagementApiError::FieldMissing {
|
||||
field: self.value_as_str(trc::Key::Key).unwrap_or_default(),
|
||||
|
@ -831,11 +859,18 @@ impl ToHttpResponse for &trc::Error {
|
|||
item: self.value_as_str(trc::Key::Key).unwrap_or_default(),
|
||||
},
|
||||
trc::ManageEvent::NotSupported => ManagementApiError::Unsupported {
|
||||
details: details_or_reason.unwrap_or("Requested action is unsupported"),
|
||||
details: self
|
||||
.value(trc::Key::Details)
|
||||
.or_else(|| self.value(trc::Key::Reason))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Requested action is unsupported"),
|
||||
},
|
||||
trc::ManageEvent::AssertFailed => ManagementApiError::AssertFailed,
|
||||
trc::ManageEvent::Error => ManagementApiError::Other {
|
||||
details: details_or_reason.unwrap_or("An error occurred."),
|
||||
reason: self.value_as_str(trc::Key::Reason),
|
||||
details: self
|
||||
.value_as_str(trc::Key::Details)
|
||||
.unwrap_or("Unknown error"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -876,6 +911,7 @@ impl ToRequestError for trc::Error {
|
|||
RequestError::limit(RequestLimitError::ConcurrentUpload)
|
||||
}
|
||||
trc::LimitEvent::Quota => RequestError::over_quota(),
|
||||
trc::LimitEvent::TenantQuota => RequestError::tenant_over_quota(),
|
||||
trc::LimitEvent::BlobQuota => RequestError::over_blob_quota(
|
||||
self.value(trc::Key::Total)
|
||||
.and_then(|v| v.to_uint())
|
||||
|
@ -888,12 +924,18 @@ impl ToRequestError for trc::Error {
|
|||
},
|
||||
trc::EventType::Auth(cause) => match cause {
|
||||
trc::AuthEvent::MissingTotp => {
|
||||
RequestError::blank(403, "TOTP code required", cause.message())
|
||||
RequestError::blank(402, "TOTP code required", cause.message())
|
||||
}
|
||||
trc::AuthEvent::TooManyAttempts => RequestError::too_many_auth_attempts(),
|
||||
_ => RequestError::unauthorized(),
|
||||
},
|
||||
trc::EventType::Security(_) => RequestError::too_many_auth_attempts(),
|
||||
trc::EventType::Security(cause) => match cause {
|
||||
trc::SecurityEvent::AuthenticationBan
|
||||
| trc::SecurityEvent::BruteForceBan
|
||||
| trc::SecurityEvent::LoiterBan
|
||||
| trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(),
|
||||
trc::SecurityEvent::Unauthorized => RequestError::forbidden(),
|
||||
},
|
||||
trc::EventType::Resource(cause) => match cause {
|
||||
trc::ResourceEvent::NotFound => RequestError::not_found(),
|
||||
trc::ResourceEvent::BadParameters => RequestError::blank(
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
use std::str::FromStr;
|
||||
|
||||
use common::config::smtp::auth::simple_pem_parse;
|
||||
use directory::backend::internal::manage;
|
||||
use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse};
|
||||
use directory::{backend::internal::manage, Permission};
|
||||
use hyper::Method;
|
||||
use mail_auth::{
|
||||
common::crypto::{Ed25519Key, RsaKey, Sha256},
|
||||
|
@ -48,10 +48,21 @@ impl JMAP {
|
|||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
match *req.method() {
|
||||
Method::GET => self.handle_get_public_key(path).await,
|
||||
Method::POST => self.handle_create_signature(body).await,
|
||||
Method::GET => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::DkimSignatureGet)?;
|
||||
|
||||
self.handle_get_public_key(path).await
|
||||
}
|
||||
Method::POST => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::DkimSignatureCreate)?;
|
||||
|
||||
self.handle_create_signature(body).await
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,17 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use directory::backend::internal::manage::{self, ManageDirectory};
|
||||
use common::auth::AccessToken;
|
||||
use directory::{
|
||||
backend::internal::manage::{self},
|
||||
Permission,
|
||||
};
|
||||
|
||||
use hyper::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sha1::Digest;
|
||||
use utils::{config::Config, url_params::UrlParams};
|
||||
use utils::config::Config;
|
||||
use x509_parser::parse_x509_certificate;
|
||||
|
||||
use crate::{
|
||||
|
@ -33,39 +37,21 @@ struct DnsRecord {
|
|||
}
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_manage_domain(
|
||||
pub async fn handle_manage_dns(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
match (path.get(1), req.method()) {
|
||||
(None, &Method::GET) => {
|
||||
// List domains
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let filter = params.get("filter");
|
||||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2),
|
||||
req.method(),
|
||||
) {
|
||||
("records", Some(domain), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::DomainGet)?;
|
||||
|
||||
let domains = self.core.storage.data.list_domains(filter).await?;
|
||||
let (total, domains) = if limit > 0 {
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
(
|
||||
domains.len(),
|
||||
domains.into_iter().skip(offset).take(limit).collect(),
|
||||
)
|
||||
} else {
|
||||
(domains.len(), domains)
|
||||
};
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": {
|
||||
"items": domains,
|
||||
"total": total,
|
||||
},
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
(Some(domain), &Method::GET) => {
|
||||
// Obtain DNS records
|
||||
let domain = decode_path_element(domain);
|
||||
Ok(JsonResponse::new(json!({
|
||||
|
@ -73,50 +59,6 @@ impl JMAP {
|
|||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
(Some(domain), &Method::POST) => {
|
||||
// Create domain
|
||||
let domain = decode_path_element(domain);
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.create_domain(domain.as_ref())
|
||||
.await?;
|
||||
// Set default domain name if missing
|
||||
if self
|
||||
.core
|
||||
.storage
|
||||
.config
|
||||
.get("lookup.default.domain")
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
self.core
|
||||
.storage
|
||||
.config
|
||||
.set([("lookup.default.domain", domain.as_ref())])
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
(Some(domain), &Method::DELETE) => {
|
||||
// Delete domain
|
||||
let domain = decode_path_element(domain);
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.delete_domain(domain.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
|
@ -173,10 +115,10 @@ impl JMAP {
|
|||
}
|
||||
for signature_id in signature_ids {
|
||||
if let (Some(algo), Some(pk), Some(selector)) = (
|
||||
keys.value(&format!("{signature_id}.algorithm"))
|
||||
keys.value(format!("{signature_id}.algorithm"))
|
||||
.and_then(|algo| algo.parse::<Algorithm>().ok()),
|
||||
keys.value(&format!("{signature_id}.private-key")),
|
||||
keys.value(&format!("{signature_id}.selector")),
|
||||
keys.value(format!("{signature_id}.private-key")),
|
||||
keys.value(format!("{signature_id}.selector")),
|
||||
) {
|
||||
match obtain_dkim_public_key(algo, pk) {
|
||||
Ok(public) => {
|
|
@ -13,11 +13,14 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::telemetry::{
|
||||
metrics::store::{Metric, MetricsStore},
|
||||
tracers::store::{TracingQuery, TracingStore},
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
telemetry::{
|
||||
metrics::store::{Metric, MetricsStore},
|
||||
tracers::store::{TracingQuery, TracingStore},
|
||||
},
|
||||
};
|
||||
use directory::backend::internal::manage;
|
||||
use directory::{backend::internal::manage, Permission};
|
||||
use http_body_util::{combinators::BoxBody, StreamBody};
|
||||
use hyper::{
|
||||
body::{Bytes, Frame},
|
||||
|
@ -46,9 +49,10 @@ impl JMAP {
|
|||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
account_id: u32,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let account_id = access_token.primary_id();
|
||||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
|
@ -56,6 +60,9 @@ impl JMAP {
|
|||
req.method(),
|
||||
) {
|
||||
("traces", None, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::TracingList)?;
|
||||
|
||||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
let mut tracing_query = Vec::new();
|
||||
|
@ -162,6 +169,9 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("traces", Some("live"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::TracingLive)?;
|
||||
|
||||
let mut key_filters = AHashMap::new();
|
||||
let mut filter = None;
|
||||
|
||||
|
@ -290,6 +300,9 @@ impl JMAP {
|
|||
})
|
||||
}
|
||||
("trace", id, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::TracingGet)?;
|
||||
|
||||
let store = &self
|
||||
.core
|
||||
.enterprise
|
||||
|
@ -327,15 +340,32 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
}
|
||||
("live", Some("token"), &Method::GET) => {
|
||||
("live", Some("tracing-token"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::TracingLive)?;
|
||||
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.issue_custom_token(account_id, "live_telemetry", "web", 60).await?,
|
||||
"data": self.issue_custom_token(account_id, "live_tracing", "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("live", Some("metrics-token"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MetricsLive)?;
|
||||
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.issue_custom_token(account_id, "live_metrics", "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("metrics", None, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MetricsList)?;
|
||||
|
||||
let before = params
|
||||
.parse::<Timestamp>("before")
|
||||
.map(|t| t.into_inner())
|
||||
|
@ -395,6 +425,9 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
("metrics", Some("live"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MetricsLive)?;
|
||||
|
||||
let interval = Duration::from_secs(
|
||||
params
|
||||
.parse::<u64>("interval")
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use common::enterprise::undelete::DeletedBlob;
|
||||
use common::{auth::AccessToken, enterprise::undelete::DeletedBlob};
|
||||
use directory::backend::internal::manage::ManageDirectory;
|
||||
use hyper::Method;
|
||||
use jmap_proto::types::collection::Collection;
|
||||
|
@ -24,6 +24,7 @@ use utils::{url_params::UrlParams, BlobHash};
|
|||
use crate::{
|
||||
api::{
|
||||
http::{HttpSessionData, ToHttpResponse},
|
||||
management::decode_path_element,
|
||||
HttpRequest, HttpResponse, JsonResponse,
|
||||
},
|
||||
email::ingest::{IngestEmail, IngestSource},
|
||||
|
@ -61,11 +62,12 @@ impl JMAP {
|
|||
) -> trc::Result<HttpResponse> {
|
||||
match (path.get(2).copied(), req.method()) {
|
||||
(Some(account_name), &Method::GET) => {
|
||||
let account_name = decode_path_element(account_name);
|
||||
let account_id = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_account_id(account_name)
|
||||
.get_principal_id(account_name.as_ref())
|
||||
.await?
|
||||
.ok_or_else(|| trc::ResourceEvent::NotFound.into_err())?;
|
||||
let mut deleted = self.core.list_deleted(account_id).await?;
|
||||
|
@ -111,11 +113,12 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
(Some(account_name), &Method::POST) => {
|
||||
let account_name = decode_path_element(account_name);
|
||||
let account_id = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_account_id(account_name)
|
||||
.get_principal_id(account_name.as_ref())
|
||||
.await?
|
||||
.ok_or_else(|| trc::ResourceEvent::NotFound.into_err())?;
|
||||
|
||||
|
@ -168,8 +171,13 @@ impl JMAP {
|
|||
.email_ingest(IngestEmail {
|
||||
raw_message: &bytes,
|
||||
message: MessageParser::new().parse(&bytes),
|
||||
account_id,
|
||||
account_quota: 0,
|
||||
resource: self
|
||||
.get_resource_token(
|
||||
&AccessToken::from_id(u32::MAX),
|
||||
account_id,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
mailbox_ids: vec![INBOX_ID],
|
||||
keywords: vec![],
|
||||
received_at: (request.time as u64).into(),
|
||||
|
|
|
@ -5,7 +5,8 @@ use std::{
|
|||
};
|
||||
|
||||
use chrono::DateTime;
|
||||
use directory::backend::internal::manage;
|
||||
use common::auth::AccessToken;
|
||||
use directory::{backend::internal::manage, Permission};
|
||||
use rev_lines::RevLines;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
@ -27,7 +28,14 @@ struct LogEntry {
|
|||
}
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_view_logs(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
|
||||
pub async fn handle_view_logs(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::LogsView)?;
|
||||
|
||||
let path = self
|
||||
.core
|
||||
.metrics
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
pub mod dkim;
|
||||
pub mod domain;
|
||||
pub mod dns;
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub mod enterprise;
|
||||
pub mod log;
|
||||
|
@ -19,25 +19,38 @@ pub mod stores;
|
|||
|
||||
use std::{borrow::Cow, str::FromStr, sync::Arc};
|
||||
|
||||
use directory::backend::internal::manage;
|
||||
use common::auth::AccessToken;
|
||||
use directory::{backend::internal::manage, Permission};
|
||||
use hyper::Method;
|
||||
use mail_parser::DateTime;
|
||||
use serde::Serialize;
|
||||
use store::write::now;
|
||||
|
||||
use super::{http::HttpSessionData, HttpRequest, HttpResponse};
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "error")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ManagementApiError<'x> {
|
||||
FieldAlreadyExists { field: &'x str, value: &'x str },
|
||||
FieldMissing { field: &'x str },
|
||||
NotFound { item: &'x str },
|
||||
Unsupported { details: &'x str },
|
||||
FieldAlreadyExists {
|
||||
field: &'x str,
|
||||
value: &'x str,
|
||||
},
|
||||
FieldMissing {
|
||||
field: &'x str,
|
||||
},
|
||||
NotFound {
|
||||
item: &'x str,
|
||||
},
|
||||
Unsupported {
|
||||
details: &'x str,
|
||||
},
|
||||
AssertFailed,
|
||||
Other { details: &'x str },
|
||||
Other {
|
||||
details: &'x str,
|
||||
reason: Option<&'x str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl JMAP {
|
||||
|
@ -50,31 +63,68 @@ impl JMAP {
|
|||
session: &HttpSessionData,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>();
|
||||
let is_superuser = access_token.is_super_user();
|
||||
|
||||
match path.first().copied().unwrap_or_default() {
|
||||
"queue" if is_superuser => self.handle_manage_queue(req, path).await,
|
||||
"settings" if is_superuser => self.handle_manage_settings(req, path, body).await,
|
||||
"reports" if is_superuser => self.handle_manage_reports(req, path).await,
|
||||
"principal" if is_superuser => self.handle_manage_principal(req, path, body).await,
|
||||
"domain" if is_superuser => self.handle_manage_domain(req, path).await,
|
||||
"store" if is_superuser => self.handle_manage_store(req, path, body, session).await,
|
||||
"reload" if is_superuser => self.handle_manage_reload(req, path).await,
|
||||
"dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await,
|
||||
"update" if is_superuser => self.handle_manage_update(req, path).await,
|
||||
"logs" if is_superuser && req.method() == Method::GET => {
|
||||
self.handle_view_logs(req).await
|
||||
"queue" => self.handle_manage_queue(req, path, &access_token).await,
|
||||
"settings" => {
|
||||
self.handle_manage_settings(req, path, body, &access_token)
|
||||
.await
|
||||
}
|
||||
"sieve" if is_superuser => self.handle_run_sieve(req, path, body).await,
|
||||
"restart" if is_superuser && req.method() == Method::GET => {
|
||||
"reports" => self.handle_manage_reports(req, path, &access_token).await,
|
||||
"principal" => {
|
||||
self.handle_manage_principal(req, path, body, &access_token)
|
||||
.await
|
||||
}
|
||||
"dns" => self.handle_manage_dns(req, path, &access_token).await,
|
||||
"store" => {
|
||||
self.handle_manage_store(req, path, body, session, &access_token)
|
||||
.await
|
||||
}
|
||||
"reload" => self.handle_manage_reload(req, path, &access_token).await,
|
||||
"dkim" => {
|
||||
self.handle_manage_dkim(req, path, body, &access_token)
|
||||
.await
|
||||
}
|
||||
"update" => self.handle_manage_update(req, path, &access_token).await,
|
||||
"logs" if req.method() == Method::GET => {
|
||||
self.handle_view_logs(req, &access_token).await
|
||||
}
|
||||
"sieve" => self.handle_run_sieve(req, path, body, &access_token).await,
|
||||
"restart" if req.method() == Method::GET => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::Restart)?;
|
||||
|
||||
Err(manage::unsupported("Restart is not yet supported"))
|
||||
}
|
||||
"oauth" => self.handle_oauth_api_request(access_token, body).await,
|
||||
"oauth" => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::AuthenticateOauth)?;
|
||||
|
||||
self.handle_oauth_api_request(access_token, body).await
|
||||
}
|
||||
"account" => match (path.get(1).copied().unwrap_or_default(), req.method()) {
|
||||
("crypto", &Method::POST) => self.handle_crypto_post(access_token, body).await,
|
||||
("crypto", &Method::GET) => self.handle_crypto_get(access_token).await,
|
||||
("auth", &Method::GET) => self.handle_account_auth_get(access_token).await,
|
||||
("crypto", &Method::POST) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::ManageEncryption)?;
|
||||
|
||||
self.handle_crypto_post(access_token, body).await
|
||||
}
|
||||
("crypto", &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::ManageEncryption)?;
|
||||
|
||||
self.handle_crypto_get(access_token).await
|
||||
}
|
||||
("auth", &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::ManagePasswords)?;
|
||||
|
||||
self.handle_account_auth_get(access_token).await
|
||||
}
|
||||
("auth", &Method::POST) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::ManagePasswords)?;
|
||||
|
||||
self.handle_account_auth_post(req, access_token, body).await
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
|
@ -83,7 +133,7 @@ impl JMAP {
|
|||
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
"telemetry" if is_superuser => {
|
||||
"telemetry" => {
|
||||
// WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED
|
||||
// Any attempt to modify, bypass, or disable this license validation mechanism
|
||||
// constitutes a severe violation of the Stalwart Enterprise License Agreement.
|
||||
|
@ -94,7 +144,7 @@ impl JMAP {
|
|||
// for copyright infringement, breach of contract, and fraud.
|
||||
|
||||
if self.core.is_enterprise_edition() {
|
||||
self.handle_telemetry_api_request(req, path, access_token.primary_id())
|
||||
self.handle_telemetry_api_request(req, path, &access_token)
|
||||
.await
|
||||
} else {
|
||||
Err(manage::enterprise())
|
||||
|
|
|
@ -4,55 +4,30 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use directory::{
|
||||
backend::internal::{
|
||||
lookup::DirectoryStore,
|
||||
manage::{self, ManageDirectory},
|
||||
manage::{self, not_found, ManageDirectory},
|
||||
PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets,
|
||||
},
|
||||
DirectoryInner, Principal, QueryBy, Type,
|
||||
DirectoryInner, Permission, Principal, QueryBy, Type,
|
||||
};
|
||||
|
||||
use hyper::{header, Method};
|
||||
use serde_json::json;
|
||||
use trc::AddContext;
|
||||
use utils::url_params::UrlParams;
|
||||
|
||||
use crate::{
|
||||
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||
auth::AccessToken,
|
||||
JMAP,
|
||||
};
|
||||
|
||||
use super::decode_path_element;
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PrincipalResponse {
|
||||
#[serde(default)]
|
||||
pub id: u32,
|
||||
#[serde(rename = "type")]
|
||||
pub typ: Type,
|
||||
#[serde(default)]
|
||||
pub quota: u64,
|
||||
#[serde(rename = "usedQuota")]
|
||||
#[serde(default)]
|
||||
pub used_quota: u64,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub emails: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<String>,
|
||||
#[serde(rename = "memberOf")]
|
||||
#[serde(default)]
|
||||
pub member_of: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub members: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -68,8 +43,6 @@ pub enum AccountAuthRequest {
|
|||
pub struct AccountAuthResponse {
|
||||
#[serde(rename = "otpEnabled")]
|
||||
pub otp_auth: bool,
|
||||
#[serde(rename = "isAdministrator")]
|
||||
pub is_admin: bool,
|
||||
#[serde(rename = "appPasswords")]
|
||||
pub app_passwords: Vec<String>,
|
||||
}
|
||||
|
@ -80,39 +53,52 @@ impl JMAP {
|
|||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
match (path.get(1), req.method()) {
|
||||
(None, &Method::POST) => {
|
||||
// Make sure the current directory supports updates
|
||||
self.assert_supported_directory()?;
|
||||
// Parse principal
|
||||
let principal =
|
||||
serde_json::from_slice::<Principal>(body.as_deref().unwrap_or_default())
|
||||
.map_err(|err| {
|
||||
trc::EventType::Resource(trc::ResourceEvent::BadParameters)
|
||||
.from_json_error(err)
|
||||
})?;
|
||||
|
||||
// Create principal
|
||||
let principal = serde_json::from_slice::<PrincipalResponse>(
|
||||
body.as_deref().unwrap_or_default(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err)
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(match principal.typ() {
|
||||
Type::Individual => Permission::IndividualCreate,
|
||||
Type::Group => Permission::GroupCreate,
|
||||
Type::List => Permission::MailingListCreate,
|
||||
Type::Domain => Permission::DomainCreate,
|
||||
Type::Tenant => Permission::TenantCreate,
|
||||
Type::Role => Permission::RoleCreate,
|
||||
Type::Resource | Type::Location | Type::Other => Permission::PrincipalCreate,
|
||||
})?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self
|
||||
#[cfg(feature = "enterprise")]
|
||||
if (matches!(principal.typ(), Type::Tenant)
|
||||
|| principal.has_field(PrincipalField::Tenant))
|
||||
&& !self.core.is_enterprise_edition()
|
||||
{
|
||||
return Err(manage::enterprise());
|
||||
}
|
||||
|
||||
// Make sure the current directory supports updates
|
||||
if matches!(principal.typ(), Type::Individual) {
|
||||
self.assert_supported_directory()?;
|
||||
}
|
||||
|
||||
// Create principal
|
||||
let result = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.create_account(
|
||||
Principal {
|
||||
id: principal.id,
|
||||
typ: principal.typ,
|
||||
quota: principal.quota,
|
||||
name: principal.name,
|
||||
secrets: principal.secrets,
|
||||
emails: principal.emails,
|
||||
member_of: principal.member_of,
|
||||
description: principal.description,
|
||||
},
|
||||
principal.members,
|
||||
)
|
||||
.await?,
|
||||
.create_principal(principal, access_token.tenant.map(|t| t.id))
|
||||
.await?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": result,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
|
@ -120,67 +106,146 @@ impl JMAP {
|
|||
// List principal ids
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let filter = params.get("filter");
|
||||
let typ = params.parse("type");
|
||||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
let count = params.get("count").is_some();
|
||||
|
||||
let accounts = self.core.storage.data.list_accounts(filter, typ).await?;
|
||||
let (total, accounts) = if limit > 0 {
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
(
|
||||
accounts.len(),
|
||||
accounts.into_iter().skip(offset).take(limit).collect(),
|
||||
)
|
||||
// Parse types
|
||||
let mut types = Vec::new();
|
||||
for typ in params
|
||||
.get("types")
|
||||
.or_else(|| params.get("type"))
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
{
|
||||
if let Some(typ) = Type::parse(typ) {
|
||||
if !types.contains(&typ) {
|
||||
types.push(typ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
let mut fields = Vec::new();
|
||||
for field in params.get("fields").unwrap_or_default().split(',') {
|
||||
if let Some(field) = PrincipalField::try_parse(field) {
|
||||
if !fields.contains(&field) {
|
||||
fields.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the access token
|
||||
let validate_types = if !types.is_empty() {
|
||||
types.as_slice()
|
||||
} else {
|
||||
(accounts.len(), accounts)
|
||||
&[
|
||||
Type::Individual,
|
||||
Type::Group,
|
||||
Type::List,
|
||||
Type::Domain,
|
||||
Type::Tenant,
|
||||
Type::Role,
|
||||
Type::Other,
|
||||
]
|
||||
};
|
||||
for typ in validate_types {
|
||||
access_token.assert_has_permission(match typ {
|
||||
Type::Individual => Permission::IndividualList,
|
||||
Type::Group => Permission::GroupList,
|
||||
Type::List => Permission::MailingListList,
|
||||
Type::Domain => Permission::DomainList,
|
||||
Type::Tenant => Permission::TenantList,
|
||||
Type::Role => Permission::RoleList,
|
||||
Type::Resource | Type::Location | Type::Other => Permission::PrincipalList,
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut tenant = access_token.tenant.map(|t| t.id);
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if self.core.is_enterprise_edition() {
|
||||
if tenant.is_none() {
|
||||
// Limit search to current tenant
|
||||
if let Some(tenant_name) = params.get("tenant") {
|
||||
tenant = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_principal_info(tenant_name)
|
||||
.await?
|
||||
.filter(|p| p.typ == Type::Tenant)
|
||||
.map(|p| p.id);
|
||||
}
|
||||
}
|
||||
} else if types.contains(&Type::Tenant) {
|
||||
return Err(manage::enterprise());
|
||||
}
|
||||
|
||||
let mut principals = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.list_principals(filter, tenant, &types, &fields, page, limit)
|
||||
.await?;
|
||||
|
||||
if count {
|
||||
principals.items.clear();
|
||||
}
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": {
|
||||
"items": accounts,
|
||||
"total": total,
|
||||
},
|
||||
"data": principals,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
(Some(name), method) => {
|
||||
// Fetch, update or delete principal
|
||||
let name = decode_path_element(name);
|
||||
let account_id = self
|
||||
let (account_id, typ) = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_account_id(name.as_ref())
|
||||
.get_principal_info(name.as_ref())
|
||||
.await?
|
||||
.ok_or_else(|| trc::ManageEvent::NotFound.into_err())?;
|
||||
.filter(|p| p.has_tenant_access(access_token.tenant.map(|t| t.id)))
|
||||
.map(|p| (p.id, p.typ))
|
||||
.ok_or_else(|| not_found(name.to_string()))?;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if matches!(typ, Type::Tenant) && !self.core.is_enterprise_edition() {
|
||||
return Err(manage::enterprise());
|
||||
}
|
||||
|
||||
match *method {
|
||||
Method::GET => {
|
||||
let principal = self
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(match typ {
|
||||
Type::Individual => Permission::IndividualGet,
|
||||
Type::Group => Permission::GroupGet,
|
||||
Type::List => Permission::MailingListGet,
|
||||
Type::Domain => Permission::DomainGet,
|
||||
Type::Tenant => Permission::TenantGet,
|
||||
Type::Role => Permission::RoleGet,
|
||||
Type::Resource | Type::Location | Type::Other => {
|
||||
Permission::PrincipalGet
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut principal = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(account_id), true)
|
||||
.await?
|
||||
.ok_or_else(|| trc::ManageEvent::NotFound.into_err())?;
|
||||
let principal = self.core.storage.data.map_group_ids(principal).await?;
|
||||
|
||||
// Obtain quota usage
|
||||
let mut principal = PrincipalResponse::from(principal);
|
||||
principal.used_quota = self.get_used_quota(account_id).await? as u64;
|
||||
|
||||
// Obtain member names
|
||||
for member_id in self.core.storage.data.get_members(account_id).await? {
|
||||
if let Some(member_principal) = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.query(QueryBy::Id(member_id), false)
|
||||
.await?
|
||||
{
|
||||
principal.members.push(member_principal.name);
|
||||
}
|
||||
}
|
||||
// Map fields
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.map_field_ids(&mut principal, &[])
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": principal,
|
||||
|
@ -188,24 +253,63 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
Method::DELETE => {
|
||||
// Remove FTS index
|
||||
self.core.storage.fts.remove_all(account_id).await?;
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(match typ {
|
||||
Type::Individual => Permission::IndividualDelete,
|
||||
Type::Group => Permission::GroupDelete,
|
||||
Type::List => Permission::MailingListDelete,
|
||||
Type::Domain => Permission::DomainDelete,
|
||||
Type::Tenant => Permission::TenantDelete,
|
||||
Type::Role => Permission::RoleDelete,
|
||||
Type::Resource | Type::Location | Type::Other => {
|
||||
Permission::PrincipalDelete
|
||||
}
|
||||
})?;
|
||||
|
||||
// Delete account
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.delete_account(QueryBy::Id(account_id))
|
||||
.delete_principal(QueryBy::Id(account_id))
|
||||
.await?;
|
||||
|
||||
// Remove FTS index
|
||||
if matches!(typ, Type::Individual | Type::Group) {
|
||||
self.core.storage.fts.remove_all(account_id).await?;
|
||||
}
|
||||
|
||||
// Remove entries from cache
|
||||
self.inner.sessions.retain(|_, id| id.item != account_id);
|
||||
|
||||
if matches!(typ, Type::Role | Type::Tenant) {
|
||||
// Update permissions cache
|
||||
self.core.security.permissions.clear();
|
||||
self.core
|
||||
.security
|
||||
.permissions_version
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
Method::PATCH => {
|
||||
// Validate the access token
|
||||
let permission_needed = match typ {
|
||||
Type::Individual => Permission::IndividualUpdate,
|
||||
Type::Group => Permission::GroupUpdate,
|
||||
Type::List => Permission::MailingListUpdate,
|
||||
Type::Domain => Permission::DomainUpdate,
|
||||
Type::Tenant => Permission::TenantUpdate,
|
||||
Type::Role => Permission::RoleUpdate,
|
||||
Type::Resource | Type::Location | Type::Other => {
|
||||
Permission::PrincipalUpdate
|
||||
}
|
||||
};
|
||||
access_token.assert_has_permission(permission_needed)?;
|
||||
|
||||
let changes = serde_json::from_slice::<Vec<PrincipalUpdate>>(
|
||||
body.as_deref().unwrap_or_default(),
|
||||
)
|
||||
|
@ -214,30 +318,86 @@ impl JMAP {
|
|||
.from_json_error(err)
|
||||
})?;
|
||||
|
||||
// Make sure the current directory supports updates
|
||||
if changes.iter().any(|change| {
|
||||
!matches!(
|
||||
change.field,
|
||||
PrincipalField::Quota | PrincipalField::Description
|
||||
)
|
||||
}) {
|
||||
// Validate changes
|
||||
let mut needs_assert = false;
|
||||
let mut expire_session = false;
|
||||
let mut expire_token = false;
|
||||
let mut is_role_change = false;
|
||||
|
||||
for change in &changes {
|
||||
match change.field {
|
||||
PrincipalField::Name | PrincipalField::Emails => {
|
||||
needs_assert = true;
|
||||
}
|
||||
PrincipalField::Secrets => {
|
||||
expire_session = true;
|
||||
needs_assert = true;
|
||||
}
|
||||
PrincipalField::Quota
|
||||
| PrincipalField::UsedQuota
|
||||
| PrincipalField::Description
|
||||
| PrincipalField::Type
|
||||
| PrincipalField::Picture
|
||||
| PrincipalField::MemberOf
|
||||
| PrincipalField::Members
|
||||
| PrincipalField::Lists => (),
|
||||
PrincipalField::Tenant => {
|
||||
// Tenants are not allowed to change their tenantId
|
||||
if access_token.tenant.is_some() {
|
||||
trc::bail!(trc::SecurityEvent::Unauthorized
|
||||
.into_err()
|
||||
.details(permission_needed.name())
|
||||
.ctx(
|
||||
trc::Key::Reason,
|
||||
"Tenants cannot change their tenantId"
|
||||
));
|
||||
}
|
||||
}
|
||||
PrincipalField::Roles
|
||||
| PrincipalField::EnabledPermissions
|
||||
| PrincipalField::DisabledPermissions => {
|
||||
if matches!(typ, Type::Role | Type::Tenant) {
|
||||
is_role_change = true;
|
||||
} else {
|
||||
expire_token = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needs_assert {
|
||||
self.assert_supported_directory()?;
|
||||
}
|
||||
|
||||
let is_password_change = changes
|
||||
.iter()
|
||||
.any(|change| matches!(change.field, PrincipalField::Secrets));
|
||||
|
||||
// Update principal
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.update_account(QueryBy::Id(account_id), changes)
|
||||
.update_principal(
|
||||
QueryBy::Id(account_id),
|
||||
changes,
|
||||
access_token.tenant.map(|t| t.id),
|
||||
)
|
||||
.await?;
|
||||
if is_password_change {
|
||||
|
||||
if expire_session {
|
||||
// Remove entries from cache
|
||||
self.inner.sessions.retain(|_, id| id.item != account_id);
|
||||
}
|
||||
|
||||
if is_role_change {
|
||||
// Update permissions cache
|
||||
self.core.security.permissions.clear();
|
||||
self.core
|
||||
.security
|
||||
.permissions_version
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if expire_token {
|
||||
self.core.security.access_tokens.remove(&account_id);
|
||||
}
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
@ -257,7 +417,6 @@ impl JMAP {
|
|||
) -> trc::Result<HttpResponse> {
|
||||
let mut response = AccountAuthResponse {
|
||||
otp_auth: false,
|
||||
is_admin: access_token.is_super_user(),
|
||||
app_passwords: Vec::new(),
|
||||
};
|
||||
|
||||
|
@ -270,7 +429,7 @@ impl JMAP {
|
|||
.await?
|
||||
.ok_or_else(|| trc::ManageEvent::NotFound.into_err())?;
|
||||
|
||||
for secret in principal.secrets {
|
||||
for secret in principal.iter_str(PrincipalField::Secrets) {
|
||||
if secret.is_otp_auth() {
|
||||
response.otp_auth = true;
|
||||
} else if let Some((app_name, _)) =
|
||||
|
@ -327,7 +486,7 @@ impl JMAP {
|
|||
}
|
||||
|
||||
// Handle Fallback admin password changes
|
||||
if access_token.is_super_user() && access_token.primary_id() == u32::MAX {
|
||||
if access_token.primary_id() == u32::MAX {
|
||||
match requests.into_iter().next().unwrap() {
|
||||
AccountAuthRequest::SetPassword { password } => {
|
||||
self.core
|
||||
|
@ -393,7 +552,11 @@ impl JMAP {
|
|||
self.core
|
||||
.storage
|
||||
.data
|
||||
.update_account(QueryBy::Id(access_token.primary_id()), actions)
|
||||
.update_principal(
|
||||
QueryBy::Id(access_token.primary_id()),
|
||||
actions,
|
||||
access_token.tenant.map(|t| t.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Remove entries from cache
|
||||
|
@ -420,26 +583,10 @@ impl JMAP {
|
|||
Err(manage::unsupported(format!(
|
||||
concat!(
|
||||
"{} directory cannot be managed. ",
|
||||
"Only internal directories support inserts and update operations."
|
||||
"Only internal directories support inserts ",
|
||||
"and update operations."
|
||||
),
|
||||
class
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Principal<String>> for PrincipalResponse {
|
||||
fn from(principal: Principal<String>) -> Self {
|
||||
PrincipalResponse {
|
||||
id: principal.id,
|
||||
typ: principal.typ,
|
||||
quota: principal.quota,
|
||||
name: principal.name,
|
||||
emails: principal.emails,
|
||||
member_of: principal.member_of,
|
||||
description: principal.description,
|
||||
secrets: principal.secrets,
|
||||
used_quota: 0,
|
||||
members: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
*/
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use common::auth::AccessToken;
|
||||
use directory::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField},
|
||||
Permission, Type,
|
||||
};
|
||||
use hyper::Method;
|
||||
use mail_auth::{
|
||||
dmarc::URI,
|
||||
|
@ -19,6 +24,7 @@ use store::{
|
|||
write::{key::DeserializeBigEndian, now, Bincode, QueueClass, ReportEvent, ValueClass},
|
||||
Deserialize, IterateParams, ValueKey,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::url_params::UrlParams;
|
||||
|
||||
use crate::{
|
||||
|
@ -105,15 +111,48 @@ impl JMAP {
|
|||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
|
||||
// Limit to tenant domains
|
||||
let mut tenant_domains = None;
|
||||
if self.core.is_enterprise_edition() {
|
||||
if let Some(tenant) = access_token.tenant {
|
||||
tenant_domains = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.list_principals(
|
||||
None,
|
||||
tenant.id.into(),
|
||||
&[Type::Domain],
|
||||
&[PrincipalField::Name],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.map(|principals| {
|
||||
principals
|
||||
.items
|
||||
.into_iter()
|
||||
.filter_map(|mut p| p.take_str(PrincipalField::Name))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.caused_by(trc::location!())?
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied().map(decode_path_element),
|
||||
req.method(),
|
||||
) {
|
||||
("messages", None, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MessageQueueList)?;
|
||||
|
||||
let text = params.get("text");
|
||||
let from = params.get("from");
|
||||
let to = params.get("to");
|
||||
|
@ -150,32 +189,35 @@ impl JMAP {
|
|||
IterateParams::new(from_key, to_key).ascending(),
|
||||
|key, value| {
|
||||
let message = Bincode::<queue::Message>::deserialize(value)?.inner;
|
||||
let matches = !has_filters
|
||||
|| (text
|
||||
.as_ref()
|
||||
.map(|text| {
|
||||
message.return_path.contains(text)
|
||||
|| message
|
||||
.recipients
|
||||
.iter()
|
||||
.any(|r| r.address_lcase.contains(text))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
from.as_ref()
|
||||
.map_or(true, |from| message.return_path.contains(from))
|
||||
&& to.as_ref().map_or(true, |to| {
|
||||
let matches = tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| message.has_domain(domains))
|
||||
&& (!has_filters
|
||||
|| (text
|
||||
.as_ref()
|
||||
.map(|text| {
|
||||
message.return_path.contains(text)
|
||||
|| message
|
||||
.recipients
|
||||
.iter()
|
||||
.any(|r| r.address_lcase.contains(text))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
from.as_ref().map_or(true, |from| {
|
||||
message.return_path.contains(from)
|
||||
}) && to.as_ref().map_or(true, |to| {
|
||||
message
|
||||
.recipients
|
||||
.iter()
|
||||
.any(|r| r.address_lcase.contains(to))
|
||||
})
|
||||
})
|
||||
&& before.as_ref().map_or(true, |before| {
|
||||
message.next_delivery_event() < *before
|
||||
})
|
||||
&& after.as_ref().map_or(true, |after| {
|
||||
message.next_delivery_event() > *after
|
||||
}));
|
||||
})
|
||||
&& before.as_ref().map_or(true, |before| {
|
||||
message.next_delivery_event() < *before
|
||||
})
|
||||
&& after.as_ref().map_or(true, |after| {
|
||||
message.next_delivery_event() > *after
|
||||
})));
|
||||
|
||||
if matches {
|
||||
if offset == 0 {
|
||||
|
@ -217,10 +259,18 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
("messages", Some(queue_id), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MessageQueueGet)?;
|
||||
|
||||
if let Some(message) = self
|
||||
.smtp
|
||||
.read_message(queue_id.parse().unwrap_or_default())
|
||||
.await
|
||||
.filter(|message| {
|
||||
tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| message.has_domain(domains))
|
||||
})
|
||||
{
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": Message::from(&message),
|
||||
|
@ -231,6 +281,9 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("messages", Some(queue_id), &Method::PATCH) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MessageQueueUpdate)?;
|
||||
|
||||
let time = params
|
||||
.parse::<FutureTimestamp>("at")
|
||||
.map(|t| t.into_inner())
|
||||
|
@ -241,6 +294,11 @@ impl JMAP {
|
|||
.smtp
|
||||
.read_message(queue_id.parse().unwrap_or_default())
|
||||
.await
|
||||
.filter(|message| {
|
||||
tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| message.has_domain(domains))
|
||||
})
|
||||
{
|
||||
let prev_event = message.next_event().unwrap_or_default();
|
||||
let mut found = false;
|
||||
|
@ -278,10 +336,18 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("messages", Some(queue_id), &Method::DELETE) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::MessageQueueDelete)?;
|
||||
|
||||
if let Some(mut message) = self
|
||||
.smtp
|
||||
.read_message(queue_id.parse().unwrap_or_default())
|
||||
.await
|
||||
.filter(|message| {
|
||||
tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| message.has_domain(domains))
|
||||
})
|
||||
{
|
||||
let mut found = false;
|
||||
let prev_event = message.next_event().unwrap_or_default();
|
||||
|
@ -358,6 +424,9 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("reports", None, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::OutgoingReportList)?;
|
||||
|
||||
let domain = params.get("domain").map(|d| d.to_lowercase());
|
||||
let type_ = params.get("type").and_then(|t| match t {
|
||||
"dmarc" => 0u8.into(),
|
||||
|
@ -399,7 +468,10 @@ impl JMAP {
|
|||
|key, _| {
|
||||
if type_.map_or(true, |t| t == *key.last().unwrap()) {
|
||||
let event = ReportEvent::deserialize(key)?;
|
||||
if event.seq_id != 0
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| domains.contains(&event.domain))
|
||||
&& event.seq_id != 0
|
||||
&& domain.as_ref().map_or(true, |d| event.domain.contains(d))
|
||||
{
|
||||
if offset == 0 {
|
||||
|
@ -436,10 +508,17 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
("reports", Some(report_id), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::OutgoingReportGet)?;
|
||||
|
||||
let mut result = None;
|
||||
if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) {
|
||||
match report_id {
|
||||
QueueClass::DmarcReportHeader(event) => {
|
||||
QueueClass::DmarcReportHeader(event)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| domains.contains(&event.domain)) =>
|
||||
{
|
||||
let mut rua = Vec::new();
|
||||
if let Some(report) = self
|
||||
.smtp
|
||||
|
@ -449,7 +528,11 @@ impl JMAP {
|
|||
result = Report::dmarc(event, report, rua).into();
|
||||
}
|
||||
}
|
||||
QueueClass::TlsReportHeader(event) => {
|
||||
QueueClass::TlsReportHeader(event)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| domains.contains(&event.domain)) =>
|
||||
{
|
||||
let mut rua = Vec::new();
|
||||
if let Some(report) = self
|
||||
.smtp
|
||||
|
@ -473,19 +556,32 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("reports", Some(report_id), &Method::DELETE) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::OutgoingReportDelete)?;
|
||||
|
||||
if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) {
|
||||
match report_id {
|
||||
QueueClass::DmarcReportHeader(event) => {
|
||||
let result = match report_id {
|
||||
QueueClass::DmarcReportHeader(event)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| domains.contains(&event.domain)) =>
|
||||
{
|
||||
self.smtp.delete_dmarc_report(event).await;
|
||||
true
|
||||
}
|
||||
QueueClass::TlsReportHeader(event) => {
|
||||
QueueClass::TlsReportHeader(event)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| domains.contains(&event.domain)) =>
|
||||
{
|
||||
self.smtp.delete_tls_report(vec![event]).await;
|
||||
true
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": true,
|
||||
"data": result,
|
||||
}))
|
||||
.into_http_response())
|
||||
} else {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use directory::Permission;
|
||||
use hyper::Method;
|
||||
use serde_json::json;
|
||||
use utils::url_params::UrlParams;
|
||||
|
@ -19,7 +21,11 @@ impl JMAP {
|
|||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SettingsReload)?;
|
||||
|
||||
match (path.get(1).copied(), req.method()) {
|
||||
(Some("lookup"), &Method::GET) => {
|
||||
let result = self.core.reload_lookups().await?;
|
||||
|
@ -92,18 +98,27 @@ impl JMAP {
|
|||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
match (path.get(1).copied(), req.method()) {
|
||||
(Some("spam-filter"), &Method::GET) => Ok(JsonResponse::new(json!({
|
||||
"data": self
|
||||
.core
|
||||
.storage
|
||||
.config
|
||||
.update_config_resource("spam-filter")
|
||||
.await?,
|
||||
}))
|
||||
.into_http_response()),
|
||||
(Some("spam-filter"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::UpdateSpamFilter)?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self
|
||||
.core
|
||||
.storage
|
||||
.config
|
||||
.update_config_resource("spam-filter")
|
||||
.await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
(Some("webadmin"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::UpdateWebadmin)?;
|
||||
|
||||
self.inner.webadmin.update_and_unpack(&self.core).await?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use directory::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalField},
|
||||
Permission, Type,
|
||||
};
|
||||
use hyper::Method;
|
||||
use mail_auth::report::{
|
||||
tlsrpt::{FailureDetails, Policy, TlsReport},
|
||||
|
@ -15,6 +20,7 @@ use store::{
|
|||
write::{key::DeserializeBigEndian, BatchBuilder, Bincode, ReportClass, ValueClass},
|
||||
Deserialize, IterateParams, ValueKey, U64_LEN,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::url_params::UrlParams;
|
||||
|
||||
use crate::{
|
||||
|
@ -35,13 +41,46 @@ impl JMAP {
|
|||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
// Limit to tenant domains
|
||||
let mut tenant_domains = None;
|
||||
if self.core.is_enterprise_edition() {
|
||||
if let Some(tenant) = access_token.tenant {
|
||||
tenant_domains = self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.list_principals(
|
||||
None,
|
||||
tenant.id.into(),
|
||||
&[Type::Domain],
|
||||
&[PrincipalField::Name],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.map(|principals| {
|
||||
principals
|
||||
.items
|
||||
.into_iter()
|
||||
.filter_map(|mut p| p.take_str(PrincipalField::Name))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.caused_by(trc::location!())?
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied().map(decode_path_element),
|
||||
req.method(),
|
||||
) {
|
||||
(class @ ("dmarc" | "tls" | "arf"), None, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::IncomingReportList)?;
|
||||
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let filter = params.get("text");
|
||||
let page: usize = params.parse::<usize>("page").unwrap_or_default();
|
||||
|
@ -92,12 +131,13 @@ impl JMAP {
|
|||
let mut offset = page.saturating_sub(1) * limit;
|
||||
let mut total = 0;
|
||||
let mut last_id = 0;
|
||||
let has_filters = filter.is_some() || tenant_domains.is_some();
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.iterate(
|
||||
IterateParams::new(from_key, to_key)
|
||||
.set_values(filter.is_some())
|
||||
.set_values(has_filters)
|
||||
.descending(),
|
||||
|key, value| {
|
||||
// Skip chunked records
|
||||
|
@ -108,22 +148,51 @@ impl JMAP {
|
|||
last_id = id;
|
||||
|
||||
// TODO: Support filtering chunked records (over 10MB) on FDB
|
||||
let matches = filter.map_or(true, |filter| match typ {
|
||||
ReportType::Dmarc => Bincode::<
|
||||
IncomingReport<mail_auth::report::Report>,
|
||||
>::deserialize(
|
||||
value
|
||||
)
|
||||
.map_or(false, |v| v.inner.contains(filter)),
|
||||
ReportType::Tls => {
|
||||
Bincode::<IncomingReport<TlsReport>>::deserialize(value)
|
||||
.map_or(false, |v| v.inner.contains(filter))
|
||||
let matches = if has_filters {
|
||||
match typ {
|
||||
ReportType::Dmarc => {
|
||||
let report = Bincode::<
|
||||
IncomingReport<mail_auth::report::Report>,
|
||||
>::deserialize(
|
||||
value
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.inner;
|
||||
|
||||
filter.map_or(true, |f| report.contains(f))
|
||||
&& tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| report.has_domain(domains))
|
||||
}
|
||||
ReportType::Tls => {
|
||||
let report =
|
||||
Bincode::<IncomingReport<TlsReport>>::deserialize(
|
||||
value,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.inner;
|
||||
|
||||
filter.map_or(true, |f| report.contains(f))
|
||||
&& tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| report.has_domain(domains))
|
||||
}
|
||||
ReportType::Arf => {
|
||||
let report =
|
||||
Bincode::<IncomingReport<Feedback>>::deserialize(value)
|
||||
.caused_by(trc::location!())?
|
||||
.inner;
|
||||
|
||||
filter.map_or(true, |f| report.contains(f))
|
||||
&& tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| report.has_domain(domains))
|
||||
}
|
||||
}
|
||||
ReportType::Arf => {
|
||||
Bincode::<IncomingReport<Feedback>>::deserialize(value)
|
||||
.map_or(false, |v| v.inner.contains(filter))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if matches {
|
||||
if offset == 0 {
|
||||
if limit == 0 || results.len() < limit {
|
||||
|
@ -154,6 +223,9 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::IncomingReportGet)?;
|
||||
|
||||
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
|
||||
match &report_id {
|
||||
ReportClass::Tls { .. } => match self
|
||||
|
@ -165,11 +237,17 @@ impl JMAP {
|
|||
))
|
||||
.await?
|
||||
{
|
||||
Some(report) => Ok(JsonResponse::new(json!({
|
||||
"data": report.inner,
|
||||
}))
|
||||
.into_http_response()),
|
||||
None => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
Some(report)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| report.inner.has_domain(domains)) =>
|
||||
{
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": report.inner,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
},
|
||||
ReportClass::Dmarc { .. } => match self
|
||||
.core
|
||||
|
@ -180,11 +258,17 @@ impl JMAP {
|
|||
)
|
||||
.await?
|
||||
{
|
||||
Some(report) => Ok(JsonResponse::new(json!({
|
||||
"data": report.inner,
|
||||
}))
|
||||
.into_http_response()),
|
||||
None => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
Some(report)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| report.inner.has_domain(domains)) =>
|
||||
{
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": report.inner,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
},
|
||||
ReportClass::Arf { .. } => match self
|
||||
.core
|
||||
|
@ -195,11 +279,17 @@ impl JMAP {
|
|||
))
|
||||
.await?
|
||||
{
|
||||
Some(report) => Ok(JsonResponse::new(json!({
|
||||
"data": report.inner,
|
||||
}))
|
||||
.into_http_response()),
|
||||
None => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
Some(report)
|
||||
if tenant_domains
|
||||
.as_ref()
|
||||
.map_or(true, |domains| report.inner.has_domain(domains)) =>
|
||||
{
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": report.inner,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
|
@ -207,7 +297,47 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::IncomingReportDelete)?;
|
||||
|
||||
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
|
||||
if let Some(domains) = &tenant_domains {
|
||||
let is_tenant_report = match &report_id {
|
||||
ReportClass::Tls { .. } => self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_value::<Bincode<IncomingReport<TlsReport>>>(ValueKey::from(
|
||||
ValueClass::Report(report_id.clone()),
|
||||
))
|
||||
.await?
|
||||
.map_or(true, |report| report.inner.has_domain(domains)),
|
||||
ReportClass::Dmarc { .. } => self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_value::<Bincode<IncomingReport<mail_auth::report::Report>>>(
|
||||
ValueKey::from(ValueClass::Report(report_id.clone())),
|
||||
)
|
||||
.await?
|
||||
.map_or(true, |report| report.inner.has_domain(domains)),
|
||||
|
||||
ReportClass::Arf { .. } => self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_value::<Bincode<IncomingReport<Feedback>>>(ValueKey::from(
|
||||
ValueClass::Report(report_id.clone()),
|
||||
))
|
||||
.await?
|
||||
.map_or(true, |report| report.inner.has_domain(domains)),
|
||||
};
|
||||
|
||||
if !is_tenant_report {
|
||||
return Err(trc::ResourceEvent::NotFound.into_err());
|
||||
}
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.clear(ValueClass::Report(report_id));
|
||||
self.core.storage.data.write(batch.build()).await?;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use directory::Permission;
|
||||
use hyper::Method;
|
||||
use serde_json::json;
|
||||
use store::ahash::AHashMap;
|
||||
|
@ -38,9 +40,13 @@ impl JMAP {
|
|||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
match (path.get(1).copied(), req.method()) {
|
||||
(Some("group"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SettingsList)?;
|
||||
|
||||
// List settings
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let prefix = params
|
||||
|
@ -168,6 +174,9 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
(Some("list"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SettingsList)?;
|
||||
|
||||
// List settings
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let prefix = params
|
||||
|
@ -200,6 +209,9 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
(Some("keys"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SettingsList)?;
|
||||
|
||||
// Obtain keys
|
||||
let params = UrlParams::new(req.uri().query());
|
||||
let keys = params
|
||||
|
@ -232,6 +244,9 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
(Some(prefix), &Method::DELETE) if !prefix.is_empty() => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SettingsDelete)?;
|
||||
|
||||
let prefix = decode_path_element(prefix);
|
||||
|
||||
self.core.storage.config.clear(prefix.as_ref()).await?;
|
||||
|
@ -242,6 +257,9 @@ impl JMAP {
|
|||
.into_http_response())
|
||||
}
|
||||
(None, &Method::POST) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SettingsUpdate)?;
|
||||
|
||||
let changes = serde_json::from_slice::<Vec<UpdateSettings>>(
|
||||
body.as_deref().unwrap_or_default(),
|
||||
)
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use common::{scripts::ScriptModification, IntoString};
|
||||
use common::{auth::AccessToken, scripts::ScriptModification, IntoString};
|
||||
use directory::Permission;
|
||||
use hyper::Method;
|
||||
use serde_json::json;
|
||||
use sieve::{runtime::Variable, Envelope};
|
||||
|
@ -41,7 +42,11 @@ impl JMAP {
|
|||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::SieveRun)?;
|
||||
|
||||
let (script, script_id) = match (
|
||||
path.get(1).and_then(|name| {
|
||||
self.core
|
||||
|
|
|
@ -5,8 +5,11 @@
|
|||
*/
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use common::manager::webadmin::Resource;
|
||||
use directory::backend::internal::manage::{self, ManageDirectory};
|
||||
use common::{auth::AccessToken, manager::webadmin::Resource};
|
||||
use directory::{
|
||||
backend::internal::manage::{self, ManageDirectory},
|
||||
Permission,
|
||||
};
|
||||
use hyper::Method;
|
||||
use serde_json::json;
|
||||
use utils::url_params::UrlParams;
|
||||
|
@ -29,6 +32,7 @@ impl JMAP {
|
|||
path: Vec<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
session: &HttpSessionData,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
match (
|
||||
path.get(1).copied(),
|
||||
|
@ -37,6 +41,9 @@ impl JMAP {
|
|||
req.method(),
|
||||
) {
|
||||
(Some("blobs"), Some(blob_hash), _, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::BlobFetch)?;
|
||||
|
||||
let blob_hash = URL_SAFE_NO_PAD
|
||||
.decode(decode_path_element(blob_hash).as_bytes())
|
||||
.map_err(|err| {
|
||||
|
@ -62,13 +69,12 @@ impl JMAP {
|
|||
.to_vec()
|
||||
};
|
||||
|
||||
Ok(Resource {
|
||||
content_type: "application/octet-stream",
|
||||
contents,
|
||||
}
|
||||
.into_http_response())
|
||||
Ok(Resource::new("application/octet-stream", contents).into_http_response())
|
||||
}
|
||||
(Some("purge"), Some("blob"), _, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::PurgeBlobStore)?;
|
||||
|
||||
self.housekeeper_request(Event::Purge(PurgeType::Blobs {
|
||||
store: self.core.storage.data.clone(),
|
||||
blob_store: self.core.storage.blob.clone(),
|
||||
|
@ -76,6 +82,9 @@ impl JMAP {
|
|||
.await
|
||||
}
|
||||
(Some("purge"), Some("data"), id, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::PurgeDataStore)?;
|
||||
|
||||
let store = if let Some(id) = id {
|
||||
if let Some(store) = self.core.storage.stores.get(id) {
|
||||
store.clone()
|
||||
|
@ -90,6 +99,9 @@ impl JMAP {
|
|||
.await
|
||||
}
|
||||
(Some("purge"), Some("lookup"), id, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::PurgeLookupStore)?;
|
||||
|
||||
let store = if let Some(id) = id {
|
||||
if let Some(store) = self.core.storage.lookups.get(id) {
|
||||
store.clone()
|
||||
|
@ -104,11 +116,14 @@ impl JMAP {
|
|||
.await
|
||||
}
|
||||
(Some("purge"), Some("account"), id, &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::PurgeAccount)?;
|
||||
|
||||
let account_id = if let Some(id) = id {
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.get_account_id(decode_path_element(id).as_ref())
|
||||
.get_principal_id(decode_path_element(id).as_ref())
|
||||
.await?
|
||||
.ok_or_else(|| trc::ManageEvent::NotFound.into_err())?
|
||||
.into()
|
||||
|
@ -133,6 +148,9 @@ impl JMAP {
|
|||
// violators to the fullest extent of the law, including but not limited to claims
|
||||
// for copyright infringement, breach of contract, and fraud.
|
||||
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::Undelete)?;
|
||||
|
||||
if self.core.is_enterprise_edition() {
|
||||
self.handle_undelete_api_request(req, path, body, session)
|
||||
.await
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
method::{
|
||||
get, query,
|
||||
|
@ -17,7 +18,7 @@ use jmap_proto::{
|
|||
};
|
||||
use trc::JmapEvent;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
use super::http::HttpSessionData;
|
||||
|
||||
|
@ -135,6 +136,11 @@ impl JMAP {
|
|||
session: &HttpSessionData,
|
||||
) -> trc::Result<ResponseMethod> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Check permissions
|
||||
access_token.assert_has_jmap_permission(&method)?;
|
||||
|
||||
// Handle method
|
||||
let response = match method {
|
||||
RequestMethod::Get(mut req) => match req.take_arguments() {
|
||||
get::RequestArguments::Email(arguments) => {
|
||||
|
@ -177,15 +183,7 @@ impl JMAP {
|
|||
|
||||
self.vacation_response_get(req).await?.into()
|
||||
}
|
||||
get::RequestArguments::Principal => {
|
||||
if self.core.jmap.principal_allow_lookups || access_token.is_super_user() {
|
||||
self.principal_get(req).await?.into()
|
||||
} else {
|
||||
return Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("Principal lookups are disabled".to_string()));
|
||||
}
|
||||
}
|
||||
get::RequestArguments::Principal => self.principal_get(req).await?.into(),
|
||||
get::RequestArguments::Quota => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
|
@ -225,13 +223,7 @@ impl JMAP {
|
|||
self.sieve_script_query(req).await?.into()
|
||||
}
|
||||
query::RequestArguments::Principal => {
|
||||
if self.core.jmap.principal_allow_lookups || access_token.is_super_user() {
|
||||
self.principal_query(req, session).await?.into()
|
||||
} else {
|
||||
return Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details("Principal lookups are disabled".to_string()));
|
||||
}
|
||||
self.principal_query(req, session).await?.into()
|
||||
}
|
||||
query::RequestArguments::Quota => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
@ -281,7 +273,7 @@ impl JMAP {
|
|||
set::RequestArguments::VacationResponse => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.vacation_response_set(req).await?.into()
|
||||
self.vacation_response_set(req, access_token).await?.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Changes(req) => self.changes(req, access_token).await?.into(),
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use directory::QueryBy;
|
||||
use common::auth::AccessToken;
|
||||
use directory::{backend::internal::PrincipalField, QueryBy};
|
||||
use jmap_proto::{
|
||||
request::capability::{Capability, Session},
|
||||
types::{acl::Acl, collection::Collection, id::Id},
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_session_resource(
|
||||
|
@ -52,7 +53,7 @@ impl JMAP {
|
|||
.query(QueryBy::Id(*id), false)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.map(|p| p.name)
|
||||
.and_then(|mut p| p.take_str(PrincipalField::Name))
|
||||
.unwrap_or_else(|| Id::from(*id).to_string()),
|
||||
is_personal,
|
||||
is_readonly,
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use directory::QueryBy;
|
||||
use common::auth::AccessToken;
|
||||
use directory::{backend::internal::PrincipalField, QueryBy};
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
object::Object,
|
||||
|
@ -22,70 +23,11 @@ use store::{
|
|||
ValueKey,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::map::bitmap::{Bitmap, BitmapItem};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
use super::AccessToken;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn update_access_token(
|
||||
&self,
|
||||
mut access_token: AccessToken,
|
||||
) -> trc::Result<AccessToken> {
|
||||
for &grant_account_id in [access_token.primary_id]
|
||||
.iter()
|
||||
.chain(access_token.member_of.clone().iter())
|
||||
{
|
||||
for acl_item in self
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.acl_query(AclQuery::HasAccess { grant_account_id })
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
if !access_token.is_member(acl_item.to_account_id) {
|
||||
let acl = Bitmap::<Acl>::from(acl_item.permissions);
|
||||
let collection = Collection::from(acl_item.to_collection);
|
||||
if !collection.is_valid() {
|
||||
return Err(trc::StoreEvent::DataCorruption
|
||||
.ctx(trc::Key::Reason, "Corrupted collection found in ACL key.")
|
||||
.details(format!("{acl_item:?}"))
|
||||
.account_id(grant_account_id)
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
|
||||
let mut collections: Bitmap<Collection> = Bitmap::new();
|
||||
if acl.contains(Acl::Read) || acl.contains(Acl::Administer) {
|
||||
collections.insert(collection);
|
||||
}
|
||||
if collection == Collection::Mailbox
|
||||
&& (acl.contains(Acl::ReadItems) || acl.contains(Acl::Administer))
|
||||
{
|
||||
collections.insert(Collection::Email);
|
||||
}
|
||||
|
||||
if !collections.is_empty() {
|
||||
if let Some((_, sharing)) = access_token
|
||||
.access_to
|
||||
.iter_mut()
|
||||
.find(|(account_id, _)| *account_id == acl_item.to_account_id)
|
||||
{
|
||||
sharing.union(&collections);
|
||||
} else {
|
||||
access_token
|
||||
.access_to
|
||||
.push((acl_item.to_account_id, collections));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(access_token)
|
||||
}
|
||||
|
||||
pub async fn shared_documents(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
|
@ -322,7 +264,7 @@ impl JMAP {
|
|||
{
|
||||
let mut acl_obj = Object::with_capacity(value.len() / 2);
|
||||
for item in value {
|
||||
if let Some(principal) = self
|
||||
if let Some(mut principal) = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
|
@ -331,7 +273,7 @@ impl JMAP {
|
|||
.unwrap_or_default()
|
||||
{
|
||||
acl_obj.append(
|
||||
Property::_T(principal.name),
|
||||
Property::_T(principal.take_str(PrincipalField::Name).unwrap_or_default()),
|
||||
item.grants
|
||||
.map(|acl_item| Value::Text(acl_item.to_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
|
@ -351,7 +293,7 @@ impl JMAP {
|
|||
current: &Option<HashedValue<Object<Value>>>,
|
||||
) {
|
||||
if let Value::Acl(acl_changes) = changes.get(&Property::Acl) {
|
||||
let access_tokens = &self.inner.access_tokens;
|
||||
let access_tokens = &self.core.security.access_tokens;
|
||||
if let Some(Value::Acl(acl_current)) = current
|
||||
.as_ref()
|
||||
.and_then(|current| current.inner.properties.get(&Property::Acl))
|
||||
|
@ -402,7 +344,7 @@ impl JMAP {
|
|||
{
|
||||
Ok(Some(principal)) => {
|
||||
acls.push(AclGrant {
|
||||
account_id: principal.id,
|
||||
account_id: principal.id(),
|
||||
grants: Bitmap::from(*grants),
|
||||
});
|
||||
}
|
||||
|
@ -443,7 +385,7 @@ impl JMAP {
|
|||
{
|
||||
Ok(Some(principal)) => Ok((
|
||||
AclGrant {
|
||||
account_id: principal.id,
|
||||
account_id: principal.id(),
|
||||
grants: Bitmap::from(*grants),
|
||||
},
|
||||
acl_patch.get(2).map(|v| v.as_bool().unwrap_or(false)),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
use std::{net::IpAddr, sync::Arc, time::Instant};
|
||||
|
||||
use common::listener::limiter::InFlight;
|
||||
use directory::{Principal, QueryBy};
|
||||
use directory::Permission;
|
||||
use hyper::header;
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use mail_send::Credentials;
|
||||
|
@ -18,7 +18,7 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::AccessToken;
|
||||
use common::auth::AccessToken;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn authenticate_headers(
|
||||
|
@ -28,7 +28,7 @@ impl JMAP {
|
|||
) -> trc::Result<(InFlight, Arc<AccessToken>)> {
|
||||
if let Some((mechanism, token)) = req.authorization() {
|
||||
let access_token = if let Some(account_id) = self.inner.sessions.get_with_ttl(token) {
|
||||
self.get_cached_access_token(account_id).await?
|
||||
self.core.get_cached_access_token(account_id).await?
|
||||
} else {
|
||||
let access_token = if mechanism.eq_ignore_ascii_case("basic") {
|
||||
// Enforce rate limit for authentication requests
|
||||
|
@ -64,7 +64,7 @@ impl JMAP {
|
|||
let (account_id, _, _) =
|
||||
self.validate_access_token("access_token", token).await?;
|
||||
|
||||
self.get_access_token(account_id).await?
|
||||
self.core.get_access_token(account_id).await?
|
||||
} else {
|
||||
// Enforce anonymous rate limit
|
||||
self.is_anonymous_allowed(&session.remote_ip).await?;
|
||||
|
@ -78,7 +78,7 @@ impl JMAP {
|
|||
// Cache session
|
||||
let access_token = Arc::new(access_token);
|
||||
self.cache_session(token.to_string(), &access_token);
|
||||
self.cache_access_token(access_token.clone());
|
||||
self.core.cache_access_token(access_token.clone());
|
||||
access_token
|
||||
};
|
||||
|
||||
|
@ -105,27 +105,6 @@ impl JMAP {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn cache_access_token(&self, access_token: Arc<AccessToken>) {
|
||||
self.inner.access_tokens.insert_with_ttl(
|
||||
access_token.primary_id(),
|
||||
access_token,
|
||||
Instant::now() + self.core.jmap.session_cache_ttl,
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> {
|
||||
if let Some(access_token) = self.inner.access_tokens.get_with_ttl(&primary_id) {
|
||||
Ok(access_token)
|
||||
} else {
|
||||
// Refresh ACL token
|
||||
self.get_access_token(primary_id).await.map(|access_token| {
|
||||
let access_token = Arc::new(access_token);
|
||||
self.cache_access_token(access_token.clone());
|
||||
access_token
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate_plain(
|
||||
&self,
|
||||
username: &str,
|
||||
|
@ -147,7 +126,15 @@ impl JMAP {
|
|||
)
|
||||
.await
|
||||
{
|
||||
Ok(principal) => Ok(AccessToken::new(principal)),
|
||||
Ok(principal) => self
|
||||
.core
|
||||
.build_access_token(principal)
|
||||
.await
|
||||
.and_then(|token| {
|
||||
token
|
||||
.assert_has_permission(Permission::Authenticate)
|
||||
.map(|_| token)
|
||||
}),
|
||||
Err(err) => {
|
||||
if !err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) {
|
||||
let _ = self.is_auth_allowed_hard(&remote_ip).await;
|
||||
|
@ -156,33 +143,6 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> {
|
||||
let err = match self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(account_id), true)
|
||||
.await
|
||||
{
|
||||
Ok(Some(principal)) => {
|
||||
return self.update_access_token(AccessToken::new(principal)).await
|
||||
}
|
||||
Ok(None) => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Account not found.")
|
||||
.caused_by(trc::location!())),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
match &self.core.jmap.fallback_admin {
|
||||
Some((_, secret)) if account_id == u32::MAX => {
|
||||
self.update_access_token(AccessToken::new(Principal::fallback_admin(secret)))
|
||||
.await
|
||||
}
|
||||
_ => err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HttpHeaders {
|
||||
|
|
|
@ -4,135 +4,18 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use aes_gcm_siv::{
|
||||
aead::{generic_array::GenericArray, Aead},
|
||||
AeadInPlace, Aes256GcmSiv, KeyInit, Nonce,
|
||||
};
|
||||
|
||||
use directory::{Principal, Type};
|
||||
use jmap_proto::types::{collection::Collection, id::Id};
|
||||
use store::blake3;
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
pub mod acl;
|
||||
pub mod authenticate;
|
||||
pub mod oauth;
|
||||
pub mod rate_limit;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AccessToken {
|
||||
pub primary_id: u32,
|
||||
pub member_of: Vec<u32>,
|
||||
pub access_to: Vec<(u32, Bitmap<Collection>)>,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quota: u64,
|
||||
pub is_superuser: bool,
|
||||
}
|
||||
|
||||
impl AccessToken {
|
||||
pub fn new(principal: Principal<u32>) -> Self {
|
||||
Self {
|
||||
primary_id: principal.id,
|
||||
member_of: principal.member_of,
|
||||
access_to: Vec::new(),
|
||||
name: principal.name,
|
||||
description: principal.description,
|
||||
quota: principal.quota,
|
||||
is_superuser: principal.typ == Type::Superuser,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_access_to(self, access_to: Vec<(u32, Bitmap<Collection>)>) -> Self {
|
||||
Self { access_to, ..self }
|
||||
}
|
||||
|
||||
pub fn state(&self) -> u32 {
|
||||
// Hash state
|
||||
let mut s = DefaultHasher::new();
|
||||
self.member_of.hash(&mut s);
|
||||
self.access_to.hash(&mut s);
|
||||
s.finish() as u32
|
||||
}
|
||||
|
||||
pub fn primary_id(&self) -> u32 {
|
||||
self.primary_id
|
||||
}
|
||||
|
||||
pub fn secondary_ids(&self) -> impl Iterator<Item = &u32> {
|
||||
self.member_of
|
||||
.iter()
|
||||
.chain(self.access_to.iter().map(|(id, _)| id))
|
||||
}
|
||||
|
||||
pub fn is_member(&self, account_id: u32) -> bool {
|
||||
self.primary_id == account_id || self.member_of.contains(&account_id) || self.is_superuser
|
||||
}
|
||||
|
||||
pub fn is_primary_id(&self, account_id: u32) -> bool {
|
||||
self.primary_id == account_id
|
||||
}
|
||||
|
||||
pub fn is_super_user(&self) -> bool {
|
||||
self.is_superuser
|
||||
}
|
||||
|
||||
pub fn is_shared(&self, account_id: u32) -> bool {
|
||||
!self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id)
|
||||
}
|
||||
|
||||
pub fn shared_accounts(&self, collection: impl Into<Collection>) -> impl Iterator<Item = &u32> {
|
||||
let collection = collection.into();
|
||||
self.member_of
|
||||
.iter()
|
||||
.chain(self.access_to.iter().filter_map(move |(id, cols)| {
|
||||
if cols.contains(collection) {
|
||||
id.into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn has_access(&self, to_account_id: u32, to_collection: impl Into<Collection>) -> bool {
|
||||
let to_collection = to_collection.into();
|
||||
self.is_member(to_account_id)
|
||||
|| self.access_to.iter().any(|(id, collections)| {
|
||||
*id == to_account_id && collections.contains(to_collection)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assert_has_access(
|
||||
&self,
|
||||
to_account_id: Id,
|
||||
to_collection: Collection,
|
||||
) -> trc::Result<&Self> {
|
||||
if self.has_access(to_account_id.document_id(), to_collection) {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden.into_err().details(format!(
|
||||
"You do not have access to account {}",
|
||||
to_account_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> {
|
||||
if self.is_member(account_id.document_id()) {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(trc::JmapEvent::Forbidden
|
||||
.into_err()
|
||||
.details(format!("You are not an owner of account {}", account_id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SymmetricEncrypt {
|
||||
aes: Aes256GcmSiv,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use rand::distributions::Standard;
|
||||
use serde_json::json;
|
||||
use store::{
|
||||
|
@ -16,7 +17,7 @@ use store::{
|
|||
|
||||
use crate::{
|
||||
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
|
||||
auth::{oauth::OAuthStatus, AccessToken},
|
||||
auth::oauth::OAuthStatus,
|
||||
JMAP,
|
||||
};
|
||||
|
||||
|
@ -91,8 +92,8 @@ impl JMAP {
|
|||
json!({
|
||||
"data": {
|
||||
"code": client_code,
|
||||
"is_admin": access_token.is_super_user(),
|
||||
"is_enterprise": is_enterprise,
|
||||
"permissions": access_token.permissions(),
|
||||
"isEnterprise": is_enterprise,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use directory::QueryBy;
|
||||
use directory::{backend::internal::PrincipalField, QueryBy};
|
||||
use hyper::StatusCode;
|
||||
use mail_builder::encoders::base64::base64_encode;
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
|
@ -187,7 +187,8 @@ impl JMAP {
|
|||
.await
|
||||
.map_err(|_| "Temporary lookup error")?
|
||||
.ok_or("Account no longer exists")?
|
||||
.secrets
|
||||
.take_str_array(PrincipalField::Secrets)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or("Failed to obtain password hash")
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
use common::listener::limiter::{ConcurrencyLimiter, InFlight};
|
||||
use directory::Permission;
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
use super::AccessToken;
|
||||
use common::auth::AccessToken;
|
||||
|
||||
pub struct ConcurrencyLimiters {
|
||||
pub concurrent_requests: ConcurrencyLimiter,
|
||||
|
@ -61,12 +62,12 @@ impl JMAP {
|
|||
if is_rate_allowed {
|
||||
if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() {
|
||||
Ok(in_flight_request)
|
||||
} else if access_token.is_super_user() {
|
||||
} else if access_token.has_permission(Permission::UnlimitedRequests) {
|
||||
Ok(InFlight::default())
|
||||
} else {
|
||||
Err(trc::LimitEvent::ConcurrentRequest.into_err())
|
||||
}
|
||||
} else if access_token.is_super_user() {
|
||||
} else if access_token.has_permission(Permission::UnlimitedRequests) {
|
||||
Ok(InFlight::default())
|
||||
} else {
|
||||
Err(trc::LimitEvent::TooManyRequests.into_err())
|
||||
|
@ -97,7 +98,7 @@ impl JMAP {
|
|||
.is_allowed()
|
||||
{
|
||||
Ok(in_flight_request)
|
||||
} else if access_token.is_super_user() {
|
||||
} else if access_token.has_permission(Permission::UnlimitedRequests) {
|
||||
Ok(InFlight::default())
|
||||
} else {
|
||||
Err(trc::LimitEvent::ConcurrentUpload.into_err())
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::copy::{CopyBlobRequest, CopyBlobResponse},
|
||||
|
@ -16,7 +17,7 @@ use store::{
|
|||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn blob_copy(
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::ops::Range;
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::types::{
|
||||
acl::Acl,
|
||||
blob::{BlobId, BlobSection},
|
||||
|
@ -19,7 +20,7 @@ use store::BlobClass;
|
|||
use trc::AddContext;
|
||||
use utils::BlobHash;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
impl JMAP {
|
||||
#[allow(clippy::blocks_in_conditions)]
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
method::{
|
||||
get::{GetRequest, GetResponse},
|
||||
|
@ -25,7 +26,7 @@ use sha2::{Sha256, Sha512};
|
|||
use store::BlobClass;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{auth::AccessToken, mailbox::UidMailbox, JMAP};
|
||||
use crate::{mailbox::UidMailbox, JMAP};
|
||||
|
||||
impl JMAP {
|
||||
pub async fn blob_get(
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use directory::Permission;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::upload::{
|
||||
|
@ -21,7 +23,7 @@ use store::{
|
|||
use trc::AddContext;
|
||||
use utils::BlobHash;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
use super::UploadResponse;
|
||||
|
||||
|
@ -149,7 +151,7 @@ impl JMAP {
|
|||
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
|
||||
|| (self.core.jmap.upload_tmp_quota_amount > 0
|
||||
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
|
||||
&& !access_token.is_super_user()
|
||||
&& !access_token.has_permission(Permission::UnlimitedUploads)
|
||||
{
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
|
@ -209,7 +211,7 @@ impl JMAP {
|
|||
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
|
||||
|| (self.core.jmap.upload_tmp_quota_amount > 0
|
||||
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
|
||||
&& !access_token.is_super_user()
|
||||
&& !access_token.has_permission(Permission::UnlimitedUploads)
|
||||
{
|
||||
let err = Err(trc::LimitEvent::BlobQuota
|
||||
.into_err()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
method::changes::{ChangesRequest, ChangesResponse, RequestArguments},
|
||||
types::{collection::Collection, property::Property, state::State},
|
||||
|
@ -11,7 +12,7 @@ use jmap_proto::{
|
|||
use store::query::log::{Change, Changes, Query};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn changes(
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::method::{
|
||||
changes::{self, ChangesRequest},
|
||||
query::{self, QueryRequest},
|
||||
query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse},
|
||||
};
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
use crate::JMAP;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn query_changes(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::{AccessToken, ResourceToken};
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::{
|
||||
|
@ -41,7 +42,7 @@ use store::{
|
|||
use trc::AddContext;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{api::http::HttpSessionData, auth::AccessToken, mailbox::UidMailbox, JMAP};
|
||||
use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP};
|
||||
|
||||
use super::{
|
||||
index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH},
|
||||
|
@ -93,7 +94,7 @@ impl JMAP {
|
|||
let mut destroy_ids = Vec::new();
|
||||
|
||||
// Obtain quota
|
||||
let account_quota = self.get_quota(access_token, account_id).await?;
|
||||
let resource_token = self.get_resource_token(access_token, account_id).await?;
|
||||
|
||||
'create: for (id, create) in request.create {
|
||||
let id = id.unwrap();
|
||||
|
@ -215,8 +216,7 @@ impl JMAP {
|
|||
.copy_message(
|
||||
from_account_id,
|
||||
from_message_id,
|
||||
account_id,
|
||||
account_quota,
|
||||
&resource_token,
|
||||
mailboxes,
|
||||
keywords,
|
||||
received_at,
|
||||
|
@ -275,14 +275,14 @@ impl JMAP {
|
|||
&self,
|
||||
from_account_id: u32,
|
||||
from_message_id: u32,
|
||||
account_id: u32,
|
||||
account_quota: i64,
|
||||
resource_token: &ResourceToken,
|
||||
mailboxes: Vec<u32>,
|
||||
keywords: Vec<Keyword>,
|
||||
received_at: Option<UTCDate>,
|
||||
session_id: u64,
|
||||
) -> trc::Result<Result<IngestedEmail, SetError>> {
|
||||
// Obtain metadata
|
||||
let account_id = resource_token.account_id;
|
||||
let mut metadata = if let Some(metadata) = self
|
||||
.get_property::<Bincode<MessageMetadata>>(
|
||||
from_account_id,
|
||||
|
@ -302,12 +302,14 @@ impl JMAP {
|
|||
|
||||
// Check quota
|
||||
match self
|
||||
.has_available_quota(account_id, account_quota, metadata.size as i64)
|
||||
.has_available_quota(resource_token, metadata.size as u64)
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) {
|
||||
if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota))
|
||||
|| err.matches(trc::EventType::Limit(trc::LimitEvent::TenantQuota))
|
||||
{
|
||||
trc::error!(err.account_id(account_id).span_id(session_id));
|
||||
return Ok(Err(SetError::over_quota()));
|
||||
} else {
|
||||
|
@ -411,8 +413,12 @@ impl JMAP {
|
|||
hash: metadata.blob_hash.clone(),
|
||||
}),
|
||||
0u64.serialize(),
|
||||
)
|
||||
.custom(EmailIndexBuilder::set(metadata));
|
||||
);
|
||||
EmailIndexBuilder::set(metadata).build(
|
||||
&mut batch,
|
||||
account_id,
|
||||
resource_token.tenant.map(|t| t.id),
|
||||
);
|
||||
|
||||
// Insert and obtain ids
|
||||
let ids = self
|
||||
|
|
|
@ -8,10 +8,10 @@ use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor, sync::Ar
|
|||
|
||||
use crate::{
|
||||
api::{http::ToHttpResponse, HttpResponse, JsonResponse},
|
||||
auth::AccessToken,
|
||||
JMAP,
|
||||
};
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
|
||||
use common::auth::AccessToken;
|
||||
use directory::backend::internal::manage;
|
||||
use jmap_proto::types::{collection::Collection, property::Property};
|
||||
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
|
||||
|
|
|
@ -399,6 +399,15 @@ impl JMAP {
|
|||
.remove(account_id, Collection::Email.into(), &tombstoned_ids)
|
||||
.await?;
|
||||
|
||||
// Obtain tenant id
|
||||
let tenant_id = self
|
||||
.core
|
||||
.get_cached_access_token(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.tenant
|
||||
.map(|t| t.id);
|
||||
|
||||
// Delete messages
|
||||
for document_id in tombstoned_ids {
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
@ -466,7 +475,7 @@ impl JMAP {
|
|||
// SPDX-SnippetEnd
|
||||
|
||||
// Delete message
|
||||
batch.custom(EmailIndexBuilder::clear(metadata.inner));
|
||||
EmailIndexBuilder::clear(metadata.inner).build(&mut batch, account_id, tenant_id);
|
||||
|
||||
// Commit batch
|
||||
self.core.storage.data.write(batch.build()).await?;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::{email::GetArguments, Object},
|
||||
|
@ -22,7 +23,7 @@ use mail_parser::HeaderName;
|
|||
use store::{write::Bincode, BlobClass};
|
||||
use trc::{AddContext, StoreEvent};
|
||||
|
||||
use crate::{auth::AccessToken, email::headers::HeaderToValue, mailbox::UidMailbox, JMAP};
|
||||
use crate::{email::headers::HeaderToValue, mailbox::UidMailbox, JMAP};
|
||||
|
||||
use super::{
|
||||
body::{ToBodyPart, TruncateBody},
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::auth::AccessToken;
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::import::{ImportEmailRequest, ImportEmailResponse},
|
||||
|
@ -19,7 +20,7 @@ use jmap_proto::{
|
|||
use mail_parser::MessageParser;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{api::http::HttpSessionData, auth::AccessToken, JMAP};
|
||||
use crate::{api::http::HttpSessionData, JMAP};
|
||||
|
||||
use super::ingest::{IngestEmail, IngestSource};
|
||||
|
||||
|
@ -46,7 +47,7 @@ impl JMAP {
|
|||
};
|
||||
|
||||
// Obtain quota
|
||||
let account_quota = self.get_quota(access_token, account_id).await?;
|
||||
let resource_token = self.get_resource_token(access_token, account_id).await?;
|
||||
|
||||
let mut response = ImportEmailResponse {
|
||||
account_id: request.account_id,
|
||||
|
@ -116,8 +117,7 @@ impl JMAP {
|
|||
.email_ingest(IngestEmail {
|
||||
raw_message: &raw_message,
|
||||
message: MessageParser::new().parse(&raw_message),
|
||||
account_id,
|
||||
account_quota,
|
||||
resource: resource_token.clone(),
|
||||
mailbox_ids,
|
||||
keywords: email.keywords,
|
||||
received_at: email.received_at.map(|r| r.into()),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue