Merge branch 'permission-impossible'

This commit is contained in:
mdecimus 2024-09-19 19:43:17 +02:00
commit ab7f17d5d9
236 changed files with 8261 additions and 2950 deletions

335
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -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")

View file

@ -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;

View file

@ -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"

View 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,
}
}
}

View 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>,
}

View 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(),
})
}

View file

@ -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),

View file

@ -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,

View file

@ -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(),
}
}
}

View file

@ -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),

View file

@ -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()))
}
}

View file

@ -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(&regex_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;

View file

@ -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()),
}
}
}

View file

@ -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> {

View file

@ -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 {

View file

@ -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(

View file

@ -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,
};

View file

@ -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,
}),
}

View file

@ -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,

View file

@ -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",

View file

@ -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()
}))
}

View file

@ -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" }

View file

@ -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

View file

@ -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

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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;
}

View file

@ -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>,

View file

@ -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)

View file

@ -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))
}
}

View file

@ -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,

View file

@ -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",
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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,
})
{

View file

@ -1,6 +1,6 @@
[package]
name = "imap"
version = "0.9.4"
version = "0.10.0"
edition = "2021"
resolver = "2"

View file

@ -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()

View file

@ -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,
}
}
}

View file

@ -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> {

View file

@ -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
}

View file

@ -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()))?,
),

View file

@ -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),

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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()?;

View file

@ -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();

View file

@ -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()?;

View file

@ -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, .. } => {

View file

@ -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()
}?;

View file

@ -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,

View file

@ -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();

View file

@ -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()
}?;

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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();

View file

@ -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()?;

View file

@ -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,

View file

@ -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"

View file

@ -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;
}
}

View file

@ -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};

View file

@ -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(

View file

@ -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()),
}
}

View file

@ -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) => {

View file

@ -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")

View file

@ -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(),

View file

@ -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

View file

@ -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())

View file

@ -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(),
}
}
}

View file

@ -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 {

View file

@ -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!({

View file

@ -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?;

View file

@ -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(),
)

View file

@ -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

View file

@ -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

View file

@ -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(),

View file

@ -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,

View file

@ -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)),

View file

@ -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 {

View file

@ -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,
}

View file

@ -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,
},
})
}

View file

@ -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")

View file

@ -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())

View file

@ -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(

View file

@ -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)]

View file

@ -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(

View file

@ -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()

View file

@ -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(

View file

@ -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(

View file

@ -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

View file

@ -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};

View file

@ -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?;

View file

@ -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},

View file

@ -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