From 6dd5613f46f695a1ff0192ef777743a4dedf6094 Mon Sep 17 00:00:00 2001 From: privacyguard Date: Sun, 30 Jun 2024 23:28:51 +0300 Subject: [PATCH 1/4] Added OAUTH2 OIDC support --- Cargo.lock | 774 +++++++----------- Cargo.toml | 13 +- config/defaults.hjson | 3 + crates/api/src/local_user/change_password.rs | 12 +- crates/api/src/local_user/login.rs | 46 +- crates/api/src/local_user/mod.rs | 15 - crates/api/src/local_user/reset_password.rs | 3 +- crates/api/src/site/leave_admin.rs | 3 + crates/api_common/src/claims.rs | 2 +- crates/api_common/src/lib.rs | 1 + crates/api_common/src/oauth_provider.rs | 83 ++ crates/api_common/src/request.rs | 1 + crates/api_common/src/site.rs | 6 + crates/api_common/src/utils.rs | 44 +- crates/api_crud/Cargo.toml | 3 + crates/api_crud/src/lib.rs | 1 + crates/api_crud/src/oauth_provider/create.rs | 59 ++ crates/api_crud/src/oauth_provider/delete.rs | 23 + crates/api_crud/src/oauth_provider/mod.rs | 3 + crates/api_crud/src/oauth_provider/update.rs | 47 ++ crates/api_crud/src/site/create.rs | 13 + crates/api_crud/src/site/read.rs | 36 +- crates/api_crud/src/site/update.rs | 13 + crates/api_crud/src/user/create.rs | 365 ++++++++- crates/api_crud/src/user/delete.rs | 11 +- crates/apub/src/api/user_settings_backup.rs | 2 +- crates/db_schema/src/impls/actor_language.rs | 4 +- crates/db_schema/src/impls/local_user.rs | 12 +- crates/db_schema/src/impls/mod.rs | 2 + crates/db_schema/src/impls/oauth_account.rs | 52 ++ crates/db_schema/src/impls/oauth_provider.rs | 106 +++ .../src/impls/password_reset_request.rs | 2 +- crates/db_schema/src/newtypes.rs | 29 + crates/db_schema/src/schema.rs | 39 +- crates/db_schema/src/source/local_site.rs | 4 + crates/db_schema/src/source/local_user.rs | 4 +- crates/db_schema/src/source/mod.rs | 2 + crates/db_schema/src/source/oauth_account.rs | 42 + crates/db_schema/src/source/oauth_provider.rs | 160 ++++ crates/db_views/src/comment_report_view.rs | 2 +- crates/db_views/src/comment_view.rs | 2 +- crates/db_views/src/local_user_view.rs | 34 +- crates/db_views/src/post_report_view.rs | 2 +- .../src/registration_application_view.rs | 6 +- crates/db_views_actor/src/community_view.rs | 2 +- crates/db_views_actor/src/person_view.rs | 4 +- crates/routes/src/images.rs | 3 +- crates/utils/src/error.rs | 3 + crates/utils/src/settings/mod.rs | 7 + crates/utils/src/settings/structs.rs | 5 + crates/utils/translations | 2 +- .../down.sql | 3 + .../up.sql | 3 + .../down.sql | 5 + .../up.sql | 24 + .../down.sql | 2 + .../up.sql | 11 + src/api_routes_http.rs | 22 +- src/code_migrations.rs | 2 +- src/session_middleware.rs | 5 +- 60 files changed, 1578 insertions(+), 611 deletions(-) create mode 100644 crates/api_common/src/oauth_provider.rs create mode 100644 crates/api_crud/src/oauth_provider/create.rs create mode 100644 crates/api_crud/src/oauth_provider/delete.rs create mode 100644 crates/api_crud/src/oauth_provider/mod.rs create mode 100644 crates/api_crud/src/oauth_provider/update.rs create mode 100644 crates/db_schema/src/impls/oauth_account.rs create mode 100644 crates/db_schema/src/impls/oauth_provider.rs create mode 100644 crates/db_schema/src/source/oauth_account.rs create mode 100644 crates/db_schema/src/source/oauth_provider.rs create mode 100644 migrations/2024-07-01-163929_user_password_encrypted_optional/down.sql create mode 100644 migrations/2024-07-01-163929_user_password_encrypted_optional/up.sql create mode 100644 migrations/2024-07-01-174833_create_oauth_provider/down.sql create mode 100644 migrations/2024-07-01-174833_create_oauth_provider/up.sql create mode 100644 migrations/2024-07-01-214047_create_oauth_accounts/down.sql create mode 100644 migrations/2024-07-01-214047_create_oauth_accounts/up.sql diff --git a/Cargo.lock b/Cargo.lock index e02388f0c..812667c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb9843d84c775696c37d9a418bbb01b932629d01870722c0f13eb3f95e2536d" +checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" dependencies = [ "actix-codec", "actix-rt", @@ -123,7 +123,7 @@ dependencies = [ "actix-utils", "ahash", "base64 0.22.1", - "bitflags 2.5.0", + "bitflags 2.6.0", "brotli", "bytes", "bytestring", @@ -157,14 +157,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "actix-multipart" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d" +checksum = "d974dd6c4f78d102d057c672dcf6faa618fafa9df91d44f9c466688fc1275a3a" dependencies = [ "actix-utils", "actix-web", @@ -177,6 +177,7 @@ dependencies = [ "log", "memchr", "mime", + "rand", "serde", "serde_json", "serde_plain", @@ -199,9 +200,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "futures-core", "tokio", @@ -209,9 +210,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" dependencies = [ "actix-rt", "actix-service", @@ -266,9 +267,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.7.0" +version = "4.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6316df3fa569627c98b12557a8b6ff0674e5be4bb9b5e4ae2550ddb4964ed6" +checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" dependencies = [ "actix-codec", "actix-http", @@ -314,7 +315,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -537,7 +538,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -548,7 +549,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -564,12 +565,6 @@ dependencies = [ "quick-xml 0.31.0", ] -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -584,9 +579,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474d7cec9d0a1126fad1b224b767fcbf351c23b0309bb21ec210bcfd379926a5" +checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -597,9 +592,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7505fc3cb7acbf42699a43a79dd9caa4ed9e99861dfbb837c5c0fb5a0a8d2980" +checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" dependencies = [ "bindgen", "cc", @@ -632,7 +627,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -708,12 +703,11 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bb8" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7c2093d15d6a1d33b1f972e1c5ea3177748742b97a5f392aa83a65262c6780" +checksum = "b10cf871f3ff2ce56432fddc2615ac7acc3aa22ca321f8fea800846fbb32f188" dependencies = [ "async-trait", - "futures-channel", "futures-util", "parking_lot 0.12.3", "tokio", @@ -757,7 +751,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -770,7 +764,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.66", + "syn 2.0.68", "which", ] @@ -797,9 +791,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -858,9 +852,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -899,9 +893,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "2755ff20a1d93490d26ba33a6f092a38a508398a5320df5d4b3014fcccce9410" dependencies = [ "jobserver", "libc", @@ -1006,7 +1000,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1344,7 +1338,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1366,7 +1360,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core 0.20.9", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1436,7 +1430,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1468,7 +1462,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1489,7 +1483,7 @@ dependencies = [ "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1499,20 +1493,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -1530,7 +1524,7 @@ version = "2.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "chrono", "diesel_derives", @@ -1566,7 +1560,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1577,7 +1571,7 @@ checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1589,7 +1583,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1619,7 +1613,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1648,17 +1642,6 @@ dependencies = [ "chrono", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "doku" version = "0.21.1" @@ -1703,9 +1686,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elementtree" @@ -1764,7 +1747,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2006,7 +1989,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2279,7 +2262,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2327,12 +2310,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -2366,9 +2349,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -2429,19 +2412,34 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.29", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", "hyper 1.3.1", "hyper-util", - "rustls 0.22.4", + "rustls 0.23.10", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots", ] [[package]] @@ -2506,7 +2504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8215279f83f9b829403812f845aa2d0dd5966332aa2fd0334a375256f3dd0322" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2532,124 +2530,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2676,18 +2556,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" -dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", -] - [[package]] name = "image" version = "0.24.9" @@ -2851,6 +2719,15 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2859,9 +2736,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -2953,6 +2830,9 @@ dependencies = [ "lemmy_utils", "moka", "once_cell", + "rand", + "serde_json", + "sha3", "tracing", "url", "uuid", @@ -3276,9 +3156,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.48.5", @@ -3311,12 +3191,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "local-channel" version = "0.1.5" @@ -3359,9 +3233,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mac" @@ -3488,9 +3362,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "metrics" @@ -3504,9 +3378,9 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26eb45aff37b45cff885538e1dcbd6c2b462c04fe84ce0155ea469f325672c98" +checksum = "bf0af7a0d7ced10c0151f870e5e3f3f8bc9ffc5992d32873566ca1f9169ae776" dependencies = [ "base64 0.22.1", "http-body-util", @@ -3566,9 +3440,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -3582,9 +3456,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", @@ -3695,9 +3569,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -3758,7 +3632,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -3775,7 +3649,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3796,23 +3670,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf9b1c4e9a6c4de793c632496fa490bdc0e1eea73f0c91394f7b6990935d22" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures", - "js-sys", - "lazy_static", - "percent-encoding", - "pin-project", - "rand", - "thiserror", -] - [[package]] name = "opentelemetry" version = "0.19.0" @@ -3960,9 +3817,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" dependencies = [ "num-traits", ] @@ -4028,7 +3885,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", "windows-targets 0.52.5", ] @@ -4178,9 +4035,9 @@ dependencies = [ "opentelemetry_sdk 0.23.0", "pin-project-lite", "refinery", - "reqwest 0.12.4", - "reqwest-middleware 0.3.1", - "reqwest-tracing 0.5.0", + "reqwest 0.12.5", + "reqwest-middleware 0.3.2", + "reqwest-tracing 0.5.1", "rustls 0.23.10", "rustls-channel-resolver", "rustls-pemfile 2.1.2", @@ -4228,7 +4085,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -4373,14 +4230,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -4391,7 +4248,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "hex", "lazy_static", "procfs-core", @@ -4404,7 +4261,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "hex", ] @@ -4468,7 +4325,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -4530,6 +4387,53 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.10", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls 0.23.10", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -4581,7 +4485,7 @@ version = "11.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -4592,7 +4496,7 @@ checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -4615,11 +4519,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -4665,7 +4569,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -4676,8 +4580,8 @@ checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -4691,20 +4595,20 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] name = "regex-lite" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" @@ -4714,9 +4618,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -4734,6 +4638,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.29", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -4744,14 +4649,17 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs", "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -4764,9 +4672,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", @@ -4776,7 +4684,7 @@ dependencies = [ "http-body 1.0.0", "http-body-util", "hyper 1.3.1", - "hyper-rustls", + "hyper-rustls 0.27.2", "hyper-util", "ipnet", "js-sys", @@ -4785,15 +4693,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.22.4", + "quinn", + "rustls 0.23.10", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tokio-util", "tower-service", "url", @@ -4822,14 +4731,14 @@ dependencies = [ [[package]] name = "reqwest-middleware" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45d100244a467870f6cb763c4484d010a6bed6bd610b3676e3825d93fb4cfbd" +checksum = "39346a33ddfe6be00cbc17a34ce996818b97b230b87229f10114693becca1268" dependencies = [ "anyhow", "async-trait", "http 1.1.0", - "reqwest 0.12.4", + "reqwest 0.12.5", "serde", "thiserror", "tower-service", @@ -4845,27 +4754,27 @@ dependencies = [ "async-trait", "getrandom", "matchit 0.7.3", - "opentelemetry 0.16.0", + "opentelemetry 0.19.0", "reqwest 0.11.27", "reqwest-middleware 0.2.5", "task-local-extensions", "tracing", - "tracing-opentelemetry 0.16.0", + "tracing-opentelemetry 0.19.0", ] [[package]] name = "reqwest-tracing" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b253954a1979e02eabccd7e9c3d61d8f86576108baa160775e7f160bb4e800a3" +checksum = "71a37668dccbd75e045f26811891dd939f28c38d3b7ca572a4fce4bc462b83ec" dependencies = [ "anyhow", "async-trait", "getrandom", "http 1.1.0", "matchit 0.8.2", - "reqwest 0.12.4", - "reqwest-middleware 0.3.1", + "reqwest 0.12.5", + "reqwest-middleware 0.3.2", "tracing", ] @@ -4877,9 +4786,9 @@ checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" [[package]] name = "rgb" -version = "0.8.37" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" dependencies = [ "bytemuck", ] @@ -4906,7 +4815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "serde", "serde_derive", ] @@ -4970,7 +4879,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -4979,16 +4888,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.4" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "rustls-webpki 0.101.7", + "sct", ] [[package]] @@ -5002,7 +4909,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] @@ -5017,6 +4924,18 @@ dependencies = [ "rustls 0.23.10", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -5042,6 +4961,16 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.102.4" @@ -5128,6 +5057,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "sdd" version = "0.2.0" @@ -5140,7 +5079,7 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -5200,7 +5139,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5272,7 +5211,7 @@ dependencies = [ "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5297,7 +5236,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5322,6 +5261,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -5440,7 +5389,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5469,12 +5418,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stacker" version = "0.1.15" @@ -5561,9 +5504,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] name = "strum_macros" @@ -5575,14 +5518,14 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -5597,9 +5540,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -5613,15 +5556,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "synstructure" -version = "0.13.1" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "syntect" @@ -5636,7 +5574,7 @@ dependencies = [ "fnv", "once_cell", "plist", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", "serde", "serde_derive", "serde_json", @@ -5730,7 +5668,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5780,21 +5718,11 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -5823,7 +5751,7 @@ checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5864,7 +5792,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5933,12 +5861,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", + "rustls 0.21.12", "tokio", ] @@ -6205,7 +6132,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -6260,19 +6187,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-opentelemetry" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffbf13a0f8b054a4e59df3a173b818e9c6177c02789871f2073977fd0062076" -dependencies = [ - "opentelemetry 0.16.0", - "tracing", - "tracing-core", - "tracing-log 0.1.4", - "tracing-subscriber", -] - [[package]] name = "tracing-opentelemetry" version = "0.19.0" @@ -6338,9 +6252,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" [[package]] name = "try-lock" @@ -6368,7 +6282,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "termcolor", ] @@ -6389,7 +6303,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -6466,12 +6380,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna 1.0.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -6488,37 +6402,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ - "atomic", "getrandom", "serde", ] @@ -6593,7 +6494,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -6627,7 +6528,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6701,9 +6602,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -6958,18 +6859,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "x509-cert" version = "0.2.5" @@ -7044,30 +6933,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -7085,28 +6950,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", + "syn 2.0.68", ] [[package]] @@ -7126,29 +6970,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -7171,9 +6993,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index ce6d3357d..c296f6d9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ console = [ "opentelemetry", "opentelemetry-otlp", "tracing-opentelemetry", - "reqwest-tracing/opentelemetry_0_16", + "reqwest-tracing/opentelemetry_0_19", ] json-log = ["tracing-subscriber/json"] default = [] @@ -121,9 +121,14 @@ tracing-error = "0.2.0" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = { version = "2.5.0", features = ["serde"] } -reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] } +reqwest = { version = "0.11.27", features = [ + "json", + "blocking", + "gzip", + "rustls-tls-native-roots", +] } reqwest-middleware = "0.2.5" -reqwest-tracing = "0.4.8" +reqwest-tracing = { version = "0.4.8", features = ["opentelemetry_0_19"] } clokwerk = "0.4.0" doku = { version = "0.21.1", features = ["url-2"] } bcrypt = "0.15.1" @@ -168,6 +173,8 @@ i-love-jesus = { version = "0.1.0" } clap = { version = "4.5.6", features = ["derive", "env"] } pretty_assertions = "1.4.0" derive-new = "0.6.0" +sha3 = "0.10.8" +rand = "0.8.5" [dependencies] lemmy_api = { workspace = true } diff --git a/config/defaults.hjson b/config/defaults.hjson index e8a70ebae..6c5d54070 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -102,6 +102,9 @@ } # the domain name of your instance (mandatory) hostname: "unset" + # the domain name of your lemmy-ui instance used for OAUTH2 (defaults to the backend instance + # hostname) + hostname_ui: "example.com" # Address where lemmy should listen for incoming requests bind: "0.0.0.0" # Port where lemmy should listen for incoming requests diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index 50ee10bb6..03f873a0f 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -28,11 +28,13 @@ pub async fn change_password( } // Check the old password - let valid: bool = verify( - &data.old_password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted + { + verify(&data.old_password, password_encrypted).unwrap_or(false) + } else { + data.old_password.is_empty() + }; + if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 19f84f703..928245dc8 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -1,4 +1,4 @@ -use crate::{check_totp_2fa_valid, local_user::check_email_verified}; +use crate::check_totp_2fa_valid; use actix_web::{ web::{Data, Json}, HttpRequest, @@ -8,12 +8,7 @@ use lemmy_api_common::{ claims::Claims, context::LemmyContext, person::{Login, LoginResponse}, - utils::check_user_valid, -}; -use lemmy_db_schema::{ - source::{local_site::LocalSite, registration_application::RegistrationApplication}, - utils::DbPool, - RegistrationMode, + utils::{check_email_verified, check_registration_application, check_user_valid}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -36,11 +31,13 @@ pub async fn login( .ok_or(LemmyErrorType::IncorrectLogin)?; // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted + { + verify(&data.password, password_encrypted).unwrap_or(false) + } else { + false + }; + if !valid { Err(LemmyErrorType::IncorrectLogin)? } @@ -67,28 +64,3 @@ pub async fn login( registration_created: false, })) } - -async fn check_registration_application( - local_user_view: &LocalUserView, - local_site: &LocalSite, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - if (local_site.registration_mode == RegistrationMode::RequireApplication - || local_site.registration_mode == RegistrationMode::Closed) - && !local_user_view.local_user.accepted_application - && !local_user_view.local_user.admin - { - // Fetch the registration application. If no admin id is present its still pending. Otherwise it - // was processed (either accepted or denied). - let local_user_id = local_user_view.local_user.id; - let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; - if registration.admin_id.is_some() { - Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? - } else { - Err(LemmyErrorType::RegistrationApplicationIsPending)? - } - } - Ok(()) -} diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index c00a4516e..b1ee7c0b6 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -1,6 +1,3 @@ -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; - pub mod add_admin; pub mod ban_person; pub mod block; @@ -20,15 +17,3 @@ pub mod save_settings; pub mod update_totp; pub mod validate_auth; pub mod verify_email; - -/// Check if the user's email is verified if email verification is turned on -/// However, skip checking verification if the user is an admin -fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> { - if !local_user_view.local_user.admin - && site_view.local_site.require_email_verification - && !local_user_view.local_user.email_verified - { - Err(LemmyErrorType::EmailNotVerified)? - } - Ok(()) -} diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index b6b113c07..8fe5351b1 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -1,9 +1,8 @@ -use crate::local_user::check_email_verified; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, person::PasswordReset, - utils::send_password_reset_email, + utils::{check_email_verified, send_password_reset_email}, SuccessResponse, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index e7a5464f3..073eeaf63 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -7,6 +7,7 @@ use lemmy_db_schema::{ local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::{LocalUser, LocalUserUpdateForm}, moderator::{ModAdd, ModAddForm}, + oauth_provider::OAuthProvider, tagline::Tagline, }, traits::Crud, @@ -65,6 +66,7 @@ pub async fn leave_admin( let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; let custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; + let oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { @@ -76,6 +78,7 @@ pub async fn leave_admin( discussion_languages, taglines, custom_emojis, + oauth_providers, blocked_urls, })) } diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 6c17d4e6a..e2d0cebb1 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -118,7 +118,7 @@ mod tests { let local_user_form = LocalUserInsertForm::builder() .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) + .password_encrypted(Some("123456".to_string())) .build(); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 9d12d2e13..48acaad21 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -7,6 +7,7 @@ pub mod community; #[cfg(feature = "full")] pub mod context; pub mod custom_emoji; +pub mod oauth_provider; pub mod person; pub mod post; pub mod private_message; diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs new file mode 100644 index 000000000..052e9fa61 --- /dev/null +++ b/crates/api_common/src/oauth_provider.rs @@ -0,0 +1,83 @@ +use lemmy_db_schema::newtypes::OAuthProviderId; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; +use url::Url; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create an external auth method. +pub struct CreateOAuthProvider { + pub display_name: String, + pub issuer: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub userinfo_endpoint: String, + pub id_claim: String, + pub name_claim: String, + pub client_id: String, + pub client_secret: String, + pub scopes: String, + pub auto_verify_email: bool, + pub auto_approve_application: bool, + pub account_linking_enabled: bool, + pub enabled: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Edit an external auth method. +pub struct EditOAuthProvider { + #[cfg_attr(feature = "full", ts(type = "string"))] + pub id: OAuthProviderId, + pub display_name: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub userinfo_endpoint: String, + pub id_claim: String, + pub name_claim: String, + pub client_secret: String, + pub scopes: String, + pub auto_verify_email: bool, + pub auto_approve_application: bool, + pub account_linking_enabled: bool, + pub enabled: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Delete an external auth method. +pub struct DeleteOAuthProvider { + #[cfg_attr(feature = "full", ts(type = "string"))] + pub id: OAuthProviderId, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Logging in with an OAuth 2.0 authorization +pub struct OAuth { + pub code: String, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub oauth_provider_id: OAuthProviderId, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub redirect_uri: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Response from OAuth token endpoint +pub struct TokenResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: Option, + pub refresh_token: Option, + pub scope: Option, +} diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index ddbb3dd0c..ff7a573b3 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -38,6 +38,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { .user_agent(user_agent.clone()) .timeout(REQWEST_TIMEOUT) .connect_timeout(REQWEST_TIMEOUT) + .use_rustls_tls() } /// Fetches metadata for the given link and optionally generates thumbnail. diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 77bfcd8ee..3e41d387b 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -7,6 +7,7 @@ use lemmy_db_schema::{ instance::Instance, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_provider::OAuthProvider, tagline::Tagline, }, ListingType, @@ -192,6 +193,7 @@ pub struct CreateSite { pub blocked_instances: Option>, pub taglines: Option>, pub registration_mode: Option, + pub oauth_registration: Option, pub content_warning: Option, pub default_post_listing_mode: Option, } @@ -274,6 +276,8 @@ pub struct EditSite { /// A list of taglines shown at the top of the front page. pub taglines: Option>, pub registration_mode: Option, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: Option, /// Whether to email admins for new reports. pub reports_email_admins: Option, /// If present, nsfw content is visible by default. Should be displayed by frontends/clients @@ -308,6 +312,8 @@ pub struct GetSiteResponse { pub taglines: Vec, /// A list of custom emojis your site supports. pub custom_emojis: Vec, + /// A list of external auth methods your site supports. + pub oauth_providers: Vec>, pub blocked_urls: Vec, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 97b12cc5b..66168ab97 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -27,14 +27,16 @@ use lemmy_db_schema::{ person::{Person, PersonUpdateForm}, person_block::PersonBlock, post::{Post, PostRead}, + registration_application::RegistrationApplication, site::Site, }, traits::Crud, utils::DbPool, + RegistrationMode, }; use lemmy_db_views::{ comment_view::CommentQuery, - structs::{LocalImageView, LocalUserView}, + structs::{LocalImageView, LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{ CommunityModeratorView, @@ -192,6 +194,46 @@ pub fn check_user_valid(person: &Person) -> LemmyResult<()> { } } +/// Check if the user's email is verified if email verification is turned on +/// However, skip checking verification if the user is an admin +pub fn check_email_verified( + local_user_view: &LocalUserView, + site_view: &SiteView, +) -> LemmyResult<()> { + if !local_user_view.local_user.admin + && site_view.local_site.require_email_verification + && !local_user_view.local_user.email_verified + { + Err(LemmyErrorType::EmailNotVerified)? + } + Ok(()) +} + +pub async fn check_registration_application( + local_user_view: &LocalUserView, + local_site: &LocalSite, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + if (local_site.registration_mode == RegistrationMode::RequireApplication + || local_site.registration_mode == RegistrationMode::Closed) + && !local_user_view.local_user.accepted_application + && !local_user_view.local_user.admin + { + // Fetch the registration application. If no admin id is present its still pending. Otherwise it + // was processed (either accepted or denied). + let local_user_id = local_user_view.local_user.id; + let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; + if registration.admin_id.is_some() { + Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? + } else { + Err(LemmyErrorType::RegistrationApplicationIsPending)? + } + } + Ok(()) +} + /// Checks that a normal user action (eg posting or voting) is allowed in a given community. /// /// In particular it checks that neither the user nor community are banned or deleted, and that diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index af50c5648..86745eb70 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -30,6 +30,9 @@ once_cell.workspace = true anyhow.workspace = true webmention = "0.5.0" accept-language = "3.1.0" +serde_json = { workspace = true } +sha3 = { workspace = true } +rand = { workspace = true } [package.metadata.cargo-machete] ignored = ["futures"] diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index aee3e8134..b138fbd30 100644 --- a/crates/api_crud/src/lib.rs +++ b/crates/api_crud/src/lib.rs @@ -1,6 +1,7 @@ pub mod comment; pub mod community; pub mod custom_emoji; +pub mod oauth_provider; pub mod post; pub mod private_message; pub mod site; diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs new file mode 100644 index 000000000..bbf08404b --- /dev/null +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -0,0 +1,59 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + oauth_provider::CreateOAuthProvider, + utils::is_admin, +}; +use lemmy_db_schema::{ + newtypes::OAuthProviderId, + source::oauth_provider::{OAuthProvider, OAuthProviderInsertForm, UnsafeOAuthProvider}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; +use sha3::{ + digest::{ExtendableOutput, Update, XofReader}, + Shake128, +}; +use url::Url; + +#[tracing::instrument(skip(context))] +pub async fn create_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + // hash the issuer and client_id to create a deterministic i64 id + let mut hasher = Shake128::default(); + hasher.update(data.issuer.as_bytes()); + hasher.update(data.client_id.as_bytes()); + let mut reader = hasher.finalize_xof(); + let mut id_bytes = [0u8; 8]; + reader.read(&mut id_bytes); + + let cloned_data = data.clone(); + let oauth_provider_form = OAuthProviderInsertForm::builder() + .id(OAuthProviderId(i64::from_ne_bytes(id_bytes))) + .display_name(cloned_data.display_name) + .issuer(Url::parse(&cloned_data.issuer)?.into()) + .authorization_endpoint(Url::parse(&cloned_data.authorization_endpoint)?.into()) + .token_endpoint(Url::parse(&cloned_data.token_endpoint)?.into()) + .userinfo_endpoint(Url::parse(&cloned_data.userinfo_endpoint)?.into()) + .id_claim(cloned_data.id_claim) + .name_claim(cloned_data.name_claim) + .client_id(data.client_id.to_string()) + .client_secret(data.client_secret.to_string()) + .scopes(data.scopes.to_string()) + .auto_verify_email(data.auto_verify_email) + .auto_approve_application(data.auto_approve_application) + .account_linking_enabled(data.account_linking_enabled) + .enabled(data.enabled) + .build(); + let unsafe_oauth_provider = + UnsafeOAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?; + Ok(Json(OAuthProvider::from_unsafe(&unsafe_oauth_provider))) +} diff --git a/crates/api_crud/src/oauth_provider/delete.rs b/crates/api_crud/src/oauth_provider/delete.rs new file mode 100644 index 000000000..96fa5783a --- /dev/null +++ b/crates/api_crud/src/oauth_provider/delete.rs @@ -0,0 +1,23 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + oauth_provider::DeleteOAuthProvider, + utils::is_admin, + SuccessResponse, +}; +use lemmy_db_schema::{source::oauth_provider::UnsafeOAuthProvider, traits::Crud}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn delete_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + UnsafeOAuthProvider::delete(&mut context.pool(), data.id).await?; + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api_crud/src/oauth_provider/mod.rs b/crates/api_crud/src/oauth_provider/mod.rs new file mode 100644 index 000000000..fdb2f5561 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/mod.rs @@ -0,0 +1,3 @@ +pub mod create; +pub mod delete; +pub mod update; diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs new file mode 100644 index 000000000..f19920b57 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -0,0 +1,47 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin}; +use lemmy_db_schema::{ + source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm, UnsafeOAuthProvider}, + traits::Crud, + utils::naive_now, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; +use url::Url; + +#[tracing::instrument(skip(context))] +pub async fn update_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let cloned_data = data.clone(); + let oauth_provider_form = OAuthProviderUpdateForm::builder() + .display_name(cloned_data.display_name) + .authorization_endpoint(Url::parse(&cloned_data.authorization_endpoint)?.into()) + .token_endpoint(Url::parse(&cloned_data.token_endpoint)?.into()) + .userinfo_endpoint(Url::parse(&cloned_data.userinfo_endpoint)?.into()) + .id_claim(data.id_claim.to_string()) + .name_claim(data.name_claim.to_string()) + .client_secret(if !data.client_secret.is_empty() { + Some(data.client_secret.to_string()) + } else { + None + }) + .scopes(data.scopes.to_string()) + .auto_verify_email(data.auto_verify_email) + .auto_approve_application(data.auto_approve_application) + .account_linking_enabled(data.account_linking_enabled) + .enabled(data.enabled) + .updated(naive_now()); + + let update_result = + UnsafeOAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form.build()).await?; + let unsafe_oauth_provider = + UnsafeOAuthProvider::get(&mut context.pool(), update_result.id).await?; + Ok(Json(OAuthProvider::from_unsafe(&unsafe_oauth_provider))) +} diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 6b1909966..28f3171be 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -231,6 +231,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -255,6 +256,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -279,6 +281,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -303,6 +306,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -327,6 +331,7 @@ mod tests { Some(true), None::, None::, + None::, ), ), ( @@ -351,6 +356,7 @@ mod tests { Some(true), None::, None::, + None::, ), ), ( @@ -375,6 +381,7 @@ mod tests { None::, None::, Some(RegistrationMode::RequireApplication), + None::, ), ), ]; @@ -433,6 +440,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -456,6 +464,7 @@ mod tests { Some(true), Some(String::new()), Some(RegistrationMode::Open), + None::, ), ), ( @@ -479,6 +488,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -502,6 +512,7 @@ mod tests { None::, None::, Some(RegistrationMode::RequireApplication), + None::, ), ), ]; @@ -552,6 +563,7 @@ mod tests { site_is_federated: Option, site_application_question: Option, site_registration_mode: Option, + site_oauth_registration: Option, ) -> CreateSite { CreateSite { name: site_name, @@ -594,6 +606,7 @@ mod tests { blocked_instances: None, taglines: None, registration_mode: site_registration_mode, + oauth_registration: site_oauth_registration, content_warning: None, default_post_listing_mode: None, } diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 69e82007c..de9623d5c 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -7,6 +7,7 @@ use lemmy_db_schema::source::{ actor_language::{LocalUserLanguage, SiteLanguage}, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_provider::OAuthProvider, tagline::Tagline, }; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; @@ -44,13 +45,14 @@ pub async fn get_site( let site_view = SiteView::read_local(&mut context.pool()) .await? .ok_or(LemmyErrorType::LocalSiteNotSetup)?; - let admins = PersonView::admins(&mut context.pool()).await?; + let admins: Vec = PersonView::admins(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; let custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; + let oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; Ok(GetSiteResponse { site_view, admins, @@ -61,6 +63,7 @@ pub async fn get_site( taglines, custom_emojis, blocked_urls, + oauth_providers, }) }) .await @@ -89,6 +92,11 @@ pub async fn get_site( )) .with_lemmy_type(LemmyErrorType::SystemErrLogin)?; + // filter oauth_providers for normal users + if !local_user_view.local_user.admin { + filter_oauth_providers(&mut site_response.oauth_providers); + } + Some(MyUserInfo { local_user_view, follows, @@ -99,8 +107,34 @@ pub async fn get_site( discussion_languages, }) } else { + // filter oauth_providers for public access + filter_oauth_providers(&mut site_response.oauth_providers); None }; Ok(Json(site_response)) } + +fn filter_oauth_providers(oauth_providers: &mut [Option]) { + for oauth_provider_opt in oauth_providers { + if let Some(oauth_provider) = oauth_provider_opt { + if oauth_provider.enabled.is_some() + && oauth_provider.enabled.expect("unexpected enabled value") + { + oauth_provider.issuer = None; + oauth_provider.token_endpoint = None; + oauth_provider.userinfo_endpoint = None; + oauth_provider.id_claim = None; + oauth_provider.name_claim = None; + oauth_provider.auto_verify_email = None; + oauth_provider.auto_approve_application = None; + oauth_provider.account_linking_enabled = None; + oauth_provider.enabled = None; + oauth_provider.published = None; + oauth_provider.updated = None; + } else { + *oauth_provider_opt = None; + } + } + } +} diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index f6377038d..1a0297698 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -122,6 +122,7 @@ pub async fn update_site( captcha_difficulty: data.captcha_difficulty.clone(), reports_email_admins: data.reports_email_admins, default_post_listing_mode: data.default_post_listing_mode, + oauth_registration: data.oauth_registration, ..Default::default() }; @@ -283,6 +284,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -306,6 +308,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -329,6 +332,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -352,6 +356,7 @@ mod tests { Some(true), None::, None::, + None::, ), ), ( @@ -375,6 +380,7 @@ mod tests { Some(true), None::, None::, + None::, ), ), ( @@ -398,6 +404,7 @@ mod tests { None::, None::, Some(RegistrationMode::RequireApplication), + None::, ), ), ]; @@ -452,6 +459,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -474,6 +482,7 @@ mod tests { Some(true), Some(String::new()), Some(RegistrationMode::Open), + None::, ), ), ( @@ -496,6 +505,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -518,6 +528,7 @@ mod tests { None::, None::, Some(RegistrationMode::RequireApplication), + None::, ), ), ]; @@ -566,6 +577,7 @@ mod tests { site_is_federated: Option, site_application_question: Option, site_registration_mode: Option, + site_oauth_registration: Option, ) -> EditSite { EditSite { name: site_name, @@ -612,6 +624,7 @@ mod tests { reports_email_admins: None, content_warning: None, default_post_listing_mode: None, + oauth_registration: site_oauth_registration, } } } diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index c84bd0a50..c65e9686f 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -3,8 +3,12 @@ use actix_web::{web::Json, HttpRequest}; use lemmy_api_common::{ claims::Claims, context::LemmyContext, + oauth_provider::{OAuth, TokenResponse}, person::{LoginResponse, Register}, utils::{ + check_email_verified, + check_registration_application, + check_user_valid, generate_inbox_url, generate_local_apub_endpoint, generate_shared_inbox_url, @@ -18,11 +22,15 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, + newtypes::{InstanceId, OAuthProviderId}, source::{ captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, language::Language, + local_site::LocalSite, local_user::{LocalUser, LocalUserInsertForm}, local_user_vote_display_mode::LocalUserVoteDisplayMode, + oauth_account::{OAuthAccount, OAuthAccountInsertForm}, + oauth_provider::UnsafeOAuthProvider, person::{Person, PersonInsertForm}, registration_application::{RegistrationApplication, RegistrationApplicationInsertForm}, }, @@ -31,15 +39,15 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ - error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ slurs::{check_slurs, check_slurs_opt}, validation::is_valid_actor_name, }, }; +use rand::Rng; use std::collections::HashSet; -#[tracing::instrument(skip(context))] pub async fn register( data: Json, req: HttpRequest, @@ -95,14 +103,6 @@ pub async fn register( check_slurs(&data.username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; - let actor_keypair = generate_actor_keypair()?; - is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?; - let actor_id = generate_local_apub_endpoint( - EndpointType::Person, - &data.username, - &context.settings().get_protocol_and_hostname(), - )?; - if let Some(email) = &data.email { if LocalUser::is_email_taken(&mut context.pool(), email).await? { Err(LemmyErrorType::EmailAlreadyExists)? @@ -110,24 +110,13 @@ pub async fn register( } // We have to create both a person, and local_user - - // Register the new person - let person_form = PersonInsertForm { - actor_id: Some(actor_id.clone()), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), - private_key: Some(actor_keypair.private_key), - ..PersonInsertForm::new( - data.username.clone(), - actor_keypair.public_key, - site_view.site.instance_id, - ) - }; - - // insert the person - let inserted_person = Person::create(&mut context.pool(), &person_form) - .await - .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + let inserted_person = create_person( + data.username.clone(), + &local_site, + site_view.site.instance_id, + &context, + ) + .await?; // Automatically set their application as accepted, if they created this with open registration. // Also fixes a bug which allows users to log in when registrations are changed to closed. @@ -153,7 +142,7 @@ pub async fn register( let local_user_form = LocalUserInsertForm::builder() .person_id(inserted_person.id) .email(data.email.as_deref().map(str::to_lowercase)) - .password_encrypted(data.password.to_string()) + .password_encrypted(Some(data.password.to_string())) .show_nsfw(Some(show_nsfw)) .accepted_application(accepted_application) .default_listing_type(Some(local_site.default_post_listing_type)) @@ -238,3 +227,321 @@ pub async fn register( Ok(Json(login_response)) } + +#[tracing::instrument(skip(context))] +pub async fn register_from_oauth( + data: Json, + req: HttpRequest, + context: Data, +) -> LemmyResult> { + let site_view = SiteView::read_local(&mut context.pool()) + .await? + .ok_or(LemmyErrorType::LocalSiteNotSetup)?; + + let local_site: LocalSite = site_view.local_site.clone(); + + // validate inputs + if data.oauth_provider_id == OAuthProviderId(0i64) + || data.redirect_uri.is_none() + || data.code.is_empty() + || data.code.len() > 300 + { + return Err(LemmyErrorType::OauthAuthorizationInvalid)?; + } + + // validate the redirect_uri + let redirect_uri = data + .redirect_uri + .as_ref() + .ok_or(LemmyErrorType::OauthAuthorizationInvalid)?; + if !redirect_uri + .host_str() + .unwrap_or("") + .eq(context.settings().get_ui_hostname()) + || !redirect_uri + .scheme() + .eq(context.settings().get_protocol_string()) + || !redirect_uri.path().eq(&String::from("/oauth/callback")) + || !redirect_uri.query().unwrap_or("").is_empty() + { + Err(LemmyErrorType::OauthAuthorizationInvalid)? + } + + // Fetch the OAUTH provider and make sure it's enabled + let oauth_provider_id = data.oauth_provider_id; + let oauth_provider = UnsafeOAuthProvider::get(&mut context.pool(), oauth_provider_id) + .await + .ok() + .ok_or(LemmyErrorType::OauthAuthorizationInvalid)?; + + if !oauth_provider.enabled { + return Err(LemmyErrorType::OauthAuthorizationInvalid)?; + } + + // Request an Access Token from the OAUTH provider + let response = context + .client() + .post(oauth_provider.token_endpoint.as_str()) + .header("Accept", "application/json") + .form(&[ + ("grant_type", "authorization_code"), + ("code", &data.code), + ("redirect_uri", redirect_uri.as_str()), + ("client_id", &oauth_provider.client_id), + ("client_secret", &oauth_provider.client_secret), + ]) + .send() + .await; + + if response.is_err() { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + let response = response.expect("invalid oauth token endpoint response"); + if !response.status().is_success() { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Extract the access token + let token_response = response + .json::() + .await + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + + // Request the user info from the OAUTH provider + let response = context + .client() + .get(oauth_provider.userinfo_endpoint.as_str()) + .header("Accept", "application/json") + .bearer_auth(token_response.access_token) + .send() + .await; + + if response.is_err() { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + let response = response.expect("invalid oauth userinfo response"); + if !response.status().is_success() { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Extract the OAUTH user_id claim from the returned user_info + let user_info = response + .json::() + .await + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + + let oauth_user_id: String; + if let Some(oauth_user_id_value) = user_info.get(oauth_provider.id_claim) { + oauth_user_id = serde_json::from_value::(oauth_user_id_value.clone()) + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + } else { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + let mut login_response = LoginResponse { + jwt: None, + registration_created: false, + verify_email_sent: false, + }; + + // Lookup user by oauth_user_id + let mut local_user_view = + LocalUserView::find_by_oauth_id(&mut context.pool(), oauth_provider.id, &oauth_user_id).await?; + + let local_user: LocalUser; + if let Some(user_view) = local_user_view { + // user found by oauth_user_id => Login user + local_user = user_view.clone().local_user; + + check_user_valid(&user_view.person)?; + check_email_verified(&user_view, &site_view)?; + check_registration_application(&user_view, &site_view.local_site, &mut context.pool()).await?; + } else { + // user has never previously registered using oauth + + // prevent registration if registration is closed + if local_site.registration_mode == RegistrationMode::Closed { + Err(LemmyErrorType::RegistrationClosed)? + } + + // prevent registration if registration is closed for OAUTH providers + if !local_site.oauth_registration { + return Err(LemmyErrorType::OauthRegistrationClosed)?; + } + + // Extract the OAUTH email claim from the returned user_info + let email: String; + if let Some(email_value) = user_info.get("email") { + email = serde_json::from_value::(email_value.clone()) + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + } else { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Lookup user by OAUTH email and link accounts + local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?; + + let person; + if let Some(user_view) = local_user_view { + // user found by email => link and login if linking is allowed + + if oauth_provider.account_linking_enabled { + // Link with OAUTH => Login user + let oauth_account_form = OAuthAccountInsertForm::builder() + .local_user_id(user_view.local_user.id) + .oauth_provider_id(oauth_provider.id) + .oauth_user_id(oauth_user_id) + .build(); + + OAuthAccount::create(&mut context.pool(), &oauth_account_form) + .await + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + + local_user = user_view.local_user; + person = user_view.person; + } else { + // email already registered + return Err(LemmyErrorType::EmailAlreadyExists)?; + } + } else { + // No user was found by email => Register as new user + + // Extract the OAUTH name claim from the returned user_info + let user_name: String; + if let Some(user_name_value) = user_info.get(oauth_provider.name_claim) { + user_name = serde_json::from_value::(user_name_value.clone()) + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + } else { + return Err(LemmyErrorType::OauthLoginFailed)?; + } + + // remove spaces from username + let username = str::replace(&user_name, " ", "_"); + let username = username + &rand::thread_rng().gen_range(0..999).to_string(); + + // check for slurs + let slur_regex = local_site_to_slur_regex(&local_site); + check_slurs(&username, &slur_regex) + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + + // We have to create a person, a local_user, and an oauth_account + person = create_person(username, &local_site, site_view.site.instance_id, &context).await?; + + // Create the local user + let local_user_form = LocalUserInsertForm::builder() + .person_id(person.id) + .email(Some(str::to_lowercase(&email))) + .password_encrypted(None) + .show_nsfw(Some(false)) + .accepted_application(Some(oauth_provider.auto_approve_application)) + .email_verified(Some(oauth_provider.auto_verify_email)) + .default_listing_type(Some(local_site.default_post_listing_type)) + // If its the initial site setup, they are an admin + .admin(Some(!local_site.site_setup)) + .build(); + + local_user = LocalUser::create(&mut context.pool(), &local_user_form, vec![]) + .await + .ok() + .ok_or(LemmyErrorType::OauthLoginFailed)?; + + // Create the oauth account + let oauth_account_form = OAuthAccountInsertForm::builder() + .local_user_id(local_user.id) + .oauth_provider_id(oauth_provider.id) + .oauth_user_id(oauth_user_id) + .build(); + + OAuthAccount::create(&mut context.pool(), &oauth_account_form) + .await + .ok() + .ok_or(LemmyErrorType::IncorrectLogin)?; + } + + // prevent sign in until application is accepted + if local_site.site_setup + && local_site.registration_mode == RegistrationMode::RequireApplication + && !local_user.accepted_application + && !local_user.admin + { + // Create the registration application + let form = RegistrationApplicationInsertForm { + local_user_id: local_user.id, + answer: String::from("SSO ") + &oauth_provider.display_name, + }; + + RegistrationApplication::create(&mut context.pool(), &form).await?; + + login_response.registration_created = true; + } + + // Check email is verified when required + if !local_user.admin && local_site.require_email_verification && !local_user.email_verified { + let local_user_view = LocalUserView { + local_user: local_user.clone(), + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person, + counts: PersonAggregates::default(), + }; + + send_verification_email( + &local_user_view, + &local_user + .email + .clone() + .expect("invalid verification email"), + &mut context.pool(), + context.settings(), + ) + .await?; + login_response.verify_email_sent = true; + } + } + + if !login_response.registration_created && !login_response.verify_email_sent { + let jwt = Claims::generate(local_user.id, req, &context).await?; + login_response.jwt = Some(jwt); + } + + return Ok(Json(login_response)); +} + +async fn create_person( + username: String, + local_site: &LocalSite, + instance_id: InstanceId, + context: &Data, +) -> Result { + let actor_keypair = generate_actor_keypair()?; + is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?; + let actor_id = generate_local_apub_endpoint( + EndpointType::Person, + &username, + &context.settings().get_protocol_and_hostname(), + )?; + + // Register the new person + let person_form = PersonInsertForm { + actor_id: Some(actor_id.clone()), + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + private_key: Some(actor_keypair.private_key), + ..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id) + }; + + // insert the person + let inserted_person = Person::create(&mut context.pool(), &person_form) + .await + .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + + Ok(inserted_person) +} diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 363230d83..5350efc2a 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -19,11 +19,12 @@ pub async fn delete_account( local_user_view: LocalUserView, ) -> LemmyResult> { // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted + { + verify(&data.password, password_encrypted).unwrap_or(false) + } else { + false + }; if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 9f2cb58c5..9f4bd28ff 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -347,7 +347,7 @@ mod tests { let user_form = LocalUserInsertForm::builder() .person_id(person.id) - .password_encrypted("pass".to_string()) + .password_encrypted(Some("pass".to_string())) .build(); let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 8483d6c20..f14986687 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -535,7 +535,7 @@ mod tests { let person = Person::create(pool, &person_form).await.unwrap(); let local_user_form = LocalUserInsertForm::builder() .person_id(person.id) - .password_encrypted("my_pw".to_string()) + .password_encrypted(Some("my_pw".to_string())) .build(); let local_user = LocalUser::create(pool, &local_user_form, vec![]) @@ -647,7 +647,7 @@ mod tests { let person = Person::create(pool, &person_form).await.unwrap(); let local_user_form = LocalUserInsertForm::builder() .person_id(person.id) - .password_encrypted("my_pw".to_string()) + .password_encrypted(Some("my_pw".to_string())) .build(); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 9b59e07ba..e73f56b33 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -32,9 +32,11 @@ impl LocalUser { ) -> Result { let conn = &mut get_conn(pool).await?; let mut form_with_encrypted_password = form.clone(); - let password_hash = - hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); - form_with_encrypted_password.password_encrypted = password_hash; + + if let Some(password_encrypted) = &form.password_encrypted { + let password_hash = hash(password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); + form_with_encrypted_password.password_encrypted = Some(password_hash); + } let local_user_ = insert_into(local_user::table) .values(form_with_encrypted_password) @@ -58,7 +60,7 @@ impl LocalUser { form: &LocalUserUpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; - let res = diesel::update(local_user::table.find(local_user_id)) + let res: Result = diesel::update(local_user::table.find(local_user_id)) .set(form) .execute(conn) .await; @@ -259,7 +261,7 @@ impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { Self::builder() .person_id(person_id) - .password_encrypted(String::new()) + .password_encrypted(Some(String::new())) .build() } } diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 3a4e71307..f115a101f 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -22,6 +22,8 @@ pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; pub mod moderator; +pub mod oauth_account; +pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; diff --git a/crates/db_schema/src/impls/oauth_account.rs b/crates/db_schema/src/impls/oauth_account.rs new file mode 100644 index 000000000..64a3787c2 --- /dev/null +++ b/crates/db_schema/src/impls/oauth_account.rs @@ -0,0 +1,52 @@ +use crate::{ + newtypes::OAuthAccountId, + schema::oauth_account, + source::oauth_account::{OAuthAccount, OAuthAccountInsertForm, OAuthAccountUpdateForm}, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for OAuthAccount { + type InsertForm = OAuthAccountInsertForm; + type UpdateForm = OAuthAccountUpdateForm; + type IdType = OAuthAccountId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(oauth_account::table) + .values(form) + .get_result::(conn) + .await + } + async fn update( + pool: &mut DbPool<'_>, + oauth_account_id: OAuthAccountId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(oauth_account::table.find(oauth_account_id)) + .set(form) + .get_result::(conn) + .await + } +} + +impl OAuthAccount { + pub async fn get(pool: &mut DbPool<'_>, oauth_account_id: OAuthAccountId) -> Result { + let conn = &mut get_conn(pool).await?; + let oauth_accounts = oauth_account::table + .find(oauth_account_id) + .select(oauth_account::all_columns) + .limit(1) + .load::(conn) + .await?; + if let Some(oauth_account) = oauth_accounts.into_iter().next() { + Ok(oauth_account) + } else { + Err(diesel::result::Error::NotFound) + } + } +} diff --git a/crates/db_schema/src/impls/oauth_provider.rs b/crates/db_schema/src/impls/oauth_provider.rs new file mode 100644 index 000000000..9e594ef54 --- /dev/null +++ b/crates/db_schema/src/impls/oauth_provider.rs @@ -0,0 +1,106 @@ +use crate::{ + newtypes::OAuthProviderId, + schema::oauth_provider, + source::oauth_provider::{ + OAuthProvider, + OAuthProviderInsertForm, + OAuthProviderUpdateForm, + UnsafeOAuthProvider, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for UnsafeOAuthProvider { + type InsertForm = OAuthProviderInsertForm; + type UpdateForm = OAuthProviderUpdateForm; + type IdType = OAuthProviderId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(oauth_provider::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(oauth_provider::table.find(oauth_provider_id)) + .set(form) + .get_result::(conn) + .await + } +} + +impl UnsafeOAuthProvider { + pub async fn get( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + let oauth_providers = oauth_provider::table + .find(oauth_provider_id) + .select(oauth_provider::all_columns) + .limit(1) + .load::(conn) + .await?; + if let Some(oauth_provider) = oauth_providers.into_iter().next() { + Ok(oauth_provider) + } else { + Err(diesel::result::Error::NotFound) + } + } + + pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let oauth_providers = oauth_provider::table + .order(oauth_provider::id) + .select(oauth_provider::all_columns) + .load::(conn) + .await?; + + Ok(oauth_providers) + } +} + +impl OAuthProvider { + pub async fn get_all(pool: &mut DbPool<'_>) -> Result>, Error> { + let oauth_providers = UnsafeOAuthProvider::get_all(pool).await?; + let mut result = Vec::>::new(); + + for oauth_provider in &oauth_providers { + result.push(Some(Self::from_unsafe(oauth_provider))); + } + + Ok(result) + } + + pub fn from_unsafe(unsafe_oauth_provider: &UnsafeOAuthProvider) -> Self { + OAuthProvider { + id: unsafe_oauth_provider.id, + display_name: unsafe_oauth_provider.display_name.clone(), + issuer: Some(unsafe_oauth_provider.issuer.clone()), + authorization_endpoint: unsafe_oauth_provider.authorization_endpoint.clone(), + token_endpoint: Some(unsafe_oauth_provider.token_endpoint.clone()), + userinfo_endpoint: Some(unsafe_oauth_provider.userinfo_endpoint.clone()), + id_claim: Some(unsafe_oauth_provider.id_claim.clone()), + name_claim: Some(unsafe_oauth_provider.name_claim.clone()), + client_id: unsafe_oauth_provider.client_id.clone(), + scopes: unsafe_oauth_provider.scopes.clone(), + auto_verify_email: Some(unsafe_oauth_provider.auto_verify_email), + auto_approve_application: Some(unsafe_oauth_provider.auto_approve_application), + account_linking_enabled: Some(unsafe_oauth_provider.account_linking_enabled), + enabled: Some(unsafe_oauth_provider.enabled), + published: Some(unsafe_oauth_provider.published), + updated: unsafe_oauth_provider.updated, + } + } +} diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 0b1351af1..3d6719967 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -74,7 +74,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await?; let new_local_user = LocalUserInsertForm::builder() .person_id(inserted_person.id) - .password_encrypted("pass".to_string()) + .password_encrypted(Some("pass".to_string())) .build(); let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c5c9e8e84..caee372e1 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -148,6 +148,20 @@ pub struct LocalSiteId(i32); /// The custom emoji id. pub struct CustomEmojiId(i32); +#[repr(transparent)] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +#[serde(into = "String", from = "String")] +/// The oauth provider id. +pub struct OAuthProviderId(pub i64); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The oauth account id. +pub struct OAuthAccountId(pub i32); + #[cfg(feature = "full")] #[derive(Serialize, Deserialize)] #[serde(remote = "Ltree")] @@ -272,6 +286,21 @@ where } } +impl From for String { + fn from(id: OAuthProviderId) -> Self { + id.0.to_string() + } +} + +impl From for OAuthProviderId { + fn from(id: String) -> Self { + OAuthProviderId( + id.parse::() + .expect("OAuthProviderID is not a valid integer"), + ) + } +} + impl InstanceId { pub fn inner(self) -> i32 { self.0 diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index c3102b578..c9f5a5b0c 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -393,6 +393,7 @@ diesel::table! { federation_signed_fetch -> Bool, default_post_listing_mode -> PostListingModeEnum, default_sort_type -> SortTypeEnum, + oauth_registration -> Bool, } } @@ -436,7 +437,7 @@ diesel::table! { local_user (id) { id -> Int4, person_id -> Int4, - password_encrypted -> Text, + password_encrypted -> Nullable, email -> Nullable, show_nsfw -> Bool, theme -> Text, @@ -613,6 +614,39 @@ diesel::table! { } } +diesel::table! { + oauth_account (id) { + id -> Int4, + local_user_id -> Int4, + oauth_provider_id -> Int8, + oauth_user_id -> Text, + published -> Timestamptz, + updated -> Nullable, + } +} + +diesel::table! { + oauth_provider (id) { + id -> Int8, + display_name -> Text, + issuer -> Text, + authorization_endpoint -> Text, + token_endpoint -> Text, + userinfo_endpoint -> Text, + id_claim -> Text, + name_claim -> Text, + client_id -> Text, + client_secret -> Text, + scopes -> Text, + auto_verify_email -> Bool, + auto_approve_application -> Bool, + account_linking_enabled -> Bool, + enabled -> Bool, + published -> Timestamptz, + updated -> Nullable, + } +} + diesel::table! { password_reset_request (id) { id -> Int4, @@ -1005,6 +1039,7 @@ diesel::joinable!(mod_remove_community -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> post (post_id)); diesel::joinable!(mod_transfer_community -> community (community_id)); +diesel::joinable!(oauth_account -> local_user (local_user_id)); diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person_aggregates -> person (person_id)); @@ -1086,6 +1121,8 @@ diesel::allow_tables_to_appear_in_same_query!( mod_remove_community, mod_remove_post, mod_transfer_community, + oauth_account, + oauth_provider, password_reset_request, person, person_aggregates, diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 05583c065..aa9c4ffab 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -70,6 +70,8 @@ pub struct LocalSite { pub default_post_listing_mode: PostListingMode, /// Default value for [LocalUser.post_listing_mode] pub default_sort_type: SortType, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: bool, } #[derive(Clone, TypedBuilder)] @@ -97,6 +99,7 @@ pub struct LocalSiteInsertForm { pub captcha_enabled: Option, pub captcha_difficulty: Option, pub registration_mode: Option, + pub oauth_registration: Option, pub reports_email_admins: Option, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, @@ -125,6 +128,7 @@ pub struct LocalSiteUpdateForm { pub captcha_enabled: Option, pub captcha_difficulty: Option, pub registration_mode: Option, + pub oauth_registration: Option, pub reports_email_admins: Option, pub updated: Option>>, pub federation_signed_fetch: Option, diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 5d592ddf1..693c54330 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -25,7 +25,7 @@ pub struct LocalUser { /// The person_id for the local user. pub person_id: PersonId, #[serde(skip)] - pub password_encrypted: SensitiveString, + pub password_encrypted: Option, pub email: Option, /// Whether to show NSFW content. pub show_nsfw: bool, @@ -77,7 +77,7 @@ pub struct LocalUserInsertForm { #[builder(!default)] pub person_id: PersonId, #[builder(!default)] - pub password_encrypted: String, + pub password_encrypted: Option, pub email: Option, pub show_nsfw: Option, pub theme: Option, diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index bbc8aafa2..377c1aaef 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -27,6 +27,8 @@ pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; pub mod moderator; +pub mod oauth_account; +pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; diff --git a/crates/db_schema/src/source/oauth_account.rs b/crates/db_schema/src/source/oauth_account.rs new file mode 100644 index 000000000..1df3ef1e7 --- /dev/null +++ b/crates/db_schema/src/source/oauth_account.rs @@ -0,0 +1,42 @@ +use crate::newtypes::{LocalUserId, OAuthAccountId, OAuthProviderId}; +#[cfg(feature = "full")] +use crate::schema::oauth_account; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; +use typed_builder::TypedBuilder; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// An auth account method. +pub struct OAuthAccount { + pub id: OAuthAccountId, + pub local_user_id: LocalUserId, + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, + pub published: DateTime, + pub updated: Option>, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +pub struct OAuthAccountInsertForm { + pub local_user_id: LocalUserId, + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +pub struct OAuthAccountUpdateForm { + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, +} diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs new file mode 100644 index 000000000..4121a5382 --- /dev/null +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -0,0 +1,160 @@ +use crate::newtypes::{DbUrl, OAuthProviderId}; +#[cfg(feature = "full")] +use crate::schema::oauth_provider; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; +use typed_builder::TypedBuilder; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// oauth provider with client_secret - should never be sent to the client +pub struct UnsafeOAuthProvider { + #[cfg_attr(feature = "full", ts(type = "string"))] + pub id: OAuthProviderId, + /// The OAuth 2.0 provider name displayed to the user on the Login page + pub display_name: String, + /// The issuer url of the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub issuer: DbUrl, + /// The authorization endpoint is used to interact with the resource owner and obtain an + /// authorization grant. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: DbUrl, + /// The token endpoint is used by the client to obtain an access token by presenting its + /// authorization grant or refresh token. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: DbUrl, + /// The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the + /// authenticated End-User. This is defined in the OIDC specification. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: DbUrl, + /// The OAuth 2.0 claim containing the unique user ID returned by the provider. Usually this + /// should be set to "sub". + pub id_claim: String, + /// The OAuth 2.0 claim containing the user name returned by the provider. This depends on the + /// provider and could be "name", "username", ... + pub name_claim: String, + /// The client_id is provided by the OAuth 2.0 provider and is a unique identifier to this + /// service + pub client_id: String, + /// The client_secret is provided by the OAuth 2.0 provider and is used to authenticate this + /// service with the provider + #[serde(skip)] + pub client_secret: String, + /// Lists the scopes requested from users. Users will have to grant access to the requested scope + /// at sign up. + pub scopes: String, + /// Automatically sets email as verified on registration + pub auto_verify_email: bool, + /// Automatically approves user application on registration + pub auto_approve_application: bool, + /// Allows linking an OAUTH account to an existing user account by matching emails + pub account_linking_enabled: bool, + /// switch to enable or disable an oauth provider + pub enabled: bool, + pub published: DateTime, + pub updated: Option>, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct OAuthProvider { + #[cfg_attr(feature = "full", ts(type = "string"))] + pub id: OAuthProviderId, + /// The OAuth 2.0 provider name displayed to the user on the Login page + pub display_name: String, + /// The issuer url of the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub issuer: Option, + /// The authorization endpoint is used to interact with the resource owner and obtain an + /// authorization grant. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: DbUrl, + /// The token endpoint is used by the client to obtain an access token by presenting its + /// authorization grant or refresh token. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: Option, + /// The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the + /// authenticated End-User. This is defined in the OIDC specification. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: Option, + /// The OAuth 2.0 claim containing the unique user ID returned by the provider. Usually this + /// should be set to "sub". + pub id_claim: Option, + /// The OAuth 2.0 claim containing the user name returned by the provider. This depends on the + /// provider and could be "name", "username", ... + pub name_claim: Option, + /// The client_id is provided by the OAuth 2.0 provider and is a unique identifier to this + /// service + pub client_id: String, + /// Lists the scopes requested from users. Users will have to grant access to the requested scope + /// at sign up. + pub scopes: String, + /// Automatically sets email as verified on registration + pub auto_verify_email: Option, + /// Automatically approves user application on registration + pub auto_approve_application: Option, + /// Allows linking an OAUTH account to an existing user account by matching emails + pub account_linking_enabled: Option, + /// switch to enable or disable an oauth provider + pub enabled: Option, + pub published: Option>, + pub updated: Option>, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +#[cfg_attr(feature = "full", ts(export))] +pub struct OAuthProviderInsertForm { + pub id: OAuthProviderId, + pub display_name: String, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub issuer: DbUrl, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: DbUrl, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: DbUrl, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: DbUrl, + pub id_claim: String, + pub name_claim: String, + pub client_id: String, + pub client_secret: String, + pub scopes: String, + pub auto_verify_email: bool, + pub auto_approve_application: bool, + pub account_linking_enabled: bool, + pub enabled: bool, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +#[cfg_attr(feature = "full", ts(export))] +pub struct OAuthProviderUpdateForm { + pub display_name: String, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: DbUrl, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: DbUrl, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: DbUrl, + pub id_claim: String, + pub name_claim: String, + pub client_secret: Option, + pub scopes: String, + pub auto_verify_email: bool, + pub auto_approve_application: bool, + pub account_linking_enabled: bool, + pub enabled: bool, + pub updated: DateTime, +} diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 950d061ba..0fd264264 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -303,7 +303,7 @@ mod tests { let new_local_user = LocalUserInsertForm::builder() .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) + .password_encrypted(Some("123".to_string())) .build(); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index cd7560a00..4cd804ea0 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -493,7 +493,7 @@ mod tests { let timmy_local_user_form = LocalUserInsertForm::builder() .person_id(inserted_timmy_person.id) .admin(Some(true)) - .password_encrypted(String::new()) + .password_encrypted(Some(String::new())) .build(); let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs index 0c13b0a68..b20dfe235 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -3,8 +3,8 @@ use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest}; use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - newtypes::{LocalUserId, PersonId}, - schema::{local_user, local_user_vote_display_mode, person, person_aggregates}, + newtypes::{LocalUserId, OAuthProviderId, PersonId}, + schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates}, utils::{ functions::{coalesce, lower}, DbConn, @@ -23,6 +23,7 @@ enum ReadBy<'a> { Name(&'a str), NameOrEmail(&'a str), Email(&'a str), + OAuthID(OAuthProviderId, &'a str), } enum ListMode { @@ -58,12 +59,21 @@ fn queries<'a>( ), _ => query, }; - query + let query = query .inner_join(local_user_vote_display_mode::table) - .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))) - .select(selection) - .first(&mut conn) - .await + .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))); + + if let ReadBy::OAuthID(oauth_provider_id, oauth_user_id) = search { + query + .inner_join(oauth_account::table) + .filter(oauth_account::oauth_provider_id.eq(oauth_provider_id)) + .filter(oauth_account::oauth_user_id.eq(oauth_user_id)) + .select(selection) + .first(&mut conn) + .await + } else { + query.select(selection).first(&mut conn).await + } }; let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { @@ -120,6 +130,16 @@ impl LocalUserView { queries().read(pool, ReadBy::Email(from_email)).await } + pub async fn find_by_oauth_id( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + oauth_user_id: &str, + ) -> Result, Error> { + queries() + .read(pool, ReadBy::OAuthID(oauth_provider_id, oauth_user_id)) + .await + } + pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result, Error> { queries().list(pool, ListMode::AdminsWithEmails).await } diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index e89b7d545..fc0bb7071 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -325,7 +325,7 @@ mod tests { let new_local_user = LocalUserInsertForm::builder() .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) + .password_encrypted(Some("123".to_string())) .build(); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index cd63859af..709896c09 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -169,7 +169,7 @@ mod tests { let timmy_local_user_form = LocalUserInsertForm::builder() .person_id(inserted_timmy_person.id) - .password_encrypted("nada".to_string()) + .password_encrypted(Some("nada".to_string())) .admin(Some(true)) .build(); @@ -183,7 +183,7 @@ mod tests { let sara_local_user_form = LocalUserInsertForm::builder() .person_id(inserted_sara_person.id) - .password_encrypted("nada".to_string()) + .password_encrypted(Some("nada".to_string())) .build(); let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]) @@ -211,7 +211,7 @@ mod tests { let jess_local_user_form = LocalUserInsertForm::builder() .person_id(inserted_jess_person.id) - .password_encrypted("nada".to_string()) + .password_encrypted(Some("nada".to_string())) .build(); let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]) diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 25e76c7b3..aa374b82b 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -290,7 +290,7 @@ mod tests { let local_user_form = LocalUserInsertForm::builder() .person_id(inserted_person.id) - .password_encrypted(String::new()) + .password_encrypted(Some(String::new())) .build(); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 98a0ca38d..4b735adc5 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -198,7 +198,7 @@ mod tests { let alice = Person::create(pool, &alice_form).await?; let alice_local_user_form = LocalUserInsertForm::builder() .person_id(alice.id) - .password_encrypted(String::new()) + .password_encrypted(Some(String::new())) .build(); let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; @@ -210,7 +210,7 @@ mod tests { let bob = Person::create(pool, &bob_form).await?; let bob_local_user_form = LocalUserInsertForm::builder() .person_id(bob.id) - .password_encrypted(String::new()) + .password_encrypted(Some(String::new())) .build(); let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?; diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 049bd6cc8..805e63786 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -4,8 +4,7 @@ use actix_web::{ header::{HeaderName, ACCEPT_ENCODING, HOST}, StatusCode, }, - web, - web::Query, + web::{self, Query}, HttpRequest, HttpResponse, }; diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 324c08ccb..9e4caced6 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -176,6 +176,9 @@ pub enum LemmyErrorType { CantBlockLocalInstance, UrlWithoutDomain, InboxTimeout, + OauthAuthorizationInvalid, + OauthLoginFailed, + OauthRegistrationClosed, Unknown(String), } diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index 6efa3fdd3..d28d52162 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -74,6 +74,13 @@ impl Settings { fs::read_to_string(Self::get_config_location()) } + pub fn get_ui_hostname(&self) -> &str { + match &self.hostname_ui { + Some(domain) => domain, + _ => &self.hostname, + } + } + /// Returns either "http" or "https", depending on tls_enabled setting pub fn get_protocol_string(&self) -> &'static str { if self.tls_enabled { diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 75aa56a88..e00274d1f 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -28,6 +28,11 @@ pub struct Settings { #[default("unset")] #[doku(example = "example.com")] pub hostname: String, + /// the domain name of your lemmy-ui instance used for OAUTH2 (defaults to the backend instance + /// hostname) + #[default(None)] + #[doku(example = "example.com")] + pub hostname_ui: Option, /// Address where lemmy should listen for incoming requests #[default(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))] #[doku(as = "String")] diff --git a/crates/utils/translations b/crates/utils/translations index ee2cffac8..e9b3b25fa 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit ee2cffac809ad466644f061ad79ac577b6c2e4fd +Subproject commit e9b3b25fa1af7e06c4ffab86624d95da0836ef36 diff --git a/migrations/2024-07-01-163929_user_password_encrypted_optional/down.sql b/migrations/2024-07-01-163929_user_password_encrypted_optional/down.sql new file mode 100644 index 000000000..111d97d2f --- /dev/null +++ b/migrations/2024-07-01-163929_user_password_encrypted_optional/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + ALTER COLUMN password_encrypted SET NOT NULL; + diff --git a/migrations/2024-07-01-163929_user_password_encrypted_optional/up.sql b/migrations/2024-07-01-163929_user_password_encrypted_optional/up.sql new file mode 100644 index 000000000..04937eef5 --- /dev/null +++ b/migrations/2024-07-01-163929_user_password_encrypted_optional/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + ALTER COLUMN password_encrypted DROP NOT NULL; + diff --git a/migrations/2024-07-01-174833_create_oauth_provider/down.sql b/migrations/2024-07-01-174833_create_oauth_provider/down.sql new file mode 100644 index 000000000..ab4218c7d --- /dev/null +++ b/migrations/2024-07-01-174833_create_oauth_provider/down.sql @@ -0,0 +1,5 @@ +DROP TABLE oauth_provider; + +ALTER TABLE local_site + DROP COLUMN oauth_registration; + diff --git a/migrations/2024-07-01-174833_create_oauth_provider/up.sql b/migrations/2024-07-01-174833_create_oauth_provider/up.sql new file mode 100644 index 000000000..035b69c2b --- /dev/null +++ b/migrations/2024-07-01-174833_create_oauth_provider/up.sql @@ -0,0 +1,24 @@ +CREATE TABLE oauth_provider ( + id bigint UNIQUE, + display_name text NOT NULL, + issuer text NOT NULL, + authorization_endpoint text NOT NULL, + token_endpoint text NOT NULL, + userinfo_endpoint text NOT NULL, + id_claim text NOT NULL, + name_claim text NOT NULL, + client_id text NOT NULL UNIQUE, + client_secret text NOT NULL, + scopes text NOT NULL, + auto_verify_email boolean DEFAULT TRUE NOT NULL, + auto_approve_application boolean DEFAULT TRUE NOT NULL, + account_linking_enabled boolean DEFAULT FALSE NOT NULL, + enabled boolean DEFAULT FALSE NOT NULL, + published timestamp with time zone DEFAULT now() NOT NULL, + updated timestamp with time zone, + PRIMARY KEY (id) +); + +ALTER TABLE local_site + ADD COLUMN oauth_registration boolean DEFAULT FALSE NOT NULL; + diff --git a/migrations/2024-07-01-214047_create_oauth_accounts/down.sql b/migrations/2024-07-01-214047_create_oauth_accounts/down.sql new file mode 100644 index 000000000..afe541e6d --- /dev/null +++ b/migrations/2024-07-01-214047_create_oauth_accounts/down.sql @@ -0,0 +1,2 @@ +DROP TABLE oauth_account; + diff --git a/migrations/2024-07-01-214047_create_oauth_accounts/up.sql b/migrations/2024-07-01-214047_create_oauth_accounts/up.sql new file mode 100644 index 000000000..3cf85f637 --- /dev/null +++ b/migrations/2024-07-01-214047_create_oauth_accounts/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE oauth_account ( + id serial PRIMARY KEY, + local_user_id int REFERENCES local_user ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + oauth_provider_id bigint NOT NULL, + oauth_user_id text NOT NULL, + published timestamp with time zone DEFAULT now() NOT NULL, + updated timestamp with time zone, + UNIQUE (oauth_provider_id, oauth_user_id), + UNIQUE (oauth_provider_id, local_user_id) +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 2a15b4ca6..280d62488 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -108,6 +108,11 @@ use lemmy_api_crud::{ delete::delete_custom_emoji, update::update_custom_emoji, }, + oauth_provider::{ + create::create_oauth_provider, + delete::delete_oauth_provider, + update::update_oauth_provider, + }, post::{ create::create_post, delete::delete_post, @@ -122,7 +127,10 @@ use lemmy_api_crud::{ update::update_private_message, }, site::{create::create_site, read::get_site, update::update_site}, - user::{create::register, delete::delete_account}, + user::{ + create::{register, register_from_oauth}, + delete::delete_account, + }, }; use lemmy_apub::api::{ list_comments::list_comments, @@ -376,6 +384,18 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("", web::post().to(create_custom_emoji)) .route("", web::put().to(update_custom_emoji)) .route("/delete", web::post().to(delete_custom_emoji)), + ) + .service( + web::scope("/oauth_provider") + .wrap(rate_limit.message()) + .route("", web::post().to(create_oauth_provider)) + .route("", web::put().to(update_oauth_provider)) + .route("/delete", web::post().to(delete_oauth_provider)), + ) + .service( + web::scope("/oauth") + .wrap(rate_limit.register()) + .route("/register", web::post().to(register_from_oauth)), ), ); cfg.service( diff --git a/src/code_migrations.rs b/src/code_migrations.rs index fd4ef66de..2bf068793 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -470,7 +470,7 @@ async fn initialize_local_site_2022_10_10( let local_user_form = LocalUserInsertForm::builder() .person_id(person_inserted.id) - .password_encrypted(setup.admin_password.clone()) + .password_encrypted(Some(setup.admin_password.clone())) .email(setup.admin_email.clone()) .admin(Some(true)) .build(); diff --git a/src/session_middleware.rs b/src/session_middleware.rs index 8b3090a47..d04d9be98 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -1,7 +1,7 @@ use actix_web::{ body::MessageBody, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, - http::header::CACHE_CONTROL, + http::header::{HeaderValue, CACHE_CONTROL}, Error, HttpMessage, }; @@ -9,7 +9,6 @@ use core::future::Ready; use futures_util::future::LocalBoxFuture; use lemmy_api::{local_user_view_from_jwt, read_auth_token}; use lemmy_api_common::context::LemmyContext; -use reqwest::header::HeaderValue; use std::{future::ready, rc::Rc}; #[derive(Clone)] @@ -148,7 +147,7 @@ mod tests { let local_user_form = LocalUserInsertForm::builder() .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) + .password_encrypted(Some("123456".to_string())) .build(); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) From 7f9b897c69ac9b5c42f6095d08bbe29401b6aac5 Mon Sep 17 00:00:00 2001 From: privacyguard Date: Sat, 6 Jul 2024 21:51:59 +0300 Subject: [PATCH 2/4] Fixes and improvements based on review feedback --- crates/api_common/src/oauth_provider.rs | 6 +-- crates/api_common/src/site.rs | 2 +- crates/api_crud/src/oauth_provider/update.rs | 7 ++-- crates/api_crud/src/site/read.rs | 39 +++++++++----------- crates/api_crud/src/user/create.rs | 15 +++----- crates/db_schema/src/impls/oauth_account.rs | 17 --------- crates/db_schema/src/impls/oauth_provider.rs | 24 ++---------- crates/utils/src/error.rs | 1 + src/api_routes_http.rs | 4 +- 9 files changed, 38 insertions(+), 77 deletions(-) diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs index 052e9fa61..a338ee8fa 100644 --- a/crates/api_common/src/oauth_provider.rs +++ b/crates/api_common/src/oauth_provider.rs @@ -57,16 +57,16 @@ pub struct DeleteOAuthProvider { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Logging in with an OAuth 2.0 authorization -pub struct OAuth { +pub struct AuthenticateWithOauth { pub code: String, #[cfg_attr(feature = "full", ts(type = "string"))] pub oauth_provider_id: OAuthProviderId, #[cfg_attr(feature = "full", ts(type = "string"))] - pub redirect_uri: Option, + pub redirect_uri: Url, } #[skip_serializing_none] diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 3e41d387b..0253d47bc 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -313,7 +313,7 @@ pub struct GetSiteResponse { /// A list of custom emojis your site supports. pub custom_emojis: Vec, /// A list of external auth methods your site supports. - pub oauth_providers: Vec>, + pub oauth_providers: Vec, pub blocked_urls: Vec, } diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs index f19920b57..5a21c5f39 100644 --- a/crates/api_crud/src/oauth_provider/update.rs +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -7,7 +7,7 @@ use lemmy_db_schema::{ utils::naive_now, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::LemmyError; +use lemmy_utils::{error::LemmyError, LemmyErrorType}; use url::Url; #[tracing::instrument(skip(context))] @@ -41,7 +41,8 @@ pub async fn update_oauth_provider( let update_result = UnsafeOAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form.build()).await?; - let unsafe_oauth_provider = - UnsafeOAuthProvider::get(&mut context.pool(), update_result.id).await?; + let unsafe_oauth_provider = UnsafeOAuthProvider::read(&mut context.pool(), update_result.id) + .await? + .ok_or(LemmyErrorType::CouldntFindOauthProvider)?; Ok(Json(OAuthProvider::from_unsafe(&unsafe_oauth_provider))) } diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index de9623d5c..49d48d320 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -115,26 +115,23 @@ pub async fn get_site( Ok(Json(site_response)) } -fn filter_oauth_providers(oauth_providers: &mut [Option]) { - for oauth_provider_opt in oauth_providers { - if let Some(oauth_provider) = oauth_provider_opt { - if oauth_provider.enabled.is_some() - && oauth_provider.enabled.expect("unexpected enabled value") - { - oauth_provider.issuer = None; - oauth_provider.token_endpoint = None; - oauth_provider.userinfo_endpoint = None; - oauth_provider.id_claim = None; - oauth_provider.name_claim = None; - oauth_provider.auto_verify_email = None; - oauth_provider.auto_approve_application = None; - oauth_provider.account_linking_enabled = None; - oauth_provider.enabled = None; - oauth_provider.published = None; - oauth_provider.updated = None; - } else { - *oauth_provider_opt = None; - } +fn filter_oauth_providers(oauth_providers: &mut Vec) { + oauth_providers.retain_mut(|oauth_provider| { + if oauth_provider.enabled.unwrap_or(false) { + oauth_provider.issuer = None; + oauth_provider.token_endpoint = None; + oauth_provider.userinfo_endpoint = None; + oauth_provider.id_claim = None; + oauth_provider.name_claim = None; + oauth_provider.auto_verify_email = None; + oauth_provider.auto_approve_application = None; + oauth_provider.account_linking_enabled = None; + oauth_provider.enabled = None; + oauth_provider.published = None; + oauth_provider.updated = None; + true + } else { + false } - } + }) } diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index c65e9686f..074bf6ae8 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -3,7 +3,7 @@ use actix_web::{web::Json, HttpRequest}; use lemmy_api_common::{ claims::Claims, context::LemmyContext, - oauth_provider::{OAuth, TokenResponse}, + oauth_provider::{AuthenticateWithOauth, TokenResponse}, person::{LoginResponse, Register}, utils::{ check_email_verified, @@ -229,8 +229,8 @@ pub async fn register( } #[tracing::instrument(skip(context))] -pub async fn register_from_oauth( - data: Json, +pub async fn authenticate_with_oauth( + data: Json, req: HttpRequest, context: Data, ) -> LemmyResult> { @@ -242,7 +242,6 @@ pub async fn register_from_oauth( // validate inputs if data.oauth_provider_id == OAuthProviderId(0i64) - || data.redirect_uri.is_none() || data.code.is_empty() || data.code.len() > 300 { @@ -250,10 +249,7 @@ pub async fn register_from_oauth( } // validate the redirect_uri - let redirect_uri = data - .redirect_uri - .as_ref() - .ok_or(LemmyErrorType::OauthAuthorizationInvalid)?; + let redirect_uri = &data.redirect_uri; if !redirect_uri .host_str() .unwrap_or("") @@ -269,9 +265,10 @@ pub async fn register_from_oauth( // Fetch the OAUTH provider and make sure it's enabled let oauth_provider_id = data.oauth_provider_id; - let oauth_provider = UnsafeOAuthProvider::get(&mut context.pool(), oauth_provider_id) + let oauth_provider = UnsafeOAuthProvider::read(&mut context.pool(), oauth_provider_id) .await .ok() + .ok_or(LemmyErrorType::OauthAuthorizationInvalid)? .ok_or(LemmyErrorType::OauthAuthorizationInvalid)?; if !oauth_provider.enabled { diff --git a/crates/db_schema/src/impls/oauth_account.rs b/crates/db_schema/src/impls/oauth_account.rs index 64a3787c2..81db68f25 100644 --- a/crates/db_schema/src/impls/oauth_account.rs +++ b/crates/db_schema/src/impls/oauth_account.rs @@ -33,20 +33,3 @@ impl Crud for OAuthAccount { .await } } - -impl OAuthAccount { - pub async fn get(pool: &mut DbPool<'_>, oauth_account_id: OAuthAccountId) -> Result { - let conn = &mut get_conn(pool).await?; - let oauth_accounts = oauth_account::table - .find(oauth_account_id) - .select(oauth_account::all_columns) - .limit(1) - .load::(conn) - .await?; - if let Some(oauth_account) = oauth_accounts.into_iter().next() { - Ok(oauth_account) - } else { - Err(diesel::result::Error::NotFound) - } - } -} diff --git a/crates/db_schema/src/impls/oauth_provider.rs b/crates/db_schema/src/impls/oauth_provider.rs index 9e594ef54..e464fa6bc 100644 --- a/crates/db_schema/src/impls/oauth_provider.rs +++ b/crates/db_schema/src/impls/oauth_provider.rs @@ -41,24 +41,6 @@ impl Crud for UnsafeOAuthProvider { } impl UnsafeOAuthProvider { - pub async fn get( - pool: &mut DbPool<'_>, - oauth_provider_id: OAuthProviderId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - let oauth_providers = oauth_provider::table - .find(oauth_provider_id) - .select(oauth_provider::all_columns) - .limit(1) - .load::(conn) - .await?; - if let Some(oauth_provider) = oauth_providers.into_iter().next() { - Ok(oauth_provider) - } else { - Err(diesel::result::Error::NotFound) - } - } - pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; let oauth_providers = oauth_provider::table @@ -72,12 +54,12 @@ impl UnsafeOAuthProvider { } impl OAuthProvider { - pub async fn get_all(pool: &mut DbPool<'_>) -> Result>, Error> { + pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { let oauth_providers = UnsafeOAuthProvider::get_all(pool).await?; - let mut result = Vec::>::new(); + let mut result = Vec::::new(); for oauth_provider in &oauth_providers { - result.push(Some(Self::from_unsafe(oauth_provider))); + result.push(Self::from_unsafe(oauth_provider)); } Ok(result) diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 9e4caced6..50903e589 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -53,6 +53,7 @@ pub enum LemmyErrorType { CouldntFindCommentReply, CouldntFindPrivateMessage, CouldntFindActivity, + CouldntFindOauthProvider, PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 280d62488..5625a8cfd 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -128,7 +128,7 @@ use lemmy_api_crud::{ }, site::{create::create_site, read::get_site, update::update_site}, user::{ - create::{register, register_from_oauth}, + create::{authenticate_with_oauth, register}, delete::delete_account, }, }; @@ -395,7 +395,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .service( web::scope("/oauth") .wrap(rate_limit.register()) - .route("/register", web::post().to(register_from_oauth)), + .route("/authenticate", web::post().to(authenticate_with_oauth)), ), ); cfg.service( From d9c7e96f31cee637ef6d3a61846ae5ce817f1fe6 Mon Sep 17 00:00:00 2001 From: privacyguard Date: Sun, 7 Jul 2024 01:09:03 +0300 Subject: [PATCH 3/4] update submodule to the latest version --- crates/utils/translations | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/utils/translations b/crates/utils/translations index e9b3b25fa..94f0c7e44 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit e9b3b25fa1af7e06c4ffab86624d95da0836ef36 +Subproject commit 94f0c7e44e967ea6d003ee03b1753f08011fcf53 From 45c0a0030a5d73279272f84f848db81731fb86b9 Mon Sep 17 00:00:00 2001 From: privacyguard Date: Sun, 7 Jul 2024 01:48:10 +0300 Subject: [PATCH 4/4] use derive_new::new instead of TypedBuilder --- crates/api_crud/src/oauth_provider/create.rs | 34 +++++++++---------- crates/api_crud/src/oauth_provider/update.rs | 33 +++++++++--------- crates/api_crud/src/user/create.rs | 14 +++----- crates/db_schema/src/source/oauth_account.rs | 5 ++- crates/db_schema/src/source/oauth_provider.rs | 5 ++- 5 files changed, 42 insertions(+), 49 deletions(-) diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs index bbf08404b..f0b091384 100644 --- a/crates/api_crud/src/oauth_provider/create.rs +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -36,23 +36,23 @@ pub async fn create_oauth_provider( reader.read(&mut id_bytes); let cloned_data = data.clone(); - let oauth_provider_form = OAuthProviderInsertForm::builder() - .id(OAuthProviderId(i64::from_ne_bytes(id_bytes))) - .display_name(cloned_data.display_name) - .issuer(Url::parse(&cloned_data.issuer)?.into()) - .authorization_endpoint(Url::parse(&cloned_data.authorization_endpoint)?.into()) - .token_endpoint(Url::parse(&cloned_data.token_endpoint)?.into()) - .userinfo_endpoint(Url::parse(&cloned_data.userinfo_endpoint)?.into()) - .id_claim(cloned_data.id_claim) - .name_claim(cloned_data.name_claim) - .client_id(data.client_id.to_string()) - .client_secret(data.client_secret.to_string()) - .scopes(data.scopes.to_string()) - .auto_verify_email(data.auto_verify_email) - .auto_approve_application(data.auto_approve_application) - .account_linking_enabled(data.account_linking_enabled) - .enabled(data.enabled) - .build(); + let oauth_provider_form = OAuthProviderInsertForm { + id: OAuthProviderId(i64::from_ne_bytes(id_bytes)), + display_name: cloned_data.display_name, + issuer: Url::parse(&cloned_data.issuer)?.into(), + authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(), + token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(), + userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(), + id_claim: cloned_data.id_claim, + name_claim: cloned_data.name_claim, + client_id: data.client_id.to_string(), + client_secret: data.client_secret.to_string(), + scopes: data.scopes.to_string(), + auto_verify_email: data.auto_verify_email, + auto_approve_application: data.auto_approve_application, + account_linking_enabled: data.account_linking_enabled, + enabled: data.enabled, + }; let unsafe_oauth_provider = UnsafeOAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?; Ok(Json(OAuthProvider::from_unsafe(&unsafe_oauth_provider))) diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs index 5a21c5f39..f368b4228 100644 --- a/crates/api_crud/src/oauth_provider/update.rs +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -20,27 +20,28 @@ pub async fn update_oauth_provider( is_admin(&local_user_view)?; let cloned_data = data.clone(); - let oauth_provider_form = OAuthProviderUpdateForm::builder() - .display_name(cloned_data.display_name) - .authorization_endpoint(Url::parse(&cloned_data.authorization_endpoint)?.into()) - .token_endpoint(Url::parse(&cloned_data.token_endpoint)?.into()) - .userinfo_endpoint(Url::parse(&cloned_data.userinfo_endpoint)?.into()) - .id_claim(data.id_claim.to_string()) - .name_claim(data.name_claim.to_string()) - .client_secret(if !data.client_secret.is_empty() { + let oauth_provider_form = OAuthProviderUpdateForm { + display_name: cloned_data.display_name, + authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(), + token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(), + userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(), + id_claim: data.id_claim.to_string(), + name_claim: data.name_claim.to_string(), + client_secret: if !data.client_secret.is_empty() { Some(data.client_secret.to_string()) } else { None - }) - .scopes(data.scopes.to_string()) - .auto_verify_email(data.auto_verify_email) - .auto_approve_application(data.auto_approve_application) - .account_linking_enabled(data.account_linking_enabled) - .enabled(data.enabled) - .updated(naive_now()); + }, + scopes: data.scopes.to_string(), + auto_verify_email: data.auto_verify_email, + auto_approve_application: data.auto_approve_application, + account_linking_enabled: data.account_linking_enabled, + enabled: data.enabled, + updated: naive_now(), + }; let update_result = - UnsafeOAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form.build()).await?; + UnsafeOAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form).await?; let unsafe_oauth_provider = UnsafeOAuthProvider::read(&mut context.pool(), update_result.id) .await? .ok_or(LemmyErrorType::CouldntFindOauthProvider)?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 074bf6ae8..09499f09b 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -390,11 +390,8 @@ pub async fn authenticate_with_oauth( if oauth_provider.account_linking_enabled { // Link with OAUTH => Login user - let oauth_account_form = OAuthAccountInsertForm::builder() - .local_user_id(user_view.local_user.id) - .oauth_provider_id(oauth_provider.id) - .oauth_user_id(oauth_user_id) - .build(); + let oauth_account_form = + OAuthAccountInsertForm::new(user_view.local_user.id, oauth_provider.id, oauth_user_id); OAuthAccount::create(&mut context.pool(), &oauth_account_form) .await @@ -452,11 +449,8 @@ pub async fn authenticate_with_oauth( .ok_or(LemmyErrorType::OauthLoginFailed)?; // Create the oauth account - let oauth_account_form = OAuthAccountInsertForm::builder() - .local_user_id(local_user.id) - .oauth_provider_id(oauth_provider.id) - .oauth_user_id(oauth_user_id) - .build(); + let oauth_account_form = + OAuthAccountInsertForm::new(local_user.id, oauth_provider.id, oauth_user_id); OAuthAccount::create(&mut context.pool(), &oauth_account_form) .await diff --git a/crates/db_schema/src/source/oauth_account.rs b/crates/db_schema/src/source/oauth_account.rs index 1df3ef1e7..87d0ba6ab 100644 --- a/crates/db_schema/src/source/oauth_account.rs +++ b/crates/db_schema/src/source/oauth_account.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -24,7 +23,7 @@ pub struct OAuthAccount { pub updated: Option>, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = oauth_account))] pub struct OAuthAccountInsertForm { @@ -33,7 +32,7 @@ pub struct OAuthAccountInsertForm { pub oauth_user_id: String, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = oauth_account))] pub struct OAuthAccountUpdateForm { diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs index 4121a5382..a1786baf7 100644 --- a/crates/db_schema/src/source/oauth_provider.rs +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -110,7 +109,7 @@ pub struct OAuthProvider { pub updated: Option>, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] #[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] #[cfg_attr(feature = "full", ts(export))] @@ -136,7 +135,7 @@ pub struct OAuthProviderInsertForm { pub enabled: bool, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] #[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] #[cfg_attr(feature = "full", ts(export))]